PhotoProgress

This commit is contained in:
Liu Lantao 2015-06-13 09:15:39 +08:00
parent 164dfb6010
commit 8ed36ae40a
36 changed files with 1600 additions and 0 deletions

42
PhotoProgress/LICENSE.txt Normal file
View File

@ -0,0 +1,42 @@
Sample code project: PhotoProgress: Using NSProgress
Version: 1.0
IMPORTANT: This Apple software is supplied to you by Apple
Inc. ("Apple") in consideration of your agreement to the following
terms, and your use, installation, modification or redistribution of
this Apple software constitutes acceptance of these terms. If you do
not agree with these terms, please do not use, install, modify or
redistribute this Apple software.
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a personal, non-exclusive
license, under Apple's copyrights in this original Apple software (the
"Apple Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may
be used to endorse or promote products derived from the Apple Software
without specific prior written permission from Apple. Except as
expressly stated in this notice, no other rights or licenses, express or
implied, are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or by other
works in which the Apple Software may be incorporated.
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Copyright (C) 2015 Apple Inc. All Rights Reserved.

View File

@ -0,0 +1,351 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
312329641B0FF89800C3BE18 /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = 312329601B0FF89800C3BE18 /* Album.swift */; };
312329651B0FF89800C3BE18 /* PhotoDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 312329611B0FF89800C3BE18 /* PhotoDownload.swift */; };
312329661B0FF89800C3BE18 /* PhotoFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 312329621B0FF89800C3BE18 /* PhotoFilter.swift */; };
312329671B0FF89800C3BE18 /* PhotoImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 312329631B0FF89800C3BE18 /* PhotoImport.swift */; };
314AEFFD1B1006CE00282B5A /* PhotoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314AEFFC1B1006CE00282B5A /* PhotoCollectionViewCell.swift */; };
3157CF961B0FCFE30012CA05 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3157CF951B0FCFE30012CA05 /* AppDelegate.swift */; };
3157CF981B0FCFE30012CA05 /* PhotosCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3157CF971B0FCFE30012CA05 /* PhotosCollectionViewController.swift */; };
3157CF9B1B0FCFE30012CA05 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3157CF991B0FCFE30012CA05 /* Main.storyboard */; };
3157CF9D1B0FCFE30012CA05 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3157CF9C1B0FCFE30012CA05 /* Images.xcassets */; };
3157CFA01B0FCFE30012CA05 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3157CF9E1B0FCFE30012CA05 /* LaunchScreen.storyboard */; };
BF1317431B1657BA005E86BE /* Photos in Resources */ = {isa = PBXBuildFile; fileRef = BF1317411B1657BA005E86BE /* Photos */; };
BF1334571B16895900A1ABE7 /* PhotosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1334561B16895900A1ABE7 /* PhotosViewController.swift */; };
BFED0B6F1B166F8B006F80C3 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFED0B6E1B166F8B006F80C3 /* Photo.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
312329601B0FF89800C3BE18 /* Album.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = "<group>"; };
312329611B0FF89800C3BE18 /* PhotoDownload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoDownload.swift; sourceTree = "<group>"; };
312329621B0FF89800C3BE18 /* PhotoFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoFilter.swift; sourceTree = "<group>"; };
312329631B0FF89800C3BE18 /* PhotoImport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoImport.swift; sourceTree = "<group>"; };
314AEFFC1B1006CE00282B5A /* PhotoCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionViewCell.swift; sourceTree = "<group>"; };
3157CF921B0FCFE30012CA05 /* PhotoProgress.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PhotoProgress.app; sourceTree = BUILT_PRODUCTS_DIR; };
3157CF951B0FCFE30012CA05 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
3157CF971B0FCFE30012CA05 /* PhotosCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosCollectionViewController.swift; sourceTree = "<group>"; };
3157CF9A1B0FCFE30012CA05 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
3157CF9C1B0FCFE30012CA05 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
3157CF9F1B0FCFE30012CA05 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
3157CFA11B0FCFE30012CA05 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
3E87FC771B1E623000B07A78 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
BF1317411B1657BA005E86BE /* Photos */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Photos; path = PhotoProgress/Photos; sourceTree = SOURCE_ROOT; };
BF1334561B16895900A1ABE7 /* PhotosViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosViewController.swift; sourceTree = "<group>"; };
BFED0B6E1B166F8B006F80C3 /* Photo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
3157CF8F1B0FCFE30012CA05 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
3157CF891B0FCFE30012CA05 = {
isa = PBXGroup;
children = (
3E87FC771B1E623000B07A78 /* README.md */,
3157CF941B0FCFE30012CA05 /* PhotoProgress */,
3157CF931B0FCFE30012CA05 /* Products */,
);
sourceTree = "<group>";
};
3157CF931B0FCFE30012CA05 /* Products */ = {
isa = PBXGroup;
children = (
3157CF921B0FCFE30012CA05 /* PhotoProgress.app */,
);
name = Products;
sourceTree = "<group>";
};
3157CF941B0FCFE30012CA05 /* PhotoProgress */ = {
isa = PBXGroup;
children = (
3157CF951B0FCFE30012CA05 /* AppDelegate.swift */,
BFED0B701B167049006F80C3 /* View Controllers */,
BFED0B6D1B166F73006F80C3 /* Album */,
3157CF991B0FCFE30012CA05 /* Main.storyboard */,
BF1317411B1657BA005E86BE /* Photos */,
3157CF9C1B0FCFE30012CA05 /* Images.xcassets */,
3157CF9E1B0FCFE30012CA05 /* LaunchScreen.storyboard */,
3157CFA11B0FCFE30012CA05 /* Info.plist */,
);
path = PhotoProgress;
sourceTree = "<group>";
};
BFED0B6C1B166F56006F80C3 /* Operations */ = {
isa = PBXGroup;
children = (
312329631B0FF89800C3BE18 /* PhotoImport.swift */,
312329611B0FF89800C3BE18 /* PhotoDownload.swift */,
312329621B0FF89800C3BE18 /* PhotoFilter.swift */,
);
name = Operations;
sourceTree = "<group>";
};
BFED0B6D1B166F73006F80C3 /* Album */ = {
isa = PBXGroup;
children = (
312329601B0FF89800C3BE18 /* Album.swift */,
BFED0B6E1B166F8B006F80C3 /* Photo.swift */,
BFED0B6C1B166F56006F80C3 /* Operations */,
);
name = Album;
sourceTree = "<group>";
};
BFED0B701B167049006F80C3 /* View Controllers */ = {
isa = PBXGroup;
children = (
BF1334561B16895900A1ABE7 /* PhotosViewController.swift */,
3157CF971B0FCFE30012CA05 /* PhotosCollectionViewController.swift */,
314AEFFC1B1006CE00282B5A /* PhotoCollectionViewCell.swift */,
);
name = "View Controllers";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
3157CF911B0FCFE30012CA05 /* PhotoProgress */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3157CFAF1B0FCFE30012CA05 /* Build configuration list for PBXNativeTarget "PhotoProgress" */;
buildPhases = (
3157CF8E1B0FCFE30012CA05 /* Sources */,
3157CF8F1B0FCFE30012CA05 /* Frameworks */,
3157CF901B0FCFE30012CA05 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = PhotoProgress;
productName = PhotoProgress;
productReference = 3157CF921B0FCFE30012CA05 /* PhotoProgress.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
3157CF8A1B0FCFE30012CA05 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0700;
ORGANIZATIONNAME = "Apple, Inc.";
TargetAttributes = {
3157CF911B0FCFE30012CA05 = {
CreatedOnToolsVersion = 7.0;
};
};
};
buildConfigurationList = 3157CF8D1B0FCFE30012CA05 /* Build configuration list for PBXProject "PhotoProgress" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 3157CF891B0FCFE30012CA05;
productRefGroup = 3157CF931B0FCFE30012CA05 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
3157CF911B0FCFE30012CA05 /* PhotoProgress */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
3157CF901B0FCFE30012CA05 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3157CFA01B0FCFE30012CA05 /* LaunchScreen.storyboard in Resources */,
3157CF9B1B0FCFE30012CA05 /* Main.storyboard in Resources */,
3157CF9D1B0FCFE30012CA05 /* Images.xcassets in Resources */,
BF1317431B1657BA005E86BE /* Photos in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
3157CF8E1B0FCFE30012CA05 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
314AEFFD1B1006CE00282B5A /* PhotoCollectionViewCell.swift in Sources */,
BFED0B6F1B166F8B006F80C3 /* Photo.swift in Sources */,
BF1334571B16895900A1ABE7 /* PhotosViewController.swift in Sources */,
312329651B0FF89800C3BE18 /* PhotoDownload.swift in Sources */,
312329661B0FF89800C3BE18 /* PhotoFilter.swift in Sources */,
3157CF981B0FCFE30012CA05 /* PhotosCollectionViewController.swift in Sources */,
312329671B0FF89800C3BE18 /* PhotoImport.swift in Sources */,
312329641B0FF89800C3BE18 /* Album.swift in Sources */,
3157CF961B0FCFE30012CA05 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
3157CF991B0FCFE30012CA05 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
3157CF9A1B0FCFE30012CA05 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
3157CF9E1B0FCFE30012CA05 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
3157CF9F1B0FCFE30012CA05 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
3157CFAD1B0FCFE30012CA05 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
3157CFAE1B0FCFE30012CA05 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
3157CFB01B0FCFE30012CA05 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = PhotoProgress/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_NAME = "$(TARGET_NAME)";
TOOLCHAINS = default;
};
name = Debug;
};
3157CFB11B0FCFE30012CA05 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
INFOPLIST_FILE = PhotoProgress/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_NAME = "$(TARGET_NAME)";
TOOLCHAINS = default;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
3157CF8D1B0FCFE30012CA05 /* Build configuration list for PBXProject "PhotoProgress" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3157CFAD1B0FCFE30012CA05 /* Debug */,
3157CFAE1B0FCFE30012CA05 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
3157CFAF1B0FCFE30012CA05 /* Build configuration list for PBXNativeTarget "PhotoProgress" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3157CFB01B0FCFE30012CA05 /* Debug */,
3157CFB11B0FCFE30012CA05 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 3157CF8A1B0FCFE30012CA05 /* Project object */;
}

