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:
Liu Lantao 2017-04-24 18:53:50 +08:00
parent 98d56164a3
commit b8fd8f130e
No known key found for this signature in database
GPG Key ID: BF35AA0CD375679D
32 changed files with 2473 additions and 0 deletions

42
Photos/LICENSE.txt Normal file
View File

@ -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.

23
Photos/README.md Normal file
View File

@ -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.

View File

@ -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 */;
}

View File

@ -0,0 +1,54 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}

View File

@ -0,0 +1,254 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}
}

View File

@ -0,0 +1,464 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}

View File

@ -0,0 +1,35 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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
}
}

View File

@ -0,0 +1,193 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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)
}
}
}
}

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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>

59
Photos/iOS/Info.plist Normal file
View File

@ -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>

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "tv",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,17 @@
{
"layers" : [
{
"filename" : "Front.imagestacklayer"
},
{
"filename" : "Middle.imagestacklayer"
},
{
"filename" : "Back.imagestacklayer"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "tv",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "tv",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "tv",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,17 @@
{
"layers" : [
{
"filename" : "Front.imagestacklayer"
},
{
"filename" : "Middle.imagestacklayer"
},
{
"filename" : "Back.imagestacklayer"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "tv",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "tv",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -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"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "tv",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"orientation" : "landscape",
"idiom" : "tv",
"extent" : "full-screen",
"minimum-system-version" : "9.0",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -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>

36
Photos/tvOS/Info.plist Normal file
View File

@ -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>