diff --git a/Photos/LICENSE.txt b/Photos/LICENSE.txt new file mode 100644 index 00000000..a6383eca --- /dev/null +++ b/Photos/LICENSE.txt @@ -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. diff --git a/Photos/README.md b/Photos/README.md new file mode 100644 index 00000000..3f93c0be --- /dev/null +++ b/Photos/README.md @@ -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. diff --git a/Photos/SamplePhotosApp.xcodeproj/project.pbxproj b/Photos/SamplePhotosApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000..9191a4c7 --- /dev/null +++ b/Photos/SamplePhotosApp.xcodeproj/project.pbxproj @@ -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 = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 118FE1171CEF0525004F2F51 /* MasterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = MasterViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 118FE1191CEF0525004F2F51 /* AssetGridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetGridViewController.swift; sourceTree = ""; }; + 118FE11C1CEF0525004F2F51 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 118FE11E1CEF0525004F2F51 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 118FE1211CEF0525004F2F51 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 118FE1231CEF0525004F2F51 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 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 = ""; }; + 118FE1361CEF07DE004F2F51 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 118FE1381CEF07DE004F2F51 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 119E70161CF3CC1700F01BF5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 11E1BB9A1CF1371A0057E18F /* GridViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridViewCell.swift; sourceTree = ""; }; + 11E1BB9D1CF137450057E18F /* AssetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AssetViewController.swift; sourceTree = ""; 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 = ""; + }; + 118FE1131CEF0525004F2F51 /* Products */ = { + isa = PBXGroup; + children = ( + 118FE1121CEF0525004F2F51 /* SamplePhotosApp.app */, + 118FE12D1CEF07DE004F2F51 /* SamplePhotosApp.app */, + ); + name = Products; + sourceTree = ""; + }; + 118FE1141CEF0525004F2F51 /* iOS */ = { + isa = PBXGroup; + children = ( + 118FE11B1CEF0525004F2F51 /* Main.storyboard */, + 118FE11E1CEF0525004F2F51 /* Assets.xcassets */, + 118FE1201CEF0525004F2F51 /* LaunchScreen.storyboard */, + 118FE1231CEF0525004F2F51 /* Info.plist */, + ); + path = iOS; + sourceTree = ""; + }; + 118FE12E1CEF07DE004F2F51 /* tvOS */ = { + isa = PBXGroup; + children = ( + 118FE1331CEF07DE004F2F51 /* Main.storyboard */, + 118FE1361CEF07DE004F2F51 /* Assets.xcassets */, + 118FE1381CEF07DE004F2F51 /* Info.plist */, + ); + path = tvOS; + sourceTree = ""; + }; + 118FE13C1CEF07EB004F2F51 /* Shared */ = { + isa = PBXGroup; + children = ( + 118FE1151CEF0525004F2F51 /* AppDelegate.swift */, + 118FE1171CEF0525004F2F51 /* MasterViewController.swift */, + 118FE1191CEF0525004F2F51 /* AssetGridViewController.swift */, + 11E1BB9A1CF1371A0057E18F /* GridViewCell.swift */, + 11E1BB9D1CF137450057E18F /* AssetViewController.swift */, + ); + path = Shared; + sourceTree = ""; + }; +/* 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 = ""; + }; + 118FE1201CEF0525004F2F51 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 118FE1211CEF0525004F2F51 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 118FE1331CEF07DE004F2F51 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 118FE1341CEF07DE004F2F51 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* 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 */; +} diff --git a/Photos/Shared/AppDelegate.swift b/Photos/Shared/AppDelegate.swift new file mode 100644 index 00000000..bdd9c8db --- /dev/null +++ b/Photos/Shared/AppDelegate.swift @@ -0,0 +1,54 @@ +/* + Copyright (C) 2017 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Manages app lifecycle split view. + */ + + +import UIKit +import Photos + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]?) -> Bool { + // Override point for customization after application launch. + let splitViewController = self.window!.rootViewController as! UISplitViewController + #if os(iOS) + let navigationController = splitViewController.viewControllers.last! as! UINavigationController + navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem + #endif + splitViewController.delegate = self + return true + } + + // MARK: Split view + + func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool { + guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false } + guard let topAsDetailController = secondaryAsNavController.topViewController as? AssetGridViewController else { return false } + if topAsDetailController.fetchResult == nil { + // Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded. + return true + } + return false + } + + func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool { + // Let the storyboard handle the segue for every case except going from detail:assetgrid to detail:asset. + guard !splitViewController.isCollapsed else { return false } + guard !(vc is UINavigationController) else { return false } + guard let detailNavController = + splitViewController.viewControllers.last! as? UINavigationController, + detailNavController.viewControllers.count == 1 + else { return false } + + detailNavController.pushViewController(vc, animated: true) + return true + } +} diff --git a/Photos/Shared/AssetGridViewController.swift b/Photos/Shared/AssetGridViewController.swift new file mode 100644 index 00000000..5e20fb31 --- /dev/null +++ b/Photos/Shared/AssetGridViewController.swift @@ -0,0 +1,254 @@ +/* + Copyright (C) 2017 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Manages the second-level collection view, a grid of photos in a collection (or all photos). + */ + + +import UIKit +import Photos +import PhotosUI + +private extension UICollectionView { + func indexPathsForElements(in rect: CGRect) -> [IndexPath] { + let allLayoutAttributes = collectionViewLayout.layoutAttributesForElements(in: rect)! + return allLayoutAttributes.map { $0.indexPath } + } +} + +class AssetGridViewController: UICollectionViewController { + + var fetchResult: PHFetchResult! + 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() + } + } +} + diff --git a/Photos/Shared/AssetViewController.swift b/Photos/Shared/AssetViewController.swift new file mode 100644 index 00000000..f604a991 --- /dev/null +++ b/Photos/Shared/AssetViewController.swift @@ -0,0 +1,464 @@ +/* + Copyright (C) 2017 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Displays a single photo, live photo, or video asset and demonstrates simple editing. + */ + + +import UIKit +import Photos +import PhotosUI + +class AssetViewController: UIViewController { + + var asset: PHAsset! + var assetCollection: PHAssetCollection! + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var livePhotoView: PHLivePhotoView! + @IBOutlet weak var editButton: UIBarButtonItem! + @IBOutlet weak var progressView: UIProgressView! + + #if os(tvOS) + @IBOutlet var livePhotoPlayButton: UIBarButtonItem! + #endif + + @IBOutlet var playButton: UIBarButtonItem! + @IBOutlet var space: UIBarButtonItem! + @IBOutlet var trashButton: UIBarButtonItem! + @IBOutlet var favoriteButton: UIBarButtonItem! + + fileprivate var playerLayer: AVPlayerLayer! + fileprivate var isPlayingHint = false + + fileprivate lazy var formatIdentifier = Bundle.main.bundleIdentifier! + fileprivate let formatVersion = "1.0" + fileprivate lazy var ciContext = CIContext() + + // MARK: UIViewController / Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + livePhotoView.delegate = self + PHPhotoLibrary.shared().register(self) + } + + deinit { + PHPhotoLibrary.shared().unregisterChangeObserver(self) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // Set the appropriate toolbarItems based on the mediaType of the asset. + if asset.mediaType == .video { + #if os(iOS) + toolbarItems = [favoriteButton, space, playButton, space, trashButton] + navigationController?.isToolbarHidden = false + #elseif os(tvOS) + navigationItem.leftBarButtonItems = [playButton, favoriteButton, trashButton] + #endif + } else { + #if os(iOS) + // In iOS, present both stills and Live Photos the same way, because + // PHLivePhotoView provides the same gesture-based UI as in Photos app. + toolbarItems = [favoriteButton, space, trashButton] + navigationController?.isToolbarHidden = false + #elseif os(tvOS) + // In tvOS, PHLivePhotoView doesn't do playback gestures, + // so add a play button for Live Photos. + if asset.mediaSubtypes.contains(.photoLive) { + navigationItem.leftBarButtonItems = [favoriteButton, trashButton] + } else { + navigationItem.leftBarButtonItems = [livePhotoPlayButton, favoriteButton, trashButton] + } + #endif + } + + // Enable editing buttons if the asset can be edited. + editButton.isEnabled = asset.canPerform(.content) + favoriteButton.isEnabled = asset.canPerform(.properties) + favoriteButton.title = asset.isFavorite ? "♥︎" : "♡" + + // Enable the trash button if the asset can be deleted. + if assetCollection != nil { + trashButton.isEnabled = assetCollection.canPerform(.removeContent) + } else { + trashButton.isEnabled = asset.canPerform(.delete) + } + + // Make sure the view layout happens before requesting an image sized to fit the view. + view.layoutIfNeeded() + updateImage() + } + + // MARK: UI Actions + + @IBAction func editAsset(_ sender: UIBarButtonItem) { + // Use a UIAlertController to display editing options to the user. + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + #if os(iOS) + alertController.modalPresentationStyle = .popover + if let popoverController = alertController.popoverPresentationController { + popoverController.barButtonItem = sender + popoverController.permittedArrowDirections = .up + } + #endif + + // Add a Cancel action to dismiss the alert without doing anything. + alertController.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), + style: .cancel, handler: nil)) + + // Allow editing only if the PHAsset supports edit operations. + if asset.canPerform(.content) { + // Add actions for some canned filters. + alertController.addAction(UIAlertAction(title: NSLocalizedString("Sepia Tone", comment: ""), + style: .default, handler: getFilter("CISepiaTone"))) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Chrome", comment: ""), + style: .default, handler: getFilter("CIPhotoEffectChrome"))) + + // Add actions to revert any edits that have been made to the PHAsset. + alertController.addAction(UIAlertAction(title: NSLocalizedString("Revert", comment: ""), + style: .default, handler: revertAsset)) + } + // Present the UIAlertController. + present(alertController, animated: true) + + } + + #if os(tvOS) + @IBAction func playLivePhoto(_ sender: Any) { + livePhotoView.startPlayback(with: .full) + } + #endif + + @IBAction func play(_ sender: AnyObject) { + if playerLayer != nil { + // An AVPlayerLayer has already been created for this asset; just play it. + playerLayer.player!.play() + } else { + let options = PHVideoRequestOptions() + options.isNetworkAccessAllowed = true + options.deliveryMode = .automatic + options.progressHandler = { progress, _, _, _ in + // Handler might not be called on the main queue, so re-dispatch for UI work. + DispatchQueue.main.sync { + self.progressView.progress = Float(progress) + } + } + + // Request an AVPlayerItem for the displayed PHAsset and set up a layer for playing it. + PHImageManager.default().requestPlayerItem(forVideo: asset, options: options, resultHandler: { playerItem, info in + DispatchQueue.main.sync { + guard self.playerLayer == nil else { return } + + // Create an AVPlayer and AVPlayerLayer with the AVPlayerItem. + let player = AVPlayer(playerItem: playerItem) + let playerLayer = AVPlayerLayer(player: player) + + // Configure the AVPlayerLayer and add it to the view. + playerLayer.videoGravity = AVLayerVideoGravityResizeAspect + playerLayer.frame = self.view.layer.bounds + self.view.layer.addSublayer(playerLayer) + + player.play() + + // Refer to the player layer so we can remove it later. + self.playerLayer = playerLayer + } + }) + } + + } + + @IBAction func removeAsset(_ sender: AnyObject) { + let completion = { (success: Bool, error: Error?) -> () in + if success { + PHPhotoLibrary.shared().unregisterChangeObserver(self) + DispatchQueue.main.sync { + _ = self.navigationController!.popViewController(animated: true) + } + } else { + print("can't remove asset: \(error)") + } + } + + if assetCollection != nil { + // Remove asset from album + PHPhotoLibrary.shared().performChanges({ + let request = PHAssetCollectionChangeRequest(for: self.assetCollection)! + request.removeAssets([self.asset] as NSArray) + }, completionHandler: completion) + } else { + // Delete asset from library + PHPhotoLibrary.shared().performChanges({ + PHAssetChangeRequest.deleteAssets([self.asset] as NSArray) + }, completionHandler: completion) + } + + } + + @IBAction func toggleFavorite(_ sender: UIBarButtonItem) { + PHPhotoLibrary.shared().performChanges({ + let request = PHAssetChangeRequest(for: self.asset) + request.isFavorite = !self.asset.isFavorite + }, completionHandler: { success, error in + if success { + DispatchQueue.main.sync { + sender.title = self.asset.isFavorite ? "♥︎" : "♡" + } + } else { + print("can't set favorite: \(error)") + } + }) + } + + // MARK: Image display + + var targetSize: CGSize { + let scale = UIScreen.main.scale + return CGSize(width: imageView.bounds.width * scale, + height: imageView.bounds.height * scale) + } + + func updateImage() { + if asset.mediaSubtypes.contains(.photoLive) { + updateLivePhoto() + } else { + updateStaticImage() + } + } + + func updateLivePhoto() { + // Prepare the options to pass when fetching the live photo. + let options = PHLivePhotoRequestOptions() + options.deliveryMode = .highQualityFormat + options.isNetworkAccessAllowed = true + options.progressHandler = { progress, _, _, _ in + // Handler might not be called on the main queue, so re-dispatch for UI work. + DispatchQueue.main.sync { + self.progressView.progress = Float(progress) + } + } + + // Request the live photo for the asset from the default PHImageManager. + PHImageManager.default().requestLivePhoto(for: asset, targetSize: targetSize, contentMode: .aspectFit, options: options, resultHandler: { livePhoto, info in + // Hide the progress view now the request has completed. + self.progressView.isHidden = true + + // If successful, show the live photo view and display the live photo. + guard let livePhoto = livePhoto else { return } + + // Now that we have the Live Photo, show it. + self.imageView.isHidden = true + self.livePhotoView.isHidden = false + self.livePhotoView.livePhoto = livePhoto + + if !self.isPlayingHint { + // Playback a short section of the live photo; similar to the Photos share sheet. + self.isPlayingHint = true + self.livePhotoView.startPlayback(with: .hint) + } + + }) + } + + func updateStaticImage() { + // Prepare the options to pass when fetching the (photo, or video preview) image. + let options = PHImageRequestOptions() + options.deliveryMode = .highQualityFormat + options.isNetworkAccessAllowed = true + options.progressHandler = { progress, _, _, _ in + // Handler might not be called on the main queue, so re-dispatch for UI work. + DispatchQueue.main.sync { + self.progressView.progress = Float(progress) + } + } + + PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFit, options: options, resultHandler: { image, _ in + // Hide the progress view now the request has completed. + self.progressView.isHidden = true + + // If successful, show the image view and display the image. + guard let image = image else { return } + + // Now that we have the image, show it. + self.livePhotoView.isHidden = true + self.imageView.isHidden = false + self.imageView.image = image + }) + } + + // MARK: Asset editing + + func revertAsset(sender: UIAlertAction) { + PHPhotoLibrary.shared().performChanges({ + let request = PHAssetChangeRequest(for: self.asset) + request.revertAssetContentToOriginal() + }, completionHandler: { success, error in + if !success { print("can't revert asset: \(error)") } + }) + } + + // Returns a filter-applier function for the named filter, to be passed as a UIAlertAction handler + func getFilter(_ filterName: String) -> (UIAlertAction) -> () { + func applyFilter(_: UIAlertAction) { + // Set up a handler to make sure we can handle prior edits. + let options = PHContentEditingInputRequestOptions() + options.canHandleAdjustmentData = { + $0.formatIdentifier == self.formatIdentifier && $0.formatVersion == self.formatVersion + } + + // Prepare for editing. + asset.requestContentEditingInput(with: options, completionHandler: { input, info in + guard let input = input + else { fatalError("can't get content editing input: \(info)") } + + // This handler gets called on the main thread; dispatch to a background queue for processing. + DispatchQueue.global(qos: .userInitiated).async { + + // Create adjustment data describing the edit. + let adjustmentData = PHAdjustmentData(formatIdentifier: self.formatIdentifier, + formatVersion: self.formatVersion, + data: filterName.data(using: .utf8)!) + + /* NOTE: + This app's filter UI is fire-and-forget. That is, the user picks a filter, + and the app applies it and outputs the saved asset immediately. There's + no UI state for having chosen but not yet committed an edit. This means + there's no role for reading adjustment data -- you do that to resume + in-progress edits, and this sample app has no notion of "in-progress". + + However, it's still good to write adjustment data so that potential future + versions of the app (or other apps that understand our adjustement data + format) could make use of it. + */ + + // Create content editing output, write the adjustment data. + let output = PHContentEditingOutput(contentEditingInput: input) + output.adjustmentData = adjustmentData + + // Select a filtering function for the asset's media type. + let applyFunc: (String, PHContentEditingInput, PHContentEditingOutput, @escaping () -> ()) -> () + if self.asset.mediaSubtypes.contains(.photoLive) { + applyFunc = self.applyLivePhotoFilter + } else if self.asset.mediaType == .image { + applyFunc = self.applyPhotoFilter + } else { + applyFunc = self.applyVideoFilter + } + + // Apply the filter. + applyFunc(filterName, input, output, { + // When rendering is done, commit the edit to the Photos library. + PHPhotoLibrary.shared().performChanges({ + let request = PHAssetChangeRequest(for: self.asset) + request.contentEditingOutput = output + }, completionHandler: { success, error in + if !success { print("can't edit asset: \(error)") } + }) + }) + } + }) + } + return applyFilter + } + + func applyPhotoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: () -> ()) { + + // Load the full size image. + guard let inputImage = CIImage(contentsOf: input.fullSizeImageURL!) + else { fatalError("can't load input image to edit") } + + // Apply the filter. + let outputImage = inputImage + .applyingOrientation(input.fullSizeImageOrientation) + .applyingFilter(filterName, withInputParameters: nil) + + // Write the edited image as a JPEG. + do { + try self.ciContext.writeJPEGRepresentation(of: outputImage, + to: output.renderedContentURL, colorSpace: inputImage.colorSpace!, options: [:]) + } catch let error { + fatalError("can't apply filter to image: \(error)") + } + completion() + } + + func applyLivePhotoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: @escaping () -> ()) { + + // This app filters assets only for output. In an app that previews + // filters while editing, create a livePhotoContext early and reuse it + // to render both for previewing and for final output. + guard let livePhotoContext = PHLivePhotoEditingContext(livePhotoEditingInput: input) + else { fatalError("can't get live photo to edit") } + + livePhotoContext.frameProcessor = { frame, _ in + return frame.image.applyingFilter(filterName, withInputParameters: nil) + } + livePhotoContext.saveLivePhoto(to: output) { success, error in + if success { + completion() + } else { + print("can't output live photo") + } + } + } + + func applyVideoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: @escaping () -> ()) { + // Load AVAsset to process from input. + guard let avAsset = input.audiovisualAsset + else { fatalError("can't get AV asset to edit") } + + // Set up a video composition to apply the filter. + let composition = AVVideoComposition( + asset: avAsset, + applyingCIFiltersWithHandler: { request in + let filtered = request.sourceImage.applyingFilter(filterName, withInputParameters: nil) + request.finish(with: filtered, context: nil) + }) + + // Export the video composition to the output URL. + guard let export = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetHighestQuality) + else { fatalError("can't set up AV export session") } + export.outputFileType = AVFileTypeQuickTimeMovie + export.outputURL = output.renderedContentURL + export.videoComposition = composition + export.exportAsynchronously(completionHandler: completion) + } +} + +// MARK: PHPhotoLibraryChangeObserver +extension AssetViewController: PHPhotoLibraryChangeObserver { + func photoLibraryDidChange(_ changeInstance: PHChange) { + // Call might come on any background queue. Re-dispatch to the main queue to handle it. + DispatchQueue.main.sync { + // Check if there are changes to the asset we're displaying. + guard let details = changeInstance.changeDetails(for: asset) else { return } + + // Get the updated asset. + asset = details.objectAfterChanges as! PHAsset + + // If the asset's content changed, update the image and stop any video playback. + if details.assetContentChanged { + updateImage() + + playerLayer?.removeFromSuperlayer() + playerLayer = nil + } + } + } +} + +// MARK: PHLivePhotoViewDelegate +extension AssetViewController: PHLivePhotoViewDelegate { + func livePhotoView(_ livePhotoView: PHLivePhotoView, willBeginPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) { + isPlayingHint = (playbackStyle == .hint) + } + + func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) { + isPlayingHint = (playbackStyle == .hint) + } +} diff --git a/Photos/Shared/GridViewCell.swift b/Photos/Shared/GridViewCell.swift new file mode 100644 index 00000000..8e327cc5 --- /dev/null +++ b/Photos/Shared/GridViewCell.swift @@ -0,0 +1,35 @@ +/* + Copyright (C) 2017 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Collection view cell for displaying an asset. + */ + + +import UIKit + +class GridViewCell: UICollectionViewCell { + + @IBOutlet var imageView: UIImageView! + @IBOutlet var livePhotoBadgeImageView: UIImageView! + + var representedAssetIdentifier: String! + + var thumbnailImage: UIImage! { + didSet { + imageView.image = thumbnailImage + } + } + var livePhotoBadgeImage: UIImage! { + didSet { + livePhotoBadgeImageView.image = livePhotoBadgeImage + } + } + + override func prepareForReuse() { + super.prepareForReuse() + imageView.image = nil + livePhotoBadgeImageView.image = nil + } +} diff --git a/Photos/Shared/MasterViewController.swift b/Photos/Shared/MasterViewController.swift new file mode 100644 index 00000000..bb07ee6b --- /dev/null +++ b/Photos/Shared/MasterViewController.swift @@ -0,0 +1,193 @@ +/* + Copyright (C) 2017 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Manages the top-level table view, a list of photo collections. + */ + + +import UIKit +import Photos + +class MasterViewController: UITableViewController { + + // MARK: Types for managing sections, cell and segue identifiers + enum Section: Int { + case allPhotos = 0 + case smartAlbums + case userCollections + + static let count = 3 + } + + enum CellIdentifier: String { + case allPhotos, collection + } + + enum SegueIdentifier: String { + case showAllPhotos + case showCollection + } + + // MARK: Properties + var allPhotos: PHFetchResult! + var smartAlbums: PHFetchResult! + var userCollections: PHFetchResult! + 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) + } + + } + } +} + diff --git a/Photos/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Photos/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/Photos/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Photos/iOS/Base.lproj/LaunchScreen.storyboard b/Photos/iOS/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..2e721e18 --- /dev/null +++ b/Photos/iOS/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Photos/iOS/Base.lproj/Main.storyboard b/Photos/iOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000..32f90ed9 --- /dev/null +++ b/Photos/iOS/Base.lproj/Main.storyboard @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Photos/iOS/Info.plist b/Photos/iOS/Info.plist new file mode 100644 index 00000000..21bc40b8 --- /dev/null +++ b/Photos/iOS/Info.plist @@ -0,0 +1,59 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarTintParameters + + UINavigationBar + + Style + UIBarStyleDefault + Translucent + + + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSPhotoLibraryUsageDescription + Displays and edits photos to demonstrate the Photos framework. + + diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json new file mode 100644 index 00000000..8bf75d9f --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json new file mode 100644 index 00000000..8bf75d9f --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json new file mode 100644 index 00000000..6d596bc7 --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json @@ -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" + } +} diff --git a/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/Contents.json b/Photos/tvOS/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Assets.xcassets/LaunchImage.launchimage/Contents.json b/Photos/tvOS/Assets.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 00000000..29d94c78 --- /dev/null +++ b/Photos/tvOS/Assets.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "orientation" : "landscape", + "idiom" : "tv", + "extent" : "full-screen", + "minimum-system-version" : "9.0", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Photos/tvOS/Base.lproj/Main.storyboard b/Photos/tvOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000..b856d868 --- /dev/null +++ b/Photos/tvOS/Base.lproj/Main.storyboard @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Photos/tvOS/Info.plist b/Photos/tvOS/Info.plist new file mode 100644 index 00000000..d9e2c39e --- /dev/null +++ b/Photos/tvOS/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UIUserInterfaceStyle + Automatic + NSPhotoLibraryUsageDescription + Displays and edits photos to demonstrate the Photos framework. + +