View File

@ -0,0 +1,45 @@
/*
    Copyright (C) 2015 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this samples licensing information
    
    Abstract:
                Album has an array of Photos loaded from the application bundle
            
*/
import UIKit
class Album: NSObject {
// MARK: Properties
let photos: [Photo]
// MARK: Initializers
override init () {
guard let imageURLs = NSBundle.mainBundle().URLsForResourcesWithExtension("jpg", subdirectory: "Photos") else {
fatalError("Unable to load photos")
}
photos = imageURLs.map { Photo(URL: $0) }
}
func importPhotos() -> NSProgress {
let progress = NSProgress()
progress.totalUnitCount = Int64(photos.count)
for photo in photos {
let importProgress = photo.startImport()
progress.addChild(importProgress, withPendingUnitCount: 1)
}
return progress
}
func resetPhotos() {
for photo in photos {
photo.reset()
}
}
}

View File

@ -0,0 +1,14 @@
/*
    Copyright (C) 2015 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this samples licensing information
*/
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: Properties
var window: UIWindow?
}

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8121.17" systemVersion="15A178r" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
<viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<animations/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,238 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8121.17" systemVersion="15A178r" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="eT1-6D-OLs">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
</dependencies>
<scenes>
<!--Photos View Controller-->
<scene sceneID="uEh-Sm-wmz">
<objects>
<viewController id="Mso-Eu-4PO" customClass="PhotosViewController" customModule="PhotoProgress" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="1E7-QS-gAv"/>
<viewControllerLayoutGuide type="bottom" id="TPT-7M-4PO"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="5Ef-xO-KnR">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ZJc-RL-hFe">
<rect key="frame" x="0.0" y="0.0" width="600" height="580"/>
<animations/>
<connections>
<segue destination="s5Z-Tx-3S8" kind="embed" identifier="collectionView" id="gb1-9L-Jua"/>
</connections>
</containerView>
<view alpha="0.0" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Hpw-j6-tjM" userLabel="Progress Container View">
<rect key="frame" x="0.0" y="514" width="600" height="42"/>
<subviews>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Jgt-Ay-v4f">
<rect key="frame" x="8" y="20" width="584" height="2"/>
<animations/>
<constraints>
<constraint firstAttribute="height" constant="10" id="2hX-cf-4yu"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="2hX-cf-4yu"/>
</mask>
</variation>
</progressView>
</subviews>
<animations/>
<color key="backgroundColor" white="1" alpha="0.77659574468085102" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="Jgt-Ay-v4f" secondAttribute="trailing" id="06v-px-EeN"/>
<constraint firstItem="Jgt-Ay-v4f" firstAttribute="leading" secondItem="Hpw-j6-tjM" secondAttribute="leadingMargin" id="63Z-dZ-4Dy"/>
<constraint firstAttribute="height" constant="50" id="HG2-IC-CAn"/>
<constraint firstAttribute="bottom" secondItem="Jgt-Ay-v4f" secondAttribute="bottom" constant="20" id="jbM-BD-pmj"/>
<constraint firstItem="Jgt-Ay-v4f" firstAttribute="top" secondItem="Hpw-j6-tjM" secondAttribute="top" constant="20" id="tAp-Mf-sVp"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="HG2-IC-CAn"/>
</mask>
</variation>
</view>
</subviews>
<animations/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="ZJc-RL-hFe" secondAttribute="trailing" id="Anf-Z0-bwC"/>
<constraint firstItem="TPT-7M-4PO" firstAttribute="top" secondItem="Hpw-j6-tjM" secondAttribute="bottom" id="F3D-xE-uM8"/>
<constraint firstItem="ZJc-RL-hFe" firstAttribute="trailing" secondItem="5Ef-xO-KnR" secondAttribute="trailingMargin" id="GR6-Da-Lzb"/>
<constraint firstAttribute="trailing" secondItem="Hpw-j6-tjM" secondAttribute="trailing" id="H07-uv-yNa"/>
<constraint firstItem="Hpw-j6-tjM" firstAttribute="leading" secondItem="5Ef-xO-KnR" secondAttribute="leading" id="JWx-DC-hU8"/>
<constraint firstAttribute="bottom" secondItem="ZJc-RL-hFe" secondAttribute="bottom" constant="20" symbolic="YES" id="Mn1-CE-ySb"/>
<constraint firstAttribute="trailing" secondItem="ZJc-RL-hFe" secondAttribute="trailing" id="R1z-2w-3Pg"/>
<constraint firstItem="ZJc-RL-hFe" firstAttribute="top" secondItem="5Ef-xO-KnR" secondAttribute="top" id="XeR-j8-vfr"/>
<constraint firstItem="ZJc-RL-hFe" firstAttribute="top" secondItem="5Ef-xO-KnR" secondAttribute="top" id="c5Z-IW-okn"/>
<constraint firstItem="ZJc-RL-hFe" firstAttribute="top" secondItem="5Ef-xO-KnR" secondAttribute="topMargin" id="edf-7x-KJf"/>
<constraint firstItem="ZJc-RL-hFe" firstAttribute="leading" secondItem="5Ef-xO-KnR" secondAttribute="leading" id="ek9-Xn-uJr"/>
<constraint firstItem="ZJc-RL-hFe" firstAttribute="leading" secondItem="5Ef-xO-KnR" secondAttribute="leading" id="gST-zx-FxO"/>
<constraint firstItem="ZJc-RL-hFe" firstAttribute="leading" secondItem="5Ef-xO-KnR" secondAttribute="leadingMargin" id="xP4-Nx-MAB"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="Anf-Z0-bwC"/>
<exclude reference="GR6-Da-Lzb"/>
<exclude reference="XeR-j8-vfr"/>
<exclude reference="edf-7x-KJf"/>
<exclude reference="gST-zx-FxO"/>
<exclude reference="xP4-Nx-MAB"/>
</mask>
</variation>
</view>
<toolbarItems>
<barButtonItem title="Import" id="mfo-aK-2ws" userLabel="Import Toolbar Item">
<connections>
<action selector="startImport" destination="Mso-Eu-4PO" id="Ai3-dZ-RFQ"/>
</connections>
</barButtonItem>
</toolbarItems>
<navigationItem key="navigationItem" id="vUJ-G8-plS"/>
<connections>
<outlet property="cancelToolbarItem" destination="Ab3-Xy-4uA" id="I0a-C8-Qcb"/>
<outlet property="pauseToolbarItem" destination="HKr-E4-lS4" id="AX1-jr-iMN"/>
<outlet property="progressContainerView" destination="Hpw-j6-tjM" id="LQx-AW-BjO"/>
<outlet property="progressView" destination="Jgt-Ay-v4f" id="lrf-h9-CdP"/>
<outlet property="resetToolbarItem" destination="SMY-2N-Q5Y" id="h2F-zh-y6y"/>
<outlet property="resumeToolbarItem" destination="Bmp-kJ-hNu" id="2rg-Hp-DvT"/>
<outlet property="startToolbarItem" destination="mfo-aK-2ws" id="FKx-RJ-dhX"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="eeK-ba-Jld" userLabel="First Responder" sceneMemberID="firstResponder"/>
<barButtonItem title="Cancel" id="Ab3-Xy-4uA">
<connections>
<action selector="cancelImport" destination="Mso-Eu-4PO" id="hAC-Gc-9lR"/>
</connections>
</barButtonItem>
<barButtonItem title="Pause" id="HKr-E4-lS4">
<connections>
<action selector="pauseImport" destination="Mso-Eu-4PO" id="dLC-Oa-4bx"/>
</connections>
</barButtonItem>
<barButtonItem title="Resume" id="Bmp-kJ-hNu">
<connections>
<action selector="resumeImport" destination="Mso-Eu-4PO" id="LEM-pq-2rm"/>
</connections>
</barButtonItem>
<barButtonItem title="Reset" id="SMY-2N-Q5Y" userLabel="Reset Toolbar Item">
<connections>
<action selector="resetImport" destination="Mso-Eu-4PO" id="QEj-lr-Xqx"/>
</connections>
</barButtonItem>
</objects>
<point key="canvasLocation" x="1248" y="184"/>
</scene>
<!--Photos Collection View Controller-->
<scene sceneID="HLW-Tb-rNQ">
<objects>
<collectionViewController id="s5Z-Tx-3S8" customClass="PhotosCollectionViewController" customModule="PhotoProgress" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="Cs4-xi-Dza">
<rect key="frame" x="0.0" y="0.0" width="600" height="580"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<animations/>
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="calibratedRGB"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="FMq-YW-MYK">
<size key="itemSize" width="300" height="300"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Photo" id="j9d-rb-zxM" customClass="PhotoCollectionViewCell" customModule="PhotoProgress" customModuleProvider="target">
<rect key="frame" x="150" y="0.0" width="300" height="300"/>
<autoresizingMask key="autoresizingMask"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="300" height="300"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" translatesAutoresizingMaskIntoConstraints="NO" id="ALQ-RY-TSP">
<rect key="frame" x="8" y="8" width="284" height="274"/>
<animations/>
</imageView>
<progressView hidden="YES" opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aFy-vJ-c96">
<rect key="frame" x="8" y="290" width="284" height="2"/>
<animations/>
<constraints>
<constraint firstAttribute="height" constant="10" id="7sQ-ZD-hiN"/>
<constraint firstAttribute="height" constant="2" id="miR-J6-Zof"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="7sQ-ZD-hiN"/>
</mask>
</variation>
</progressView>
</subviews>
<animations/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
</view>
<animations/>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="ALQ-RY-TSP" secondAttribute="trailing" id="3Jk-2U-KEC"/>
<constraint firstAttribute="bottomMargin" secondItem="ALQ-RY-TSP" secondAttribute="bottom" constant="8" id="9u5-mU-FQu"/>
<constraint firstItem="aFy-vJ-c96" firstAttribute="top" secondItem="ALQ-RY-TSP" secondAttribute="bottom" constant="8" symbolic="YES" id="AgT-PE-RAD"/>
<constraint firstItem="aFy-vJ-c96" firstAttribute="leading" secondItem="j9d-rb-zxM" secondAttribute="leadingMargin" id="Mo9-PS-he8"/>
<constraint firstItem="ALQ-RY-TSP" firstAttribute="top" secondItem="j9d-rb-zxM" secondAttribute="topMargin" id="MuU-bI-eNQ"/>
<constraint firstItem="ALQ-RY-TSP" firstAttribute="leading" secondItem="j9d-rb-zxM" secondAttribute="leadingMargin" id="QkX-MO-Cqk"/>
<constraint firstItem="aFy-vJ-c96" firstAttribute="bottom" secondItem="j9d-rb-zxM" secondAttribute="bottomMargin" id="Uly-TV-NY5"/>
<constraint firstAttribute="trailingMargin" secondItem="aFy-vJ-c96" secondAttribute="trailing" id="jh2-Cp-hMN"/>
<constraint firstAttribute="bottomMargin" secondItem="aFy-vJ-c96" secondAttribute="bottom" id="ycJ-OY-NQ1"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="9u5-mU-FQu"/>
<exclude reference="ycJ-OY-NQ1"/>
</mask>
</variation>
<connections>
<outlet property="imageView" destination="ALQ-RY-TSP" id="aFT-2a-amY"/>
<outlet property="progressView" destination="aFy-vJ-c96" id="7IU-Af-fBh"/>
</connections>
</collectionViewCell>
</cells>
<connections>
<outlet property="dataSource" destination="s5Z-Tx-3S8" id="8LD-lB-cJh"/>
<outlet property="delegate" destination="s5Z-Tx-3S8" id="dKK-9z-6UK"/>
</connections>
</collectionView>
<toolbarItems/>
<navigationItem key="navigationItem" id="QjC-7y-Rne"/>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="collectionView" destination="Cs4-xi-Dza" id="0eB-76-G7I"/>
</connections>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="kxf-aO-E8S" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1980" y="183.5"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="dDd-lK-VlZ">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" navigationBarHidden="YES" toolbarHidden="NO" id="eT1-6D-OLs" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="j4N-h3-5Xi">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<animations/>
</navigationBar>
<nil name="viewControllers"/>
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="5Og-7f-jeE">
<rect key="frame" x="0.0" y="556" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<animations/>
</toolbar>
<connections>
<segue destination="Mso-Eu-4PO" kind="relationship" relationship="rootViewController" id="j9T-my-S0p"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="7rk-35-Omp" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="435" y="184"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,68 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

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

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "PhotoPlaceholder.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>com.example.apple-sampleocode.PhotoProgress</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,65 @@
/*
    Copyright (C) 2015 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this samples licensing information
    
    Abstract:
                Photo represents an image that can be imported.
            
*/
import UIKit
class Photo: NSObject {
// MARK: Properties
let imageURL: NSURL
/// Marked "dynamic" so it is KVO observable.
dynamic var image: UIImage?
/// The photoImport is KVO observable for its progress.
dynamic var photoImport: PhotoImport?
// MARK: Initializers
init(URL: NSURL) {
imageURL = URL.copy() as! NSURL
image = UIImage(named: "PhotoPlaceholder")
}
/// Kick off the import
func startImport() -> NSProgress {
let newImport = PhotoImport(URL: imageURL)
newImport.completionHandler = { image, error in
if let image = image {
// The import is finished. Set our image to the result
self.image = image
}
else {
self.reportError(error!)
}
self.photoImport = nil
}
newImport.start()
photoImport = newImport
return newImport.progress
}
private func reportError(error: NSError) {
if error.domain != NSCocoaErrorDomain || error.code != NSUserCancelledError {
print("Error importing photo: \(error.localizedDescription)")
}
}
/// Go back to the initial state.
func reset() {
image = UIImage(named: "PhotoPlaceholder")
photoImport = nil
}
}

