Photos: Version 4.3, 2017-06-06

Updated for new API.

Signed-off-by: Liu Lantao <liulantao@gmail.com>
This commit is contained in:
Liu Lantao 2017-10-16 22:43:27 +08:00
parent 5e9e0184a7
commit e1a030bc78
No known key found for this signature in database
GPG Key ID: BF35AA0CD375679D
15 changed files with 753 additions and 231 deletions

View File

@ -0,0 +1,11 @@
//
// SampleCode.xcconfig
//
// The `SAMPLE_CODE_DISAMBIGUATOR` configuration is to make it easier to build
// and run a sample code project. Once you set your project's development team,
// you'll have a unique bundle identifier. This is because the bundle identifier
// is derived based on the 'SAMPLE_CODE_DISAMBIGUATOR' value. Do not use this
// approach in your own projects—it's only useful for sample code projects because
// they are frequently downloaded and don't have a development team set.
SAMPLE_CODE_DISAMBIGUATOR=${DEVELOPMENT_TEAM}

View File

@ -1,5 +1,5 @@
Sample code project: Example app using Photos framework
Version: 4.2
Version: 4.3
IMPORTANT: This Apple software is supplied to you by Apple
Inc. ("Apple") in consideration of your agreement to the following

View File

@ -1,23 +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.
# Example app using Photos framework
A basic Photos-like app to demonstrate the Photos framework.
## Overview
- 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, looping 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 9.0 (iOS 11.0 / tvOS 11.0 SDK) or later
## Runtime Requirements
iOS 11.0, tvOS 11.0, or later

View File

