Photos: Version 4.3, 2017-06-06
Updated for new API. Signed-off-by: Liu Lantao <liulantao@gmail.com>
This commit is contained in:
parent
5e9e0184a7
commit
e1a030bc78
|
@ -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}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
See LICENSE.txt for this sample’s 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]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
See LICENSE.txt for this sample’s 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)
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
/*
|
||||
Copyright (C) 2017 Apple Inc. All Rights Reserved.
|
||||
See LICENSE.txt for this sample’s licensing information
|
||||
|
||||
Abstract:
|
||||
Manages app lifecycle split view.
|
||||
*/
|
||||
See LICENSE.txt for this sample’s 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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
/*
|
||||
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).
|
||||
*/
|
||||
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
|
||||
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
See LICENSE.txt for this sample’s 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
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
|
||||
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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" : {
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue