Example app using Photos framework: Version 4.2, 2017-02-24
Bug fixes Signed-off-by: Liu Lantao <liulantao@gmail.com>
This commit is contained in:
parent
98d56164a3
commit
b8fd8f130e
|
@ -0,0 +1,42 @@
|
|||
Sample code project: Example app using Photos framework
|
||||
Version: 4.2
|
||||
|
||||
IMPORTANT: This Apple software is supplied to you by Apple
|
||||
Inc. ("Apple") in consideration of your agreement to the following
|
||||
terms, and your use, installation, modification or redistribution of
|
||||
this Apple software constitutes acceptance of these terms. If you do
|
||||
not agree with these terms, please do not use, install, modify or
|
||||
redistribute this Apple software.
|
||||
|
||||
In consideration of your agreement to abide by the following terms, and
|
||||
subject to these terms, Apple grants you a personal, non-exclusive
|
||||
license, under Apple's copyrights in this original Apple software (the
|
||||
"Apple Software"), to use, reproduce, modify and redistribute the Apple
|
||||
Software, with or without modifications, in source and/or binary forms;
|
||||
provided that if you redistribute the Apple Software in its entirety and
|
||||
without modifications, you must retain this notice and the following
|
||||
text and disclaimers in all such redistributions of the Apple Software.
|
||||
Neither the name, trademarks, service marks or logos of Apple Inc. may
|
||||
be used to endorse or promote products derived from the Apple Software
|
||||
without specific prior written permission from Apple. Except as
|
||||
expressly stated in this notice, no other rights or licenses, express or
|
||||
implied, are granted by Apple herein, including but not limited to any
|
||||
patent rights that may be infringed by your derivative works or by other
|
||||
works in which the Apple Software may be incorporated.
|
||||
|
||||
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
|
||||
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
|
||||
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
|
||||
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
|
||||
|
||||
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
|
||||
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
|
||||
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
|
||||
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Copyright (C) 2017 Apple Inc. All Rights Reserved.
|
|
@ -0,0 +1,23 @@
|
|||
# Example app using Photos framework
|
||||
|
||||
A basic Photos-like app to demonstrate the Photos framework.
|
||||
|
||||
- Lists albums and built-in collections (Recently Added, Favorites, etc)
|
||||
- Displays assets (all photos or those from a collection) in a thumbnail grid
|
||||
- Displays a single photo, video, or Live Photo asset
|
||||
- Allows the following actions:
|
||||
* simple edit with canned filters (for still photos, Live Photos, and videos)
|
||||
* creating an album and adding assets to it
|
||||
* removing assets from an album
|
||||
* deleting assets and albums
|
||||
* favoriting an asset
|
||||
|
||||
## Build Requirements
|
||||
|
||||
Xcode 8.0 (iOS 10.0 / tvOS 10.0 SDK) or later
|
||||
|
||||
## Runtime Requirements
|
||||
|
||||
iOS 10.0, tvOS 10.0, or later
|
||||
|
||||
Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
@ -0,0 +1,450 @@
|
|||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
118FE1161CEF0525004F2F51 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118FE1151CEF0525004F2F51 /* AppDelegate.swift */; };
|
||||
118FE1181CEF0525004F2F51 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118FE1171CEF0525004F2F51 /* MasterViewController.swift */; };
|
||||
118FE11A1CEF0525004F2F51 /* AssetGridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118FE1191CEF0525004F2F51 /* AssetGridViewController.swift */; };
|
||||
118FE11D1CEF0525004F2F51 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 118FE11B1CEF0525004F2F51 /* Main.storyboard */; };
|
||||
118FE11F1CEF0525004F2F51 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 118FE11E1CEF0525004F2F51 /* Assets.xcassets */; };
|
||||
118FE1221CEF0525004F2F51 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 118FE1201CEF0525004F2F51 /* LaunchScreen.storyboard */; };
|
||||
118FE1351CEF07DE004F2F51 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 118FE1331CEF07DE004F2F51 /* Main.storyboard */; };
|
||||
118FE1371CEF07DE004F2F51 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 118FE1361CEF07DE004F2F51 /* Assets.xcassets */; };
|
||||
118FE13D1CEF08A6004F2F51 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118FE1151CEF0525004F2F51 /* AppDelegate.swift */; };
|
||||
118FE13E1CEF0A0B004F2F51 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118FE1171CEF0525004F2F51 /* MasterViewController.swift */; };
|
||||
118FE13F1CEF0A0B004F2F51 /* AssetGridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118FE1191CEF0525004F2F51 /* AssetGridViewController.swift */; };
|
||||
11E1BB9B1CF1371A0057E18F /* GridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E1BB9A1CF1371A0057E18F /* GridViewCell.swift */; };
|
||||
11E1BB9C1CF1371A0057E18F /* GridViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E1BB9A1CF1371A0057E18F /* GridViewCell.swift */; };
|
||||
11E1BB9E1CF137450057E18F /* AssetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E1BB9D1CF137450057E18F /* AssetViewController.swift */; };
|
||||
11E1BB9F1CF137450057E18F /* AssetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E1BB9D1CF137450057E18F /* AssetViewController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
118FE1121CEF0525004F2F51 /* SamplePhotosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SamplePhotosApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
118FE1151CEF0525004F2F51 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
118FE1171CEF0525004F2F51 /* MasterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = MasterViewController.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
118FE1191CEF0525004F2F51 /* AssetGridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetGridViewController.swift; sourceTree = "<group>"; };
|
||||
118FE11C1CEF0525004F2F51 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
118FE11E1CEF0525004F2F51 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
118FE1211CEF0525004F2F51 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
118FE1231CEF0525004F2F51 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
118FE12D1CEF07DE004F2F51 /* SamplePhotosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SamplePhotosApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
118FE1341CEF07DE004F2F51 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
118FE1361CEF07DE004F2F51 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
118FE1381CEF07DE004F2F51 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
119E70161CF3CC1700F01BF5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
11E1BB9A1CF1371A0057E18F /* GridViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridViewCell.swift; sourceTree = "<group>"; };
|
||||
11E1BB9D1CF137450057E18F /* AssetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AssetViewController.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
118FE10F1CEF0525004F2F51 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
118FE12A1CEF07DE004F2F51 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
118FE1091CEF0525004F2F51 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
119E70161CF3CC1700F01BF5 /* README.md */,
|
||||
118FE13C1CEF07EB004F2F51 /* Shared */,
|
||||
118FE1141CEF0525004F2F51 /* iOS */,
|
||||
118FE12E1CEF07DE004F2F51 /* tvOS */,
|
||||
118FE1131CEF0525004F2F51 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
118FE1131CEF0525004F2F51 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
118FE1121CEF0525004F2F51 /* SamplePhotosApp.app */,
|
||||
118FE12D1CEF07DE004F2F51 /* SamplePhotosApp.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
118FE1141CEF0525004F2F51 /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
118FE11B1CEF0525004F2F51 /* Main.storyboard */,
|
||||
118FE11E1CEF0525004F2F51 /* Assets.xcassets */,
|
||||
118FE1201CEF0525004F2F51 /* LaunchScreen.storyboard */,
|
||||
118FE1231CEF0525004F2F51 /* Info.plist */,
|
||||
);
|
||||
path = iOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
118FE12E1CEF07DE004F2F51 /* tvOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
118FE1331CEF07DE004F2F51 /* Main.storyboard */,
|
||||
118FE1361CEF07DE004F2F51 /* Assets.xcassets */,
|
||||
118FE1381CEF07DE004F2F51 /* Info.plist */,
|
||||
);
|
||||
path = tvOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
118FE13C1CEF07EB004F2F51 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
118FE1151CEF0525004F2F51 /* AppDelegate.swift */,
|
||||
118FE1171CEF0525004F2F51 /* MasterViewController.swift */,
|
||||
118FE1191CEF0525004F2F51 /* AssetGridViewController.swift */,
|
||||
11E1BB9A1CF1371A0057E18F /* GridViewCell.swift */,
|
||||
11E1BB9D1CF137450057E18F /* AssetViewController.swift */,
|
||||
);
|
||||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
118FE1111CEF0525004F2F51 /* SamplePhotosApp iOS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 118FE1261CEF0525004F2F51 /* Build configuration list for PBXNativeTarget "SamplePhotosApp iOS" */;
|
||||
buildPhases = (
|
||||
118FE10E1CEF0525004F2F51 /* Sources */,
|
||||
118FE10F1CEF0525004F2F51 /* Frameworks */,
|
||||
118FE1101CEF0525004F2F51 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "SamplePhotosApp iOS";
|
||||
productName = SamplePhotosApp;
|
||||
productReference = 118FE1121CEF0525004F2F51 /* SamplePhotosApp.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
118FE12C1CEF07DE004F2F51 /* SamplePhotosApp tvOS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 118FE1391CEF07DE004F2F51 /* Build configuration list for PBXNativeTarget "SamplePhotosApp tvOS" */;
|
||||
buildPhases = (
|
||||
118FE1291CEF07DE004F2F51 /* Sources */,
|
||||
118FE12A1CEF07DE004F2F51 /* Frameworks */,
|
||||
118FE12B1CEF07DE004F2F51 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "SamplePhotosApp tvOS";
|
||||
productName = SamplePhotosApp;
|
||||
productReference = 118FE12D1CEF07DE004F2F51 /* SamplePhotosApp.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
118FE10A1CEF0525004F2F51 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0800;
|
||||
LastUpgradeCheck = 0800;
|
||||
ORGANIZATIONNAME = Apple;
|
||||
TargetAttributes = {
|
||||
118FE1111CEF0525004F2F51 = {
|
||||
CreatedOnToolsVersion = 8.0;
|
||||
LastSwiftMigration = 0800;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
118FE12C1CEF07DE004F2F51 = {
|
||||
CreatedOnToolsVersion = 8.0;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 118FE10D1CEF0525004F2F51 /* Build configuration list for PBXProject "SamplePhotosApp" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 118FE1091CEF0525004F2F51;
|
||||
productRefGroup = 118FE1131CEF0525004F2F51 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
118FE1111CEF0525004F2F51 /* SamplePhotosApp iOS */,
|
||||
118FE12C1CEF07DE004F2F51 /* SamplePhotosApp tvOS */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
118FE1101CEF0525004F2F51 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
118FE1221CEF0525004F2F51 /* LaunchScreen.storyboard in Resources */,
|
||||
118FE11F1CEF0525004F2F51 /* Assets.xcassets in Resources */,
|
||||
118FE11D1CEF0525004F2F51 /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
118FE12B1CEF07DE004F2F51 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
118FE1371CEF07DE004F2F51 /* Assets.xcassets in Resources */,
|
||||
118FE1351CEF07DE004F2F51 /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
118FE10E1CEF0525004F2F51 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
11E1BB9E1CF137450057E18F /* AssetViewController.swift in Sources */,
|
||||
11E1BB9B1CF1371A0057E18F /* GridViewCell.swift in Sources */,
|
||||
118FE11A1CEF0525004F2F51 /* AssetGridViewController.swift in Sources */,
|
||||
118FE1181CEF0525004F2F51 /* MasterViewController.swift in Sources */,
|
||||
118FE1161CEF0525004F2F51 /* AppDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
118FE1291CEF07DE004F2F51 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
11E1BB9F1CF137450057E18F /* AssetViewController.swift in Sources */,
|
||||
11E1BB9C1CF1371A0057E18F /* GridViewCell.swift in Sources */,
|
||||
118FE13D1CEF08A6004F2F51 /* AppDelegate.swift in Sources */,
|
||||
118FE13E1CEF0A0B004F2F51 /* MasterViewController.swift in Sources */,
|
||||
118FE13F1CEF0A0B004F2F51 /* AssetGridViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
118FE11B1CEF0525004F2F51 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
118FE11C1CEF0525004F2F51 /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
118FE1201CEF0525004F2F51 /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
118FE1211CEF0525004F2F51 /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
118FE1331CEF07DE004F2F51 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
118FE1341CEF07DE004F2F51 /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
118FE1241CEF0525004F2F51 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 3.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
118FE1251CEF0525004F2F51 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 3.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
118FE1271CEF0525004F2F51 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp";
|
||||
PRODUCT_NAME = "$(PROJECT_NAME)";
|
||||
SWIFT_VERSION = 3.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
118FE1281CEF0525004F2F51 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
INFOPLIST_FILE = iOS/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp";
|
||||
PRODUCT_NAME = "$(PROJECT_NAME)";
|
||||
SWIFT_VERSION = 3.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
118FE13A1CEF07DE004F2F51 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
|
||||
INFOPLIST_FILE = tvOS/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp";
|
||||
PRODUCT_NAME = "$(PROJECT_NAME)";
|
||||
SDKROOT = appletvos;
|
||||
SWIFT_VERSION = 3.0;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TVOS_DEPLOYMENT_TARGET = 10.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
118FE13B1CEF07DE004F2F51 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
|
||||
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
|
||||
INFOPLIST_FILE = tvOS/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp";
|
||||
PRODUCT_NAME = "$(PROJECT_NAME)";
|
||||
SDKROOT = appletvos;
|
||||
SWIFT_VERSION = 3.0;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TVOS_DEPLOYMENT_TARGET = 10.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
118FE10D1CEF0525004F2F51 /* Build configuration list for PBXProject "SamplePhotosApp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
118FE1241CEF0525004F2F51 /* Debug */,
|
||||
118FE1251CEF0525004F2F51 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
118FE1261CEF0525004F2F51 /* Build configuration list for PBXNativeTarget "SamplePhotosApp iOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
118FE1271CEF0525004F2F51 /* Debug */,
|
||||
118FE1281CEF0525004F2F51 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
118FE1391CEF07DE004F2F51 /* Build configuration list for PBXNativeTarget "SamplePhotosApp tvOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
118FE13A1CEF07DE004F2F51 /* Debug */,
|
||||
118FE13B1CEF07DE004F2F51 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 118FE10A1CEF0525004F2F51 /* Project object */;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright (C) 2017 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Manages app lifecycle split view.
|
||||
*/
|
||||
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
let splitViewController = self.window!.rootViewController as! UISplitViewController
|
||||
#if os(iOS)
|
||||
let navigationController = splitViewController.viewControllers.last! as! UINavigationController
|
||||
navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
|
||||
#endif
|
||||
splitViewController.delegate = self
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: Split view
|
||||
|
||||
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
|
||||
guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
|
||||
guard let topAsDetailController = secondaryAsNavController.topViewController as? AssetGridViewController else { return false }
|
||||
if topAsDetailController.fetchResult == nil {
|
||||
// Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
|
||||
// Let the storyboard handle the segue for every case except going from detail:assetgrid to detail:asset.
|
||||
guard !splitViewController.isCollapsed else { return false }
|
||||
guard !(vc is UINavigationController) else { return false }
|
||||
guard let detailNavController =
|
||||
splitViewController.viewControllers.last! as? UINavigationController,
|
||||
detailNavController.viewControllers.count == 1
|
||||
else { return false }
|
||||
|
||||
detailNavController.pushViewController(vc, animated: true)
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
Copyright (C) 2017 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Manages the second-level collection view, a grid of photos in a collection (or all photos).
|
||||
*/
|
||||
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
import PhotosUI
|
||||
|
||||
private extension UICollectionView {
|
||||
func indexPathsForElements(in rect: CGRect) -> [IndexPath] {
|
||||
let allLayoutAttributes = collectionViewLayout.layoutAttributesForElements(in: rect)!
|
||||
return allLayoutAttributes.map { $0.indexPath }
|
||||
}
|
||||
}
|
||||
|
||||
class AssetGridViewController: UICollectionViewController {
|
||||
|
||||
var fetchResult: PHFetchResult<PHAsset>!
|
||||
var assetCollection: PHAssetCollection!
|
||||
|
||||
@IBOutlet var addButtonItem: UIBarButtonItem!
|
||||
|
||||
fileprivate let imageManager = PHCachingImageManager()
|
||||
fileprivate var thumbnailSize: CGSize!
|
||||
fileprivate var previousPreheatRect = CGRect.zero
|
||||
|
||||
// MARK: UIViewController / Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
resetCachedAssets()
|
||||
PHPhotoLibrary.shared().register(self)
|
||||
|
||||
// If we get here without a segue, it's because we're visible at app launch,
|
||||
// so match the behavior of segue from the default "All Photos" view.
|
||||
if fetchResult == nil {
|
||||
let allPhotosOptions = PHFetchOptions()
|
||||
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
|
||||
fetchResult = PHAsset.fetchAssets(with: allPhotosOptions)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// Determine the size of the thumbnails to request from the PHCachingImageManager
|
||||
let scale = UIScreen.main.scale
|
||||
let cellSize = (collectionViewLayout as! UICollectionViewFlowLayout).itemSize
|
||||
thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
|
||||
|
||||
// Add button to the navigation bar if the asset collection supports adding content.
|
||||
if assetCollection == nil || assetCollection.canPerform(.addContent) {
|
||||
navigationItem.rightBarButtonItem = addButtonItem
|
||||
} else {
|
||||
navigationItem.rightBarButtonItem = nil
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
updateCachedAssets()
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
guard let destination = segue.destination as? AssetViewController
|
||||
else { fatalError("unexpected view controller for segue") }
|
||||
|
||||
let indexPath = collectionView!.indexPath(for: sender as! UICollectionViewCell)!
|
||||
destination.asset = fetchResult.object(at: indexPath.item)
|
||||
destination.assetCollection = assetCollection
|
||||
}
|
||||
|
||||
// MARK: UICollectionView
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return fetchResult.count
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let asset = fetchResult.object(at: indexPath.item)
|
||||
|
||||
// Dequeue a GridViewCell.
|
||||
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: GridViewCell.self), for: indexPath) as? GridViewCell
|
||||
else { fatalError("unexpected cell in collection view") }
|
||||
|
||||
// Add a badge to the cell if the PHAsset represents a Live Photo.
|
||||
if asset.mediaSubtypes.contains(.photoLive) {
|
||||
cell.livePhotoBadgeImage = PHLivePhotoView.livePhotoBadgeImage(options: .overContent)
|
||||
}
|
||||
|
||||
// Request an image for the asset from the PHCachingImageManager.
|
||||
cell.representedAssetIdentifier = asset.localIdentifier
|
||||
imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: { image, _ in
|
||||
// The cell may have been recycled by the time this handler gets called;
|
||||
// set the cell's thumbnail image only if it's still showing the same asset.
|
||||
if cell.representedAssetIdentifier == asset.localIdentifier {
|
||||
cell.thumbnailImage = image
|
||||
}
|
||||
})
|
||||
|
||||
return cell
|
||||
|
||||
}
|
||||
|
||||
// MARK: UIScrollView
|
||||
|
||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
updateCachedAssets()
|
||||
}
|
||||
|
||||
// MARK: Asset Caching
|
||||
|
||||
fileprivate func resetCachedAssets() {
|
||||
imageManager.stopCachingImagesForAllAssets()
|
||||
previousPreheatRect = .zero
|
||||
}
|
||||
|
||||
fileprivate func updateCachedAssets() {
|
||||
// Update only if the view is visible.
|
||||
guard isViewLoaded && view.window != nil else { return }
|
||||
|
||||
// The preheat window is twice the height of the visible rect.
|
||||
let visibleRect = CGRect(origin: collectionView!.contentOffset, size: collectionView!.bounds.size)
|
||||
let preheatRect = visibleRect.insetBy(dx: 0, dy: -0.5 * visibleRect.height)
|
||||
|
||||
// Update only if the visible area is significantly different from the last preheated area.
|
||||
let delta = abs(preheatRect.midY - previousPreheatRect.midY)
|
||||
guard delta > view.bounds.height / 3 else { return }
|
||||
|
||||
// Compute the assets to start caching and to stop caching.
|
||||
let (addedRects, removedRects) = differencesBetweenRects(previousPreheatRect, preheatRect)
|
||||
let addedAssets = addedRects
|
||||
.flatMap { rect in collectionView!.indexPathsForElements(in: rect) }
|
||||
.map { indexPath in fetchResult.object(at: indexPath.item) }
|
||||
let removedAssets = removedRects
|
||||
.flatMap { rect in collectionView!.indexPathsForElements(in: rect) }
|
||||
.map { indexPath in fetchResult.object(at: indexPath.item) }
|
||||
|
||||
// Update the assets the PHCachingImageManager is caching.
|
||||
imageManager.startCachingImages(for: addedAssets,
|
||||
targetSize: thumbnailSize, contentMode: .aspectFill, options: nil)
|
||||
imageManager.stopCachingImages(for: removedAssets,
|
||||
targetSize: thumbnailSize, contentMode: .aspectFill, options: nil)
|
||||
|
||||
// Store the preheat rect to compare against in the future.
|
||||
previousPreheatRect = preheatRect
|
||||
}
|
||||
|
||||
fileprivate func differencesBetweenRects(_ old: CGRect, _ new: CGRect) -> (added: [CGRect], removed: [CGRect]) {
|
||||
if old.intersects(new) {
|
||||
var added = [CGRect]()
|
||||
if new.maxY > old.maxY {
|
||||
added += [CGRect(x: new.origin.x, y: old.maxY,
|
||||
width: new.width, height: new.maxY - old.maxY)]
|
||||
}
|
||||
if old.minY > new.minY {
|
||||
added += [CGRect(x: new.origin.x, y: new.minY,
|
||||
width: new.width, height: old.minY - new.minY)]
|
||||
}
|
||||
var removed = [CGRect]()
|
||||
if new.maxY < old.maxY {
|
||||
removed += [CGRect(x: new.origin.x, y: new.maxY,
|
||||
width: new.width, height: old.maxY - new.maxY)]
|
||||
}
|
||||
if old.minY < new.minY {
|
||||
removed += [CGRect(x: new.origin.x, y: old.minY,
|
||||
width: new.width, height: new.minY - old.minY)]
|
||||
}
|
||||
return (added, removed)
|
||||
} else {
|
||||
return ([new], [old])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: UI Actions
|
||||
|
||||
@IBAction func addAsset(_ sender: AnyObject?) {
|
||||
|
||||
// Create a dummy image of a random solid color and random orientation.
|
||||
let size = (arc4random_uniform(2) == 0) ?
|
||||
CGSize(width: 400, height: 300) :
|
||||
CGSize(width: 300, height: 400)
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
let image = renderer.image { context in
|
||||
UIColor(hue: CGFloat(arc4random_uniform(100))/100,
|
||||
saturation: 1, brightness: 1, alpha: 1).setFill()
|
||||
context.fill(context.format.bounds)
|
||||
}
|
||||
|
||||
// Add it to the photo library.
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let creationRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
|
||||
if let assetCollection = self.assetCollection {
|
||||
let addAssetRequest = PHAssetCollectionChangeRequest(for: assetCollection)
|
||||
addAssetRequest?.addAssets([creationRequest.placeholderForCreatedAsset!] as NSArray)
|
||||
}
|
||||
}, completionHandler: {success, error in
|
||||
if !success { print("error creating asset: \(error)") }
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: PHPhotoLibraryChangeObserver
|
||||
extension AssetGridViewController: PHPhotoLibraryChangeObserver {
|
||||
func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||||
|
||||
guard let changes = changeInstance.changeDetails(for: fetchResult)
|
||||
else { return }
|
||||
|
||||
// Change notifications may be made on a background queue. Re-dispatch to the
|
||||
// main queue before acting on the change as we'll be updating the UI.
|
||||
DispatchQueue.main.sync {
|
||||
// Hang on to the new fetch result.
|
||||
fetchResult = changes.fetchResultAfterChanges
|
||||
if changes.hasIncrementalChanges {
|
||||
// If we have incremental diffs, animate them in the collection view.
|
||||
guard let collectionView = self.collectionView else { fatalError() }
|
||||
collectionView.performBatchUpdates({
|
||||
// For indexes to make sense, updates must be in this order:
|
||||
// delete, insert, reload, move
|
||||
if let removed = changes.removedIndexes, removed.count > 0 {
|
||||
collectionView.deleteItems(at: removed.map({ IndexPath(item: $0, section: 0) }))
|
||||
}
|
||||
if let inserted = changes.insertedIndexes, inserted.count > 0 {
|
||||
collectionView.insertItems(at: inserted.map({ IndexPath(item: $0, section: 0) }))
|
||||
}
|
||||
if let changed = changes.changedIndexes, changed.count > 0 {
|
||||
collectionView.reloadItems(at: changed.map({ IndexPath(item: $0, section: 0) }))
|
||||
}
|
||||
changes.enumerateMoves { fromIndex, toIndex in
|
||||
collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0),
|
||||
to: IndexPath(item: toIndex, section: 0))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Reload the collection view if incremental diffs are not available.
|
||||
collectionView!.reloadData()
|
||||
}
|
||||
resetCachedAssets()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,464 @@
|
|||
/*
|
||||
Copyright (C) 2017 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Displays a single photo, live photo, or video asset and demonstrates simple editing.
|
||||
*/
|
||||
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
import PhotosUI
|
||||
|
||||
class AssetViewController: UIViewController {
|
||||
|
||||
var asset: PHAsset!
|
||||
var assetCollection: PHAssetCollection!
|
||||
|
||||
@IBOutlet weak var imageView: UIImageView!
|
||||
@IBOutlet weak var livePhotoView: PHLivePhotoView!
|
||||
@IBOutlet weak var editButton: UIBarButtonItem!
|
||||
@IBOutlet weak var progressView: UIProgressView!
|
||||
|
||||
#if os(tvOS)
|
||||
@IBOutlet var livePhotoPlayButton: UIBarButtonItem!
|
||||
#endif
|
||||
|
||||
@IBOutlet var playButton: UIBarButtonItem!
|
||||
@IBOutlet var space: UIBarButtonItem!
|
||||
@IBOutlet var trashButton: UIBarButtonItem!
|
||||
@IBOutlet var favoriteButton: UIBarButtonItem!
|
||||
|
||||
fileprivate var playerLayer: AVPlayerLayer!
|
||||
fileprivate var isPlayingHint = false
|
||||
|
||||
fileprivate lazy var formatIdentifier = Bundle.main.bundleIdentifier!
|
||||
fileprivate let formatVersion = "1.0"
|
||||
fileprivate lazy var ciContext = CIContext()
|
||||
|
||||
// MARK: UIViewController / Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
livePhotoView.delegate = self
|
||||
PHPhotoLibrary.shared().register(self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// Set the appropriate toolbarItems based on the mediaType of the asset.
|
||||
if asset.mediaType == .video {
|
||||
#if os(iOS)
|
||||
toolbarItems = [favoriteButton, space, playButton, space, trashButton]
|
||||
navigationController?.isToolbarHidden = false
|
||||
#elseif os(tvOS)
|
||||
navigationItem.leftBarButtonItems = [playButton, favoriteButton, trashButton]
|
||||
#endif
|
||||
} else {
|
||||
#if os(iOS)
|
||||
// In iOS, present both stills and Live Photos the same way, because
|
||||
// PHLivePhotoView provides the same gesture-based UI as in Photos app.
|
||||
toolbarItems = [favoriteButton, space, trashButton]
|
||||
navigationController?.isToolbarHidden = false
|
||||
#elseif os(tvOS)
|
||||
// In tvOS, PHLivePhotoView doesn't do playback gestures,
|
||||
// so add a play button for Live Photos.
|
||||
if asset.mediaSubtypes.contains(.photoLive) {
|
||||
navigationItem.leftBarButtonItems = [favoriteButton, trashButton]
|
||||
} else {
|
||||
navigationItem.leftBarButtonItems = [livePhotoPlayButton, favoriteButton, trashButton]
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Enable editing buttons if the asset can be edited.
|
||||
editButton.isEnabled = asset.canPerform(.content)
|
||||
favoriteButton.isEnabled = asset.canPerform(.properties)
|
||||
favoriteButton.title = asset.isFavorite ? "♥︎" : "♡"
|
||||
|
||||
// Enable the trash button if the asset can be deleted.
|
||||
if assetCollection != nil {
|
||||
trashButton.isEnabled = assetCollection.canPerform(.removeContent)
|
||||
} else {
|
||||
trashButton.isEnabled = asset.canPerform(.delete)
|
||||
}
|
||||
|
||||
// Make sure the view layout happens before requesting an image sized to fit the view.
|
||||
view.layoutIfNeeded()
|
||||
updateImage()
|
||||
}
|
||||
|
||||
// MARK: UI Actions
|
||||
|
||||
@IBAction func editAsset(_ sender: UIBarButtonItem) {
|
||||
// Use a UIAlertController to display editing options to the user.
|
||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
#if os(iOS)
|
||||
alertController.modalPresentationStyle = .popover
|
||||
if let popoverController = alertController.popoverPresentationController {
|
||||
popoverController.barButtonItem = sender
|
||||
popoverController.permittedArrowDirections = .up
|
||||
}
|
||||
#endif
|
||||
|
||||
// Add a Cancel action to dismiss the alert without doing anything.
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""),
|
||||
style: .cancel, handler: nil))
|
||||
|
||||
// Allow editing only if the PHAsset supports edit operations.
|
||||
if asset.canPerform(.content) {
|
||||
// Add actions for some canned filters.
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Sepia Tone", comment: ""),
|
||||
style: .default, handler: getFilter("CISepiaTone")))
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Chrome", comment: ""),
|
||||
style: .default, handler: getFilter("CIPhotoEffectChrome")))
|
||||
|
||||
// Add actions to revert any edits that have been made to the PHAsset.
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Revert", comment: ""),
|
||||
style: .default, handler: revertAsset))
|
||||
}
|
||||
// Present the UIAlertController.
|
||||
present(alertController, animated: true)
|
||||
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
@IBAction func playLivePhoto(_ sender: Any) {
|
||||
livePhotoView.startPlayback(with: .full)
|
||||
}
|
||||
#endif
|
||||
|
||||
@IBAction func play(_ sender: AnyObject) {
|
||||
if playerLayer != nil {
|
||||
// An AVPlayerLayer has already been created for this asset; just play it.
|
||||
playerLayer.player!.play()
|
||||
} else {
|
||||
let options = PHVideoRequestOptions()
|
||||
options.isNetworkAccessAllowed = true
|
||||
options.deliveryMode = .automatic
|
||||
options.progressHandler = { progress, _, _, _ in
|
||||
// Handler might not be called on the main queue, so re-dispatch for UI work.
|
||||
DispatchQueue.main.sync {
|
||||
self.progressView.progress = Float(progress)
|
||||
}
|
||||
}
|
||||
|
||||
// Request an AVPlayerItem for the displayed PHAsset and set up a layer for playing it.
|
||||
PHImageManager.default().requestPlayerItem(forVideo: asset, options: options, resultHandler: { playerItem, info in
|
||||
DispatchQueue.main.sync {
|
||||
guard self.playerLayer == nil else { return }
|
||||
|
||||
// Create an AVPlayer and AVPlayerLayer with the AVPlayerItem.
|
||||
let player = AVPlayer(playerItem: playerItem)
|
||||
let playerLayer = AVPlayerLayer(player: player)
|
||||
|
||||
// Configure the AVPlayerLayer and add it to the view.
|
||||
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect
|
||||
playerLayer.frame = self.view.layer.bounds
|
||||
self.view.layer.addSublayer(playerLayer)
|
||||
|
||||
player.play()
|
||||
|
||||
// Refer to the player layer so we can remove it later.
|
||||
self.playerLayer = playerLayer
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@IBAction func removeAsset(_ sender: AnyObject) {
|
||||
let completion = { (success: Bool, error: Error?) -> () in
|
||||
if success {
|
||||
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||||
DispatchQueue.main.sync {
|
||||
_ = self.navigationController!.popViewController(animated: true)
|
||||
}
|
||||
} else {
|
||||
print("can't remove asset: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
if assetCollection != nil {
|
||||
// Remove asset from album
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let request = PHAssetCollectionChangeRequest(for: self.assetCollection)!
|
||||
request.removeAssets([self.asset] as NSArray)
|
||||
}, completionHandler: completion)
|
||||
} else {
|
||||
// Delete asset from library
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
PHAssetChangeRequest.deleteAssets([self.asset] as NSArray)
|
||||
}, completionHandler: completion)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@IBAction func toggleFavorite(_ sender: UIBarButtonItem) {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let request = PHAssetChangeRequest(for: self.asset)
|
||||
request.isFavorite = !self.asset.isFavorite
|
||||
}, completionHandler: { success, error in
|
||||
if success {
|
||||
DispatchQueue.main.sync {
|
||||
sender.title = self.asset.isFavorite ? "♥︎" : "♡"
|
||||
}
|
||||
} else {
|
||||
print("can't set favorite: \(error)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: Image display
|
||||
|
||||
var targetSize: CGSize {
|
||||
let scale = UIScreen.main.scale
|
||||
return CGSize(width: imageView.bounds.width * scale,
|
||||
height: imageView.bounds.height * scale)
|
||||
}
|
||||
|
||||
func updateImage() {
|
||||
if asset.mediaSubtypes.contains(.photoLive) {
|
||||
updateLivePhoto()
|
||||
} else {
|
||||
updateStaticImage()
|
||||
}
|
||||
}
|
||||
|
||||
func updateLivePhoto() {
|
||||
// Prepare the options to pass when fetching the live photo.
|
||||
let options = PHLivePhotoRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
options.isNetworkAccessAllowed = true
|
||||
options.progressHandler = { progress, _, _, _ in
|
||||
// Handler might not be called on the main queue, so re-dispatch for UI work.
|
||||
DispatchQueue.main.sync {
|
||||
self.progressView.progress = Float(progress)
|
||||
}
|
||||
}
|
||||
|
||||
// Request the live photo for the asset from the default PHImageManager.
|
||||
PHImageManager.default().requestLivePhoto(for: asset, targetSize: targetSize, contentMode: .aspectFit, options: options, resultHandler: { livePhoto, info in
|
||||
// Hide the progress view now the request has completed.
|
||||
self.progressView.isHidden = true
|
||||
|
||||
// If successful, show the live photo view and display the live photo.
|
||||
guard let livePhoto = livePhoto else { return }
|
||||
|
||||
// Now that we have the Live Photo, show it.
|
||||
self.imageView.isHidden = true
|
||||
self.livePhotoView.isHidden = false
|
||||
self.livePhotoView.livePhoto = livePhoto
|
||||
|
||||
if !self.isPlayingHint {
|
||||
// Playback a short section of the live photo; similar to the Photos share sheet.
|
||||
self.isPlayingHint = true
|
||||
self.livePhotoView.startPlayback(with: .hint)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func updateStaticImage() {
|
||||
// Prepare the options to pass when fetching the (photo, or video preview) image.
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = .highQualityFormat
|
||||
options.isNetworkAccessAllowed = true
|
||||
options.progressHandler = { progress, _, _, _ in
|
||||
// Handler might not be called on the main queue, so re-dispatch for UI work.
|
||||
DispatchQueue.main.sync {
|
||||
self.progressView.progress = Float(progress)
|
||||
}
|
||||
}
|
||||
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFit, options: options, resultHandler: { image, _ in
|
||||
// Hide the progress view now the request has completed.
|
||||
self.progressView.isHidden = true
|
||||
|
||||
// If successful, show the image view and display the image.
|
||||
guard let image = image else { return }
|
||||
|
||||
// Now that we have the image, show it.
|
||||
self.livePhotoView.isHidden = true
|
||||
self.imageView.isHidden = false
|
||||
self.imageView.image = image
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: Asset editing
|
||||
|
||||
func revertAsset(sender: UIAlertAction) {
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let request = PHAssetChangeRequest(for: self.asset)
|
||||
request.revertAssetContentToOriginal()
|
||||
}, completionHandler: { success, error in
|
||||
if !success { print("can't revert asset: \(error)") }
|
||||
})
|
||||
}
|
||||
|
||||
// Returns a filter-applier function for the named filter, to be passed as a UIAlertAction handler
|
||||
func getFilter(_ filterName: String) -> (UIAlertAction) -> () {
|
||||
func applyFilter(_: UIAlertAction) {
|
||||
// Set up a handler to make sure we can handle prior edits.
|
||||
let options = PHContentEditingInputRequestOptions()
|
||||
options.canHandleAdjustmentData = {
|
||||
$0.formatIdentifier == self.formatIdentifier && $0.formatVersion == self.formatVersion
|
||||
}
|
||||
|
||||
// Prepare for editing.
|
||||
asset.requestContentEditingInput(with: options, completionHandler: { input, info in
|
||||
guard let input = input
|
||||
else { fatalError("can't get content editing input: \(info)") }
|
||||
|
||||
// This handler gets called on the main thread; dispatch to a background queue for processing.
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
|
||||
// Create adjustment data describing the edit.
|
||||
let adjustmentData = PHAdjustmentData(formatIdentifier: self.formatIdentifier,
|
||||
formatVersion: self.formatVersion,
|
||||
data: filterName.data(using: .utf8)!)
|
||||
|
||||
/* NOTE:
|
||||
This app's filter UI is fire-and-forget. That is, the user picks a filter,
|
||||
and the app applies it and outputs the saved asset immediately. There's
|
||||
no UI state for having chosen but not yet committed an edit. This means
|
||||
there's no role for reading adjustment data -- you do that to resume
|
||||
in-progress edits, and this sample app has no notion of "in-progress".
|
||||
|
||||
However, it's still good to write adjustment data so that potential future
|
||||
versions of the app (or other apps that understand our adjustement data
|
||||
format) could make use of it.
|
||||
*/
|
||||
|
||||
// Create content editing output, write the adjustment data.
|
||||
let output = PHContentEditingOutput(contentEditingInput: input)
|
||||
output.adjustmentData = adjustmentData
|
||||
|
||||
// Select a filtering function for the asset's media type.
|
||||
let applyFunc: (String, PHContentEditingInput, PHContentEditingOutput, @escaping () -> ()) -> ()
|
||||
if self.asset.mediaSubtypes.contains(.photoLive) {
|
||||
applyFunc = self.applyLivePhotoFilter
|
||||
} else if self.asset.mediaType == .image {
|
||||
applyFunc = self.applyPhotoFilter
|
||||
} else {
|
||||
applyFunc = self.applyVideoFilter
|
||||
}
|
||||
|
||||
// Apply the filter.
|
||||
applyFunc(filterName, input, output, {
|
||||
// When rendering is done, commit the edit to the Photos library.
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
let request = PHAssetChangeRequest(for: self.asset)
|
||||
request.contentEditingOutput = output
|
||||
}, completionHandler: { success, error in
|
||||
if !success { print("can't edit asset: \(error)") }
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return applyFilter
|
||||
}
|
||||
|
||||
func applyPhotoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: () -> ()) {
|
||||
|
||||
// Load the full size image.
|
||||
guard let inputImage = CIImage(contentsOf: input.fullSizeImageURL!)
|
||||
else { fatalError("can't load input image to edit") }
|
||||
|
||||
// Apply the filter.
|
||||
let outputImage = inputImage
|
||||
.applyingOrientation(input.fullSizeImageOrientation)
|
||||
.applyingFilter(filterName, withInputParameters: nil)
|
||||
|
||||
// Write the edited image as a JPEG.
|
||||
do {
|
||||
try self.ciContext.writeJPEGRepresentation(of: outputImage,
|
||||
to: output.renderedContentURL, colorSpace: inputImage.colorSpace!, options: [:])
|
||||
} catch let error {
|
||||
fatalError("can't apply filter to image: \(error)")
|
||||
}
|
||||
completion()
|
||||
}
|
||||
|
||||
func applyLivePhotoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: @escaping () -> ()) {
|
||||
|
||||
// This app filters assets only for output. In an app that previews
|
||||
// filters while editing, create a livePhotoContext early and reuse it
|
||||
// to render both for previewing and for final output.
|
||||
guard let livePhotoContext = PHLivePhotoEditingContext(livePhotoEditingInput: input)
|
||||
else { fatalError("can't get live photo to edit") }
|
||||
|
||||
livePhotoContext.frameProcessor = { frame, _ in
|
||||
return frame.image.applyingFilter(filterName, withInputParameters: nil)
|
||||
}
|
||||
livePhotoContext.saveLivePhoto(to: output) { success, error in
|
||||
if success {
|
||||
completion()
|
||||
} else {
|
||||
print("can't output live photo")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyVideoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: @escaping () -> ()) {
|
||||
// Load AVAsset to process from input.
|
||||
guard let avAsset = input.audiovisualAsset
|
||||
else { fatalError("can't get AV asset to edit") }
|
||||
|
||||
// Set up a video composition to apply the filter.
|
||||
let composition = AVVideoComposition(
|
||||
asset: avAsset,
|
||||
applyingCIFiltersWithHandler: { request in
|
||||
let filtered = request.sourceImage.applyingFilter(filterName, withInputParameters: nil)
|
||||
request.finish(with: filtered, context: nil)
|
||||
})
|
||||
|
||||
// Export the video composition to the output URL.
|
||||
guard let export = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetHighestQuality)
|
||||
else { fatalError("can't set up AV export session") }
|
||||
export.outputFileType = AVFileTypeQuickTimeMovie
|
||||
export.outputURL = output.renderedContentURL
|
||||
export.videoComposition = composition
|
||||
export.exportAsynchronously(completionHandler: completion)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: PHPhotoLibraryChangeObserver
|
||||
extension AssetViewController: PHPhotoLibraryChangeObserver {
|
||||
func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||||
// Call might come on any background queue. Re-dispatch to the main queue to handle it.
|
||||
DispatchQueue.main.sync {
|
||||
// Check if there are changes to the asset we're displaying.
|
||||
guard let details = changeInstance.changeDetails(for: asset) else { return }
|
||||
|
||||
// Get the updated asset.
|
||||
asset = details.objectAfterChanges as! PHAsset
|
||||
|
||||
// If the asset's content changed, update the image and stop any video playback.
|
||||
if details.assetContentChanged {
|
||||
updateImage()
|
||||
|
||||
playerLayer?.removeFromSuperlayer()
|
||||
playerLayer = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: PHLivePhotoViewDelegate
|
||||
extension AssetViewController: PHLivePhotoViewDelegate {
|
||||
func livePhotoView(_ livePhotoView: PHLivePhotoView, willBeginPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
|
||||
isPlayingHint = (playbackStyle == .hint)
|
||||
}
|
||||
|
||||
func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) {
|
||||
isPlayingHint = (playbackStyle == .hint)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright (C) 2017 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Collection view cell for displaying an asset.
|
||||
*/
|
||||
|
||||
|
||||
import UIKit
|
||||
|
||||
class GridViewCell: UICollectionViewCell {
|
||||
|
||||
@IBOutlet var imageView: UIImageView!
|
||||
@IBOutlet var livePhotoBadgeImageView: UIImageView!
|
||||
|
||||
var representedAssetIdentifier: String!
|
||||
|
||||
var thumbnailImage: UIImage! {
|
||||
didSet {
|
||||
imageView.image = thumbnailImage
|
||||
}
|
||||
}
|
||||
var livePhotoBadgeImage: UIImage! {
|
||||
didSet {
|
||||
livePhotoBadgeImageView.image = livePhotoBadgeImage
|
||||
}
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
imageView.image = nil
|
||||
livePhotoBadgeImageView.image = nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
Copyright (C) 2017 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Manages the top-level table view, a list of photo collections.
|
||||
*/
|
||||
|
||||
|
||||
import UIKit
|
||||
import Photos
|
||||
|
||||
class MasterViewController: UITableViewController {
|
||||
|
||||
// MARK: Types for managing sections, cell and segue identifiers
|
||||
enum Section: Int {
|
||||
case allPhotos = 0
|
||||
case smartAlbums
|
||||
case userCollections
|
||||
|
||||
static let count = 3
|
||||
}
|
||||
|
||||
enum CellIdentifier: String {
|
||||
case allPhotos, collection
|
||||
}
|
||||
|
||||
enum SegueIdentifier: String {
|
||||
case showAllPhotos
|
||||
case showCollection
|
||||
}
|
||||
|
||||
// MARK: Properties
|
||||
var allPhotos: PHFetchResult<PHAsset>!
|
||||
var smartAlbums: PHFetchResult<PHAssetCollection>!
|
||||
var userCollections: PHFetchResult<PHCollection>!
|
||||
let sectionLocalizedTitles = ["", NSLocalizedString("Smart Albums", comment: ""), NSLocalizedString("Albums", comment: "")]
|
||||
|
||||
// MARK: UIViewController / Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(self.addAlbum))
|
||||
self.navigationItem.rightBarButtonItem = addButton
|
||||
|
||||
|
||||
// Create a PHFetchResult object for each section in the table view.
|
||||
let allPhotosOptions = PHFetchOptions()
|
||||
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
|
||||
allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
|
||||
smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options: nil)
|
||||
userCollections = PHCollectionList.fetchTopLevelUserCollections(with: nil)
|
||||
|
||||
PHPhotoLibrary.shared().register(self)
|
||||
|
||||
}
|
||||
|
||||
deinit {
|
||||
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
self.clearsSelectionOnViewWillAppear = self.splitViewController!.isCollapsed
|
||||
super.viewWillAppear(animated)
|
||||
}
|
||||
|
||||
func addAlbum(_ sender: AnyObject) {
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("New Album", comment: ""), message: nil, preferredStyle: .alert)
|
||||
alertController.addTextField { textField in
|
||||
textField.placeholder = NSLocalizedString("Album Name", comment: "")
|
||||
}
|
||||
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Create", comment: ""), style: .default) { action in
|
||||
let textField = alertController.textFields!.first!
|
||||
if let title = textField.text, !title.isEmpty {
|
||||
// Create a new album with the title entered.
|
||||
PHPhotoLibrary.shared().performChanges({
|
||||
PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: title)
|
||||
}, completionHandler: { success, error in
|
||||
if !success { print("error creating album: \(error)") }
|
||||
})
|
||||
}
|
||||
})
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Segues
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
|
||||
guard let destination = (segue.destination as? UINavigationController)?.topViewController as? AssetGridViewController
|
||||
else { fatalError("unexpected view controller for segue") }
|
||||
let cell = sender as! UITableViewCell
|
||||
|
||||
destination.title = cell.textLabel?.text
|
||||
|
||||
switch SegueIdentifier(rawValue: segue.identifier!)! {
|
||||
case .showAllPhotos:
|
||||
destination.fetchResult = allPhotos
|
||||
case .showCollection:
|
||||
|
||||
// get the asset collection for the selected row
|
||||
let indexPath = tableView.indexPath(for: cell)!
|
||||
let collection: PHCollection
|
||||
switch Section(rawValue: indexPath.section)! {
|
||||
case .smartAlbums:
|
||||
collection = smartAlbums.object(at: indexPath.row)
|
||||
case .userCollections:
|
||||
collection = userCollections.object(at: indexPath.row)
|
||||
default: return // not reached; all photos section already handled by other segue
|
||||
}
|
||||
|
||||
// configure the view controller with the asset collection
|
||||
guard let assetCollection = collection as? PHAssetCollection
|
||||
else { fatalError("expected asset collection") }
|
||||
destination.fetchResult = PHAsset.fetchAssets(in: assetCollection, options: nil)
|
||||
destination.assetCollection = assetCollection
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Table View
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return Section.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch Section(rawValue: section)! {
|
||||
case .allPhotos: return 1
|
||||
case .smartAlbums: return smartAlbums.count
|
||||
case .userCollections: return userCollections.count
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
switch Section(rawValue: indexPath.section)! {
|
||||
case .allPhotos:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.allPhotos.rawValue, for: indexPath)
|
||||
cell.textLabel!.text = NSLocalizedString("All Photos", comment: "")
|
||||
return cell
|
||||
|
||||
case .smartAlbums:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.collection.rawValue, for: indexPath)
|
||||
let collection = smartAlbums.object(at: indexPath.row)
|
||||
cell.textLabel!.text = collection.localizedTitle
|
||||
return cell
|
||||
|
||||
case .userCollections:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifier.collection.rawValue, for: indexPath)
|
||||
let collection = userCollections.object(at: indexPath.row)
|
||||
cell.textLabel!.text = collection.localizedTitle
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
return sectionLocalizedTitles[section]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: PHPhotoLibraryChangeObserver
|
||||
extension MasterViewController: PHPhotoLibraryChangeObserver {
|
||||
|
||||
func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||||
// Change notifications may be made on a background queue. Re-dispatch to the
|
||||
// main queue before acting on the change as we'll be updating the UI.
|
||||
DispatchQueue.main.sync {
|
||||
// Check each of the three top-level fetches for changes.
|
||||
|
||||
if let changeDetails = changeInstance.changeDetails(for: allPhotos) {
|
||||
// Update the cached fetch result.
|
||||
allPhotos = changeDetails.fetchResultAfterChanges
|
||||
// (The table row for this one doesn't need updating, it always says "All Photos".)
|
||||
}
|
||||
|
||||
// Update the cached fetch results, and reload the table sections to match.
|
||||
if let changeDetails = changeInstance.changeDetails(for: smartAlbums) {
|
||||
smartAlbums = changeDetails.fetchResultAfterChanges
|
||||
tableView.reloadSections(IndexSet(integer: Section.smartAlbums.rawValue), with: .automatic)
|
||||
}
|
||||
if let changeDetails = changeInstance.changeDetails(for: userCollections) {
|
||||
userCollections = changeDetails.fetchResultAfterChanges
|
||||
tableView.reloadSections(IndexSet(integer: Section.userCollections.rawValue), with: .automatic)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8150" systemVersion="15A204g" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8122"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<animations/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
|
@ -0,0 +1,280 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11129.3" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="H1p-Uh-vWS">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11103.2"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="pY4-Hu-kfo">
|
||||
<objects>
|
||||
<navigationController id="RMx-3f-FxP" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" id="Pmd-2v-anx">
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="7bK-jq-Zjz" kind="relationship" relationship="rootViewController" id="tsl-Nk-0bq"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="8fS-aE-onr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-38" y="-630"/>
|
||||
</scene>
|
||||
<!--Split View Controller-->
|
||||
<scene sceneID="Nki-YV-4Qg">
|
||||
<objects>
|
||||
<splitViewController id="H1p-Uh-vWS" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<connections>
|
||||
<segue destination="RMx-3f-FxP" kind="relationship" relationship="masterViewController" id="BlO-5A-QYV"/>
|
||||
<segue destination="vC3-pB-5Vb" kind="relationship" relationship="detailViewController" id="Tll-UG-LXB"/>
|
||||
</connections>
|
||||
</splitViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cZU-Oi-B1e" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-856" y="-330"/>
|
||||
</scene>
|
||||
<!--Photos-->
|
||||
<scene sceneID="smW-Zh-WAh">
|
||||
<objects>
|
||||
<tableViewController title="Photos" clearsSelectionOnViewWillAppear="NO" id="7bK-jq-Zjz" customClass="MasterViewController" customModule="SamplePhotosApp" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="r7i-6Z-zg0">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<prototypes>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="allPhotos" textLabel="Arm-wq-HPj" detailTextLabel="SHe-rx-Zgt" style="IBUITableViewCellStyleValue1" id="WCw-Qf-5nD">
|
||||
<frame key="frameInset" minY="86" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="WCw-Qf-5nD" id="37f-cq-3Eg">
|
||||
<frame key="frameInset" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="left" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Arm-wq-HPj">
|
||||
<frame key="frameInset" minX="15" minY="12" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="SHe-rx-Zgt">
|
||||
<frame key="frameInset" minX="316" minY="12" width="44" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.55686274509803924" green="0.55686274509803924" blue="0.57647058823529407" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<connections>
|
||||
<segue destination="vC3-pB-5Vb" kind="showDetail" identifier="showAllPhotos" id="a9Z-nh-I5x"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="collection" textLabel="K6l-cR-29h" detailTextLabel="e2s-0C-bH8" style="IBUITableViewCellStyleValue1" id="ymh-1j-ojm">
|
||||
<frame key="frameInset" minY="130" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="ymh-1j-ojm" id="JdQ-yA-MZm">
|
||||
<frame key="frameInset" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="left" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="K6l-cR-29h">
|
||||
<frame key="frameInset" minX="15" minY="12" width="33.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="e2s-0C-bH8">
|
||||
<frame key="frameInset" minX="316" minY="12" width="44" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.55686274509803924" green="0.55686274509803924" blue="0.57647058823529407" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<connections>
|
||||
<segue destination="vC3-pB-5Vb" kind="showDetail" identifier="showCollection" id="isy-3O-iv3"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<sections/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="7bK-jq-Zjz" id="Gho-Na-rnu"/>
|
||||
<outlet property="delegate" destination="7bK-jq-Zjz" id="RA6-mI-bju"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Photos" id="Zdf-7t-Un8"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Rux-fX-hf1" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="709" y="-630"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="r7l-gg-dq7">
|
||||
<objects>
|
||||
<navigationController toolbarHidden="NO" id="vC3-pB-5Vb" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" id="DjV-YW-jjY">
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="6fW-3P-Xnz">
|
||||
<rect key="frame" x="0.0" y="623" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</toolbar>
|
||||
<connections>
|
||||
<segue destination="mEd-c0-4A5" kind="relationship" relationship="rootViewController" id="WN8-vB-Ad2"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="SLD-UC-DBI" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-45" y="129"/>
|
||||
</scene>
|
||||
<!--Asset View Controller-->
|
||||
<scene sceneID="bu8-E5-AG6">
|
||||
<objects>
|
||||
<viewController id="Q4y-Ku-OQN" customClass="AssetViewController" customModule="SamplePhotosApp" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="avy-i4-98V"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="Q7w-mX-dJY"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="QS7-aO-HHV">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="G0H-dS-NYh"/>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ofH-UU-rOE" customClass="PHLivePhotoView">
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="G0H-dS-NYh" secondAttribute="bottom" id="EBO-Jw-DWg"/>
|
||||
<constraint firstItem="G0H-dS-NYh" firstAttribute="leading" secondItem="QS7-aO-HHV" secondAttribute="leading" id="SQm-L1-OLB"/>
|
||||
<constraint firstItem="G0H-dS-NYh" firstAttribute="top" secondItem="QS7-aO-HHV" secondAttribute="top" id="bjZ-CL-aRn"/>
|
||||
<constraint firstItem="Q7w-mX-dJY" firstAttribute="top" secondItem="ofH-UU-rOE" secondAttribute="bottom" id="gSi-Jk-0Uh"/>
|
||||
<constraint firstItem="ofH-UU-rOE" firstAttribute="leading" secondItem="QS7-aO-HHV" secondAttribute="leading" id="ghq-TE-vYX"/>
|
||||
<constraint firstAttribute="trailing" secondItem="ofH-UU-rOE" secondAttribute="trailing" id="mZ7-I4-rB7"/>
|
||||
<constraint firstAttribute="trailing" secondItem="G0H-dS-NYh" secondAttribute="trailing" id="mvG-36-BxV"/>
|
||||
<constraint firstItem="ofH-UU-rOE" firstAttribute="top" secondItem="avy-i4-98V" secondAttribute="bottom" id="ydA-en-xeN"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<toolbarItems/>
|
||||
<navigationItem key="navigationItem" id="9TU-R7-B5W">
|
||||
<nil key="title"/>
|
||||
<progressView key="titleView" hidden="YES" opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progressViewStyle="bar" progress="0.5" id="qfn-hS-RWZ" userLabel="Progress View">
|
||||
<rect key="frame" x="16" y="21" width="307" height="2.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</progressView>
|
||||
<barButtonItem key="rightBarButtonItem" systemItem="edit" id="QMG-MP-PiU">
|
||||
<connections>
|
||||
<action selector="editAsset:" destination="Q4y-Ku-OQN" id="TIb-Vf-Xhp"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="editButton" destination="QMG-MP-PiU" id="ox2-Y5-LCf"/>
|
||||
<outlet property="favoriteButton" destination="CDv-U5-I2s" id="BDg-Ml-EQt"/>
|
||||
<outlet property="imageView" destination="G0H-dS-NYh" id="UbA-Gm-yEZ"/>
|
||||
<outlet property="livePhotoView" destination="ofH-UU-rOE" id="Y6M-O2-ALQ"/>
|
||||
<outlet property="playButton" destination="8y0-Go-kn7" id="1rd-XY-DMz"/>
|
||||
<outlet property="progressView" destination="qfn-hS-RWZ" id="77I-Mb-5rM"/>
|
||||
<outlet property="space" destination="FbF-A9-Ixl" id="Zsu-Bg-bZp"/>
|
||||
<outlet property="trashButton" destination="Y0K-UE-MCp" id="EfO-GP-VQH"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="9wA-9V-Ox2" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<barButtonItem systemItem="play" id="8y0-Go-kn7">
|
||||
<connections>
|
||||
<action selector="play:" destination="Q4y-Ku-OQN" id="dqU-Kz-obT"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem style="plain" systemItem="flexibleSpace" id="FbF-A9-Ixl"/>
|
||||
<barButtonItem systemItem="trash" id="Y0K-UE-MCp">
|
||||
<connections>
|
||||
<action selector="removeAsset:" destination="Q4y-Ku-OQN" id="qiC-oE-ZfN"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem title="♡" id="CDv-U5-I2s">
|
||||
<connections>
|
||||
<action selector="toggleFavorite:" destination="Q4y-Ku-OQN" id="ekZ-uz-vzz"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1509" y="128"/>
|
||||
</scene>
|
||||
<!--Asset Grid View Controller-->
|
||||
<scene sceneID="AG0-cu-bpp">
|
||||
<objects>
|
||||
<collectionViewController id="mEd-c0-4A5" customClass="AssetGridViewController" customModule="SamplePhotosApp" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="bIC-Zl-0JL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="0.0" id="DJA-Lb-Xgj">
|
||||
<size key="itemSize" width="80" height="80"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="GridViewCell" id="cUE-R9-xrz" customClass="GridViewCell" customModule="SamplePhotosApp" customModuleProvider="target">
|
||||
<frame key="frameInset" minY="64" width="80" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="80" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="2Hr-HS-oNO"/>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="EVd-RX-S0n">
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="28" placeholder="YES" id="4TK-Cf-cYb"/>
|
||||
<constraint firstAttribute="width" constant="28" placeholder="YES" id="7HB-sg-DaU"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="2Hr-HS-oNO" secondAttribute="bottom" id="5vU-gV-LKM"/>
|
||||
<constraint firstItem="2Hr-HS-oNO" firstAttribute="leading" secondItem="cUE-R9-xrz" secondAttribute="leading" id="Jms-ht-lIG"/>
|
||||
<constraint firstItem="2Hr-HS-oNO" firstAttribute="top" secondItem="cUE-R9-xrz" secondAttribute="top" id="nHN-gk-Mkq"/>
|
||||
<constraint firstAttribute="trailing" secondItem="2Hr-HS-oNO" secondAttribute="trailing" id="wIQ-ec-TXk"/>
|
||||
<constraint firstItem="EVd-RX-S0n" firstAttribute="top" secondItem="cUE-R9-xrz" secondAttribute="top" id="yD5-nc-7cH"/>
|
||||
<constraint firstItem="EVd-RX-S0n" firstAttribute="leading" secondItem="cUE-R9-xrz" secondAttribute="leading" id="zFF-tW-3Kz"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="imageView" destination="2Hr-HS-oNO" id="65f-Fw-8hK"/>
|
||||
<outlet property="livePhotoBadgeImageView" destination="EVd-RX-S0n" id="AVi-Zf-Zvr"/>
|
||||
<segue destination="Q4y-Ku-OQN" kind="showDetail" id="OXs-Hc-Pjw"/>
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="mEd-c0-4A5" id="Eb8-bN-PvQ"/>
|
||||
<outlet property="delegate" destination="mEd-c0-4A5" id="ATN-SF-2Ph"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" id="7Fh-Zb-LGx"/>
|
||||
<connections>
|
||||
<outlet property="addButtonItem" destination="haJ-ZN-72v" id="V4J-Wr-qeE"/>
|
||||
</connections>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="sNK-oD-waS" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<barButtonItem systemItem="add" id="haJ-ZN-72v">
|
||||
<connections>
|
||||
<action selector="addAsset:" destination="mEd-c0-4A5" id="9Y8-kC-iK0"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="708" y="128"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="a9Z-nh-I5x"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
</document>
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarTintParameters</key>
|
||||
<dict>
|
||||
<key>UINavigationBar</key>
|
||||
<dict>
|
||||
<key>Style</key>
|
||||
<string>UIBarStyleDefault</string>
|
||||
<key>Translucent</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Displays and edits photos to demonstrate the Photos framework.</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"layers" : [
|
||||
{
|
||||
"filename" : "Front.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Middle.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Back.imagestacklayer"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"layers" : [
|
||||
{
|
||||
"filename" : "Front.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Middle.imagestacklayer"
|
||||
},
|
||||
{
|
||||
"filename" : "Back.imagestacklayer"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"assets" : [
|
||||
{
|
||||
"size" : "1280x768",
|
||||
"idiom" : "tv",
|
||||
"filename" : "App Icon - Large.imagestack",
|
||||
"role" : "primary-app-icon"
|
||||
},
|
||||
{
|
||||
"size" : "400x240",
|
||||
"idiom" : "tv",
|
||||
"filename" : "App Icon - Small.imagestack",
|
||||
"role" : "primary-app-icon"
|
||||
},
|
||||
{
|
||||
"size" : "2320x720",
|
||||
"idiom" : "tv",
|
||||
"filename" : "Top Shelf Image Wide.imageset",
|
||||
"role" : "top-shelf-image-wide"
|
||||
},
|
||||
{
|
||||
"size" : "1920x720",
|
||||
"idiom" : "tv",
|
||||
"filename" : "Top Shelf Image.imageset",
|
||||
"role" : "top-shelf-image"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "tv",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"orientation" : "landscape",
|
||||
"idiom" : "tv",
|
||||
"extent" : "full-screen",
|
||||
"minimum-system-version" : "9.0",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,281 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="11542" systemVersion="16C39" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="YyW-FY-ilL">
|
||||
<device id="appleTV" orientation="landscape">
|
||||
<adaptation id="light"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="tvOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11524"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Asset Grid View Controller-->
|
||||
<scene sceneID="1js-rZ-uoo">
|
||||
<objects>
|
||||
<collectionViewController id="uZN-iM-VWn" customClass="AssetGridViewController" customModule="SamplePhotosApp" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="GPf-Pl-xo6">
|
||||
<rect key="frame" x="0.0" y="0.0" width="1285" height="1080"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="30" minimumInteritemSpacing="30" id="SiP-rR-HIh">
|
||||
<size key="itemSize" width="280" height="280"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="30" minY="0.0" maxX="30" maxY="0.0"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="GridViewCell" id="P1W-st-1B8" customClass="GridViewCell" customModule="SamplePhotosApp" customModuleProvider="target">
|
||||
<rect key="frame" x="30" y="0.0" width="280" height="280"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="280"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" adjustsImageWhenAncestorFocused="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zju-ST-Rcb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="280" height="280"/>
|
||||
</imageView>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="HJJ-W8-i4Z">
|
||||
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="32" id="1e7-td-G9q"/>
|
||||
<constraint firstAttribute="width" constant="32" id="vYv-5Q-pSs"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
</view>
|
||||
<constraints>
|
||||
<constraint firstItem="zju-ST-Rcb" firstAttribute="leading" secondItem="P1W-st-1B8" secondAttribute="leading" id="5Ld-hY-rGO"/>
|
||||
<constraint firstItem="HJJ-W8-i4Z" firstAttribute="leading" secondItem="P1W-st-1B8" secondAttribute="leading" id="INW-KA-gRP"/>
|
||||
<constraint firstItem="zju-ST-Rcb" firstAttribute="top" secondItem="P1W-st-1B8" secondAttribute="top" id="WKM-QN-aZd"/>
|
||||
<constraint firstAttribute="trailing" secondItem="zju-ST-Rcb" secondAttribute="trailing" id="xkC-N5-FDL"/>
|
||||
<constraint firstAttribute="bottom" secondItem="zju-ST-Rcb" secondAttribute="bottom" id="xtG-kf-0i5"/>
|
||||
<constraint firstItem="HJJ-W8-i4Z" firstAttribute="top" secondItem="P1W-st-1B8" secondAttribute="top" id="zeW-JL-QfU"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="imageView" destination="zju-ST-Rcb" id="o4N-oo-IbL"/>
|
||||
<outlet property="livePhotoBadgeImageView" destination="HJJ-W8-i4Z" id="HAI-iE-HYt"/>
|
||||
<segue destination="bCd-wL-H8p" kind="show" identifier="showAsset" action="showDetailViewController:sender:" id="7FM-u3-Rfu"/>
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="uZN-iM-VWn" id="EJf-40-2SK"/>
|
||||
<outlet property="delegate" destination="uZN-iM-VWn" id="6bh-xI-qYf"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" id="YYJ-cl-BUI"/>
|
||||
<connections>
|
||||
<outlet property="addButtonItem" destination="1oI-Z1-ujj" id="K51-8L-feH"/>
|
||||
</connections>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="JPu-Zl-zKS" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<barButtonItem systemItem="add" id="1oI-Z1-ujj">
|
||||
<connections>
|
||||
<action selector="addAsset:" destination="uZN-iM-VWn" id="cCs-1A-zEQ"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="49" y="3562"/>
|
||||
</scene>
|
||||
<!--Asset View Controller-->
|
||||
<scene sceneID="az8-GC-4Sk">
|
||||
<objects>
|
||||
<viewController id="bCd-wL-H8p" customClass="AssetViewController" customModule="SamplePhotosApp" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="6ri-2e-T1u"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="AOS-Mc-JYH"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Uk4-X8-vlf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="1285" height="1080"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" fixedFrame="YES" progress="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="JJB-9e-QzR">
|
||||
<rect key="frame" x="45" y="535" width="1194" height="10"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</progressView>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Sgg-Mw-sxc">
|
||||
<rect key="frame" x="0.0" y="145" width="1285" height="935"/>
|
||||
</imageView>
|
||||
<view contentMode="scaleAspectFit" translatesAutoresizingMaskIntoConstraints="NO" id="BUX-Ue-KcN" customClass="PHLivePhotoView">
|
||||
<rect key="frame" x="0.0" y="145" width="1285" height="935"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="BUX-Ue-KcN" firstAttribute="height" secondItem="Sgg-Mw-sxc" secondAttribute="height" id="5wD-un-KNy"/>
|
||||
<constraint firstItem="BUX-Ue-KcN" firstAttribute="centerX" secondItem="Sgg-Mw-sxc" secondAttribute="centerX" id="ARM-gJ-3AH"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Sgg-Mw-sxc" secondAttribute="trailing" id="DUl-An-3ma"/>
|
||||
<constraint firstItem="Sgg-Mw-sxc" firstAttribute="leading" secondItem="Uk4-X8-vlf" secondAttribute="leading" id="R7R-m4-0h9"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Sgg-Mw-sxc" secondAttribute="bottom" id="WEo-T4-fCP"/>
|
||||
<constraint firstItem="BUX-Ue-KcN" firstAttribute="width" secondItem="Sgg-Mw-sxc" secondAttribute="width" id="X5I-Oa-F5A"/>
|
||||
<constraint firstItem="Sgg-Mw-sxc" firstAttribute="top" secondItem="Uk4-X8-vlf" secondAttribute="top" constant="145" id="fOh-ai-Bbj"/>
|
||||
<constraint firstItem="BUX-Ue-KcN" firstAttribute="centerY" secondItem="Sgg-Mw-sxc" secondAttribute="centerY" id="kuo-Q9-aCb"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<toolbarItems>
|
||||
<barButtonItem systemItem="trash" id="230-Hp-jWK">
|
||||
<connections>
|
||||
<action selector="removeAsset:" destination="bCd-wL-H8p" id="4yB-XT-HOe"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem title="Item" id="Byt-B4-2Cc" userLabel="Favorite">
|
||||
<connections>
|
||||
<action selector="toggleFavorite:" destination="bCd-wL-H8p" id="hkP-Tf-kpX"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem systemItem="play" id="sRh-Lm-hsr">
|
||||
<connections>
|
||||
<action selector="play:" destination="bCd-wL-H8p" id="UzY-2T-oSM"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem systemItem="play" id="3xs-xo-GU1">
|
||||
<connections>
|
||||
<action selector="playLivePhoto:" destination="bCd-wL-H8p" id="q2q-rw-paC"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem id="W1X-a7-bW7"/>
|
||||
</toolbarItems>
|
||||
<navigationItem key="navigationItem" id="HS1-jw-edF">
|
||||
<barButtonItem key="rightBarButtonItem" title="Edit" id="n9I-UT-e7E">
|
||||
<connections>
|
||||
<action selector="editAsset:" destination="bCd-wL-H8p" id="cer-dP-5oV"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="editButton" destination="n9I-UT-e7E" id="QsN-0t-W5m"/>
|
||||
<outlet property="favoriteButton" destination="Byt-B4-2Cc" id="TSG-pD-kzW"/>
|
||||
<outlet property="imageView" destination="Sgg-Mw-sxc" id="uB7-xJ-Qas"/>
|
||||
<outlet property="livePhotoPlayButton" destination="3xs-xo-GU1" id="w6a-aC-Nrg"/>
|
||||
<outlet property="livePhotoView" destination="BUX-Ue-KcN" id="9gR-HB-xUO"/>
|
||||
<outlet property="playButton" destination="sRh-Lm-hsr" id="V0W-In-c3G"/>
|
||||
<outlet property="progressView" destination="JJB-9e-QzR" id="ToP-tq-28C"/>
|
||||
<outlet property="space" destination="W1X-a7-bW7" id="1Xs-9S-2Oe"/>
|
||||
<outlet property="trashButton" destination="230-Hp-jWK" id="OCH-OE-wwk"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="91w-yt-Yke" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2272" y="3562"/>
|
||||
</scene>
|
||||
<!--Photos-->
|
||||
<scene sceneID="he0-o0-ZkF">
|
||||
<objects>
|
||||
<tableViewController clearsSelectionOnViewWillAppear="NO" id="rjh-Es-5bR" customClass="MasterViewController" customModule="SamplePhotosApp" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="66" sectionHeaderHeight="40" sectionFooterHeight="40" id="Ktr-pj-pIF">
|
||||
<rect key="frame" x="0.0" y="0.0" width="634" height="1080"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<prototypes>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="allPhotos" textLabel="IAQ-pK-Ek6" style="IBUITableViewCellStyleDefault" id="R4c-1u-qra">
|
||||
<rect key="frame" x="0.0" y="40" width="634" height="66"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="R4c-1u-qra" id="o1Q-8L-jQQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="618" height="66"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Title" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="IAQ-pK-Ek6">
|
||||
<rect key="frame" x="20" y="0.0" width="578" height="66"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="38"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="AqZ-RF-JG4" kind="showDetail" identifier="showAllPhotos" id="hqc-hK-aLY"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="collection" textLabel="kS8-tq-WcS" style="IBUITableViewCellStyleDefault" id="NZP-NC-HmX">
|
||||
<rect key="frame" x="0.0" y="120" width="634" height="66"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="NZP-NC-HmX" id="bji-Q3-kOT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="618" height="66"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Title" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="kS8-tq-WcS">
|
||||
<rect key="frame" x="20" y="0.0" width="578" height="66"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="38"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<segue destination="AqZ-RF-JG4" kind="showDetail" identifier="showCollection" id="uva-be-2hc"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="rjh-Es-5bR" id="Ftr-O4-sQn"/>
|
||||
<outlet property="delegate" destination="rjh-Es-5bR" id="KsN-Dl-eP4"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Photos" id="coV-ZA-BEa"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="2vL-3a-Bhx" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2272" y="1573"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="T4l-8s-4Ae">
|
||||
<objects>
|
||||
<navigationController id="5o7-R4-v7l" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="eZn-FG-CfB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="1920" height="145"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="rjh-Es-5bR" kind="relationship" relationship="rootViewController" id="KSh-7o-fdr"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="zm1-Hh-Hcv" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="49" y="1573"/>
|
||||
</scene>
|
||||
<!--Split View Controller-->
|
||||
<scene sceneID="TCs-5M-m33">
|
||||
<objects>
|
||||
<splitViewController id="YyW-FY-ilL" sceneMemberID="viewController">
|
||||
<connections>
|
||||
<segue destination="5o7-R4-v7l" kind="relationship" relationship="masterViewController" id="nNU-m3-7Ot"/>
|
||||
<segue destination="AqZ-RF-JG4" kind="relationship" relationship="detailViewController" id="WmA-V8-U3r"/>
|
||||
</connections>
|
||||
</splitViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="ulN-Wq-Nms" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-2171" y="1573"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="g8L-oe-ZdH">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="AqZ-RF-JG4" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="LoG-VL-lpn">
|
||||
<rect key="frame" x="0.0" y="0.0" width="1920" height="145"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="96b-7U-gNF">
|
||||
<rect key="frame" x="0.0" y="1036" width="1285" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</toolbar>
|
||||
<connections>
|
||||
<segue destination="uZN-iM-VWn" kind="relationship" relationship="rootViewController" id="svz-54-iyh"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Z72-7v-eaF" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-2171" y="3562"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<nil key="statusBar"/>
|
||||
<simulatedOrientationMetrics key="orientation" orientation="landscapeRight"/>
|
||||
<simulatedScreenMetrics key="destination"/>
|
||||
</simulatedMetricsContainer>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="uva-be-2hc"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
</document>
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Automatic</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Displays and edits photos to demonstrate the Photos framework.</string>
|
||||
</dict>
|
||||
</plist>
|
Loading…
Reference in New Issue