View File

@ -0,0 +1,79 @@
/*
    Copyright (C) 2015 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this samples licensing information
    
    Abstract:
                PhotoCollectionViewCell is a UICollectionViewCell subclass that shows a Photo.
            
*/
import UIKit
/// The KVO context used for all `PhotoCollectionViewCell` instances.
private var photoCollectionViewCellObservationContext = 0
class PhotoCollectionViewCell: UICollectionViewCell {
// MARK: Properties
@IBOutlet var imageView: UIImageView!
@IBOutlet var progressView: UIProgressView!
private let fractionCompletedKeyPath = "photoImport.progress.fractionCompleted"
private let imageKeyPath = "image"
var photo: Photo? {
willSet {
if let formerPhoto = photo {
formerPhoto.removeObserver(self, forKeyPath: fractionCompletedKeyPath, context: &photoCollectionViewCellObservationContext)
formerPhoto.removeObserver(self, forKeyPath: imageKeyPath, context: &photoCollectionViewCellObservationContext)
}
}
didSet {
if let newPhoto = photo {
newPhoto.addObserver(self, forKeyPath: fractionCompletedKeyPath, options: [], context: &photoCollectionViewCellObservationContext)
newPhoto.addObserver(self, forKeyPath: imageKeyPath, options: [], context: &photoCollectionViewCellObservationContext)
}
updateImageView()
updateProgressView()
}
}
private func updateProgressView() {
if let photoImport = photo?.photoImport {
let fraction = Float(photoImport.progress.fractionCompleted)
progressView.progress = fraction
progressView.hidden = false
}
else {
progressView.hidden = true
}
}
private func updateImageView() {
UIView.transitionWithView(imageView, duration: 0.5, options: .TransitionCrossDissolve, animations: {
self.imageView.image = self.photo?.image
}, completion: nil)
}
// MARK: Key-Value Observing
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject: AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard context == &photoCollectionViewCellObservationContext else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
return
}
NSOperationQueue.mainQueue().addOperationWithBlock {
if keyPath == self.fractionCompletedKeyPath {
self.updateProgressView()
}
else if keyPath == self.imageKeyPath {
self.updateImageView()
}
}
}
}