@ -22,6 +22,10 @@
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 */; };
E14F743C1ED1502B0087C2DB /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F743B1ED1502B0087C2DB /* AnimatedImageView.swift */; };
E14F743D1ED1502B0087C2DB /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F743B1ED1502B0087C2DB /* AnimatedImageView.swift */; };
E1AF5AEB1ED5FF80005C4651 /* AnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AF5AEA1ED5FF80005C4651 /* AnimatedImage.swift */; };
E1AF5AEC1ED5FF80005C4651 /* AnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AF5AEA1ED5FF80005C4651 /* AnimatedImage.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -40,6 +44,10 @@
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; };
5C441EDB5058D9EFF915D183 /* LICENSE.txt */ = {isa = PBXFileReference; includeInIndex = 1; path = LICENSE.txt; sourceTree = "<group>"; };
E14F743B1ED1502B0087C2DB /* AnimatedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = "<group>"; };
E1AF5AEA1ED5FF80005C4651 /* AnimatedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedImage.swift; sourceTree = "<group>"; };
E702C04239D35F96583B09F0 /* SampleCode.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = SampleCode.xcconfig; path = Configuration/SampleCode.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -68,6 +76,8 @@
118FE1141CEF0525004F2F51 /* iOS */,
118FE12E1CEF07DE004F2F51 /* tvOS */,
118FE1131CEF0525004F2F51 /* Products */,
E27DE9D4F643517367810AD9 /* Configuration */,
A6909A21B7977592BFA400D8 /* LICENSE */,
);
sourceTree = "<group>";
};
@ -109,10 +119,29 @@
118FE1191CEF0525004F2F51 /* AssetGridViewController.swift */,
11E1BB9A1CF1371A0057E18F /* GridViewCell.swift */,
11E1BB9D1CF137450057E18F /* AssetViewController.swift */,
E1AF5AEA1ED5FF80005C4651 /* AnimatedImage.swift */,
E14F743B1ED1502B0087C2DB /* AnimatedImageView.swift */,
);
path = Shared;
sourceTree = "<group>";
};
A6909A21B7977592BFA400D8 /* LICENSE */ = {
isa = PBXGroup;
children = (
5C441EDB5058D9EFF915D183 /* LICENSE.txt */,
);
name = LICENSE;
path = .;
sourceTree = "<group>";
};
E27DE9D4F643517367810AD9 /* Configuration */ = {
isa = PBXGroup;
children = (
E702C04239D35F96583B09F0 /* SampleCode.xcconfig */,
);
name = Configuration;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -157,12 +186,12 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0800;
LastUpgradeCheck = 0800;
LastUpgradeCheck = 0900;
ORGANIZATIONNAME = Apple;
TargetAttributes = {
118FE1111CEF0525004F2F51 = {
CreatedOnToolsVersion = 8.0;
LastSwiftMigration = 0800;
LastSwiftMigration = 0900;
ProvisioningStyle = Automatic;
};
118FE12C1CEF07DE004F2F51 = {
@ -221,7 +250,9 @@
11E1BB9B1CF1371A0057E18F /* GridViewCell.swift in Sources */,
118FE11A1CEF0525004F2F51 /* AssetGridViewController.swift in Sources */,
118FE1181CEF0525004F2F51 /* MasterViewController.swift in Sources */,
E1AF5AEB1ED5FF80005C4651 /* AnimatedImage.swift in Sources */,
118FE1161CEF0525004F2F51 /* AppDelegate.swift in Sources */,
E14F743C1ED1502B0087C2DB /* AnimatedImageView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -233,7 +264,9 @@
11E1BB9C1CF1371A0057E18F /* GridViewCell.swift in Sources */,
118FE13D1CEF08A6004F2F51 /* AppDelegate.swift in Sources */,
118FE13E1CEF0A0B004F2F51 /* MasterViewController.swift in Sources */,
E1AF5AEC1ED5FF80005C4651 /* AnimatedImage.swift in Sources */,
118FE13F1CEF0A0B004F2F51 /* AssetGridViewController.swift in Sources */,
E14F743D1ED1502B0087C2DB /* AnimatedImageView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -269,6 +302,7 @@
/* Begin XCBuildConfiguration section */
118FE1241CEF0525004F2F51 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E702C04239D35F96583B09F0 /* SampleCode.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@ -282,8 +316,11 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@ -305,19 +342,20 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.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;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
118FE1251CEF0525004F2F51 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E702C04239D35F96583B09F0 /* SampleCode.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
@ -331,8 +369,11 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
@ -348,11 +389,11 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 3.0;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
@ -360,57 +401,69 @@
};
118FE1271CEF0525004F2F51 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E702C04239D35F96583B09F0 /* SampleCode.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = iOS/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp${SAMPLE_CODE_DISAMBIGUATOR}";
PRODUCT_NAME = "$(PROJECT_NAME)";
SWIFT_VERSION = 3.0;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 4.0;
};
name = Debug;
};
118FE1281CEF0525004F2F51 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E702C04239D35F96583B09F0 /* SampleCode.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = iOS/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp${SAMPLE_CODE_DISAMBIGUATOR}";
PRODUCT_NAME = "$(PROJECT_NAME)";
SWIFT_VERSION = 3.0;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 4.0;
};
name = Release;
};
118FE13A1CEF07DE004F2F51 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E702C04239D35F96583B09F0 /* SampleCode.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = tvOS/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp${SAMPLE_CODE_DISAMBIGUATOR}";
PRODUCT_NAME = "$(PROJECT_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = appletvos;
SWIFT_VERSION = 3.0;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 10.0;
TVOS_DEPLOYMENT_TARGET = 11.0;
};
name = Debug;
};
118FE13B1CEF07DE004F2F51 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E702C04239D35F96583B09F0 /* SampleCode.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image";
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = tvOS/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SamplePhotosApp${SAMPLE_CODE_DISAMBIGUATOR}";
PRODUCT_NAME = "$(PROJECT_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = appletvos;
SWIFT_VERSION = 3.0;
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 10.0;
TVOS_DEPLOYMENT_TARGET = 11.0;
};
name = Release;
};

View File

@ -0,0 +1,107 @@
/*
See LICENSE.txt for this samples licensing information.
Abstract:
Model object encapsulating an animated GIF.
*/
import UIKit
import ImageIO
class AnimatedImage {
public let frameCount: Int
public let duration: Double
public let loopCount: Int
public let size: CGSize
private let imageSource: CGImageSource
private let delays: [Double]
convenience init?(url: URL) {
guard let src = CGImageSourceCreateWithURL(url as CFURL, nil) else {
return nil
}
self.init(source: src)
}
convenience init?(data: Data) {
guard let src = CGImageSourceCreateWithData(data as CFData, nil) else {
return nil
}
self.init(source: src)
}
init(source: CGImageSource) {
imageSource = source
frameCount = CGImageSourceGetCount(imageSource)
if let imageProperties = CGImageSourceCopyProperties(source, nil) as? [String: AnyObject] {
loopCount = AnimatedImage.loopCountForProperties(properties: imageProperties)
} else {
// The default loop count for a GIF with no loop count specified is 1.
// Infinite loops are indicated by an explicit value of 0 for this property.
loopCount = 1
}
if let firstImage = CGImageSourceCreateImageAtIndex(source, 0, nil) {
size = CGSize(width: firstImage.width, height: firstImage.height)
} else {
size = CGSize.zero
}
var delayTimes = [Double](repeating: (1.0 / 30.0), count: frameCount)
var totalDuration: Double = 0.0
for index in 0..<frameCount {
if let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [String: AnyObject] {
if let time = AnimatedImage.frameDelayForProperties(properties: imageProperties) {
delayTimes[index] = time
}
}
totalDuration += delayTimes[index]
}
duration = totalDuration
delays = delayTimes
}
static func frameDelayForProperties(properties: [String: AnyObject]) -> Double? {
// Read the delay time for a GIF.
guard let gifDictionary = properties[kCGImagePropertyGIFDictionary as String] as? [String: AnyObject] else {
return nil
}
if let delay = (gifDictionary[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber)?.doubleValue {
if delay > 0.0 {
return delay
}
}
if let delay = gifDictionary[kCGImagePropertyGIFDelayTime as String]?.doubleValue {
if delay > 0.0 {
return delay
}
}
return nil
}
static func loopCountForProperties(properties: [String: AnyObject]) -> Int {
if let gifDictionary: [String: AnyObject] = properties[kCGImagePropertyGIFDictionary as String] as? [String: AnyObject] {
if let loopCount = (gifDictionary[kCGImagePropertyGIFLoopCount as String] as? NSNumber)?.intValue {
return loopCount
}
}
// A single playthrough is the default if loop count metadata is missing.
return 1
}
func imageAtIndex(index: Int) -> CGImage? {
if index < frameCount {
return CGImageSourceCreateImageAtIndex(imageSource, index, nil)
} else {
return nil
}
}
func delayAtIndex(index: Int) -> Double {
return delays[index]
}
}

View File

@ -0,0 +1,209 @@
/*
See LICENSE.txt for this samples licensing information.
Abstract:
View for displaying an animated GIF.
*/
import UIKit
import ImageIO
class AnimatedImageView: UIView {
var animatedImage: AnimatedImage? {
didSet {
resetAnimationState()
updateAnimation()
setNeedsLayout()
}
}
var isPlaying: Bool = false {
didSet {
if isPlaying != oldValue {
updateAnimation()
}
}
}
private var displayLink: CADisplayLink?
private var displayedIndex: Int = 0
private var displayView: UIView?
private lazy var displayLinkProxy: DisplayLinkProxyObject = {
return DisplayLinkProxyObject(listener: self)
}()
// Animation state
private var hasStartedAnimating: Bool = false
private var hasFinishedAnimating: Bool = false
private var isInfiniteLoop: Bool = false
private var remainingLoopCount: Int = 0
private var elapsedTime: Double = 0.0
private var previousTime: Double = 0.0
deinit {
displayLink?.invalidate()
}
override func layoutSubviews() {
super.layoutSubviews()
var viewAspect: CGFloat = 0.0
if bounds.height > 0.0 {
viewAspect = bounds.width / bounds.height
}
var imageAspect: CGFloat = 0.0
if let imageSize = animatedImage?.size {
if imageSize.height > 0.0 {
imageAspect = imageSize.width / imageSize.height
}
}
var viewFrame = CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height)
if imageAspect < viewAspect {
viewFrame.size.width = bounds.height * imageAspect
viewFrame.origin.x = (bounds.width / 2.0) - (0.5 * viewFrame.size.width)
} else if imageAspect > 0.0 {
viewFrame.size.height = bounds.width / imageAspect
viewFrame.origin.y = (bounds.height / 2.0) - (0.5 * viewFrame.size.height)
}
if animatedImage != nil {
if displayView == nil {
let newView = UIView(frame: CGRect.zero)
addSubview(newView)
displayView = newView
updateImage()
}
} else {
displayView?.removeFromSuperview()
displayView = nil
}
displayView?.frame = viewFrame
}
override func didMoveToWindow() {
super.didMoveToWindow()
updateAnimation()
}
override func didMoveToSuperview() {
super.didMoveToSuperview()
updateAnimation()
}
override var alpha: CGFloat {
didSet {
updateAnimation()
}
}
override var isHidden: Bool {
didSet {
updateAnimation()
}
}
func shouldAnimate() -> Bool {
let isShown = window != nil && superview != nil && !isHidden && alpha > 0.0
return isShown && animatedImage != nil && isPlaying && !hasFinishedAnimating
}
func resetAnimationState() {
displayedIndex = 0
hasStartedAnimating = false
hasFinishedAnimating = false
isInfiniteLoop = animatedImage?.frameCount == 0
if let count = animatedImage?.loopCount {
remainingLoopCount = count
} else {
remainingLoopCount = 0
}
elapsedTime = 0.0
previousTime = 0.0
}
func updateAnimation() {
if shouldAnimate() {
displayLink = CADisplayLink(target: self.displayLinkProxy, selector: #selector(DisplayLinkProxyObject.proxyTimerFired))
displayLink?.add(to: RunLoop.main, forMode: .commonModes)
displayLink?.preferredFramesPerSecond = 60
} else {
displayLink?.invalidate()
displayLink = nil
}
}
func updateImage() {
if let image = animatedImage?.imageAtIndex(index: displayedIndex) {
displayView?.layer.contents = image
}
}
func timerFired(link: CADisplayLink) {
if !shouldAnimate() {
return
}
guard let image = animatedImage else { return }
let timestamp = link.timestamp
// If this is the first callback, set things up
if !hasStartedAnimating {
elapsedTime = 0.0
previousTime = timestamp
hasStartedAnimating = true
}
let currentDelayTime = image.delayAtIndex(index: displayedIndex)
elapsedTime += timestamp - previousTime
previousTime = timestamp
// Aaccount for big gaps in playback by just resuming from now
// e.g. user presses home button and comes back after a while.
// Allow for the possibility of the current delay time being relatively long
if elapsedTime >= max(10.0, currentDelayTime + 1.0) {
elapsedTime = 0.0
}
var changedFrame = false
while elapsedTime >= currentDelayTime {
elapsedTime -= currentDelayTime
displayedIndex += 1
changedFrame = true
if displayedIndex >= image.frameCount {
// Time to loop. Start infinite loops over, otherwise decrement loop count and stop if done
if isInfiniteLoop {
displayedIndex = 0
} else {
remainingLoopCount -= 1
if remainingLoopCount == 0 {
hasFinishedAnimating = true
DispatchQueue.main.async {
self.updateAnimation()
}
} else {
displayedIndex = 0
}
}
}
}
if changedFrame {
updateImage()
}
}
}
// Use a proxy object to break the CADisplayLink retain cycle
class DisplayLinkProxyObject {
weak var myListener: AnimatedImageView?
init(listener: AnimatedImageView) {
myListener = listener
}
@objc
func proxyTimerFired(link: CADisplayLink) {
myListener?.timerFired(link: link)
}
}

View File

@ -1,11 +1,9 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Manages app lifecycle split view.
*/
See LICENSE.txt for this samples licensing information.
Abstract:
Manages app lifecycle & split view.
*/
import UIKit
import Photos
@ -14,22 +12,22 @@ import Photos
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
if let splitViewController = self.window?.rootViewController as? UISplitViewController {
#if os(iOS)
if 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 {
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 {
@ -51,4 +49,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
detailNavController.pushViewController(vc, animated: true)
return true
}
}

View File

@ -1,11 +1,9 @@
/*
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).
*/
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
@ -53,17 +51,20 @@ class AssetGridViewController: UICollectionViewController {
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
}
updateItemSize()
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
updateItemSize()
}
override func viewDidAppear(_ animated: Bool) {
@ -74,12 +75,35 @@ class AssetGridViewController: UICollectionViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destination = segue.destination as? AssetViewController
else { fatalError("unexpected view controller for segue") }
guard let cell = sender as? UICollectionViewCell else { fatalError("unexpected sender") }
let indexPath = collectionView!.indexPath(for: sender as! UICollectionViewCell)!
destination.asset = fetchResult.object(at: indexPath.item)
if let indexPath = collectionView?.indexPath(for: cell) {
destination.asset = fetchResult.object(at: indexPath.item)
}
destination.assetCollection = assetCollection
}
private func updateItemSize() {
let viewWidth = view.bounds.size.width
let desiredItemWidth: CGFloat = 100
let columns: CGFloat = max(floor(viewWidth / desiredItemWidth), 4)
let padding: CGFloat = 1
let itemWidth = floor((viewWidth - (columns - 1) * padding) / columns)
let itemSize = CGSize(width: itemWidth, height: itemWidth)
if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
layout.itemSize = itemSize
layout.minimumInteritemSpacing = padding
layout.minimumLineSpacing = padding
}
// Determine the size of the thumbnails to request from the PHCachingImageManager
let scale = UIScreen.main.scale
thumbnailSize = CGSize(width: itemSize.width * scale, height: itemSize.height * scale)
}
// MARK: UICollectionView
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
@ -90,20 +114,21 @@ class AssetGridViewController: UICollectionViewController {
let asset = fetchResult.object(at: indexPath.item)
// Dequeue a GridViewCell.
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: GridViewCell.self), for: indexPath) as? 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 {
if cell.representedAssetIdentifier == asset.localIdentifier && image != nil {
cell.thumbnailImage = image
}
})
@ -192,7 +217,7 @@ class AssetGridViewController: UICollectionViewController {
CGSize(width: 300, height: 400)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { context in
UIColor(hue: CGFloat(arc4random_uniform(100))/100,
UIColor(hue: CGFloat(arc4random_uniform(100)) / 100,
saturation: 1, brightness: 1, alpha: 1).setFill()
context.fill(context.format.bounds)
}
@ -205,7 +230,7 @@ class AssetGridViewController: UICollectionViewController {
addAssetRequest?.addAssets([creationRequest.placeholderForCreatedAsset!] as NSArray)
}
}, completionHandler: {success, error in
if !success { print("error creating asset: \(error)") }
if !success { print("error creating asset: \(String(describing: error))") }
})
}
@ -229,13 +254,13 @@ extension AssetGridViewController: PHPhotoLibraryChangeObserver {
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 {
if let removed = changes.removedIndexes, !removed.isEmpty {
collectionView.deleteItems(at: removed.map({ IndexPath(item: $0, section: 0) }))
}
if let inserted = changes.insertedIndexes, inserted.count > 0 {
if let inserted = changes.insertedIndexes, !inserted.isEmpty {
collectionView.insertItems(at: inserted.map({ IndexPath(item: $0, section: 0) }))
}
if let changed = changes.changedIndexes, changed.count > 0 {
if let changed = changes.changedIndexes, !changed.isEmpty {
collectionView.reloadItems(at: changed.map({ IndexPath(item: $0, section: 0) }))
}
changes.enumerateMoves { fromIndex, toIndex in
@ -251,4 +276,3 @@ extension AssetGridViewController: PHPhotoLibraryChangeObserver {
}
}
}

View File

@ -1,11 +1,9 @@
/*
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.
*/
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
@ -18,19 +16,21 @@ class AssetViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var livePhotoView: PHLivePhotoView!
@IBOutlet weak var animatedImageView: AnimatedImageView!
@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 playerLooper: AVPlayerLooper?
fileprivate var isPlayingHint = false
fileprivate lazy var formatIdentifier = Bundle.main.bundleIdentifier!
@ -43,6 +43,9 @@ class AssetViewController: UIViewController {
super.viewDidLoad()
livePhotoView.delegate = self
PHPhotoLibrary.shared().register(self)
livePhotoView.isHidden = true
animatedImageView.isHidden = true
}
deinit {
@ -52,46 +55,11 @@ class AssetViewController: UIViewController {
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)
}
updateToolbars()
// Make sure the view layout happens before requesting an image sized to fit the view.
view.layoutIfNeeded()
updateImage()
updateContent()
}
// MARK: UI Actions
@ -130,10 +98,10 @@ class AssetViewController: UIViewController {
#if os(tvOS)
@IBAction func playLivePhoto(_ sender: Any) {
livePhotoView.startPlayback(with: .full)
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.
@ -150,16 +118,24 @@ class AssetViewController: UIViewController {
}
// 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
PHImageManager.default().requestPlayerItem(forVideo: asset, options: options, resultHandler: { playerItem, _ in
DispatchQueue.main.sync {
guard self.playerLayer == nil else { return }
guard self.playerLayer == nil && playerItem != nil else { return }
// Create an AVPlayer and AVPlayerLayer with the AVPlayerItem.
let player = AVPlayer(playerItem: playerItem)
let player: AVPlayer
if self.asset.playbackStyle == .videoLooping {
let queuePlayer = AVQueuePlayer(playerItem: playerItem)
self.playerLooper = AVPlayerLooper(player: queuePlayer, templateItem: playerItem!)
player = queuePlayer
} else {
player = AVPlayer(playerItem: playerItem)
}
let playerLayer = AVPlayerLayer(player: player)
// Configure the AVPlayerLayer and add it to the view.
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspect
playerLayer.frame = self.view.layer.bounds
self.view.layer.addSublayer(playerLayer)
@ -174,14 +150,14 @@ class AssetViewController: UIViewController {
}
@IBAction func removeAsset(_ sender: AnyObject) {
let completion = { (success: Bool, error: Error?) -> () in
let completion = { (success: Bool, error: Error?) -> Void in
if success {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
DispatchQueue.main.sync {
_ = self.navigationController!.popViewController(animated: true)
}
} else {
print("can't remove asset: \(error)")
print("can't remove asset: \(String(describing: error))")
}
}
@ -210,7 +186,7 @@ class AssetViewController: UIViewController {
sender.title = self.asset.isFavorite ? "♥︎" : ""
}
} else {
print("can't set favorite: \(error)")
print("can't set favorite: \(String(describing: error))")
}
})
}
@ -223,14 +199,100 @@ class AssetViewController: UIViewController {
height: imageView.bounds.height * scale)
}
func updateImage() {
if asset.mediaSubtypes.contains(.photoLive) {
func updateContent() {
switch asset.playbackStyle {
case .unsupported:
let alertController = UIAlertController(title: NSLocalizedString("Unsupported Format", comment:""), message: nil, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Ok", comment: ""), style: .default, handler: nil))
self.present(alertController, animated: true, completion: nil)
case .image:
updateStillImage()
case .livePhoto:
updateLivePhoto()
} else {
updateStaticImage()
case .imageAnimated:
updateAnimatedImage()
case .video:
updateStillImage() // as a placeholder until play is tapped
case .videoLooping:
play(self)
}
}
func updateToolbars() {
// Enable editing buttons if the asset can be edited.
editButton.isEnabled = asset.canPerform(.content) && asset.playbackStyle != .imageAnimated
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)
}
// Set the appropriate toolbarItems based on the playbackStyle of the asset.
if asset.playbackStyle == .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.playbackStyle == .livePhoto {
navigationItem.leftBarButtonItems = [livePhotoPlayButton, favoriteButton, trashButton]
} else {
navigationItem.leftBarButtonItems = [favoriteButton, trashButton]
}
#endif
}
}
func updateStillImage() {
// 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)
}
}
self.progressView.isHidden = false
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.imageView.isHidden = false
self.imageView.image = image
})
}
func updateLivePhoto() {
// Prepare the options to pass when fetching the live photo.
let options = PHLivePhotoRequestOptions()
@ -243,8 +305,13 @@ class AssetViewController: UIViewController {
}
}
self.progressView.isHidden = false
// 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
PHImageManager.default().requestLivePhoto(for: asset,
targetSize: targetSize,
contentMode: .aspectFit,
options: options,
resultHandler: { livePhoto, _ in
// Hide the progress view now the request has completed.
self.progressView.isHidden = true
@ -253,6 +320,7 @@ class AssetViewController: UIViewController {
// Now that we have the Live Photo, show it.
self.imageView.isHidden = true
self.animatedImageView.isHidden = true
self.livePhotoView.isHidden = false
self.livePhotoView.livePhoto = livePhoto
@ -265,10 +333,11 @@ class AssetViewController: UIViewController {
})
}
func updateStaticImage() {
func updateAnimatedImage() {
// Prepare the options to pass when fetching the (photo, or video preview) image.
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.version = .original
options.isNetworkAccessAllowed = true
options.progressHandler = { progress, _, _, _ in
// Handler might not be called on the main queue, so re-dispatch for UI work.
@ -277,18 +346,22 @@ class AssetViewController: UIViewController {
}
}
PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFit, options: options, resultHandler: { image, _ in
self.progressView.isHidden = false
PHImageManager.default().requestImageData(for: asset, options: options) { (data, _, _, _) 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 }
guard let data = data else { return }
let animatedImage = AnimatedImage(data: data)
// Now that we have the image, show it.
self.livePhotoView.isHidden = true
self.imageView.isHidden = false
self.imageView.image = image
})
self.imageView.isHidden = true
self.animatedImageView.isHidden = false
self.animatedImageView.animatedImage = animatedImage
self.animatedImageView.isPlaying = true
}
}
// MARK: Asset editing
@ -298,12 +371,12 @@ class AssetViewController: UIViewController {
let request = PHAssetChangeRequest(for: self.asset)
request.revertAssetContentToOriginal()
}, completionHandler: { success, error in
if !success { print("can't revert asset: \(error)") }
if !success { print("can't revert asset: \(String(describing: error))") }
})
}
// Returns a filter-applier function for the named filter, to be passed as a UIAlertAction handler
func getFilter(_ filterName: String) -> (UIAlertAction) -> () {
func getFilter(_ filterName: String) -> (UIAlertAction) -> Void {
func applyFilter(_: UIAlertAction) {
// Set up a handler to make sure we can handle prior edits.
let options = PHContentEditingInputRequestOptions()
@ -325,8 +398,8 @@ class AssetViewController: UIViewController {
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
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".
@ -341,7 +414,7 @@ class AssetViewController: UIViewController {
output.adjustmentData = adjustmentData
// Select a filtering function for the asset's media type.
let applyFunc: (String, PHContentEditingInput, PHContentEditingOutput, @escaping () -> ()) -> ()
let applyFunc: (String, PHContentEditingInput, PHContentEditingOutput, @escaping () -> Void) -> Void
if self.asset.mediaSubtypes.contains(.photoLive) {
applyFunc = self.applyLivePhotoFilter
} else if self.asset.mediaType == .image {
@ -357,7 +430,7 @@ class AssetViewController: UIViewController {
let request = PHAssetChangeRequest(for: self.asset)
request.contentEditingOutput = output
}, completionHandler: { success, error in
if !success { print("can't edit asset: \(error)") }
if !success { print("can't edit asset: \(String(describing: error))") }
})
})
}
@ -366,7 +439,7 @@ class AssetViewController: UIViewController {
return applyFilter
}
func applyPhotoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: () -> ()) {
func applyPhotoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: () -> Void) {
// Load the full size image.
guard let inputImage = CIImage(contentsOf: input.fullSizeImageURL!)
@ -380,14 +453,14 @@ class AssetViewController: UIViewController {
// Write the edited image as a JPEG.
do {
try self.ciContext.writeJPEGRepresentation(of: outputImage,
to: output.renderedContentURL, colorSpace: inputImage.colorSpace!, options: [:])
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 () -> ()) {
func applyLivePhotoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: @escaping () -> Void) {
// This app filters assets only for output. In an app that previews
// filters while editing, create a livePhotoContext early and reuse it
@ -402,12 +475,12 @@ class AssetViewController: UIViewController {
if success {
completion()
} else {
print("can't output live photo")
print("can't output live photo, error:\(String(describing: error))")
}
}
}
func applyVideoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: @escaping () -> ()) {
func applyVideoFilter(_ filterName: String, input: PHContentEditingInput, output: PHContentEditingOutput, completion: @escaping () -> Void) {
// Load AVAsset to process from input.
guard let avAsset = input.audiovisualAsset
else { fatalError("can't get AV asset to edit") }
@ -423,7 +496,7 @@ class AssetViewController: UIViewController {
// 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.outputFileType = AVFileType.mov
export.outputURL = output.renderedContentURL
export.videoComposition = composition
export.exportAsynchronously(completionHandler: completion)
@ -437,16 +510,18 @@ extension AssetViewController: PHPhotoLibraryChangeObserver {
DispatchQueue.main.sync {
// Check if there are changes to the asset we're displaying.
guard let details = changeInstance.changeDetails(for: asset) else { return }
guard let assetAfterChanges = details.objectAfterChanges as? PHAsset else { return }
// Get the updated asset.
asset = details.objectAfterChanges as! PHAsset
asset = assetAfterChanges
// If the asset's content changed, update the image and stop any video playback.
if details.assetContentChanged {
updateImage()
updateContent()
playerLayer?.removeFromSuperlayer()
playerLayer = nil
playerLooper = nil
}
}
}

View File

@ -1,11 +1,9 @@
/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Collection view cell for displaying an asset.
*/
See LICENSE.txt for this samples licensing information.
Abstract:
Collection view cell for displaying an asset.
*/
import UIKit
@ -29,7 +27,7 @@ class GridViewCell: UICollectionViewCell {
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
thumbnailImage = nil
livePhotoBadgeImageView.image = nil
}
}

View File

@ -1,11 +1,9 @@
/*
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.
*/
See LICENSE.txt for this samples licensing information.
Abstract:
Manages the top-level table view, a list of photo collections.
*/
import UIKit
import Photos
@ -37,14 +35,13 @@ class MasterViewController: UITableViewController {
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)]
@ -65,35 +62,33 @@ class MasterViewController: UITableViewController {
super.viewWillAppear(animated)
}
func addAlbum(_ sender: AnyObject) {
@objc 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
alertController.addAction(UIAlertAction(title: NSLocalizedString("Create", comment: ""), style: .default) { _ 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)") }
if !success { print("error creating album: \(String(describing: 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
guard let cell = sender as? UITableViewCell else { fatalError("unexpected cell for segue") }
destination.title = cell.textLabel?.text
@ -172,8 +167,8 @@ extension MasterViewController: PHPhotoLibraryChangeObserver {
// 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
// Update the cached fetch result.
allPhotos = changeDetails.fetchResultAfterChanges
// (The table row for this one doesn't need updating, it always says "All Photos".)
}
@ -190,4 +185,3 @@ extension MasterViewController: PHPhotoLibraryChangeObserver {
}
}
}

View File

@ -1,5 +1,15 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
@ -30,6 +40,16 @@
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
@ -59,6 +79,16 @@
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {

View File

@ -1,8 +1,11 @@
<?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">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.17" systemVersion="17A263m" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="H1p-Uh-vWS">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11103.2"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.13"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -11,6 +14,7 @@
<objects>
<navigationController id="RMx-3f-FxP" sceneMemberID="viewController">
<navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" id="Pmd-2v-anx">
<rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
@ -45,21 +49,20 @@
<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"/>
<rect key="frame" x="0.0" y="22" 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"/>
<rect key="frame" x="0.0" y="0.0" 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"/>
<rect key="frame" x="16" y="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"/>
<rect key="frame" x="316" y="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"/>
@ -73,21 +76,20 @@
</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"/>
<rect key="frame" x="0.0" y="66" 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"/>
<rect key="frame" x="0.0" y="0.0" 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"/>
<rect key="frame" x="16" y="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"/>
<rect key="frame" x="316" y="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"/>
@ -118,6 +120,7 @@
<objects>
<navigationController toolbarHidden="NO" id="vC3-pB-5Vb" sceneMemberID="viewController">
<navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" id="DjV-YW-jjY">
<rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="6fW-3P-Xnz">
@ -136,37 +139,45 @@
<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"/>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="G0H-dS-NYh">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
</imageView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ofH-UU-rOE" customClass="PHLivePhotoView">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="g7o-bb-iqS" customClass="AnimatedImageView" customModule="SamplePhotosApp">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</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"/>
<constraint firstItem="G0H-dS-NYh" firstAttribute="leading" secondItem="1Of-UR-oCF" secondAttribute="leading" id="0Zx-OK-wWr"/>
<constraint firstItem="G0H-dS-NYh" firstAttribute="top" secondItem="QS7-aO-HHV" secondAttribute="top" id="0wS-GQ-3us"/>
<constraint firstItem="ofH-UU-rOE" firstAttribute="leading" secondItem="1Of-UR-oCF" secondAttribute="leading" id="Cmb-hA-VG4"/>
<constraint firstAttribute="bottom" secondItem="ofH-UU-rOE" secondAttribute="bottom" id="Km2-yT-nTf"/>
<constraint firstItem="1Of-UR-oCF" firstAttribute="trailing" secondItem="ofH-UU-rOE" secondAttribute="trailing" id="VIa-H8-1bP"/>
<constraint firstItem="ofH-UU-rOE" firstAttribute="top" secondItem="QS7-aO-HHV" secondAttribute="top" id="bj8-7a-Uik"/>
<constraint firstAttribute="bottom" secondItem="g7o-bb-iqS" secondAttribute="bottom" id="et6-AZ-bLj"/>
<constraint firstItem="1Of-UR-oCF" firstAttribute="trailing" secondItem="G0H-dS-NYh" secondAttribute="trailing" id="gPU-r7-SeY"/>
<constraint firstItem="g7o-bb-iqS" firstAttribute="leading" secondItem="1Of-UR-oCF" secondAttribute="leading" id="juP-Ms-xxN"/>
<constraint firstItem="g7o-bb-iqS" firstAttribute="top" secondItem="QS7-aO-HHV" secondAttribute="top" id="kgX-uO-Uhj"/>
<constraint firstAttribute="bottom" secondItem="G0H-dS-NYh" secondAttribute="bottom" id="yiW-bm-Gw8"/>
<constraint firstItem="1Of-UR-oCF" firstAttribute="trailing" secondItem="g7o-bb-iqS" secondAttribute="trailing" id="znr-XK-n5m"/>
</constraints>
<viewLayoutGuide key="safeArea" id="1Of-UR-oCF"/>
</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"/>
<rect key="frame" x="16" y="21" width="213" height="2.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
</progressView>
<barButtonItem key="rightBarButtonItem" systemItem="edit" id="QMG-MP-PiU">
<connections>
@ -177,6 +188,7 @@
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="animatedImageView" destination="g7o-bb-iqS" id="ZhM-kR-By6"/>
<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"/>
@ -205,7 +217,7 @@
</connections>
</barButtonItem>
</objects>
<point key="canvasLocation" x="1509" y="128"/>
<point key="canvasLocation" x="1508" y="127.28635682158921"/>
</scene>
<!--Asset Grid View Controller-->
<scene sceneID="AG0-cu-bpp">
@ -223,21 +235,23 @@
</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"/>
<rect key="frame" x="0.0" y="0.0" 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 clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="2Hr-HS-oNO">
<rect key="frame" x="0.0" y="0.0" width="80" height="80"/>
</imageView>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="EVd-RX-S0n">
<rect key="frame" x="0.0" y="0.0" width="28" height="28"/>
<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"/>

View File

@ -22,6 +22,8 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSPhotoLibraryUsageDescription</key>
<string>Displays and edits photos to demonstrate the Photos framework.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -53,7 +55,5 @@
<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

@ -1,11 +1,10 @@
<?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">
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="13122.8" systemVersion="17A263m" 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"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.6"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -13,7 +12,7 @@
<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">
<collectionView key="view" multipleTouchEnabled="YES" contentMode="scaleToFill" misplaced="YES" 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">
@ -30,7 +29,7 @@
<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">
<imageView contentMode="scaleAspectFill" 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">
@ -95,6 +94,10 @@
<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="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aah-G1-Ngw" customClass="AnimatedImageView" customModule="SamplePhotosApp" customModuleProvider="target">
<rect key="frame" x="0.0" y="145" width="1285" height="935"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<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"/>
@ -104,11 +107,15 @@
<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="aah-G1-Ngw" firstAttribute="centerX" secondItem="Sgg-Mw-sxc" secondAttribute="centerX" id="Ph1-MQ-lzb"/>
<constraint firstItem="aah-G1-Ngw" firstAttribute="width" secondItem="Sgg-Mw-sxc" secondAttribute="width" id="Qrx-q1-XhI"/>
<constraint firstItem="Sgg-Mw-sxc" firstAttribute="leading" secondItem="Uk4-X8-vlf" secondAttribute="leading" id="R7R-m4-0h9"/>
<constraint firstItem="aah-G1-Ngw" firstAttribute="centerY" secondItem="Sgg-Mw-sxc" secondAttribute="centerY" id="Sr2-yv-eFT"/>
<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"/>
<constraint firstItem="aah-G1-Ngw" firstAttribute="height" secondItem="Sgg-Mw-sxc" secondAttribute="height" id="lWP-fA-twZ"/>
</constraints>
</view>
<toolbarItems>
@ -143,6 +150,7 @@
</navigationItem>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="animatedImageView" destination="aah-G1-Ngw" id="vwh-Ga-swy"/>
<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"/>
@ -166,15 +174,15 @@
<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"/>
<tableViewCell contentMode="scaleToFill" misplaced="YES" 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="499" 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"/>
<rect key="frame" x="0.0" y="0.0" width="499" 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"/>
<rect key="frame" x="20" y="0.0" width="459" height="66"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="38"/>
<nil key="textColor"/>
@ -186,15 +194,15 @@
<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"/>
<tableViewCell contentMode="scaleToFill" misplaced="YES" 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="499" 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"/>
<rect key="frame" x="0.0" y="0.0" width="499" 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"/>
<rect key="frame" x="20" y="0.0" width="459" height="66"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="38"/>
<nil key="textColor"/>
@ -222,7 +230,7 @@
<scene sceneID="T4l-8s-4Ae">
<objects>
<navigationController id="5o7-R4-v7l" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" id="eZn-FG-CfB">
<navigationBar key="navigationBar" contentMode="scaleToFill" misplaced="YES" id="eZn-FG-CfB">
<rect key="frame" x="0.0" y="0.0" width="1920" height="145"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
@ -252,7 +260,7 @@
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="AqZ-RF-JG4" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="LoG-VL-lpn">
<navigationBar key="navigationBar" contentMode="scaleToFill" misplaced="YES" id="LoG-VL-lpn">
<rect key="frame" x="0.0" y="0.0" width="1920" height="145"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>