View File

@ -0,0 +1,234 @@
/*
    Copyright (C) 2015 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this samples licensing information
    
    Abstract:
                PhotoDownload supports NSProgressReporting and "downloads" a file URL.
            
*/
import Foundation
class PhotoDownload: NSObject, NSProgressReporting {
// MARK: Properties
/// The URL to be downloaded.
let downloadURL: NSURL
/**
The completionHandler is called once the download is finished with either
the downloaded data, or an `NSError`.
*/
var completionHandler: ((data: NSData?, error: NSError?) -> Void)?
let progress: NSProgress
/// A class containing the fake parts of our download.
private class DownloadState {
/// The dispatch queue that all of our callbacks will be invoked on.
var queue: dispatch_queue_t!
/// The timer that drives the "download".
var downloadTimer: dispatch_source_t?
/// The error that our didFail callback should be called with.
var downloadError: NSError?
/// Whether or not we're paused.
var isPaused = false
}
private var downloadState: DownloadState
// MARK: Initializers
init(URL: NSURL) {
downloadURL = URL.copy() as! NSURL
downloadState = DownloadState()
progress = NSProgress()
/*
The progress starts out as indeterminate, since we don't know how many
bytes there are to download yet.
*/
progress.totalUnitCount = -1
/*
Since our units are bytes, we use NSProgressKindFile so the NSProgress's
localizedDescription and localizedAdditionalDescription return
something nicer.
*/
progress.kind = NSProgressKindFile
// We say we're a file operation so the localized descriptions are a little nicer.
progress.setUserInfoObject(NSProgressFileOperationKindDownloading, forKey: NSProgressFileOperationKindKey)
}
/// Start the download. Can only be called once.
func start() {
assert(nil == downloadState.queue, "`downloadState.queue` must not be nil in \(__FUNCTION__).")
// Fake a download.
downloadState.queue = dispatch_queue_create("download queue", nil)
dispatch_async(downloadState.queue) {
do {
// Fetch the data
let data = try NSData(contentsOfURL: self.downloadURL, options: [])
// Our parameters for the "download".
// Update every 0.5 seconds.
let interval: Double = 0.5
// Bytes per second.
let throughput: Double = 5000
// Create a timer
let downloadTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.downloadState.queue)
self.downloadState.downloadTimer = downloadTimer
var downloadedBytes = 0
// Add a random delay to the start, to simulate latency.
let randomMilliseconds = Int64(arc4random_uniform(500))
let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(interval * Double(NSEC_PER_SEC)) - Int64(randomMilliseconds * Int64(NSEC_PER_MSEC)))
dispatch_source_set_timer(downloadTimer, delay, UInt64(interval * Double(NSEC_PER_SEC)), 0)
dispatch_source_set_event_handler(downloadTimer) {
// Update the downloaded bytes.
downloadedBytes += Int(throughput * interval)
if downloadedBytes >= data.length {
// We've finished!
dispatch_source_cancel(downloadTimer)
return
}
// Call out that we've "downloaded" new data.
self.didDownloadData(data, numberOfBytes: downloadedBytes)
}
dispatch_source_set_cancel_handler(downloadTimer) {
if downloadedBytes >= data.length {
// Call out that we finished "downloading" data.
self.didFinishDownload(data)
}
else {
// Call out that we finished "downloading" data.
self.didFailDownloadWithError(self.downloadState.downloadError!)
}
self.downloadState.downloadTimer = nil
}
// Call out that we will begin to "download" data.
self.willBeginDownload(data.length)
dispatch_resume(downloadTimer)
}
catch let error {
// Call out that we failed to "download" data.
self.didFailDownloadWithError(error as NSError)
}
}
}
private func failDownloadWithError(error: NSError) {
guard let downloadTimer = downloadState.downloadTimer else { return }
dispatch_async(downloadState.queue) {
/*
Set the downloadError, then cancel. The timer's cancellation handler
will invoke the fail callback with the error, if we haven't finished
by then.
*/
self.downloadState.downloadError = error
// Resume the timer before cancelling it.
if self.downloadState.isPaused {
dispatch_resume(downloadTimer)
}
dispatch_source_cancel(downloadTimer)
}
}
private func suspendDownload() {
if let downloadTimer = downloadState.downloadTimer {
dispatch_async(downloadState.queue) {
// Do not suspend if we're already suspended, or if we're cancelled.
guard !self.downloadState.isPaused && 0 == dispatch_source_testcancel(downloadTimer) else { return }
// Simply suspend the timer.
self.downloadState.isPaused = true
dispatch_suspend(downloadTimer)
}
}
}
private func resumeDownload() {
if let downloadTimer = downloadState.downloadTimer {
dispatch_async(downloadState.queue) {
// Only resume if we're suspended and we're not cancelled.
guard self.downloadState.isPaused && 0 == dispatch_source_testcancel(downloadTimer) else { return }
// Simply resume the timer.
dispatch_resume(downloadTimer)
self.downloadState.isPaused = false
}
}
}
private func callCompletionHandler(data data: NSData?, error: NSError?) {
// Call the completion handler if we have one.
completionHandler?(data:data, error: error)
// Break any retain cycles by setting it to nil.
completionHandler = nil
}
// Called when the "download" begins
func willBeginDownload(downloadLength: Int) {
progress.totalUnitCount = Int64(downloadLength)
progress.cancellable = true
progress.cancellationHandler = {
let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
self.failDownloadWithError(error)
}
progress.pausable = true
progress.pausingHandler = {
self.suspendDownload()
}
progress.resumingHandler = {
self.resumeDownload()
}
}
/**
Called periodically as the "download" occurs. data and numberOfBytes are
aggregated values, and contain the entire download up to that point.
*/
func didDownloadData(data: NSData, numberOfBytes: Int) {
progress.completedUnitCount = Int64(numberOfBytes)
}
/// Called when the "download" is completed.
func didFinishDownload(downloadedData: NSData) {
progress.completedUnitCount = Int64(downloadedData.length)
callCompletionHandler(data: downloadedData, error: nil)
}
/// Called if an error occurs during the "download"
func didFailDownloadWithError(error: NSError) {
callCompletionHandler(data: nil, error: error)
}
}

View File

@ -0,0 +1,47 @@
/*
    Copyright (C) 2015 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this samples licensing information
    
    Abstract:
                PhotoFilter has a class function for returning a filtered image.
            
*/
import UIKit
class PhotoFilter {
/// This supports implicit progress composition.
class func filteredImage(image: UIImage) -> UIImage? {
/*
Set up the progress. First thing! It's indeterminate since we don't
have any information about how long this is going to take.
`NSProgress(totalUnitCount:)` convenience adds itself as a child to the currentProgress.
*/
let progress = NSProgress(totalUnitCount: -1)
progress.cancellable = false
progress.pausable = false
var outputImage: UIImage? = nil
if let filter = CIFilter(name: "CIPhotoEffectTransfer"), cgImage = image.CGImage {
let ciImage = CIImage(CGImage: cgImage)
filter.setValue(ciImage, forKey: "inputImage")
let outputCIImage = filter.outputImage
let ciContext = CIContext(options: [:])
let outputCGImage = ciContext.createCGImage(outputCIImage, fromRect: outputCIImage.extent)
outputImage = UIImage(CGImage: outputCGImage)
}
// We have finished.
progress.completedUnitCount = 1
progress.totalUnitCount = 1
return outputImage
}
}

View File

@ -0,0 +1,66 @@
/*
    Copyright (C) 2015 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this samples licensing information
    
    Abstract:
                PhotoImport represents the import operation of a Photo. It combines both the PhotoDownload and PhotoFilter operations.
            
*/
import UIKit
class PhotoImport: NSObject, NSProgressReporting {
// MARK: Properties
var completionHandler: ((image: UIImage?, error: NSError?) -> Void)?
let progress: NSProgress
let download: PhotoDownload
// MARK: Initializers
init(URL: NSURL) {
progress = NSProgress()
/*
This progress's children are weighted: The download takes up 90%
and the filter takes the remaining portion.
*/
progress.totalUnitCount = 10
download = PhotoDownload(URL: URL)
}
func start() {
/*
Use explicit composition to add the download's progress to ours,
taking 9/10 units.
*/
progress.addChild(download.progress, withPendingUnitCount: 9)
download.completionHandler = { data, error in
guard let imageData = data, image = UIImage(data: imageData) else {
self.callCompletionHandler(image: nil, error: error)
return
}
/*
Make self.progress the currentProgress. Since the filteredImage
supports implicit progress reporting, it will add its progress
to ours.
*/
self.progress.becomeCurrentWithPendingUnitCount(1)
let filteredImage = PhotoFilter.filteredImage(image)
self.progress.resignCurrent()
self.callCompletionHandler(image: filteredImage, error: nil)
}
download.start()
}
private func callCompletionHandler(image image: UIImage?, error: NSError?) {
completionHandler?(image: image, error: error)
completionHandler = nil
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,38 @@
/*
    Copyright (C) 2015 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this samples licensing information
    
    Abstract:
                PhotosCollectionViewController is a UICollectionViewController subclass that has a reference to the Album.
            
*/
import UIKit
class PhotosCollectionViewController: UICollectionViewController {
// MARK: Properties
var album: Album? {
didSet {
collectionView?.reloadData()
}
}
// MARK: UICollectionViewController
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return album?.photos.count ?? 0
}
/**
The cell that is returned must be retrieved from a call to
`dequeueReusableCellWithReuseIdentifier(_:forIndexPath:)`.
*/
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Photo", forIndexPath: indexPath) as! PhotoCollectionViewCell
cell.photo = album?.photos[indexPath.row]
return cell
}
}

View File

@ -0,0 +1,178 @@
/*
    Copyright (C) 2015 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this samples licensing information
    
    Abstract:
                PhotosViewController is our root UIViewController which is responsible for showing the overall progress and a tool bar.
            
*/
import UIKit
/**
The KVO context used for all `PhotosViewController` instances. This provides a stable
address to use as the context parameter for the KVO observation methods.
*/
private var photosViewControllerObservationContext = 0
class PhotosViewController: UIViewController {
// MARK: Properties
/// The album that the app is importing
private var album: Album?
/// Keys that we observe on `overallProgress`.
private let overalProgressObservedKeys = [
"fractionCompleted",
"completedUnitCount",
"totalUnitCount",
"cancelled",
"paused"
]
/// The overall progress for the import that is shown to the user
private var overallProgress: NSProgress? {
willSet {
guard let formerProgress = overallProgress else { return }
for overalProgressObservedKey in overalProgressObservedKeys {
formerProgress.removeObserver(self, forKeyPath: overalProgressObservedKey, context: &photosViewControllerObservationContext)
}
}
didSet {
if let newProgress = overallProgress {
for overalProgressObservedKey in overalProgressObservedKeys {
newProgress.addObserver(self, forKeyPath: overalProgressObservedKey, options: [], context: &photosViewControllerObservationContext)
}
}
updateProgressView()
updateToolbar()
}
}
private var progressViewIsHidden = true
private var overallProgressIsFinished: Bool {
let completed = overallProgress!.completedUnitCount
let total = overallProgress!.totalUnitCount
// An NSProgress is finished if it's not indeterminate, and the completedUnitCount > totalUnitCount.
return (completed >= total && total > 0 && completed > 0) || (completed > 0 && total == 0)
}
// MARK: IBOutlets
@IBOutlet var progressView: UIProgressView!
@IBOutlet var progressContainerView: UIView!
@IBOutlet var startToolbarItem: UIBarButtonItem!
@IBOutlet var cancelToolbarItem: UIBarButtonItem!
@IBOutlet var pauseToolbarItem: UIBarButtonItem!
@IBOutlet var resumeToolbarItem: UIBarButtonItem!
@IBOutlet var resetToolbarItem: UIBarButtonItem!
// MARK: IBActions
@IBAction func startImport() {
overallProgress = album?.importPhotos()
}
@IBAction func cancelImport() {
overallProgress?.cancel()
}
@IBAction func pauseImport() {
overallProgress?.pause()
}
@IBAction func resumeImport() {
overallProgress?.resume()
}
@IBAction func resetImport() {
album?.resetPhotos()
overallProgress = nil
}
// MARK: Key-Value Observing
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [NSObject : AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard context == &photosViewControllerObservationContext else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
return
}
NSOperationQueue.mainQueue().addOperationWithBlock {
self.updateProgressView()
self.updateToolbar()
}
}
// MARK: Update UI
private func updateProgressView() {
let shouldHide: Bool
if let overallProgress = self.overallProgress {
shouldHide = overallProgressIsFinished || overallProgress.cancelled
progressView.progress = Float(overallProgress.fractionCompleted)
}
else {
shouldHide = true
}
if progressViewIsHidden != shouldHide {
UIView.animateWithDuration(0.2) {
self.progressContainerView.alpha = shouldHide ? 0.0 : 1.0
}
progressViewIsHidden = shouldHide
}
}
private func updateToolbar() {
var items = [UIBarButtonItem]()
if let overallProgress = overallProgress {
if overallProgressIsFinished || overallProgress.cancelled {
items.append(resetToolbarItem)
}
else {
// The import is running.
items.append(cancelToolbarItem)
if overallProgress.paused {
items.append(resumeToolbarItem)
}
else {
items.append(pauseToolbarItem)
}
}
}
else {
items.append(startToolbarItem)
}
navigationController?.toolbar?.setItems(items, animated: true)
}
// MARK: Nib Loading
override func awakeFromNib() {
album = Album()
}
// MARK: Segue Handling
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "collectionView" {
if let collectionViewController = segue.destinationViewController as? PhotosCollectionViewController {
collectionViewController.album = album
}
}
}
}

33
PhotoProgress/README.md Normal file
View File

@ -0,0 +1,33 @@
# PhotoProgress: Using NSProgress
This sample demonstrates how to create and compose NSProgress objects, and show their progress in your app.
The app presents a collection view of photos, which will initially be placeholder images. Tap the "Import" button to import the album of photos, showing progress for each individual import as well as an overall progress. The import operation for each photo is composed of a faked "download" step followed by a filter step. Once the import of a photo finishes, the final image is displayed in the collection view cell.
1) Album: Model object that represents an array of Photos. Loads Photos from the mainBundle's resources.
2) Photo: Model object that represents an image that can be imported. Has a method startImport which creates a PhotoImport.
3) PhotoImport: Single use object that composes the PhotoDownload and PhotoFilter operations. Conforms to NSProgressReporting.
4) PhotoDownload: Single use object that fakes a download of a fileURL. Conforms to NSProgressReporting.
5) PhotoFilter: Has a single class method, filteredImage, which synchronously runs a filter using CoreImage on a UIImage. Supports implicit progress reporting.
6) PhotosViewController: Our root view controller. Observes the overall progress using key-value observing and updates a UIProgressView. Handles interactions with the tool bar buttons.
7) PhotosCollectionViewController: Our UICollectionViewController. Has an Album which it uses in the data source methods.
8) PhotoCollectionViewCell: A UICollectionViewCell subclass that shows an individual Photo as well as progress for the import of that photo. Uses KVO to observe the progress of the import, as well as the image for the photo.
## Requirements
### Build
Xcode 7.0, iOS 9.0 SDK
### Runtime
iOS 9.0
Copyright (C) 2015 Apple Inc. All rights reserved.