diff --git a/AVCam/LICENSE.txt b/AVCam/LICENSE.txt new file mode 100644 index 00000000..1cad6491 --- /dev/null +++ b/AVCam/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: AVCam-iOS: Using AVFoundation to Capture Images and Movies +Version: 6.1 + +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) 2016 Apple Inc. All Rights Reserved. diff --git a/AVCam/Objective-C/AVCam Objective-C.xcodeproj/project.pbxproj b/AVCam/Objective-C/AVCam Objective-C.xcodeproj/project.pbxproj new file mode 100644 index 00000000..fbf0d5a8 --- /dev/null +++ b/AVCam/Objective-C/AVCam Objective-C.xcodeproj/project.pbxproj @@ -0,0 +1,310 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 2206265F1A1E330400A45150 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 2206265E1A1E330400A45150 /* main.m */; }; + 220626881A1E345E00A45150 /* AVCamAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 220626831A1E345E00A45150 /* AVCamAppDelegate.m */; }; + 220626891A1E345E00A45150 /* AVCamPreviewView.m in Sources */ = {isa = PBXBuildFile; fileRef = 220626851A1E345E00A45150 /* AVCamPreviewView.m */; }; + 2206268A1A1E345E00A45150 /* AVCamCameraViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 220626871A1E345E00A45150 /* AVCamCameraViewController.m */; }; + 22CA31B81B022D1300D2DE70 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 22CA31B61B022D1300D2DE70 /* LaunchScreen.storyboard */; }; + 7A74447A1CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A7444791CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.m */; }; + 7A74447C1CEE6B4B00C70C83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A74447B1CEE6B4B00C70C83 /* Assets.xcassets */; }; + 7A74447E1CEE6B5900C70C83 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7A74447D1CEE6B5900C70C83 /* Main.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 220626591A1E330400A45150 /* AVCam.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AVCam.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2206265D1A1E330400A45150 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 2206265E1A1E330400A45150 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 220626821A1E345E00A45150 /* AVCamAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AVCamAppDelegate.h; sourceTree = ""; }; + 220626831A1E345E00A45150 /* AVCamAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AVCamAppDelegate.m; sourceTree = ""; }; + 220626841A1E345E00A45150 /* AVCamPreviewView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AVCamPreviewView.h; sourceTree = ""; }; + 220626851A1E345E00A45150 /* AVCamPreviewView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AVCamPreviewView.m; sourceTree = ""; }; + 220626861A1E345E00A45150 /* AVCamCameraViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AVCamCameraViewController.h; sourceTree = ""; }; + 220626871A1E345E00A45150 /* AVCamCameraViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AVCamCameraViewController.m; sourceTree = ""; }; + 22CA31B71B022D1300D2DE70 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = AVCam/Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 22CA31B91B0250C300D2DE70 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; + 7A7444781CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AVCamPhotoCaptureDelegate.h; sourceTree = ""; }; + 7A7444791CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AVCamPhotoCaptureDelegate.m; sourceTree = ""; }; + 7A74447B1CEE6B4B00C70C83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 7A74447D1CEE6B5900C70C83 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 220626561A1E330400A45150 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 220626501A1E330400A45150 = { + isa = PBXGroup; + children = ( + 22CA31B91B0250C300D2DE70 /* README.md */, + 2206265B1A1E330400A45150 /* AVCam */, + 2206265A1A1E330400A45150 /* Products */, + ); + sourceTree = ""; + }; + 2206265A1A1E330400A45150 /* Products */ = { + isa = PBXGroup; + children = ( + 220626591A1E330400A45150 /* AVCam.app */, + ); + name = Products; + sourceTree = ""; + }; + 2206265B1A1E330400A45150 /* AVCam */ = { + isa = PBXGroup; + children = ( + 220626821A1E345E00A45150 /* AVCamAppDelegate.h */, + 220626831A1E345E00A45150 /* AVCamAppDelegate.m */, + 220626841A1E345E00A45150 /* AVCamPreviewView.h */, + 220626851A1E345E00A45150 /* AVCamPreviewView.m */, + 220626861A1E345E00A45150 /* AVCamCameraViewController.h */, + 220626871A1E345E00A45150 /* AVCamCameraViewController.m */, + 7A7444781CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.h */, + 7A7444791CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.m */, + 7A74447D1CEE6B5900C70C83 /* Main.storyboard */, + 7A74447B1CEE6B4B00C70C83 /* Assets.xcassets */, + 22CA31B61B022D1300D2DE70 /* LaunchScreen.storyboard */, + 2206265D1A1E330400A45150 /* Info.plist */, + 2206265E1A1E330400A45150 /* main.m */, + ); + path = AVCam; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 220626581A1E330400A45150 /* AVCam */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2206267C1A1E330400A45150 /* Build configuration list for PBXNativeTarget "AVCam" */; + buildPhases = ( + 220626551A1E330400A45150 /* Sources */, + 220626561A1E330400A45150 /* Frameworks */, + 220626571A1E330400A45150 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AVCam; + productName = AVCam; + productReference = 220626591A1E330400A45150 /* AVCam.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 220626511A1E330400A45150 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + TargetAttributes = { + 220626581A1E330400A45150 = { + CreatedOnToolsVersion = 6.1; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 220626541A1E330400A45150 /* Build configuration list for PBXProject "AVCam Objective-C" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 220626501A1E330400A45150; + productRefGroup = 2206265A1A1E330400A45150 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 220626581A1E330400A45150 /* AVCam */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 220626571A1E330400A45150 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7A74447E1CEE6B5900C70C83 /* Main.storyboard in Resources */, + 7A74447C1CEE6B4B00C70C83 /* Assets.xcassets in Resources */, + 22CA31B81B022D1300D2DE70 /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 220626551A1E330400A45150 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 220626881A1E345E00A45150 /* AVCamAppDelegate.m in Sources */, + 2206265F1A1E330400A45150 /* main.m in Sources */, + 220626891A1E345E00A45150 /* AVCamPreviewView.m in Sources */, + 2206268A1A1E345E00A45150 /* AVCamCameraViewController.m in Sources */, + 7A74447A1CEE6B0F00C70C83 /* AVCamPhotoCaptureDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 22CA31B61B022D1300D2DE70 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 22CA31B71B022D1300D2DE70 /* Base */, + ); + name = LaunchScreen.storyboard; + path = ..; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 2206267A1A1E330400A45150 /* 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; + 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_SYMBOLS_PRIVATE_EXTERN = NO; + 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 = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2206267B1A1E330400A45150 /* 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 = YES; + 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 = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 2206267D1A1E330400A45150 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = AVCam/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVCam"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 2206267E1A1E330400A45150 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = AVCam/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVCam"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 220626541A1E330400A45150 /* Build configuration list for PBXProject "AVCam Objective-C" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2206267A1A1E330400A45150 /* Debug */, + 2206267B1A1E330400A45150 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2206267C1A1E330400A45150 /* Build configuration list for PBXNativeTarget "AVCam" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2206267D1A1E330400A45150 /* Debug */, + 2206267E1A1E330400A45150 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 220626511A1E330400A45150 /* Project object */; +} diff --git a/AVCam/Objective-C/AVCam Objective-C.xcodeproj/xcshareddata/xcschemes/AVCam Objective-C.xcscheme b/AVCam/Objective-C/AVCam Objective-C.xcodeproj/xcshareddata/xcschemes/AVCam Objective-C.xcscheme new file mode 100644 index 00000000..582adda4 --- /dev/null +++ b/AVCam/Objective-C/AVCam Objective-C.xcodeproj/xcshareddata/xcschemes/AVCam Objective-C.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVCam/Objective-C/AVCam/AVCamAppDelegate.h b/AVCam/Objective-C/AVCam/AVCamAppDelegate.h new file mode 100644 index 00000000..14e25714 --- /dev/null +++ b/AVCam/Objective-C/AVCam/AVCamAppDelegate.h @@ -0,0 +1,15 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate. +*/ + +@import UIKit; + +@interface AVCamAppDelegate : UIResponder + +@property (nonatomic) UIWindow *window; + +@end diff --git a/AVCam/Objective-C/AVCam/AVCamAppDelegate.m b/AVCam/Objective-C/AVCam/AVCamAppDelegate.m new file mode 100644 index 00000000..701bccfb --- /dev/null +++ b/AVCam/Objective-C/AVCam/AVCamAppDelegate.m @@ -0,0 +1,13 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate. +*/ + +#import "AVCamAppDelegate.h" + +@implementation AVCamAppDelegate + +@end diff --git a/AVCam/Objective-C/AVCam/AVCamCameraViewController.h b/AVCam/Objective-C/AVCam/AVCamCameraViewController.h new file mode 100644 index 00000000..35c7dc0b --- /dev/null +++ b/AVCam/Objective-C/AVCam/AVCamCameraViewController.h @@ -0,0 +1,13 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller for camera interface. +*/ + +@import UIKit; + +@interface AVCamCameraViewController : UIViewController + +@end diff --git a/AVCam/Objective-C/AVCam/AVCamCameraViewController.m b/AVCam/Objective-C/AVCam/AVCamCameraViewController.m new file mode 100644 index 00000000..7d51edc1 --- /dev/null +++ b/AVCam/Objective-C/AVCam/AVCamCameraViewController.m @@ -0,0 +1,947 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller for camera interface. +*/ + +@import AVFoundation; +@import Photos; + +#import "AVCamCameraViewController.h" +#import "AVCamPreviewView.h" +#import "AVCamPhotoCaptureDelegate.h" + +static void * SessionRunningContext = &SessionRunningContext; + +typedef NS_ENUM( NSInteger, AVCamSetupResult ) { + AVCamSetupResultSuccess, + AVCamSetupResultCameraNotAuthorized, + AVCamSetupResultSessionConfigurationFailed +}; + +typedef NS_ENUM( NSInteger, AVCamCaptureMode ) { + AVCamCaptureModePhoto = 0, + AVCamCaptureModeMovie = 1 +}; + +typedef NS_ENUM( NSInteger, AVCamLivePhotoMode ) { + AVCamLivePhotoModeOn, + AVCamLivePhotoModeOff +}; + +@interface AVCaptureDeviceDiscoverySession (Utilities) + +- (NSInteger)uniqueDevicePositionsCount; + +@end + +@implementation AVCaptureDeviceDiscoverySession (Utilities) + +- (NSInteger)uniqueDevicePositionsCount +{ + NSMutableArray *uniqueDevicePositions = [NSMutableArray array]; + + for ( AVCaptureDevice *device in self.devices ) { + if ( ! [uniqueDevicePositions containsObject:@(device.position)] ) { + [uniqueDevicePositions addObject:@(device.position)]; + } + } + + return uniqueDevicePositions.count; +} + +@end + +@interface AVCamCameraViewController () + +// Session management. +@property (nonatomic, weak) IBOutlet AVCamPreviewView *previewView; +@property (nonatomic, weak) IBOutlet UISegmentedControl *captureModeControl; + +@property (nonatomic) AVCamSetupResult setupResult; +@property (nonatomic) dispatch_queue_t sessionQueue; +@property (nonatomic) AVCaptureSession *session; +@property (nonatomic, getter=isSessionRunning) BOOL sessionRunning; +@property (nonatomic) AVCaptureDeviceInput *videoDeviceInput; + +// Device configuration. +@property (nonatomic, weak) IBOutlet UIButton *cameraButton; +@property (nonatomic, weak) IBOutlet UILabel *cameraUnavailableLabel; +@property (nonatomic) AVCaptureDeviceDiscoverySession *videoDeviceDiscoverySession; + +// Capturing photos. +@property (nonatomic, weak) IBOutlet UIButton *photoButton; +@property (nonatomic, weak) IBOutlet UIButton *livePhotoModeButton; +@property (nonatomic) AVCamLivePhotoMode livePhotoMode; +@property (nonatomic, weak) IBOutlet UILabel *capturingLivePhotoLabel; + +@property (nonatomic) AVCapturePhotoOutput *photoOutput; +@property (nonatomic) NSMutableDictionary *inProgressPhotoCaptureDelegates; +@property (nonatomic) NSInteger inProgressLivePhotoCapturesCount; + +// Recording movies. +@property (nonatomic, weak) IBOutlet UIButton *recordButton; +@property (nonatomic, weak) IBOutlet UIButton *resumeButton; + +@property (nonatomic, strong) AVCaptureMovieFileOutput *movieFileOutput; +@property (nonatomic) UIBackgroundTaskIdentifier backgroundRecordingID; + +@end + +@implementation AVCamCameraViewController + +#pragma mark View Controller Life Cycle + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + // Disable UI. The UI is enabled if and only if the session starts running. + self.cameraButton.enabled = NO; + self.recordButton.enabled = NO; + self.photoButton.enabled = NO; + self.livePhotoModeButton.enabled = NO; + self.captureModeControl.enabled = NO; + + // Create the AVCaptureSession. + self.session = [[AVCaptureSession alloc] init]; + + // Create a device discovery session. + NSArray *deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera, AVCaptureDeviceTypeBuiltInDuoCamera]; + self.videoDeviceDiscoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionUnspecified]; + + // Set up the preview view. + self.previewView.session = self.session; + + // Communicate with the session and other session objects on this queue. + self.sessionQueue = dispatch_queue_create( "session queue", DISPATCH_QUEUE_SERIAL ); + + self.setupResult = AVCamSetupResultSuccess; + + /* + Check video authorization status. Video access is required and audio + access is optional. If audio access is denied, audio is not recorded + during movie recording. + */ + switch ( [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] ) + { + case AVAuthorizationStatusAuthorized: + { + // The user has previously granted access to the camera. + break; + } + case AVAuthorizationStatusNotDetermined: + { + /* + The user has not yet been presented with the option to grant + video access. We suspend the session queue to delay session + setup until the access request has completed. + + Note that audio access will be implicitly requested when we + create an AVCaptureDeviceInput for audio during session setup. + */ + dispatch_suspend( self.sessionQueue ); + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted ) { + if ( ! granted ) { + self.setupResult = AVCamSetupResultCameraNotAuthorized; + } + dispatch_resume( self.sessionQueue ); + }]; + break; + } + default: + { + // The user has previously denied access. + self.setupResult = AVCamSetupResultCameraNotAuthorized; + break; + } + } + + /* + Setup the capture session. + In general it is not safe to mutate an AVCaptureSession or any of its + inputs, outputs, or connections from multiple threads at the same time. + + Why not do all of this on the main queue? + Because -[AVCaptureSession startRunning] is a blocking call which can + take a long time. We dispatch session setup to the sessionQueue so + that the main queue isn't blocked, which keeps the UI responsive. + */ + dispatch_async( self.sessionQueue, ^{ + [self configureSession]; + } ); +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; + + dispatch_async( self.sessionQueue, ^{ + switch ( self.setupResult ) + { + case AVCamSetupResultSuccess: + { + // Only setup observers and start the session running if setup succeeded. + [self addObservers]; + [self.session startRunning]; + self.sessionRunning = self.session.isRunning; + break; + } + case AVCamSetupResultCameraNotAuthorized: + { + dispatch_async( dispatch_get_main_queue(), ^{ + NSString *message = NSLocalizedString( @"AVCam doesn't have permission to use the camera, please change privacy settings", @"Alert message when the user has denied access to the camera" ); + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"AVCam" message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString( @"OK", @"Alert OK button" ) style:UIAlertActionStyleCancel handler:nil]; + [alertController addAction:cancelAction]; + // Provide quick access to Settings. + UIAlertAction *settingsAction = [UIAlertAction actionWithTitle:NSLocalizedString( @"Settings", @"Alert button to open Settings" ) style:UIAlertActionStyleDefault handler:^( UIAlertAction *action ) { + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil]; + }]; + [alertController addAction:settingsAction]; + [self presentViewController:alertController animated:YES completion:nil]; + } ); + break; + } + case AVCamSetupResultSessionConfigurationFailed: + { + dispatch_async( dispatch_get_main_queue(), ^{ + NSString *message = NSLocalizedString( @"Unable to capture media", @"Alert message when something goes wrong during capture session configuration" ); + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"AVCam" message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString( @"OK", @"Alert OK button" ) style:UIAlertActionStyleCancel handler:nil]; + [alertController addAction:cancelAction]; + [self presentViewController:alertController animated:YES completion:nil]; + } ); + break; + } + } + } ); +} + +- (void)viewDidDisappear:(BOOL)animated +{ + dispatch_async( self.sessionQueue, ^{ + if ( self.setupResult == AVCamSetupResultSuccess ) { + [self.session stopRunning]; + [self removeObservers]; + } + } ); + + [super viewDidDisappear:animated]; +} + +- (BOOL)shouldAutorotate +{ + // Disable autorotation of the interface when recording is in progress. + return ! self.movieFileOutput.isRecording; +} + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations +{ + return UIInterfaceOrientationMaskAll; +} + +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator +{ + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + + UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation; + + if ( UIDeviceOrientationIsPortrait( deviceOrientation ) || UIDeviceOrientationIsLandscape( deviceOrientation ) ) { + self.previewView.videoPreviewLayer.connection.videoOrientation = (AVCaptureVideoOrientation)deviceOrientation; + } +} + +#pragma mark Session Management + +// Call this on the session queue. +- (void)configureSession +{ + if ( self.setupResult != AVCamSetupResultSuccess ) { + return; + } + + NSError *error = nil; + + [self.session beginConfiguration]; + + /* + We do not create an AVCaptureMovieFileOutput when setting up the session because the + AVCaptureMovieFileOutput does not support movie recording with AVCaptureSessionPresetPhoto. + */ + self.session.sessionPreset = AVCaptureSessionPresetPhoto; + + // Add video input. + + // Choose the back dual camera if available, otherwise default to a wide angle camera. + AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInDuoCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack]; + if ( ! videoDevice ) { + // If the back dual camera is not available, default to the back wide angle camera. + videoDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack]; + + // In some cases where users break their phones, the back wide angle camera is not available. In this case, we should default to the front wide angle camera. + if ( ! videoDevice ) { + videoDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront]; + } + } + AVCaptureDeviceInput *videoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error]; + if ( ! videoDeviceInput ) { + NSLog( @"Could not create video device input: %@", error ); + self.setupResult = AVCamSetupResultSessionConfigurationFailed; + [self.session commitConfiguration]; + return; + } + if ( [self.session canAddInput:videoDeviceInput] ) { + [self.session addInput:videoDeviceInput]; + self.videoDeviceInput = videoDeviceInput; + + dispatch_async( dispatch_get_main_queue(), ^{ + /* + Why are we dispatching this to the main queue? + Because AVCaptureVideoPreviewLayer is the backing layer for AVCamPreviewView and UIView + can only be manipulated on the main thread. + Note: As an exception to the above rule, it is not necessary to serialize video orientation changes + on the AVCaptureVideoPreviewLayer’s connection with other session manipulation. + + Use the status bar orientation as the initial video orientation. Subsequent orientation changes are + handled by -[AVCamCameraViewController viewWillTransitionToSize:withTransitionCoordinator:]. + */ + UIInterfaceOrientation statusBarOrientation = [UIApplication sharedApplication].statusBarOrientation; + AVCaptureVideoOrientation initialVideoOrientation = AVCaptureVideoOrientationPortrait; + if ( statusBarOrientation != UIInterfaceOrientationUnknown ) { + initialVideoOrientation = (AVCaptureVideoOrientation)statusBarOrientation; + } + + self.previewView.videoPreviewLayer.connection.videoOrientation = initialVideoOrientation; + } ); + } + else { + NSLog( @"Could not add video device input to the session" ); + self.setupResult = AVCamSetupResultSessionConfigurationFailed; + [self.session commitConfiguration]; + return; + } + + // Add audio input. + AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; + AVCaptureDeviceInput *audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error]; + if ( ! audioDeviceInput ) { + NSLog( @"Could not create audio device input: %@", error ); + } + if ( [self.session canAddInput:audioDeviceInput] ) { + [self.session addInput:audioDeviceInput]; + } + else { + NSLog( @"Could not add audio device input to the session" ); + } + + // Add photo output. + AVCapturePhotoOutput *photoOutput = [[AVCapturePhotoOutput alloc] init]; + if ( [self.session canAddOutput:photoOutput] ) { + [self.session addOutput:photoOutput]; + self.photoOutput = photoOutput; + + self.photoOutput.highResolutionCaptureEnabled = YES; + self.photoOutput.livePhotoCaptureEnabled = self.photoOutput.livePhotoCaptureSupported; + self.livePhotoMode = self.photoOutput.livePhotoCaptureSupported ? AVCamLivePhotoModeOn : AVCamLivePhotoModeOff; + + self.inProgressPhotoCaptureDelegates = [NSMutableDictionary dictionary]; + self.inProgressLivePhotoCapturesCount = 0; + } + else { + NSLog( @"Could not add photo output to the session" ); + self.setupResult = AVCamSetupResultSessionConfigurationFailed; + [self.session commitConfiguration]; + return; + } + + self.backgroundRecordingID = UIBackgroundTaskInvalid; + + [self.session commitConfiguration]; +} + +- (IBAction)resumeInterruptedSession:(id)sender +{ + dispatch_async( self.sessionQueue, ^{ + /* + The session might fail to start running, e.g., if a phone or FaceTime call is still + using audio or video. A failure to start the session running will be communicated via + a session runtime error notification. To avoid repeatedly failing to start the session + running, we only try to restart the session running in the session runtime error handler + if we aren't trying to resume the session running. + */ + [self.session startRunning]; + self.sessionRunning = self.session.isRunning; + if ( ! self.session.isRunning ) { + dispatch_async( dispatch_get_main_queue(), ^{ + NSString *message = NSLocalizedString( @"Unable to resume", @"Alert message when unable to resume the session running" ); + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"AVCam" message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString( @"OK", @"Alert OK button" ) style:UIAlertActionStyleCancel handler:nil]; + [alertController addAction:cancelAction]; + [self presentViewController:alertController animated:YES completion:nil]; + } ); + } + else { + dispatch_async( dispatch_get_main_queue(), ^{ + self.resumeButton.hidden = YES; + } ); + } + } ); +} + +- (IBAction)toggleCaptureMode:(UISegmentedControl *)captureModeControl +{ + if ( captureModeControl.selectedSegmentIndex == AVCamCaptureModePhoto ) { + self.recordButton.enabled = NO; + + dispatch_async( self.sessionQueue, ^{ + /* + Remove the AVCaptureMovieFileOutput from the session because movie recording is + not supported with AVCaptureSessionPresetPhoto. Additionally, Live Photo + capture is not supported when an AVCaptureMovieFileOutput is connected to the session. + */ + [self.session beginConfiguration]; + [self.session removeOutput:self.movieFileOutput]; + self.session.sessionPreset = AVCaptureSessionPresetPhoto; + [self.session commitConfiguration]; + + self.movieFileOutput = nil; + + if ( self.photoOutput.livePhotoCaptureSupported ) { + self.photoOutput.livePhotoCaptureEnabled = YES; + + dispatch_async( dispatch_get_main_queue(), ^{ + self.livePhotoModeButton.enabled = YES; + self.livePhotoModeButton.hidden = NO; + } ); + } + } ); + } + else if ( captureModeControl.selectedSegmentIndex == AVCamCaptureModeMovie ) { + self.livePhotoModeButton.hidden = YES; + + dispatch_async( self.sessionQueue, ^{ + AVCaptureMovieFileOutput *movieFileOutput = [[AVCaptureMovieFileOutput alloc] init]; + + if ( [self.session canAddOutput:movieFileOutput] ) + { + [self.session beginConfiguration]; + [self.session addOutput:movieFileOutput]; + self.session.sessionPreset = AVCaptureSessionPresetHigh; + AVCaptureConnection *connection = [movieFileOutput connectionWithMediaType:AVMediaTypeVideo]; + if ( connection.isVideoStabilizationSupported ) { + connection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto; + } + [self.session commitConfiguration]; + + self.movieFileOutput = movieFileOutput; + + dispatch_async( dispatch_get_main_queue(), ^{ + self.recordButton.enabled = YES; + } ); + } + } ); + } +} + +#pragma mark Device Configuration + +- (IBAction)changeCamera:(id)sender +{ + self.cameraButton.enabled = NO; + self.recordButton.enabled = NO; + self.photoButton.enabled = NO; + self.livePhotoModeButton.enabled = NO; + self.captureModeControl.enabled = NO; + + dispatch_async( self.sessionQueue, ^{ + AVCaptureDevice *currentVideoDevice = self.videoDeviceInput.device; + AVCaptureDevicePosition currentPosition = currentVideoDevice.position; + + AVCaptureDevicePosition preferredPosition; + AVCaptureDeviceType preferredDeviceType; + + switch ( currentPosition ) + { + case AVCaptureDevicePositionUnspecified: + case AVCaptureDevicePositionFront: + preferredPosition = AVCaptureDevicePositionBack; + preferredDeviceType = AVCaptureDeviceTypeBuiltInDuoCamera; + break; + case AVCaptureDevicePositionBack: + preferredPosition = AVCaptureDevicePositionFront; + preferredDeviceType = AVCaptureDeviceTypeBuiltInWideAngleCamera; + break; + } + + NSArray *devices = self.videoDeviceDiscoverySession.devices; + AVCaptureDevice *newVideoDevice = nil; + + // First, look for a device with both the preferred position and device type. + for ( AVCaptureDevice *device in devices ) { + if ( device.position == preferredPosition && [device.deviceType isEqualToString:preferredDeviceType] ) { + newVideoDevice = device; + break; + } + } + + // Otherwise, look for a device with only the preferred position. + if ( ! newVideoDevice ) { + for ( AVCaptureDevice *device in devices ) { + if ( device.position == preferredPosition ) { + newVideoDevice = device; + break; + } + } + } + + if ( newVideoDevice ) { + AVCaptureDeviceInput *videoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:newVideoDevice error:NULL]; + + [self.session beginConfiguration]; + + // Remove the existing device input first, since using the front and back camera simultaneously is not supported. + [self.session removeInput:self.videoDeviceInput]; + + if ( [self.session canAddInput:videoDeviceInput] ) { + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureDeviceSubjectAreaDidChangeNotification object:currentVideoDevice]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(subjectAreaDidChange:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:newVideoDevice]; + + [self.session addInput:videoDeviceInput]; + self.videoDeviceInput = videoDeviceInput; + } + else { + [self.session addInput:self.videoDeviceInput]; + } + + AVCaptureConnection *movieFileOutputConnection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeVideo]; + if ( movieFileOutputConnection.isVideoStabilizationSupported ) { + movieFileOutputConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto; + } + + /* + Set Live Photo capture enabled if it is supported. When changing cameras, the + `livePhotoCaptureEnabled` property of the AVCapturePhotoOutput gets set to NO when + a video device is disconnected from the session. After the new video device is + added to the session, re-enable Live Photo capture on the AVCapturePhotoOutput if it is supported. + */ + self.photoOutput.livePhotoCaptureEnabled = self.photoOutput.livePhotoCaptureSupported; + + [self.session commitConfiguration]; + } + + dispatch_async( dispatch_get_main_queue(), ^{ + self.cameraButton.enabled = YES; + self.recordButton.enabled = self.captureModeControl.selectedSegmentIndex == AVCamCaptureModeMovie; + self.photoButton.enabled = YES; + self.livePhotoModeButton.enabled = YES; + self.captureModeControl.enabled = YES; + } ); + } ); +} + +- (IBAction)focusAndExposeTap:(UIGestureRecognizer *)gestureRecognizer +{ + CGPoint devicePoint = [self.previewView.videoPreviewLayer captureDevicePointOfInterestForPoint:[gestureRecognizer locationInView:gestureRecognizer.view]]; + [self focusWithMode:AVCaptureFocusModeAutoFocus exposeWithMode:AVCaptureExposureModeAutoExpose atDevicePoint:devicePoint monitorSubjectAreaChange:YES]; +} + +- (void)focusWithMode:(AVCaptureFocusMode)focusMode exposeWithMode:(AVCaptureExposureMode)exposureMode atDevicePoint:(CGPoint)point monitorSubjectAreaChange:(BOOL)monitorSubjectAreaChange +{ + dispatch_async( self.sessionQueue, ^{ + AVCaptureDevice *device = self.videoDeviceInput.device; + NSError *error = nil; + if ( [device lockForConfiguration:&error] ) { + /* + Setting (focus/exposure)PointOfInterest alone does not initiate a (focus/exposure) operation. + Call set(Focus/Exposure)Mode() to apply the new point of interest. + */ + if ( device.isFocusPointOfInterestSupported && [device isFocusModeSupported:focusMode] ) { + device.focusPointOfInterest = point; + device.focusMode = focusMode; + } + + if ( device.isExposurePointOfInterestSupported && [device isExposureModeSupported:exposureMode] ) { + device.exposurePointOfInterest = point; + device.exposureMode = exposureMode; + } + + device.subjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange; + [device unlockForConfiguration]; + } + else { + NSLog( @"Could not lock device for configuration: %@", error ); + } + } ); +} + +#pragma mark Capturing Photos + +- (IBAction)capturePhoto:(id)sender +{ + /* + Retrieve the video preview layer's video orientation on the main queue before + entering the session queue. We do this to ensure UI elements are accessed on + the main thread and session configuration is done on the session queue. + */ + AVCaptureVideoOrientation videoPreviewLayerVideoOrientation = self.previewView.videoPreviewLayer.connection.videoOrientation; + + dispatch_async( self.sessionQueue, ^{ + + // Update the photo output's connection to match the video orientation of the video preview layer. + AVCaptureConnection *photoOutputConnection = [self.photoOutput connectionWithMediaType:AVMediaTypeVideo]; + photoOutputConnection.videoOrientation = videoPreviewLayerVideoOrientation; + + // Capture a JPEG photo with flash set to auto and high resolution photo enabled. + AVCapturePhotoSettings *photoSettings = [AVCapturePhotoSettings photoSettings]; + photoSettings.flashMode = AVCaptureFlashModeAuto; + photoSettings.highResolutionPhotoEnabled = YES; + if ( photoSettings.availablePreviewPhotoPixelFormatTypes.count > 0 ) { + photoSettings.previewPhotoFormat = @{ (NSString *)kCVPixelBufferPixelFormatTypeKey : photoSettings.availablePreviewPhotoPixelFormatTypes.firstObject }; + } + if ( self.livePhotoMode == AVCamLivePhotoModeOn && self.photoOutput.livePhotoCaptureSupported ) { // Live Photo capture is not supported in movie mode. + NSString *livePhotoMovieFileName = [NSUUID UUID].UUIDString; + NSString *livePhotoMovieFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[livePhotoMovieFileName stringByAppendingPathExtension:@"mov"]]; + photoSettings.livePhotoMovieFileURL = [NSURL fileURLWithPath:livePhotoMovieFilePath]; + } + + // Use a separate object for the photo capture delegate to isolate each capture life cycle. + AVCamPhotoCaptureDelegate *photoCaptureDelegate = [[AVCamPhotoCaptureDelegate alloc] initWithRequestedPhotoSettings:photoSettings willCapturePhotoAnimation:^{ + dispatch_async( dispatch_get_main_queue(), ^{ + self.previewView.videoPreviewLayer.opacity = 0.0; + [UIView animateWithDuration:0.25 animations:^{ + self.previewView.videoPreviewLayer.opacity = 1.0; + }]; + } ); + } capturingLivePhoto:^( BOOL capturing ) { + /* + Because Live Photo captures can overlap, we need to keep track of the + number of in progress Live Photo captures to ensure that the + Live Photo label stays visible during these captures. + */ + dispatch_async( self.sessionQueue, ^{ + if ( capturing ) { + self.inProgressLivePhotoCapturesCount++; + } + else { + self.inProgressLivePhotoCapturesCount--; + } + + NSInteger inProgressLivePhotoCapturesCount = self.inProgressLivePhotoCapturesCount; + dispatch_async( dispatch_get_main_queue(), ^{ + if ( inProgressLivePhotoCapturesCount > 0 ) { + self.capturingLivePhotoLabel.hidden = NO; + } + else if ( inProgressLivePhotoCapturesCount == 0 ) { + self.capturingLivePhotoLabel.hidden = YES; + } + else { + NSLog( @"Error: In progress live photo capture count is less than 0" ); + } + } ); + } ); + } completed:^( AVCamPhotoCaptureDelegate *photoCaptureDelegate ) { + // When the capture is complete, remove a reference to the photo capture delegate so it can be deallocated. + dispatch_async( self.sessionQueue, ^{ + self.inProgressPhotoCaptureDelegates[@(photoCaptureDelegate.requestedPhotoSettings.uniqueID)] = nil; + } ); + }]; + + /* + The Photo Output keeps a weak reference to the photo capture delegate so + we store it in an array to maintain a strong reference to this object + until the capture is completed. + */ + self.inProgressPhotoCaptureDelegates[@(photoCaptureDelegate.requestedPhotoSettings.uniqueID)] = photoCaptureDelegate; + [self.photoOutput capturePhotoWithSettings:photoSettings delegate:photoCaptureDelegate]; + } ); +} + +- (IBAction)toggleLivePhotoMode:(UIButton *)livePhotoModeButton +{ + dispatch_async( self.sessionQueue, ^{ + self.livePhotoMode = ( self.livePhotoMode == AVCamLivePhotoModeOn ) ? AVCamLivePhotoModeOff : AVCamLivePhotoModeOn; + AVCamLivePhotoMode livePhotoMode = self.livePhotoMode; + + dispatch_async( dispatch_get_main_queue(), ^{ + if ( livePhotoMode == AVCamLivePhotoModeOn ) { + [self.livePhotoModeButton setTitle:NSLocalizedString( @"Live Photo Mode: On", @"Live photo mode button on title" ) forState:UIControlStateNormal]; + } + else { + [self.livePhotoModeButton setTitle:NSLocalizedString( @"Live Photo Mode: Off", @"Live photo mode button off title" ) forState:UIControlStateNormal]; + } + } ); + } ); +} + +#pragma mark Recording Movies + +- (IBAction)toggleMovieRecording:(id)sender +{ + /* + Disable the Camera button until recording finishes, and disable + the Record button until recording starts or finishes. + + See the AVCaptureFileOutputRecordingDelegate methods. + */ + self.cameraButton.enabled = NO; + self.recordButton.enabled = NO; + self.captureModeControl.enabled = NO; + + /* + Retrieve the video preview layer's video orientation on the main queue + before entering the session queue. We do this to ensure UI elements are + accessed on the main thread and session configuration is done on the session queue. + */ + AVCaptureVideoOrientation videoPreviewLayerVideoOrientation = self.previewView.videoPreviewLayer.connection.videoOrientation; + + dispatch_async( self.sessionQueue, ^{ + if ( ! self.movieFileOutput.isRecording ) { + if ( [UIDevice currentDevice].isMultitaskingSupported ) { + /* + Setup background task. + This is needed because the -[captureOutput:didFinishRecordingToOutputFileAtURL:fromConnections:error:] + callback is not received until AVCam returns to the foreground unless you request background execution time. + This also ensures that there will be time to write the file to the photo library when AVCam is backgrounded. + To conclude this background execution, -[endBackgroundTask:] is called in + -[captureOutput:didFinishRecordingToOutputFileAtURL:fromConnections:error:] after the recorded file has been saved. + */ + self.backgroundRecordingID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil]; + } + + // Update the orientation on the movie file output video connection before starting recording. + AVCaptureConnection *movieFileOutputConnection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeVideo]; + movieFileOutputConnection.videoOrientation = videoPreviewLayerVideoOrientation; + + // Start recording to a temporary file. + NSString *outputFileName = [NSUUID UUID].UUIDString; + NSString *outputFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[outputFileName stringByAppendingPathExtension:@"mov"]]; + [self.movieFileOutput startRecordingToOutputFileURL:[NSURL fileURLWithPath:outputFilePath] recordingDelegate:self]; + } + else { + [self.movieFileOutput stopRecording]; + } + } ); +} + +- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections +{ + // Enable the Record button to let the user stop the recording. + dispatch_async( dispatch_get_main_queue(), ^{ + self.recordButton.enabled = YES; + [self.recordButton setTitle:NSLocalizedString( @"Stop", @"Recording button stop title" ) forState:UIControlStateNormal]; + }); +} + +- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error +{ + /* + Note that currentBackgroundRecordingID is used to end the background task + associated with this recording. This allows a new recording to be started, + associated with a new UIBackgroundTaskIdentifier, once the movie file output's + `recording` property is back to NO — which happens sometime after this method + returns. + + Note: Since we use a unique file path for each recording, a new recording will + not overwrite a recording currently being saved. + */ + UIBackgroundTaskIdentifier currentBackgroundRecordingID = self.backgroundRecordingID; + self.backgroundRecordingID = UIBackgroundTaskInvalid; + + dispatch_block_t cleanup = ^{ + if ( [[NSFileManager defaultManager] fileExistsAtPath:outputFileURL.path] ) { + [[NSFileManager defaultManager] removeItemAtPath:outputFileURL.path error:NULL]; + } + + if ( currentBackgroundRecordingID != UIBackgroundTaskInvalid ) { + [[UIApplication sharedApplication] endBackgroundTask:currentBackgroundRecordingID]; + } + }; + + BOOL success = YES; + + if ( error ) { + NSLog( @"Movie file finishing error: %@", error ); + success = [error.userInfo[AVErrorRecordingSuccessfullyFinishedKey] boolValue]; + } + if ( success ) { + // Check authorization status. + [PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status ) { + if ( status == PHAuthorizationStatusAuthorized ) { + // Save the movie file to the photo library and cleanup. + [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ + PHAssetResourceCreationOptions *options = [[PHAssetResourceCreationOptions alloc] init]; + options.shouldMoveFile = YES; + PHAssetCreationRequest *creationRequest = [PHAssetCreationRequest creationRequestForAsset]; + [creationRequest addResourceWithType:PHAssetResourceTypeVideo fileURL:outputFileURL options:options]; + } completionHandler:^( BOOL success, NSError *error ) { + if ( ! success ) { + NSLog( @"Could not save movie to photo library: %@", error ); + } + cleanup(); + }]; + } + else { + cleanup(); + } + }]; + } + else { + cleanup(); + } + + // Enable the Camera and Record buttons to let the user switch camera and start another recording. + dispatch_async( dispatch_get_main_queue(), ^{ + // Only enable the ability to change camera if the device has more than one camera. + self.cameraButton.enabled = ( self.videoDeviceDiscoverySession.uniqueDevicePositionsCount > 1 ); + self.recordButton.enabled = YES; + self.captureModeControl.enabled = YES; + [self.recordButton setTitle:NSLocalizedString( @"Record", @"Recording button record title" ) forState:UIControlStateNormal]; + }); +} + +#pragma mark KVO and Notifications + +- (void)addObservers +{ + [self.session addObserver:self forKeyPath:@"running" options:NSKeyValueObservingOptionNew context:SessionRunningContext]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(subjectAreaDidChange:) name:AVCaptureDeviceSubjectAreaDidChangeNotification object:self.videoDeviceInput.device]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:self.session]; + + /* + A session can only run when the app is full screen. It will be interrupted + in a multi-app layout, introduced in iOS 9, see also the documentation of + AVCaptureSessionInterruptionReason. Add observers to handle these session + interruptions and show a preview is paused message. See the documentation + of AVCaptureSessionWasInterruptedNotification for other interruption reasons. + */ + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionWasInterrupted:) name:AVCaptureSessionWasInterruptedNotification object:self.session]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionInterruptionEnded:) name:AVCaptureSessionInterruptionEndedNotification object:self.session]; +} + +- (void)removeObservers +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [self.session removeObserver:self forKeyPath:@"running" context:SessionRunningContext]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if ( context == SessionRunningContext ) { + BOOL isSessionRunning = [change[NSKeyValueChangeNewKey] boolValue]; + BOOL livePhotoCaptureSupported = self.photoOutput.livePhotoCaptureSupported; + BOOL livePhotoCaptureEnabled = self.photoOutput.livePhotoCaptureEnabled; + + dispatch_async( dispatch_get_main_queue(), ^{ + // Only enable the ability to change camera if the device has more than one camera. + self.cameraButton.enabled = isSessionRunning && ( self.videoDeviceDiscoverySession.uniqueDevicePositionsCount > 1 ); + self.recordButton.enabled = isSessionRunning && ( self.captureModeControl.selectedSegmentIndex == AVCamCaptureModeMovie ); + self.photoButton.enabled = isSessionRunning; + self.captureModeControl.enabled = isSessionRunning; + self.livePhotoModeButton.enabled = isSessionRunning && livePhotoCaptureEnabled; + self.livePhotoModeButton.hidden = ! ( isSessionRunning && livePhotoCaptureSupported ); + } ); + } + else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)subjectAreaDidChange:(NSNotification *)notification +{ + CGPoint devicePoint = CGPointMake( 0.5, 0.5 ); + [self focusWithMode:AVCaptureFocusModeContinuousAutoFocus exposeWithMode:AVCaptureExposureModeContinuousAutoExposure atDevicePoint:devicePoint monitorSubjectAreaChange:NO]; +} + +- (void)sessionRuntimeError:(NSNotification *)notification +{ + NSError *error = notification.userInfo[AVCaptureSessionErrorKey]; + NSLog( @"Capture session runtime error: %@", error ); + + /* + Automatically try to restart the session running if media services were + reset and the last start running succeeded. Otherwise, enable the user + to try to resume the session running. + */ + if ( error.code == AVErrorMediaServicesWereReset ) { + dispatch_async( self.sessionQueue, ^{ + if ( self.isSessionRunning ) { + [self.session startRunning]; + self.sessionRunning = self.session.isRunning; + } + else { + dispatch_async( dispatch_get_main_queue(), ^{ + self.resumeButton.hidden = NO; + } ); + } + } ); + } + else { + self.resumeButton.hidden = NO; + } +} + +- (void)sessionWasInterrupted:(NSNotification *)notification +{ + /* + In some scenarios we want to enable the user to resume the session running. + For example, if music playback is initiated via control center while + using AVCam, then the user can let AVCam resume + the session running, which will stop music playback. Note that stopping + music playback in control center will not automatically resume the session + running. Also note that it is not always possible to resume, see -[resumeInterruptedSession:]. + */ + BOOL showResumeButton = NO; + + AVCaptureSessionInterruptionReason reason = [notification.userInfo[AVCaptureSessionInterruptionReasonKey] integerValue]; + NSLog( @"Capture session was interrupted with reason %ld", (long)reason ); + + if ( reason == AVCaptureSessionInterruptionReasonAudioDeviceInUseByAnotherClient || + reason == AVCaptureSessionInterruptionReasonVideoDeviceInUseByAnotherClient ) { + showResumeButton = YES; + } + else if ( reason == AVCaptureSessionInterruptionReasonVideoDeviceNotAvailableWithMultipleForegroundApps ) { + // Simply fade-in a label to inform the user that the camera is unavailable. + self.cameraUnavailableLabel.alpha = 0.0; + self.cameraUnavailableLabel.hidden = NO; + [UIView animateWithDuration:0.25 animations:^{ + self.cameraUnavailableLabel.alpha = 1.0; + }]; + } + + if ( showResumeButton ) { + // Simply fade-in a button to enable the user to try to resume the session running. + self.resumeButton.alpha = 0.0; + self.resumeButton.hidden = NO; + [UIView animateWithDuration:0.25 animations:^{ + self.resumeButton.alpha = 1.0; + }]; + } +} + +- (void)sessionInterruptionEnded:(NSNotification *)notification +{ + NSLog( @"Capture session interruption ended" ); + + if ( ! self.resumeButton.hidden ) { + [UIView animateWithDuration:0.25 animations:^{ + self.resumeButton.alpha = 0.0; + } completion:^( BOOL finished ) { + self.resumeButton.hidden = YES; + }]; + } + if ( ! self.cameraUnavailableLabel.hidden ) { + [UIView animateWithDuration:0.25 animations:^{ + self.cameraUnavailableLabel.alpha = 0.0; + } completion:^( BOOL finished ) { + self.cameraUnavailableLabel.hidden = YES; + }]; + } +} + +@end diff --git a/AVCam/Objective-C/AVCam/AVCamPhotoCaptureDelegate.h b/AVCam/Objective-C/AVCam/AVCamPhotoCaptureDelegate.h new file mode 100644 index 00000000..7b333c88 --- /dev/null +++ b/AVCam/Objective-C/AVCam/AVCamPhotoCaptureDelegate.h @@ -0,0 +1,17 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Photo capture delegate. +*/ + +@import AVFoundation; + +@interface AVCamPhotoCaptureDelegate : NSObject + +- (instancetype)initWithRequestedPhotoSettings:(AVCapturePhotoSettings *)requestedPhotoSettings willCapturePhotoAnimation:(void (^)())willCapturePhotoAnimation capturingLivePhoto:(void (^)( BOOL capturing ))capturingLivePhoto completed:(void (^)( AVCamPhotoCaptureDelegate *photoCaptureDelegate ))completed; + +@property (nonatomic, readonly) AVCapturePhotoSettings *requestedPhotoSettings; + +@end diff --git a/AVCam/Objective-C/AVCam/AVCamPhotoCaptureDelegate.m b/AVCam/Objective-C/AVCam/AVCamPhotoCaptureDelegate.m new file mode 100644 index 00000000..927d84c9 --- /dev/null +++ b/AVCam/Objective-C/AVCam/AVCamPhotoCaptureDelegate.m @@ -0,0 +1,130 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Photo capture delegate. +*/ + +#import "AVCamPhotoCaptureDelegate.h" + +@import Photos; + +@interface AVCamPhotoCaptureDelegate () + +@property (nonatomic, readwrite) AVCapturePhotoSettings *requestedPhotoSettings; +@property (nonatomic) void (^willCapturePhotoAnimation)(); +@property (nonatomic) void (^capturingLivePhoto)(BOOL capturing); +@property (nonatomic) void (^completed)(AVCamPhotoCaptureDelegate *photoCaptureDelegate); + +@property (nonatomic) NSData *photoData; +@property (nonatomic) NSURL *livePhotoCompanionMovieURL; + +@end + +@implementation AVCamPhotoCaptureDelegate + +- (instancetype)initWithRequestedPhotoSettings:(AVCapturePhotoSettings *)requestedPhotoSettings willCapturePhotoAnimation:(void (^)())willCapturePhotoAnimation capturingLivePhoto:(void (^)(BOOL))capturingLivePhoto completed:(void (^)(AVCamPhotoCaptureDelegate *))completed +{ + self = [super init]; + if ( self ) { + self.requestedPhotoSettings = requestedPhotoSettings; + self.willCapturePhotoAnimation = willCapturePhotoAnimation; + self.capturingLivePhoto = capturingLivePhoto; + self.completed = completed; + } + return self; +} + +- (void)didFinish +{ + if ( [[NSFileManager defaultManager] fileExistsAtPath:self.livePhotoCompanionMovieURL.path] ) { + NSError *error = nil; + [[NSFileManager defaultManager] removeItemAtPath:self.livePhotoCompanionMovieURL.path error:&error]; + + if ( error ) { + NSLog( @"Could not remove file at url: %@", self.livePhotoCompanionMovieURL.path ); + } + } + + self.completed( self ); +} + +- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput willBeginCaptureForResolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings +{ + if ( ( resolvedSettings.livePhotoMovieDimensions.width > 0 ) && ( resolvedSettings.livePhotoMovieDimensions.height > 0 ) ) { + self.capturingLivePhoto( YES ); + } +} + +- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput willCapturePhotoForResolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings +{ + self.willCapturePhotoAnimation(); +} + +- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings error:(NSError *)error +{ + if ( error != nil ) { + NSLog( @"Error capturing photo: %@", error ); + return; + } + + self.photoData = [AVCapturePhotoOutput JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer previewPhotoSampleBuffer:previewPhotoSampleBuffer]; +} + +- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishRecordingLivePhotoMovieForEventualFileAtURL:(NSURL *)outputFileURL resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings +{ + self.capturingLivePhoto(NO); +} + +- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishProcessingLivePhotoToMovieFileAtURL:(NSURL *)outputFileURL duration:(CMTime)duration photoDisplayTime:(CMTime)photoDisplayTime resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings error:(NSError *)error +{ + if ( error != nil ) { + NSLog( @"Error processing live photo companion movie: %@", error ); + return; + } + + self.livePhotoCompanionMovieURL = outputFileURL; +} + +- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput didFinishCaptureForResolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings error:(NSError *)error +{ + if ( error != nil ) { + NSLog( @"Error capturing photo: %@", error ); + [self didFinish]; + return; + } + + if ( self.photoData == nil ) { + NSLog( @"No photo data resource" ); + [self didFinish]; + return; + } + + [PHPhotoLibrary requestAuthorization:^( PHAuthorizationStatus status ) { + if ( status == PHAuthorizationStatusAuthorized ) { + [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{ + PHAssetCreationRequest *creationRequest = [PHAssetCreationRequest creationRequestForAsset]; + [creationRequest addResourceWithType:PHAssetResourceTypePhoto data:self.photoData options:nil]; + + if ( self.livePhotoCompanionMovieURL ) { + PHAssetResourceCreationOptions *livePhotoCompanionMovieResourceOptions = [[PHAssetResourceCreationOptions alloc] init]; + livePhotoCompanionMovieResourceOptions.shouldMoveFile = YES; + [creationRequest addResourceWithType:PHAssetResourceTypePairedVideo fileURL:self.livePhotoCompanionMovieURL options:livePhotoCompanionMovieResourceOptions]; + } + } completionHandler:^( BOOL success, NSError * _Nullable error ) { + if ( ! success ) { + NSLog( @"Error occurred while saving photo to photo library: %@", error ); + } + + [self didFinish]; + }]; + } + else { + NSLog( @"Not authorized to save photo" ); + [self didFinish]; + } + }]; +} + +@end diff --git a/AVCam/Objective-C/AVCam/AVCamPreviewView.h b/AVCam/Objective-C/AVCam/AVCamPreviewView.h new file mode 100644 index 00000000..11cea717 --- /dev/null +++ b/AVCam/Objective-C/AVCam/AVCamPreviewView.h @@ -0,0 +1,19 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application preview view. +*/ + +@import UIKit; + +@class AVCaptureSession; + +@interface AVCamPreviewView : UIView + +@property (nonatomic, readonly) AVCaptureVideoPreviewLayer *videoPreviewLayer; + +@property (nonatomic) AVCaptureSession *session; + +@end diff --git a/AVCam/Objective-C/AVCam/AVCamPreviewView.m b/AVCam/Objective-C/AVCam/AVCamPreviewView.m new file mode 100644 index 00000000..d803c9b9 --- /dev/null +++ b/AVCam/Objective-C/AVCam/AVCamPreviewView.m @@ -0,0 +1,35 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application preview view. +*/ + +@import AVFoundation; + +#import "AVCamPreviewView.h" + +@implementation AVCamPreviewView + ++ (Class)layerClass +{ + return [AVCaptureVideoPreviewLayer class]; +} + +- (AVCaptureVideoPreviewLayer *)videoPreviewLayer +{ + return (AVCaptureVideoPreviewLayer *)self.layer; +} + +- (AVCaptureSession *)session +{ + return self.videoPreviewLayer.session; +} + +- (void)setSession:(AVCaptureSession *)session +{ + self.videoPreviewLayer.session = session; +} + +@end diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Contents.json b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a4b3297e --- /dev/null +++ b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,128 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-40@3x.png", + "scale" : "3x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "Icon.png", + "scale" : "1x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "Icon@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "Icon-Small-50.png", + "scale" : "1x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "Icon-Small-50@2x.png", + "scale" : "2x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "Icon-72.png", + "scale" : "1x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "Icon-72@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40.png new file mode 100644 index 00000000..385aecae Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png new file mode 100644 index 00000000..7210b688 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png new file mode 100644 index 00000000..7eb304d3 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png new file mode 100644 index 00000000..7eb304d3 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png new file mode 100644 index 00000000..60b66d88 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72.png new file mode 100644 index 00000000..64e9cd17 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png new file mode 100644 index 00000000..fd8b0f5b Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76.png new file mode 100644 index 00000000..d1df2b34 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png new file mode 100644 index 00000000..94f25ff5 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png new file mode 100644 index 00000000..64888c26 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png new file mode 100644 index 00000000..7ec70046 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png new file mode 100644 index 00000000..cfe10b98 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small.png new file mode 100644 index 00000000..0a194870 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png new file mode 100644 index 00000000..240eca4b Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png new file mode 100644 index 00000000..4c966b21 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon.png new file mode 100644 index 00000000..d0974778 Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon@2x.png b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon@2x.png new file mode 100644 index 00000000..a8f5258d Binary files /dev/null and b/AVCam/Objective-C/AVCam/Assets.xcassets/AppIcon.appiconset/Icon@2x.png differ diff --git a/AVCam/Objective-C/AVCam/Assets.xcassets/Contents.json b/AVCam/Objective-C/AVCam/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/AVCam/Objective-C/AVCam/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVCam/Objective-C/AVCam/Base.lproj/LaunchScreen.storyboard b/AVCam/Objective-C/AVCam/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..5dc6a62c --- /dev/null +++ b/AVCam/Objective-C/AVCam/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVCam/Objective-C/AVCam/Base.lproj/Main.storyboard b/AVCam/Objective-C/AVCam/Base.lproj/Main.storyboard new file mode 100644 index 00000000..8c9ffe3b --- /dev/null +++ b/AVCam/Objective-C/AVCam/Base.lproj/Main.storyboard @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVCam/Objective-C/AVCam/Info.plist b/AVCam/Objective-C/AVCam/Info.plist new file mode 100644 index 00000000..5b80e04f --- /dev/null +++ b/AVCam/Objective-C/AVCam/Info.plist @@ -0,0 +1,60 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 5.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSCameraUsageDescription + to take photos and videos + NSMicrophoneUsageDescription + to record Live Photos and movies + NSPhotoLibraryUsageDescription + to save photos and videos to your Photo Library + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/AVCam/Objective-C/AVCam/main.m b/AVCam/Objective-C/AVCam/main.m new file mode 100644 index 00000000..60069f93 --- /dev/null +++ b/AVCam/Objective-C/AVCam/main.m @@ -0,0 +1,17 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Main application entry point. +*/ + +@import UIKit; + +#import "AVCamAppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain( argc, argv, nil, NSStringFromClass( [AVCamAppDelegate class] ) ); + } +} diff --git a/AVCam/README.md b/AVCam/README.md new file mode 100644 index 00000000..4f4b54b4 --- /dev/null +++ b/AVCam/README.md @@ -0,0 +1,24 @@ +# AVCam-iOS: Using AVFoundation to Capture Photos and Movies + +AVCam demonstrates how to use the AVFoundation capture API to record movies and capture photos. The sample has a record button for recording movies, a photo button for capturing photos, a Live Photo mode button for enabling Live Photo capture, a capture mode control for toggling between photo and movie capture modes, and a camera button for switching between front and back cameras (on supported devices). AVCam runs only on an actual device, either an iPad or iPhone, and cannot be run in Simulator. + +## Requirements + +### Build + +Xcode 8.0, iOS 10.0 SDK + +### Runtime + +iOS 10.0 or later + +## Changes from Previous Version + +- Adopt AVCapturePhotoOutput +- Capture Live Photos +- Add privacy keys to Info.plist +- Add a version of AVCam in Swift 3 +- Remove support for AVCaptureStillImageOutput +- Bug fixes + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/AVCam/Swift/AVCam Swift.xcodeproj/project.pbxproj b/AVCam/Swift/AVCam Swift.xcodeproj/project.pbxproj new file mode 100644 index 00000000..d2e592d8 --- /dev/null +++ b/AVCam/Swift/AVCam Swift.xcodeproj/project.pbxproj @@ -0,0 +1,317 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 7AA677151CFF765600B353FB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA677141CFF765600B353FB /* AppDelegate.swift */; }; + 7AA677171CFF765600B353FB /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA677161CFF765600B353FB /* CameraViewController.swift */; }; + 7AA6771A1CFF765600B353FB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7AA677181CFF765600B353FB /* Main.storyboard */; }; + 7AA6771C1CFF765600B353FB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7AA6771B1CFF765600B353FB /* Assets.xcassets */; }; + 7AA6771F1CFF765600B353FB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7AA6771D1CFF765600B353FB /* LaunchScreen.storyboard */; }; + 7AA677271CFF774800B353FB /* PhotoCaptureDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA677261CFF774800B353FB /* PhotoCaptureDelegate.swift */; }; + 7AA677291CFF7B5C00B353FB /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA677281CFF7B5C00B353FB /* PreviewView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 7AA677111CFF765600B353FB /* AVCam.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AVCam.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AA677141CFF765600B353FB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AA677161CFF765600B353FB /* CameraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = ""; }; + 7AA677191CFF765600B353FB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 7AA6771B1CFF765600B353FB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 7AA6771E1CFF765600B353FB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 7AA677201CFF765600B353FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7AA677261CFF774800B353FB /* PhotoCaptureDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureDelegate.swift; sourceTree = ""; }; + 7AA677281CFF7B5C00B353FB /* PreviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = ""; }; + 7AE4754E1D00FFA900C2CB9E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7AA6770E1CFF765500B353FB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7AA677081CFF765500B353FB = { + isa = PBXGroup; + children = ( + 7AE4754E1D00FFA900C2CB9E /* README.md */, + 7AA677131CFF765600B353FB /* AVCam */, + 7AA677121CFF765600B353FB /* Products */, + ); + sourceTree = ""; + }; + 7AA677121CFF765600B353FB /* Products */ = { + isa = PBXGroup; + children = ( + 7AA677111CFF765600B353FB /* AVCam.app */, + ); + name = Products; + sourceTree = ""; + }; + 7AA677131CFF765600B353FB /* AVCam */ = { + isa = PBXGroup; + children = ( + 7AA677141CFF765600B353FB /* AppDelegate.swift */, + 7AA677281CFF7B5C00B353FB /* PreviewView.swift */, + 7AA677161CFF765600B353FB /* CameraViewController.swift */, + 7AA677261CFF774800B353FB /* PhotoCaptureDelegate.swift */, + 7AA677181CFF765600B353FB /* Main.storyboard */, + 7AA6771B1CFF765600B353FB /* Assets.xcassets */, + 7AA6771D1CFF765600B353FB /* LaunchScreen.storyboard */, + 7AA677201CFF765600B353FB /* Info.plist */, + ); + path = AVCam; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7AA677101CFF765500B353FB /* AVCam */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7AA677231CFF765600B353FB /* Build configuration list for PBXNativeTarget "AVCam" */; + buildPhases = ( + 7AA6770D1CFF765500B353FB /* Sources */, + 7AA6770E1CFF765500B353FB /* Frameworks */, + 7AA6770F1CFF765500B353FB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AVCam; + productName = AVCam; + productReference = 7AA677111CFF765600B353FB /* AVCam.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7AA677091CFF765500B353FB /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Apple, Inc."; + TargetAttributes = { + 7AA677101CFF765500B353FB = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 7AA6770C1CFF765500B353FB /* Build configuration list for PBXProject "AVCam Swift" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7AA677081CFF765500B353FB; + productRefGroup = 7AA677121CFF765600B353FB /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7AA677101CFF765500B353FB /* AVCam */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7AA6770F1CFF765500B353FB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7AA6771F1CFF765600B353FB /* LaunchScreen.storyboard in Resources */, + 7AA6771C1CFF765600B353FB /* Assets.xcassets in Resources */, + 7AA6771A1CFF765600B353FB /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7AA6770D1CFF765500B353FB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7AA677271CFF774800B353FB /* PhotoCaptureDelegate.swift in Sources */, + 7AA677291CFF7B5C00B353FB /* PreviewView.swift in Sources */, + 7AA677171CFF765600B353FB /* CameraViewController.swift in Sources */, + 7AA677151CFF765600B353FB /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 7AA677181CFF765600B353FB /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7AA677191CFF765600B353FB /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 7AA6771D1CFF765600B353FB /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7AA6771E1CFF765600B353FB /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 7AA677211CFF765600B353FB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7AA677221CFF765600B353FB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7AA677241CFF765600B353FB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = AVCam/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVCam"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 7AA677251CFF765600B353FB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = AVCam/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVCam"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7AA6770C1CFF765500B353FB /* Build configuration list for PBXProject "AVCam Swift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7AA677211CFF765600B353FB /* Debug */, + 7AA677221CFF765600B353FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7AA677231CFF765600B353FB /* Build configuration list for PBXNativeTarget "AVCam" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7AA677241CFF765600B353FB /* Debug */, + 7AA677251CFF765600B353FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7AA677091CFF765500B353FB /* Project object */; +} diff --git a/AVCam/Swift/AVCam Swift.xcodeproj/xcshareddata/xcschemes/AVCam Swift.xcscheme b/AVCam/Swift/AVCam Swift.xcodeproj/xcshareddata/xcschemes/AVCam Swift.xcscheme new file mode 100644 index 00000000..c675c312 --- /dev/null +++ b/AVCam/Swift/AVCam Swift.xcodeproj/xcshareddata/xcschemes/AVCam Swift.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVCam/Swift/AVCam/AppDelegate.swift b/AVCam/Swift/AVCam/AppDelegate.swift new file mode 100644 index 00000000..43271539 --- /dev/null +++ b/AVCam/Swift/AVCam/AppDelegate.swift @@ -0,0 +1,14 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate. +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? +} diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Contents.json b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a4b3297e --- /dev/null +++ b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,128 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-40@3x.png", + "scale" : "3x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "Icon.png", + "scale" : "1x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "Icon@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "Icon-Small-50.png", + "scale" : "1x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "Icon-Small-50@2x.png", + "scale" : "2x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "Icon-72.png", + "scale" : "1x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "Icon-72@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40.png new file mode 100644 index 00000000..385aecae Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png new file mode 100644 index 00000000..7210b688 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png new file mode 100644 index 00000000..7eb304d3 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png new file mode 100644 index 00000000..7eb304d3 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png new file mode 100644 index 00000000..60b66d88 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72.png new file mode 100644 index 00000000..64e9cd17 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png new file mode 100644 index 00000000..fd8b0f5b Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76.png new file mode 100644 index 00000000..d1df2b34 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png new file mode 100644 index 00000000..94f25ff5 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png new file mode 100644 index 00000000..64888c26 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png new file mode 100644 index 00000000..7ec70046 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png new file mode 100644 index 00000000..cfe10b98 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small.png new file mode 100644 index 00000000..0a194870 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png new file mode 100644 index 00000000..240eca4b Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png new file mode 100644 index 00000000..4c966b21 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon.png new file mode 100644 index 00000000..d0974778 Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon@2x.png b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon@2x.png new file mode 100644 index 00000000..a8f5258d Binary files /dev/null and b/AVCam/Swift/AVCam/Assets.xcassets/AppIcon.appiconset/Icon@2x.png differ diff --git a/AVCam/Swift/AVCam/Assets.xcassets/Contents.json b/AVCam/Swift/AVCam/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/AVCam/Swift/AVCam/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVCam/Swift/AVCam/Base.lproj/LaunchScreen.storyboard b/AVCam/Swift/AVCam/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..5dc6a62c --- /dev/null +++ b/AVCam/Swift/AVCam/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVCam/Swift/AVCam/Base.lproj/Main.storyboard b/AVCam/Swift/AVCam/Base.lproj/Main.storyboard new file mode 100644 index 00000000..ab31ae19 --- /dev/null +++ b/AVCam/Swift/AVCam/Base.lproj/Main.storyboard @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVCam/Swift/AVCam/CameraViewController.swift b/AVCam/Swift/AVCam/CameraViewController.swift new file mode 100644 index 00000000..39146e14 --- /dev/null +++ b/AVCam/Swift/AVCam/CameraViewController.swift @@ -0,0 +1,940 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller for camera interface. +*/ + +import UIKit +import AVFoundation +import Photos + +class CameraViewController: UIViewController, AVCaptureFileOutputRecordingDelegate { + // MARK: View Controller Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Disable UI. The UI is enabled if and only if the session starts running. + cameraButton.isEnabled = false + recordButton.isEnabled = false + photoButton.isEnabled = false + livePhotoModeButton.isEnabled = false + captureModeControl.isEnabled = false + + // Set up the video preview view. + previewView.session = session + + /* + Check video authorization status. Video access is required and audio + access is optional. If audio access is denied, audio is not recorded + during movie recording. + */ + switch AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo) { + case .authorized: + // The user has previously granted access to the camera. + break + + case .notDetermined: + /* + The user has not yet been presented with the option to grant + video access. We suspend the session queue to delay session + setup until the access request has completed. + + Note that audio access will be implicitly requested when we + create an AVCaptureDeviceInput for audio during session setup. + */ + sessionQueue.suspend() + AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo, completionHandler: { [unowned self] granted in + if !granted { + self.setupResult = .notAuthorized + } + self.sessionQueue.resume() + }) + + default: + // The user has previously denied access. + setupResult = .notAuthorized + } + + /* + Setup the capture session. + In general it is not safe to mutate an AVCaptureSession or any of its + inputs, outputs, or connections from multiple threads at the same time. + + Why not do all of this on the main queue? + Because AVCaptureSession.startRunning() is a blocking call which can + take a long time. We dispatch session setup to the sessionQueue so + that the main queue isn't blocked, which keeps the UI responsive. + */ + sessionQueue.async { [unowned self] in + self.configureSession() + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + sessionQueue.async { + switch self.setupResult { + case .success: + // Only setup observers and start the session running if setup succeeded. + self.addObservers() + self.session.startRunning() + self.isSessionRunning = self.session.isRunning + + case .notAuthorized: + DispatchQueue.main.async { [unowned self] in + let message = NSLocalizedString("AVCam doesn't have permission to use the camera, please change privacy settings", comment: "Alert message when the user has denied access to the camera") + let alertController = UIAlertController(title: "AVCam", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil)) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Settings", comment: "Alert button to open Settings"), style: .`default`, handler: { action in + UIApplication.shared.open(URL(string: UIApplicationOpenSettingsURLString)!, options: [:], completionHandler: nil) + })) + + self.present(alertController, animated: true, completion: nil) + } + + case .configurationFailed: + DispatchQueue.main.async { [unowned self] in + let message = NSLocalizedString("Unable to capture media", comment: "Alert message when something goes wrong during capture session configuration") + let alertController = UIAlertController(title: "AVCam", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil)) + + self.present(alertController, animated: true, completion: nil) + } + } + } + } + + override func viewWillDisappear(_ animated: Bool) { + sessionQueue.async { [unowned self] in + if self.setupResult == .success { + self.session.stopRunning() + self.isSessionRunning = self.session.isRunning + self.removeObservers() + } + } + + super.viewWillDisappear(animated) + } + + override var shouldAutorotate: Bool { + // Disable autorotation of the interface when recording is in progress. + if let movieFileOutput = movieFileOutput { + return !movieFileOutput.isRecording + } + return true + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .all + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + if let videoPreviewLayerConnection = previewView.videoPreviewLayer.connection { + let deviceOrientation = UIDevice.current.orientation + guard let newVideoOrientation = deviceOrientation.videoOrientation, deviceOrientation.isPortrait || deviceOrientation.isLandscape else { + return + } + + videoPreviewLayerConnection.videoOrientation = newVideoOrientation + } + } + + // MARK: Session Management + + private enum SessionSetupResult { + case success + case notAuthorized + case configurationFailed + } + + private let session = AVCaptureSession() + + private var isSessionRunning = false + + private let sessionQueue = DispatchQueue(label: "session queue", attributes: [], target: nil) // Communicate with the session and other session objects on this queue. + + private var setupResult: SessionSetupResult = .success + + var videoDeviceInput: AVCaptureDeviceInput! + + @IBOutlet private weak var previewView: PreviewView! + + // Call this on the session queue. + private func configureSession() { + if setupResult != .success { + return + } + + session.beginConfiguration() + + /* + We do not create an AVCaptureMovieFileOutput when setting up the session because the + AVCaptureMovieFileOutput does not support movie recording with AVCaptureSessionPresetPhoto. + */ + session.sessionPreset = AVCaptureSessionPresetPhoto + + // Add video input. + do { + var defaultVideoDevice: AVCaptureDevice? + + // Choose the back dual camera if available, otherwise default to a wide angle camera. + if let dualCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInDuoCamera, mediaType: AVMediaTypeVideo, position: .back) { + defaultVideoDevice = dualCameraDevice + } + else if let backCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .back) { + // If the back dual camera is not available, default to the back wide angle camera. + defaultVideoDevice = backCameraDevice + } + else if let frontCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .front) { + // In some cases where users break their phones, the back wide angle camera is not available. In this case, we should default to the front wide angle camera. + defaultVideoDevice = frontCameraDevice + } + + let videoDeviceInput = try AVCaptureDeviceInput(device: defaultVideoDevice) + + if session.canAddInput(videoDeviceInput) { + session.addInput(videoDeviceInput) + self.videoDeviceInput = videoDeviceInput + + DispatchQueue.main.async { + /* + Why are we dispatching this to the main queue? + Because AVCaptureVideoPreviewLayer is the backing layer for PreviewView and UIView + can only be manipulated on the main thread. + Note: As an exception to the above rule, it is not necessary to serialize video orientation changes + on the AVCaptureVideoPreviewLayer’s connection with other session manipulation. + + Use the status bar orientation as the initial video orientation. Subsequent orientation changes are + handled by CameraViewController.viewWillTransition(to:with:). + */ + let statusBarOrientation = UIApplication.shared.statusBarOrientation + var initialVideoOrientation: AVCaptureVideoOrientation = .portrait + if statusBarOrientation != .unknown { + if let videoOrientation = statusBarOrientation.videoOrientation { + initialVideoOrientation = videoOrientation + } + } + + self.previewView.videoPreviewLayer.connection.videoOrientation = initialVideoOrientation + } + } + else { + print("Could not add video device input to the session") + setupResult = .configurationFailed + session.commitConfiguration() + return + } + } + catch { + print("Could not create video device input: \(error)") + setupResult = .configurationFailed + session.commitConfiguration() + return + } + + // Add audio input. + do { + let audioDevice = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeAudio) + let audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice) + + if session.canAddInput(audioDeviceInput) { + session.addInput(audioDeviceInput) + } + else { + print("Could not add audio device input to the session") + } + } + catch { + print("Could not create audio device input: \(error)") + } + + // Add photo output. + if session.canAddOutput(photoOutput) + { + session.addOutput(photoOutput) + + photoOutput.isHighResolutionCaptureEnabled = true + photoOutput.isLivePhotoCaptureEnabled = photoOutput.isLivePhotoCaptureSupported + livePhotoMode = photoOutput.isLivePhotoCaptureSupported ? .on : .off + } + else { + print("Could not add photo output to the session") + setupResult = .configurationFailed + session.commitConfiguration() + return + } + + session.commitConfiguration() + } + + @IBAction private func resumeInterruptedSession(_ resumeButton: UIButton) + { + sessionQueue.async { [unowned self] in + /* + The session might fail to start running, e.g., if a phone or FaceTime call is still + using audio or video. A failure to start the session running will be communicated via + a session runtime error notification. To avoid repeatedly failing to start the session + running, we only try to restart the session running in the session runtime error handler + if we aren't trying to resume the session running. + */ + self.session.startRunning() + self.isSessionRunning = self.session.isRunning + if !self.session.isRunning { + DispatchQueue.main.async { [unowned self] in + let message = NSLocalizedString("Unable to resume", comment: "Alert message when unable to resume the session running") + let alertController = UIAlertController(title: "AVCam", message: message, preferredStyle: .alert) + let cancelAction = UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil) + alertController.addAction(cancelAction) + self.present(alertController, animated: true, completion: nil) + } + } + else { + DispatchQueue.main.async { [unowned self] in + self.resumeButton.isHidden = true + } + } + } + } + + private enum CaptureMode: Int { + case photo = 0 + case movie = 1 + } + + @IBOutlet private weak var captureModeControl: UISegmentedControl! + + @IBAction private func toggleCaptureMode(_ captureModeControl: UISegmentedControl) { + if captureModeControl.selectedSegmentIndex == CaptureMode.photo.rawValue { + recordButton.isEnabled = false + + sessionQueue.async { [unowned self] in + /* + Remove the AVCaptureMovieFileOutput from the session because movie recording is + not supported with AVCaptureSessionPresetPhoto. Additionally, Live Photo + capture is not supported when an AVCaptureMovieFileOutput is connected to the session. + */ + self.session.beginConfiguration() + self.session.removeOutput(self.movieFileOutput) + self.session.sessionPreset = AVCaptureSessionPresetPhoto + self.session.commitConfiguration() + + self.movieFileOutput = nil + + if self.photoOutput.isLivePhotoCaptureSupported { + self.photoOutput.isLivePhotoCaptureEnabled = true + + DispatchQueue.main.async { + self.livePhotoModeButton.isEnabled = true + self.livePhotoModeButton.isHidden = false + } + } + } + } + else if captureModeControl.selectedSegmentIndex == CaptureMode.movie.rawValue + { + livePhotoModeButton.isHidden = true + + sessionQueue.async { [unowned self] in + let movieFileOutput = AVCaptureMovieFileOutput() + + if self.session.canAddOutput(movieFileOutput) { + self.session.beginConfiguration() + self.session.addOutput(movieFileOutput) + self.session.sessionPreset = AVCaptureSessionPresetHigh + if let connection = movieFileOutput.connection(withMediaType: AVMediaTypeVideo) { + if connection.isVideoStabilizationSupported { + connection.preferredVideoStabilizationMode = .auto + } + } + self.session.commitConfiguration() + + self.movieFileOutput = movieFileOutput + + DispatchQueue.main.async { [unowned self] in + self.recordButton.isEnabled = true + } + } + } + } + } + + // MARK: Device Configuration + + @IBOutlet private weak var cameraButton: UIButton! + + @IBOutlet private weak var cameraUnavailableLabel: UILabel! + + private let videoDeviceDiscoverySession = AVCaptureDeviceDiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDuoCamera], mediaType: AVMediaTypeVideo, position: .unspecified)! + + @IBAction private func changeCamera(_ cameraButton: UIButton) { + cameraButton.isEnabled = false + recordButton.isEnabled = false + photoButton.isEnabled = false + livePhotoModeButton.isEnabled = false + captureModeControl.isEnabled = false + + sessionQueue.async { [unowned self] in + let currentVideoDevice = self.videoDeviceInput.device + let currentPosition = currentVideoDevice!.position + + let preferredPosition: AVCaptureDevicePosition + let preferredDeviceType: AVCaptureDeviceType + + switch currentPosition { + case .unspecified, .front: + preferredPosition = .back + preferredDeviceType = .builtInDuoCamera + + case .back: + preferredPosition = .front + preferredDeviceType = .builtInWideAngleCamera + } + + let devices = self.videoDeviceDiscoverySession.devices! + var newVideoDevice: AVCaptureDevice? = nil + + // First, look for a device with both the preferred position and device type. Otherwise, look for a device with only the preferred position. + if let device = devices.filter({ $0.position == preferredPosition && $0.deviceType == preferredDeviceType }).first { + newVideoDevice = device + } + else if let device = devices.filter({ $0.position == preferredPosition }).first { + newVideoDevice = device + } + + if let videoDevice = newVideoDevice { + do { + let videoDeviceInput = try AVCaptureDeviceInput(device: videoDevice) + + self.session.beginConfiguration() + + // Remove the existing device input first, since using the front and back camera simultaneously is not supported. + self.session.removeInput(self.videoDeviceInput) + + if self.session.canAddInput(videoDeviceInput) { + NotificationCenter.default.removeObserver(self, name: Notification.Name("AVCaptureDeviceSubjectAreaDidChangeNotification"), object: currentVideoDevice!) + + NotificationCenter.default.addObserver(self, selector: #selector(self.subjectAreaDidChange), name: Notification.Name("AVCaptureDeviceSubjectAreaDidChangeNotification"), object: videoDeviceInput.device) + + self.session.addInput(videoDeviceInput) + self.videoDeviceInput = videoDeviceInput + } + else { + self.session.addInput(self.videoDeviceInput); + } + + if let connection = self.movieFileOutput?.connection(withMediaType: AVMediaTypeVideo) { + if connection.isVideoStabilizationSupported { + connection.preferredVideoStabilizationMode = .auto + } + } + + /* + Set Live Photo capture enabled if it is supported. When changing cameras, the + `isLivePhotoCaptureEnabled` property of the AVCapturePhotoOutput gets set to NO when + a video device is disconnected from the session. After the new video device is + added to the session, re-enable Live Photo capture on the AVCapturePhotoOutput if it is supported. + */ + self.photoOutput.isLivePhotoCaptureEnabled = self.photoOutput.isLivePhotoCaptureSupported; + + self.session.commitConfiguration() + } + catch { + print("Error occured while creating video device input: \(error)") + } + } + + DispatchQueue.main.async { [unowned self] in + self.cameraButton.isEnabled = true + self.recordButton.isEnabled = self.movieFileOutput != nil + self.photoButton.isEnabled = true + self.livePhotoModeButton.isEnabled = true + self.captureModeControl.isEnabled = true + } + } + } + + @IBAction private func focusAndExposeTap(_ gestureRecognizer: UITapGestureRecognizer) { + let devicePoint = self.previewView.videoPreviewLayer.captureDevicePointOfInterest(for: gestureRecognizer.location(in: gestureRecognizer.view)) + focus(with: .autoFocus, exposureMode: .autoExpose, at: devicePoint, monitorSubjectAreaChange: true) + } + + private func focus(with focusMode: AVCaptureFocusMode, exposureMode: AVCaptureExposureMode, at devicePoint: CGPoint, monitorSubjectAreaChange: Bool) { + sessionQueue.async { [unowned self] in + if let device = self.videoDeviceInput.device { + do { + try device.lockForConfiguration() + + /* + Setting (focus/exposure)PointOfInterest alone does not initiate a (focus/exposure) operation. + Call set(Focus/Exposure)Mode() to apply the new point of interest. + */ + if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(focusMode) { + device.focusPointOfInterest = devicePoint + device.focusMode = focusMode + } + + if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(exposureMode) { + device.exposurePointOfInterest = devicePoint + device.exposureMode = exposureMode + } + + device.isSubjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange + device.unlockForConfiguration() + } + catch { + print("Could not lock device for configuration: \(error)") + } + } + } + } + + // MARK: Capturing Photos + + private let photoOutput = AVCapturePhotoOutput() + + private var inProgressPhotoCaptureDelegates = [Int64 : PhotoCaptureDelegate]() + + @IBOutlet private weak var photoButton: UIButton! + + @IBAction private func capturePhoto(_ photoButton: UIButton) { + /* + Retrieve the video preview layer's video orientation on the main queue before + entering the session queue. We do this to ensure UI elements are accessed on + the main thread and session configuration is done on the session queue. + */ + let videoPreviewLayerOrientation = previewView.videoPreviewLayer.connection.videoOrientation + + sessionQueue.async { + // Update the photo output's connection to match the video orientation of the video preview layer. + if let photoOutputConnection = self.photoOutput.connection(withMediaType: AVMediaTypeVideo) { + photoOutputConnection.videoOrientation = videoPreviewLayerOrientation + } + + // Capture a JPEG photo with flash set to auto and high resolution photo enabled. + let photoSettings = AVCapturePhotoSettings() + photoSettings.flashMode = .auto + photoSettings.isHighResolutionPhotoEnabled = true + if photoSettings.availablePreviewPhotoPixelFormatTypes.count > 0 { + photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String : photoSettings.availablePreviewPhotoPixelFormatTypes.first!] + } + if self.livePhotoMode == .on && self.photoOutput.isLivePhotoCaptureSupported { // Live Photo capture is not supported in movie mode. + let livePhotoMovieFileName = NSUUID().uuidString + let livePhotoMovieFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent((livePhotoMovieFileName as NSString).appendingPathExtension("mov")!) + photoSettings.livePhotoMovieFileURL = URL(fileURLWithPath: livePhotoMovieFilePath) + } + + // Use a separate object for the photo capture delegate to isolate each capture life cycle. + let photoCaptureDelegate = PhotoCaptureDelegate(with: photoSettings, willCapturePhotoAnimation: { + DispatchQueue.main.async { [unowned self] in + self.previewView.videoPreviewLayer.opacity = 0 + UIView.animate(withDuration: 0.25) { [unowned self] in + self.previewView.videoPreviewLayer.opacity = 1 + } + } + }, capturingLivePhoto: { capturing in + /* + Because Live Photo captures can overlap, we need to keep track of the + number of in progress Live Photo captures to ensure that the + Live Photo label stays visible during these captures. + */ + self.sessionQueue.async { [unowned self] in + if capturing { + self.inProgressLivePhotoCapturesCount += 1 + } + else { + self.inProgressLivePhotoCapturesCount -= 1 + } + + let inProgressLivePhotoCapturesCount = self.inProgressLivePhotoCapturesCount + DispatchQueue.main.async { [unowned self] in + if inProgressLivePhotoCapturesCount > 0 { + self.capturingLivePhotoLabel.isHidden = false + } + else if inProgressLivePhotoCapturesCount == 0 { + self.capturingLivePhotoLabel.isHidden = true + } + else { + print("Error: In progress live photo capture count is less than 0"); + } + } + } + }, completed: { [unowned self] photoCaptureDelegate in + // When the capture is complete, remove a reference to the photo capture delegate so it can be deallocated. + self.sessionQueue.async { [unowned self] in + self.inProgressPhotoCaptureDelegates[photoCaptureDelegate.requestedPhotoSettings.uniqueID] = nil + } + } + ) + + /* + The Photo Output keeps a weak reference to the photo capture delegate so + we store it in an array to maintain a strong reference to this object + until the capture is completed. + */ + self.inProgressPhotoCaptureDelegates[photoCaptureDelegate.requestedPhotoSettings.uniqueID] = photoCaptureDelegate + self.photoOutput.capturePhoto(with: photoSettings, delegate: photoCaptureDelegate) + } + } + + private enum LivePhotoMode { + case on + case off + } + + private var livePhotoMode: LivePhotoMode = .off + + @IBOutlet private weak var livePhotoModeButton: UIButton! + + @IBAction private func toggleLivePhotoMode(_ livePhotoModeButton: UIButton) { + sessionQueue.async { [unowned self] in + self.livePhotoMode = (self.livePhotoMode == .on) ? .off : .on + let livePhotoMode = self.livePhotoMode + + DispatchQueue.main.async { [unowned self] in + if livePhotoMode == .on { + self.livePhotoModeButton.setTitle(NSLocalizedString("Live Photo Mode: On", comment: "Live photo mode button on title"), for: []) + } + else { + self.livePhotoModeButton.setTitle(NSLocalizedString("Live Photo Mode: Off", comment: "Live photo mode button off title"), for: []) + } + } + } + } + + private var inProgressLivePhotoCapturesCount = 0 + + @IBOutlet var capturingLivePhotoLabel: UILabel! + + // MARK: Recording Movies + + private var movieFileOutput: AVCaptureMovieFileOutput? = nil + + private var backgroundRecordingID: UIBackgroundTaskIdentifier? = nil + + @IBOutlet private weak var recordButton: UIButton! + + @IBOutlet private weak var resumeButton: UIButton! + + @IBAction private func toggleMovieRecording(_ recordButton: UIButton) { + guard let movieFileOutput = self.movieFileOutput else { + return + } + + /* + Disable the Camera button until recording finishes, and disable + the Record button until recording starts or finishes. + + See the AVCaptureFileOutputRecordingDelegate methods. + */ + cameraButton.isEnabled = false + recordButton.isEnabled = false + captureModeControl.isEnabled = false + + /* + Retrieve the video preview layer's video orientation on the main queue + before entering the session queue. We do this to ensure UI elements are + accessed on the main thread and session configuration is done on the session queue. + */ + let videoPreviewLayerOrientation = previewView.videoPreviewLayer.connection.videoOrientation + + sessionQueue.async { [unowned self] in + if !movieFileOutput.isRecording { + if UIDevice.current.isMultitaskingSupported { + /* + Setup background task. + This is needed because the `capture(_:, didFinishRecordingToOutputFileAt:, fromConnections:, error:)` + callback is not received until AVCam returns to the foreground unless you request background execution time. + This also ensures that there will be time to write the file to the photo library when AVCam is backgrounded. + To conclude this background execution, endBackgroundTask(_:) is called in + `capture(_:, didFinishRecordingToOutputFileAt:, fromConnections:, error:)` after the recorded file has been saved. + */ + self.backgroundRecordingID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) + } + + // Update the orientation on the movie file output video connection before starting recording. + let movieFileOutputConnection = self.movieFileOutput?.connection(withMediaType: AVMediaTypeVideo) + movieFileOutputConnection?.videoOrientation = videoPreviewLayerOrientation + + // Start recording to a temporary file. + let outputFileName = NSUUID().uuidString + let outputFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent((outputFileName as NSString).appendingPathExtension("mov")!) + movieFileOutput.startRecording(toOutputFileURL: URL(fileURLWithPath: outputFilePath), recordingDelegate: self) + } + else { + movieFileOutput.stopRecording() + } + } + } + + func capture(_ captureOutput: AVCaptureFileOutput!, didStartRecordingToOutputFileAt fileURL: URL!, fromConnections connections: [Any]!) { + // Enable the Record button to let the user stop the recording. + DispatchQueue.main.async { [unowned self] in + self.recordButton.isEnabled = true + self.recordButton.setTitle(NSLocalizedString("Stop", comment: "Recording button stop title"), for: []) + } + } + + func capture(_ captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAt outputFileURL: URL!, fromConnections connections: [Any]!, error: Error!) { + /* + Note that currentBackgroundRecordingID is used to end the background task + associated with this recording. This allows a new recording to be started, + associated with a new UIBackgroundTaskIdentifier, once the movie file output's + `isRecording` property is back to false — which happens sometime after this method + returns. + + Note: Since we use a unique file path for each recording, a new recording will + not overwrite a recording currently being saved. + */ + func cleanup() { + let path = outputFileURL.path + if FileManager.default.fileExists(atPath: path) { + do { + try FileManager.default.removeItem(atPath: path) + } + catch { + print("Could not remove file at url: \(outputFileURL)") + } + } + + if let currentBackgroundRecordingID = backgroundRecordingID { + backgroundRecordingID = UIBackgroundTaskInvalid + + if currentBackgroundRecordingID != UIBackgroundTaskInvalid { + UIApplication.shared.endBackgroundTask(currentBackgroundRecordingID) + } + } + } + + var success = true + + if error != nil { + print("Movie file finishing error: \(error)") + success = (((error as NSError).userInfo[AVErrorRecordingSuccessfullyFinishedKey] as AnyObject).boolValue)! + } + + if success { + // Check authorization status. + PHPhotoLibrary.requestAuthorization { status in + if status == .authorized { + // Save the movie file to the photo library and cleanup. + PHPhotoLibrary.shared().performChanges({ + let options = PHAssetResourceCreationOptions() + options.shouldMoveFile = true + let creationRequest = PHAssetCreationRequest.forAsset() + creationRequest.addResource(with: .video, fileURL: outputFileURL, options: options) + }, completionHandler: { success, error in + if !success { + print("Could not save movie to photo library: \(error)") + } + cleanup() + } + ) + } + else { + cleanup() + } + } + } + else { + cleanup() + } + + // Enable the Camera and Record buttons to let the user switch camera and start another recording. + DispatchQueue.main.async { [unowned self] in + // Only enable the ability to change camera if the device has more than one camera. + self.cameraButton.isEnabled = self.videoDeviceDiscoverySession.uniqueDevicePositionsCount() > 1 + self.recordButton.isEnabled = true + self.captureModeControl.isEnabled = true + self.recordButton.setTitle(NSLocalizedString("Record", comment: "Recording button record title"), for: []) + } + } + + // MARK: KVO and Notifications + + private var sessionRunningObserveContext = 0 + + private func addObservers() { + session.addObserver(self, forKeyPath: "running", options: .new, context: &sessionRunningObserveContext) + + NotificationCenter.default.addObserver(self, selector: #selector(subjectAreaDidChange), name: Notification.Name("AVCaptureDeviceSubjectAreaDidChangeNotification"), object: videoDeviceInput.device) + NotificationCenter.default.addObserver(self, selector: #selector(sessionRuntimeError), name: Notification.Name("AVCaptureSessionRuntimeErrorNotification"), object: session) + + /* + A session can only run when the app is full screen. It will be interrupted + in a multi-app layout, introduced in iOS 9, see also the documentation of + AVCaptureSessionInterruptionReason. Add observers to handle these session + interruptions and show a preview is paused message. See the documentation + of AVCaptureSessionWasInterruptedNotification for other interruption reasons. + */ + NotificationCenter.default.addObserver(self, selector: #selector(sessionWasInterrupted), name: Notification.Name("AVCaptureSessionWasInterruptedNotification"), object: session) + NotificationCenter.default.addObserver(self, selector: #selector(sessionInterruptionEnded), name: Notification.Name("AVCaptureSessionInterruptionEndedNotification"), object: session) + } + + private func removeObservers() { + NotificationCenter.default.removeObserver(self) + + session.removeObserver(self, forKeyPath: "running", context: &sessionRunningObserveContext) + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if context == &sessionRunningObserveContext { + let newValue = change?[.newKey] as AnyObject? + guard let isSessionRunning = newValue?.boolValue else { return } + let isLivePhotoCaptureSupported = photoOutput.isLivePhotoCaptureSupported + let isLivePhotoCaptureEnabled = photoOutput.isLivePhotoCaptureEnabled + + DispatchQueue.main.async { [unowned self] in + // Only enable the ability to change camera if the device has more than one camera. + self.cameraButton.isEnabled = isSessionRunning && self.videoDeviceDiscoverySession.uniqueDevicePositionsCount() > 1 + self.recordButton.isEnabled = isSessionRunning && self.movieFileOutput != nil + self.photoButton.isEnabled = isSessionRunning + self.captureModeControl.isEnabled = isSessionRunning + self.livePhotoModeButton.isEnabled = isSessionRunning && isLivePhotoCaptureEnabled + self.livePhotoModeButton.isHidden = !(isSessionRunning && isLivePhotoCaptureSupported) + } + } + else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + } + } + + func subjectAreaDidChange(notification: NSNotification) { + let devicePoint = CGPoint(x: 0.5, y: 0.5) + focus(with: .autoFocus, exposureMode: .continuousAutoExposure, at: devicePoint, monitorSubjectAreaChange: false) + } + + func sessionRuntimeError(notification: NSNotification) { + guard let errorValue = notification.userInfo?[AVCaptureSessionErrorKey] as? NSError else { + return + } + + let error = AVError(_nsError: errorValue) + print("Capture session runtime error: \(error)") + + /* + Automatically try to restart the session running if media services were + reset and the last start running succeeded. Otherwise, enable the user + to try to resume the session running. + */ + if error.code == .mediaServicesWereReset { + sessionQueue.async { [unowned self] in + if self.isSessionRunning { + self.session.startRunning() + self.isSessionRunning = self.session.isRunning + } + else { + DispatchQueue.main.async { [unowned self] in + self.resumeButton.isHidden = false + } + } + } + } + else { + resumeButton.isHidden = false + } + } + + func sessionWasInterrupted(notification: NSNotification) { + /* + In some scenarios we want to enable the user to resume the session running. + For example, if music playback is initiated via control center while + using AVCam, then the user can let AVCam resume + the session running, which will stop music playback. Note that stopping + music playback in control center will not automatically resume the session + running. Also note that it is not always possible to resume, see `resumeInterruptedSession(_:)`. + */ + if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?, let reasonIntegerValue = userInfoValue.integerValue, let reason = AVCaptureSessionInterruptionReason(rawValue: reasonIntegerValue) { + print("Capture session was interrupted with reason \(reason)") + + var showResumeButton = false + + if reason == AVCaptureSessionInterruptionReason.audioDeviceInUseByAnotherClient || reason == AVCaptureSessionInterruptionReason.videoDeviceInUseByAnotherClient { + showResumeButton = true + } + else if reason == AVCaptureSessionInterruptionReason.videoDeviceNotAvailableWithMultipleForegroundApps { + // Simply fade-in a label to inform the user that the camera is unavailable. + cameraUnavailableLabel.alpha = 0 + cameraUnavailableLabel.isHidden = false + UIView.animate(withDuration: 0.25) { [unowned self] in + self.cameraUnavailableLabel.alpha = 1 + } + } + + if showResumeButton { + // Simply fade-in a button to enable the user to try to resume the session running. + resumeButton.alpha = 0 + resumeButton.isHidden = false + UIView.animate(withDuration: 0.25) { [unowned self] in + self.resumeButton.alpha = 1 + } + } + } + } + + func sessionInterruptionEnded(notification: NSNotification) { + print("Capture session interruption ended") + + if !resumeButton.isHidden { + UIView.animate(withDuration: 0.25, + animations: { [unowned self] in + self.resumeButton.alpha = 0 + }, completion: { [unowned self] finished in + self.resumeButton.isHidden = true + } + ) + } + if !cameraUnavailableLabel.isHidden { + UIView.animate(withDuration: 0.25, + animations: { [unowned self] in + self.cameraUnavailableLabel.alpha = 0 + }, completion: { [unowned self] finished in + self.cameraUnavailableLabel.isHidden = true + } + ) + } + } +} + +extension UIDeviceOrientation { + var videoOrientation: AVCaptureVideoOrientation? { + switch self { + case .portrait: return .portrait + case .portraitUpsideDown: return .portraitUpsideDown + case .landscapeLeft: return .landscapeRight + case .landscapeRight: return .landscapeLeft + default: return nil + } + } +} + +extension UIInterfaceOrientation { + var videoOrientation: AVCaptureVideoOrientation? { + switch self { + case .portrait: return .portrait + case .portraitUpsideDown: return .portraitUpsideDown + case .landscapeLeft: return .landscapeLeft + case .landscapeRight: return .landscapeRight + default: return nil + } + } +} + +extension AVCaptureDeviceDiscoverySession { + func uniqueDevicePositionsCount() -> Int { + var uniqueDevicePositions = [AVCaptureDevicePosition]() + + for device in devices { + if !uniqueDevicePositions.contains(device.position) { + uniqueDevicePositions.append(device.position) + } + } + + return uniqueDevicePositions.count + } +} diff --git a/AVCam/Swift/AVCam/Info.plist b/AVCam/Swift/AVCam/Info.plist new file mode 100644 index 00000000..8191d29e --- /dev/null +++ b/AVCam/Swift/AVCam/Info.plist @@ -0,0 +1,60 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 5.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSCameraUsageDescription + to take photos and video + NSMicrophoneUsageDescription + to record Live Photos and movies + NSPhotoLibraryUsageDescription + to save photos and videos to your Photo Library + UILaunchStoryboardName + Launch Screen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/AVCam/Swift/AVCam/PhotoCaptureDelegate.swift b/AVCam/Swift/AVCam/PhotoCaptureDelegate.swift new file mode 100644 index 00000000..78e72ab7 --- /dev/null +++ b/AVCam/Swift/AVCam/PhotoCaptureDelegate.swift @@ -0,0 +1,119 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Photo capture delegate. +*/ + +import AVFoundation +import Photos + +class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate { + private(set) var requestedPhotoSettings: AVCapturePhotoSettings + + private let willCapturePhotoAnimation: () -> () + + private let capturingLivePhoto: (Bool) -> () + + private let completed: (PhotoCaptureDelegate) -> () + + private var photoData: Data? = nil + + private var livePhotoCompanionMovieURL: URL? = nil + + init(with requestedPhotoSettings: AVCapturePhotoSettings, willCapturePhotoAnimation: @escaping () -> (), capturingLivePhoto: @escaping (Bool) -> (), completed: @escaping (PhotoCaptureDelegate) -> ()) { + self.requestedPhotoSettings = requestedPhotoSettings + self.willCapturePhotoAnimation = willCapturePhotoAnimation + self.capturingLivePhoto = capturingLivePhoto + self.completed = completed + } + + private func didFinish() { + if let livePhotoCompanionMoviePath = livePhotoCompanionMovieURL?.path { + if FileManager.default.fileExists(atPath: livePhotoCompanionMoviePath) { + do { + try FileManager.default.removeItem(atPath: livePhotoCompanionMoviePath) + } + catch { + print("Could not remove file at url: \(livePhotoCompanionMoviePath)") + } + } + } + + completed(self) + } + + func capture(_ captureOutput: AVCapturePhotoOutput, willBeginCaptureForResolvedSettings resolvedSettings: AVCaptureResolvedPhotoSettings) { + if resolvedSettings.livePhotoMovieDimensions.width > 0 && resolvedSettings.livePhotoMovieDimensions.height > 0 { + capturingLivePhoto(true) + } + } + + func capture(_ captureOutput: AVCapturePhotoOutput, willCapturePhotoForResolvedSettings resolvedSettings: AVCaptureResolvedPhotoSettings) { + willCapturePhotoAnimation() + } + + func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?, previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) { + if let photoSampleBuffer = photoSampleBuffer { + photoData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: photoSampleBuffer, previewPhotoSampleBuffer: previewPhotoSampleBuffer) + } + else { + print("Error capturing photo: \(error)") + return + } + } + + func capture(_ captureOutput: AVCapturePhotoOutput, didFinishRecordingLivePhotoMovieForEventualFileAt outputFileURL: URL, resolvedSettings: AVCaptureResolvedPhotoSettings) { + capturingLivePhoto(false) + } + + func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingLivePhotoToMovieFileAt outputFileURL: URL, duration: CMTime, photoDisplay photoDisplayTime: CMTime, resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) { + if let _ = error { + print("Error processing live photo companion movie: \(error)") + return + } + + livePhotoCompanionMovieURL = outputFileURL + } + + func capture(_ captureOutput: AVCapturePhotoOutput, didFinishCaptureForResolvedSettings resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) { + if let error = error { + print("Error capturing photo: \(error)") + didFinish() + return + } + + guard let photoData = photoData else { + print("No photo data resource") + didFinish() + return + } + + PHPhotoLibrary.requestAuthorization { [unowned self] status in + if status == .authorized { + PHPhotoLibrary.shared().performChanges({ [unowned self] in + let creationRequest = PHAssetCreationRequest.forAsset() + creationRequest.addResource(with: .photo, data: photoData, options: nil) + + if let livePhotoCompanionMovieURL = self.livePhotoCompanionMovieURL { + let livePhotoCompanionMovieFileResourceOptions = PHAssetResourceCreationOptions() + livePhotoCompanionMovieFileResourceOptions.shouldMoveFile = true + creationRequest.addResource(with: .pairedVideo, fileURL: livePhotoCompanionMovieURL, options: livePhotoCompanionMovieFileResourceOptions) + } + + }, completionHandler: { [unowned self] success, error in + if let error = error { + print("Error occurered while saving photo to photo library: \(error)") + } + + self.didFinish() + } + ) + } + else { + self.didFinish() + } + } + } +} diff --git a/AVCam/Swift/AVCam/PreviewView.swift b/AVCam/Swift/AVCam/PreviewView.swift new file mode 100644 index 00000000..f2222ea6 --- /dev/null +++ b/AVCam/Swift/AVCam/PreviewView.swift @@ -0,0 +1,31 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application preview view. +*/ + +import UIKit +import AVFoundation + +class PreviewView: UIView { + var videoPreviewLayer: AVCaptureVideoPreviewLayer { + return layer as! AVCaptureVideoPreviewLayer + } + + var session: AVCaptureSession? { + get { + return videoPreviewLayer.session + } + set { + videoPreviewLayer.session = newValue + } + } + + // MARK: UIView + + override class var layerClass: AnyClass { + return AVCaptureVideoPreviewLayer.self + } +} diff --git a/AVCamBarcode/AVCamBarcode.xcodeproj/project.pbxproj b/AVCamBarcode/AVCamBarcode.xcodeproj/project.pbxproj new file mode 100644 index 00000000..cfb9d182 --- /dev/null +++ b/AVCamBarcode/AVCamBarcode.xcodeproj/project.pbxproj @@ -0,0 +1,324 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 7A5BA9721CD2B5EE0091A264 /* ItemSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5BA9711CD2B5EE0091A264 /* ItemSelectionViewController.swift */; }; + 7A921C841CD2858B00E7B04B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A921C831CD2858B00E7B04B /* AppDelegate.swift */; }; + 7A921C861CD2858B00E7B04B /* CameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A921C851CD2858B00E7B04B /* CameraViewController.swift */; }; + 7A921C891CD2858B00E7B04B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7A921C871CD2858B00E7B04B /* Main.storyboard */; }; + 7A921C8B1CD2858B00E7B04B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7A921C8A1CD2858B00E7B04B /* Assets.xcassets */; }; + 7A921C8E1CD2858B00E7B04B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7A921C8C1CD2858B00E7B04B /* LaunchScreen.storyboard */; }; + 7A921C961CD2861000E7B04B /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A921C951CD2861000E7B04B /* PreviewView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 7A00104A1CD28B5500302C83 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 7A5BA9711CD2B5EE0091A264 /* ItemSelectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemSelectionViewController.swift; sourceTree = ""; }; + 7A921C801CD2858B00E7B04B /* AVCamBarcode.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AVCamBarcode.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7A921C831CD2858B00E7B04B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7A921C851CD2858B00E7B04B /* CameraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraViewController.swift; sourceTree = ""; }; + 7A921C881CD2858B00E7B04B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 7A921C8A1CD2858B00E7B04B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 7A921C8D1CD2858B00E7B04B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 7A921C8F1CD2858B00E7B04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7A921C951CD2861000E7B04B /* PreviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7A921C7D1CD2858B00E7B04B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7A921C771CD2858B00E7B04B = { + isa = PBXGroup; + children = ( + 7A00104A1CD28B5500302C83 /* README.md */, + 7A921C821CD2858B00E7B04B /* AVCamBarcode */, + 7A921C811CD2858B00E7B04B /* Products */, + ); + sourceTree = ""; + }; + 7A921C811CD2858B00E7B04B /* Products */ = { + isa = PBXGroup; + children = ( + 7A921C801CD2858B00E7B04B /* AVCamBarcode.app */, + ); + name = Products; + sourceTree = ""; + }; + 7A921C821CD2858B00E7B04B /* AVCamBarcode */ = { + isa = PBXGroup; + children = ( + 7A921C831CD2858B00E7B04B /* AppDelegate.swift */, + 7A921C951CD2861000E7B04B /* PreviewView.swift */, + 7A921C851CD2858B00E7B04B /* CameraViewController.swift */, + 7A5BA9711CD2B5EE0091A264 /* ItemSelectionViewController.swift */, + 7A921C871CD2858B00E7B04B /* Main.storyboard */, + 7A921C8A1CD2858B00E7B04B /* Assets.xcassets */, + 7A921C8C1CD2858B00E7B04B /* LaunchScreen.storyboard */, + 7A921C8F1CD2858B00E7B04B /* Info.plist */, + ); + path = AVCamBarcode; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7A921C7F1CD2858B00E7B04B /* AVCamBarcode */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7A921C921CD2858B00E7B04B /* Build configuration list for PBXNativeTarget "AVCamBarcode" */; + buildPhases = ( + 7A921C7C1CD2858B00E7B04B /* Sources */, + 7A921C7D1CD2858B00E7B04B /* Frameworks */, + 7A921C7E1CD2858B00E7B04B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AVCamBarcode; + productName = AVCamBarcode; + productReference = 7A921C801CD2858B00E7B04B /* AVCamBarcode.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7A921C781CD2858B00E7B04B /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Apple, Inc."; + TargetAttributes = { + 7A921C7F1CD2858B00E7B04B = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 7A921C7B1CD2858B00E7B04B /* Build configuration list for PBXProject "AVCamBarcode" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7A921C771CD2858B00E7B04B; + productRefGroup = 7A921C811CD2858B00E7B04B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7A921C7F1CD2858B00E7B04B /* AVCamBarcode */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7A921C7E1CD2858B00E7B04B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7A921C8E1CD2858B00E7B04B /* LaunchScreen.storyboard in Resources */, + 7A921C8B1CD2858B00E7B04B /* Assets.xcassets in Resources */, + 7A921C891CD2858B00E7B04B /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7A921C7C1CD2858B00E7B04B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7A5BA9721CD2B5EE0091A264 /* ItemSelectionViewController.swift in Sources */, + 7A921C861CD2858B00E7B04B /* CameraViewController.swift in Sources */, + 7A921C841CD2858B00E7B04B /* AppDelegate.swift in Sources */, + 7A921C961CD2861000E7B04B /* PreviewView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 7A921C871CD2858B00E7B04B /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7A921C881CD2858B00E7B04B /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 7A921C8C1CD2858B00E7B04B /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 7A921C8D1CD2858B00E7B04B /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 7A921C901CD2858B00E7B04B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = lossless; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7A921C911CD2858B00E7B04B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = "respect-asset-catalog"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7A921C931CD2858B00E7B04B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = AVCamBarcode/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVCamBarcode"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 7A921C941CD2858B00E7B04B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = AVCamBarcode/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVCamBarcode"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7A921C7B1CD2858B00E7B04B /* Build configuration list for PBXProject "AVCamBarcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7A921C901CD2858B00E7B04B /* Debug */, + 7A921C911CD2858B00E7B04B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7A921C921CD2858B00E7B04B /* Build configuration list for PBXNativeTarget "AVCamBarcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7A921C931CD2858B00E7B04B /* Debug */, + 7A921C941CD2858B00E7B04B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7A921C781CD2858B00E7B04B /* Project object */; +} diff --git a/AVCamBarcode/AVCamBarcode/AppDelegate.swift b/AVCamBarcode/AVCamBarcode/AppDelegate.swift new file mode 100644 index 00000000..43271539 --- /dev/null +++ b/AVCamBarcode/AVCamBarcode/AppDelegate.swift @@ -0,0 +1,14 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate. +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? +} diff --git a/AVCamBarcode/AVCamBarcode/Assets.xcassets/AppIcon.appiconset/Contents.json b/AVCamBarcode/AVCamBarcode/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..4a051112 --- /dev/null +++ b/AVCamBarcode/AVCamBarcode/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,108 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "1x" + }, + { + "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" : "57x57", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "57x57", + "scale" : "2x" + }, + { + "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" : "50x50", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "72x72", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVCamBarcode/AVCamBarcode/Assets.xcassets/Contents.json b/AVCamBarcode/AVCamBarcode/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/AVCamBarcode/AVCamBarcode/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVCamBarcode/AVCamBarcode/Base.lproj/LaunchScreen.storyboard b/AVCamBarcode/AVCamBarcode/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..2eeebdf9 --- /dev/null +++ b/AVCamBarcode/AVCamBarcode/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVCamBarcode/AVCamBarcode/Base.lproj/Main.storyboard b/AVCamBarcode/AVCamBarcode/Base.lproj/Main.storyboard new file mode 100644 index 00000000..95b39cae --- /dev/null +++ b/AVCamBarcode/AVCamBarcode/Base.lproj/Main.storyboard @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVCamBarcode/AVCamBarcode/CameraViewController.swift b/AVCamBarcode/AVCamBarcode/CameraViewController.swift new file mode 100644 index 00000000..3279f11a --- /dev/null +++ b/AVCamBarcode/AVCamBarcode/CameraViewController.swift @@ -0,0 +1,816 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller for camera interface. +*/ + +import UIKit +import AVFoundation + +class CameraViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate, ItemSelectionViewControllerDelegate { + // MARK: View Controller Life Cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Disable UI. The UI is enabled if and only if the session starts running. + metadataObjectTypesButton.isEnabled = false + sessionPresetsButton.isEnabled = false + cameraButton.isEnabled = false + zoomSlider.isEnabled = false + + // Add the open barcode gesture recognizer to the region of interest view. + previewView.addGestureRecognizer(openBarcodeURLGestureRecognizer) + + // Set up the video preview view. + previewView.session = session + + /* + Check video authorization status. Video access is required and audio + access is optional. If audio access is denied, audio is not recorded + during movie recording. + */ + switch AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo) { + case .authorized: + // The user has previously granted access to the camera. + break + + case .notDetermined: + /* + The user has not yet been presented with the option to grant + video access. We suspend the session queue to delay session + setup until the access request has completed. + */ + sessionQueue.suspend() + AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo, completionHandler: { [unowned self] granted in + if !granted { + self.setupResult = .notAuthorized + } + self.sessionQueue.resume() + }) + + default: + // The user has previously denied access. + setupResult = .notAuthorized + } + + /* + Setup the capture session. + In general it is not safe to mutate an AVCaptureSession or any of its + inputs, outputs, or connections from multiple threads at the same time. + + Why not do all of this on the main queue? + Because AVCaptureSession.startRunning() is a blocking call which can + take a long time. We dispatch session setup to the sessionQueue so + that the main queue isn't blocked, which keeps the UI responsive. + */ + sessionQueue.async { [unowned self] in + self.configureSession() + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + sessionQueue.async { [unowned self] in + switch self.setupResult { + case .success: + // Only setup observers and start the session running if setup succeeded. + self.addObservers() + self.session.startRunning() + self.isSessionRunning = self.session.isRunning + + case .notAuthorized: + DispatchQueue.main.async { [unowned self] in + let message = NSLocalizedString("AVCamBarcode doesn't have permission to use the camera, please change privacy settings", comment: "Alert message when the user has denied access to the camera") + let alertController = UIAlertController(title: "AVCamBarcode", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil)) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Settings", comment: "Alert button to open Settings"), style: .`default`, handler: { action in + UIApplication.shared.open(URL(string: UIApplicationOpenSettingsURLString)!, options: [:], completionHandler: nil) + })) + + self.present(alertController, animated: true, completion: nil) + } + + case .configurationFailed: + DispatchQueue.main.async { [unowned self] in + let message = NSLocalizedString("Unable to capture media", comment: "Alert message when something goes wrong during capture session configuration") + let alertController = UIAlertController(title: "AVCamBarcode", message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Alert OK button"), style: .cancel, handler: nil)) + + self.present(alertController, animated: true, completion: nil) + } + } + } + } + + override func viewWillDisappear(_ animated: Bool) { + sessionQueue.async { [unowned self] in + if self.setupResult == .success { + self.session.stopRunning() + self.isSessionRunning = self.session.isRunning + self.removeObservers() + } + } + + super.viewWillDisappear(animated) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "SelectMetadataObjectTypes" { + let navigationController = segue.destination as! UINavigationController + + let itemSelectionViewController = navigationController.viewControllers[0] as! ItemSelectionViewController + itemSelectionViewController.title = NSLocalizedString("Metadata Object Types", comment: "The title when selecting metadata object types.") + itemSelectionViewController.delegate = self + itemSelectionViewController.identifier = metadataObjectTypeItemSelectionIdentifier + itemSelectionViewController.allItems = metadataOutput.availableMetadataObjectTypes as! [String] + itemSelectionViewController.selectedItems = metadataOutput.metadataObjectTypes as! [String] + itemSelectionViewController.allowsMultipleSelection = true + } + else if segue.identifier == "SelectSessionPreset" { + let navigationController = segue.destination as! UINavigationController + + let itemSelectionViewController = navigationController.viewControllers[0] as! ItemSelectionViewController + itemSelectionViewController.title = NSLocalizedString("Session Presets", comment: "The title when selecting a session preset.") + itemSelectionViewController.delegate = self + itemSelectionViewController.identifier = sessionPresetItemSelectionIdentifier + itemSelectionViewController.allItems = availableSessionPresets() + itemSelectionViewController.selectedItems = [session.sessionPreset] + itemSelectionViewController.allowsMultipleSelection = false + } + } + + override var shouldAutorotate: Bool { + // Do not allow rotation if the region of interest is being resized. + return !previewView.isResizingRegionOfInterest + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + if let videoPreviewLayerConnection = previewView.videoPreviewLayer.connection { + let deviceOrientation = UIDevice.current.orientation + guard let newVideoOrientation = deviceOrientation.videoOrientation, deviceOrientation.isPortrait || deviceOrientation.isLandscape else { + return + } + + let oldSize = view.frame.size + let oldVideoOrientation = videoPreviewLayerConnection.videoOrientation + videoPreviewLayerConnection.videoOrientation = newVideoOrientation + + /* + When we transition to the new size, we need to adjust the region + of interest's origin and size so that it stays anchored relative + to the camera. + */ + coordinator.animate(alongsideTransition: { [unowned self] context in + + let oldRegionOfInterest = self.previewView.regionOfInterest + var newRegionOfInterest = CGRect() + + if oldVideoOrientation == .landscapeRight && newVideoOrientation == .landscapeLeft { + newRegionOfInterest.origin.x = oldSize.width - oldRegionOfInterest.origin.x - oldRegionOfInterest.size.width + newRegionOfInterest.origin.y = oldRegionOfInterest.origin.y + newRegionOfInterest.size.width = oldRegionOfInterest.size.width + newRegionOfInterest.size.height = oldRegionOfInterest.size.height + } + else if oldVideoOrientation == .landscapeRight && newVideoOrientation == .portrait { + newRegionOfInterest.origin.x = size.width - oldRegionOfInterest.origin.y - oldRegionOfInterest.size.height + newRegionOfInterest.origin.y = oldRegionOfInterest.origin.x + newRegionOfInterest.size.width = oldRegionOfInterest.size.height + newRegionOfInterest.size.height = oldRegionOfInterest.size.width + } + else if oldVideoOrientation == .landscapeLeft && newVideoOrientation == .landscapeRight { + newRegionOfInterest.origin.x = oldSize.width - oldRegionOfInterest.origin.x - oldRegionOfInterest.size.width + newRegionOfInterest.origin.y = oldRegionOfInterest.origin.y + newRegionOfInterest.size.width = oldRegionOfInterest.size.width + newRegionOfInterest.size.height = oldRegionOfInterest.size.height + } + else if oldVideoOrientation == .landscapeLeft && newVideoOrientation == .portrait { + newRegionOfInterest.origin.x = oldRegionOfInterest.origin.y + newRegionOfInterest.origin.y = oldSize.width - oldRegionOfInterest.origin.x - oldRegionOfInterest.size.width + newRegionOfInterest.size.width = oldRegionOfInterest.size.height + newRegionOfInterest.size.height = oldRegionOfInterest.size.width + } + else if oldVideoOrientation == .portrait && newVideoOrientation == .landscapeRight { + newRegionOfInterest.origin.x = oldRegionOfInterest.origin.y + newRegionOfInterest.origin.y = size.height - oldRegionOfInterest.origin.x - oldRegionOfInterest.size.width + newRegionOfInterest.size.width = oldRegionOfInterest.size.height + newRegionOfInterest.size.height = oldRegionOfInterest.size.width + } + else if oldVideoOrientation == .portrait && newVideoOrientation == .landscapeLeft { + newRegionOfInterest.origin.x = oldSize.height - oldRegionOfInterest.origin.y - oldRegionOfInterest.size.height + newRegionOfInterest.origin.y = oldRegionOfInterest.origin.x + newRegionOfInterest.size.width = oldRegionOfInterest.size.height + newRegionOfInterest.size.height = oldRegionOfInterest.size.width + } + + self.previewView.setRegionOfInterestWithProposedRegionOfInterest(newRegionOfInterest) + + }, + completion: { [unowned self] context in + self.sessionQueue.async { + self.metadataOutput.rectOfInterest = self.previewView.videoPreviewLayer.metadataOutputRectOfInterest(for: self.previewView.regionOfInterest) + } + + // Remove the old metadata object overlays. + self.removeMetadataObjectOverlayLayers() + } + ) + } + } + + // MARK: Session Management + + private enum SessionSetupResult { + case success + case notAuthorized + case configurationFailed + } + + private let session = AVCaptureSession() + + private var isSessionRunning = false + + private let sessionQueue = DispatchQueue(label: "session queue", attributes: [], target: nil) // Communicate with the session and other session objects on this queue. + + private var setupResult: SessionSetupResult = .success + + var videoDeviceInput: AVCaptureDeviceInput! + + @IBOutlet private var previewView: PreviewView! + + // Call this on the session queue. + private func configureSession() { + if self.setupResult != .success { + return + } + + session.beginConfiguration() + + // Add video input. + do { + var defaultVideoDevice: AVCaptureDevice? + + // Choose the back dual camera if available, otherwise default to a wide angle camera. + if let dualCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInDuoCamera, mediaType: AVMediaTypeVideo, position: .back) { + defaultVideoDevice = dualCameraDevice + } + else if let backCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .back) { + // If the back dual camera is not available, default to the back wide angle camera. + defaultVideoDevice = backCameraDevice + } + else if let frontCameraDevice = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .front) { + // In some cases where users break their phones, the back wide angle camera is not available. In this case, we should default to the front wide angle camera. + defaultVideoDevice = frontCameraDevice + } + + let videoDeviceInput = try AVCaptureDeviceInput(device: defaultVideoDevice) + + if session.canAddInput(videoDeviceInput) { + session.addInput(videoDeviceInput) + self.videoDeviceInput = videoDeviceInput + + DispatchQueue.main.async { + /* + Why are we dispatching this to the main queue? + Because AVCaptureVideoPreviewLayer is the backing layer for PreviewView and UIView + can only be manipulated on the main thread. + Note: As an exception to the above rule, it is not necessary to serialize video orientation changes + on the AVCaptureVideoPreviewLayer’s connection with other session manipulation. + + Use the status bar orientation as the initial video orientation. Subsequent orientation changes are + handled by CameraViewController.viewWillTransition(to:with:). + */ + let statusBarOrientation = UIApplication.shared.statusBarOrientation + var initialVideoOrientation: AVCaptureVideoOrientation = .portrait + if statusBarOrientation != .unknown { + if let videoOrientation = statusBarOrientation.videoOrientation { + initialVideoOrientation = videoOrientation + } + } + + self.previewView.videoPreviewLayer.connection.videoOrientation = initialVideoOrientation + } + } + else { + print("Could not add video device input to the session") + setupResult = .configurationFailed + session.commitConfiguration() + return + } + } + catch { + print("Could not create video device input: \(error)") + setupResult = .configurationFailed + session.commitConfiguration() + return + } + + // Add metadata output. + if session.canAddOutput(metadataOutput) { + session.addOutput(metadataOutput) + + // Set this view controller as the delegate for metadata objects. + metadataOutput.setMetadataObjectsDelegate(self, queue: metadataObjectsQueue) + metadataOutput.metadataObjectTypes = metadataOutput.availableMetadataObjectTypes // Use all metadata object types by default. + metadataOutput.rectOfInterest = CGRect.zero + } + else { + print("Could not add metadata output to the session") + setupResult = .configurationFailed + session.commitConfiguration() + return + } + + session.commitConfiguration() + } + + private let metadataOutput = AVCaptureMetadataOutput() + + private let metadataObjectsQueue = DispatchQueue(label: "metadata objects queue", attributes: [], target: nil) + + @IBOutlet private var sessionPresetsButton: UIButton! + + private func availableSessionPresets() -> [String] { + let allSessionPresets = [AVCaptureSessionPresetPhoto, + AVCaptureSessionPresetLow, + AVCaptureSessionPresetMedium, + AVCaptureSessionPresetHigh, + AVCaptureSessionPreset352x288, + AVCaptureSessionPreset640x480, + AVCaptureSessionPreset1280x720, + AVCaptureSessionPresetiFrame960x540, + AVCaptureSessionPresetiFrame1280x720, + AVCaptureSessionPreset1920x1080, + AVCaptureSessionPreset3840x2160] + + var availableSessionPresets = [String]() + for sessionPreset in allSessionPresets { + if session.canSetSessionPreset(sessionPreset) { + availableSessionPresets.append(sessionPreset) + } + } + + return availableSessionPresets + } + + // MARK: Device Configuration + + @IBOutlet private var cameraButton: UIButton! + + @IBOutlet private var cameraUnavailableLabel: UILabel! + + private let videoDeviceDiscoverySession = AVCaptureDeviceDiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInDuoCamera], mediaType: AVMediaTypeVideo, position: .unspecified)! + + @IBAction private func changeCamera() { + metadataObjectTypesButton.isEnabled = false + sessionPresetsButton.isEnabled = false + cameraButton.isEnabled = false + zoomSlider.isEnabled = false + + // Remove the metadata overlay layers, if any. + removeMetadataObjectOverlayLayers() + + DispatchQueue.main.async { [unowned self] in + let currentVideoDevice = self.videoDeviceInput.device + let currentPosition = currentVideoDevice!.position + + let preferredPosition: AVCaptureDevicePosition + let preferredDeviceType: AVCaptureDeviceType + + switch currentPosition { + case .unspecified, .front: + preferredPosition = .back + preferredDeviceType = .builtInDuoCamera + + case .back: + preferredPosition = .front + preferredDeviceType = .builtInWideAngleCamera + } + + let devices = self.videoDeviceDiscoverySession.devices! + var newVideoDevice: AVCaptureDevice? = nil + + // First, look for a device with both the preferred position and device type. Otherwise, look for a device with only the preferred position. + if let device = devices.filter({ $0.position == preferredPosition && $0.deviceType == preferredDeviceType }).first { + newVideoDevice = device + } + else if let device = devices.filter({ $0.position == preferredPosition }).first { + newVideoDevice = device + } + + if let videoDevice = newVideoDevice { + do { + let videoDeviceInput = try AVCaptureDeviceInput.init(device: videoDevice) + + self.session.beginConfiguration() + + // Remove the existing device input first, since using the front and back camera simultaneously is not supported. + self.session.removeInput(self.videoDeviceInput) + + /* + When changing devices, a session preset that may be supported + on one device may not be supported by another. To allow the + user to successfully switch devices, we must save the previous + session preset, set the default session preset (High), and + attempt to restore it after the new video device has been + added. For example, the 4K session preset is only supported + by the back device on the iPhone 6s and iPhone 6s Plus. As a + result, the session will not let us add a video device that + does not support the current session preset. + */ + let previousSessionPreset = self.session.sessionPreset + self.session.sessionPreset = AVCaptureSessionPresetHigh + + if self.session.canAddInput(videoDeviceInput) { + self.session.addInput(videoDeviceInput) + self.videoDeviceInput = videoDeviceInput + } + else { + self.session.addInput(self.videoDeviceInput) + } + + // Restore the previous session preset if we can. + if self.session.canSetSessionPreset(previousSessionPreset) { + self.session.sessionPreset = previousSessionPreset + } + + self.session.commitConfiguration() + } + catch { + print("Error occured while creating video device input: \(error)") + } + } + + DispatchQueue.main.async { [unowned self] in + self.metadataObjectTypesButton.isEnabled = true + self.sessionPresetsButton.isEnabled = true + self.cameraButton.isEnabled = self.videoDeviceDiscoverySession.uniqueDevicePositionsCount() > 1 + self.zoomSlider.isEnabled = true + self.zoomSlider.maximumValue = Float(min(self.videoDeviceInput.device.activeFormat.videoMaxZoomFactor, CGFloat(8.0))) + self.zoomSlider.value = Float(self.videoDeviceInput.device.videoZoomFactor) + } + } + } + + @IBOutlet private var zoomSlider: UISlider! + + @IBAction private func zoomCamera(with zoomSlider: UISlider) { + do { + try videoDeviceInput.device.lockForConfiguration() + videoDeviceInput.device.videoZoomFactor = CGFloat(zoomSlider.value) + videoDeviceInput.device.unlockForConfiguration() + } + catch { + print("Could not lock for configuration: \(error)") + } + } + + // MARK: KVO and Notifications + + private var sessionRunningObserveContext = 0 + + private var previewViewRegionOfInterestObserveContext = 0 + + private func addObservers() { + session.addObserver(self, forKeyPath: "running", options: .new, context: &sessionRunningObserveContext) + /* + Observe the previewView's regionOfInterest to update the AVCaptureMetadataOutput's + rectOfInterest when the user finishes resizing the region of interest. + */ + previewView.addObserver(self, forKeyPath: "regionOfInterest", options: .new, context: &previewViewRegionOfInterestObserveContext) + + NotificationCenter.default.addObserver(self, selector: #selector(sessionRuntimeError), name: Notification.Name("AVCaptureSessionRuntimeErrorNotification"), object: session) + + /* + A session can only run when the app is full screen. It will be interrupted + in a multi-app layout, introduced in iOS 9, see also the documentation of + AVCaptureSessionInterruptionReason. Add observers to handle these session + interruptions and show a preview is paused message. See the documentation + of AVCaptureSessionWasInterruptedNotification for other interruption reasons. + */ + NotificationCenter.default.addObserver(self, selector: #selector(sessionWasInterrupted), name: Notification.Name("AVCaptureSessionWasInterruptedNotification"), object: session) + NotificationCenter.default.addObserver(self, selector: #selector(sessionInterruptionEnded), name: Notification.Name("AVCaptureSessionInterruptionEndedNotification"), object: session) + } + + private func removeObservers() { + NotificationCenter.default.removeObserver(self) + + session.removeObserver(self, forKeyPath: "running", context: &sessionRunningObserveContext) + previewView.removeObserver(self, forKeyPath: "regionOfInterest", context: &previewViewRegionOfInterestObserveContext) + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + let newValue = change?[.newKey] as AnyObject? + + if context == &sessionRunningObserveContext { + guard let isSessionRunning = newValue?.boolValue else { return } + + DispatchQueue.main.async { [unowned self] in + self.metadataObjectTypesButton.isEnabled = isSessionRunning + self.sessionPresetsButton.isEnabled = isSessionRunning + self.cameraButton.isEnabled = isSessionRunning && self.videoDeviceDiscoverySession.uniqueDevicePositionsCount() > 1 + self.zoomSlider.isEnabled = isSessionRunning + self.zoomSlider.maximumValue = Float(min(self.videoDeviceInput.device.activeFormat.videoMaxZoomFactor, CGFloat(8.0))) + self.zoomSlider.value = Float(self.videoDeviceInput.device.videoZoomFactor) + + /* + After the session stop running, remove the metadata object overlays, + if any, so that if the view appears again, the previously displayed + metadata object overlays are removed. + */ + if !isSessionRunning { + self.removeMetadataObjectOverlayLayers() + } + } + } + else if context == &previewViewRegionOfInterestObserveContext { + guard let regionOfInterest = newValue?.cgRectValue else { return } + + // Update the AVCaptureMetadataOutput with the new region of interest. + sessionQueue.async { + // Translate the preview view's region of interest to the metadata output's coordinate system. + self.metadataOutput.rectOfInterest = self.previewView.videoPreviewLayer.metadataOutputRectOfInterest(for: regionOfInterest) + + // Ensure we are not drawing old metadata object overlays. + DispatchQueue.main.async { [unowned self] in + self.removeMetadataObjectOverlayLayers() + } + } + } + else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + } + } + + func sessionRuntimeError(notification: NSNotification) { + guard let errorValue = notification.userInfo?[AVCaptureSessionErrorKey] as? NSError else { return } + + let error = AVError(_nsError: errorValue) + print("Capture session runtime error: \(error)") + + /* + Automatically try to restart the session running if media services were + reset and the last start running succeeded. Otherwise, enable the user + to try to resume the session running. + */ + if error.code == .mediaServicesWereReset { + sessionQueue.async { [unowned self] in + if self.isSessionRunning { + self.session.startRunning() + self.isSessionRunning = self.session.isRunning + } + } + } + } + + func sessionWasInterrupted(notification: NSNotification) { + /* + In some scenarios we want to enable the user to resume the session running. + For example, if music playback is initiated via control center while + using AVCamBarcode, then the user can let AVCamBarcode resume + the session running, which will stop music playback. Note that stopping + music playback in control center will not automatically resume the session + running. Also note that it is not always possible to resume, see `resumeInterruptedSession(_:)`. + */ + if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?, let reasonIntegerValue = userInfoValue.integerValue, let reason = AVCaptureSessionInterruptionReason(rawValue: reasonIntegerValue) { + print("Capture session was interrupted with reason \(reason)") + + if reason == AVCaptureSessionInterruptionReason.videoDeviceNotAvailableWithMultipleForegroundApps { + // Simply fade-in a label to inform the user that the camera is unavailable. + self.cameraUnavailableLabel.isHidden = false + self.cameraUnavailableLabel.alpha = 0 + UIView.animate(withDuration: 0.25) { + self.cameraUnavailableLabel.alpha = 1 + } + } + } + } + + func sessionInterruptionEnded(notification: NSNotification) { + print("Capture session interruption ended") + + if cameraUnavailableLabel.isHidden { + UIView.animate(withDuration: 0.25, + animations: { [unowned self] in + self.cameraUnavailableLabel.alpha = 0 + }, completion: { [unowned self] finished in + self.cameraUnavailableLabel.isHidden = true + } + ) + } + } + + // MARK: Drawing Metadata Object Overlay Layers + + @IBOutlet private var metadataObjectTypesButton: UIButton! + + private class MetadataObjectLayer: CAShapeLayer { + var metadataObject: AVMetadataObject? + } + + /** + A dispatch semaphore is used for drawing metadata object overlays so that + only one group of metadata object overlays is drawn at a time. + */ + private let metadataObjectsOverlayLayersDrawingSemaphore = DispatchSemaphore(value: 1) + + private var metadataObjectOverlayLayers = [MetadataObjectLayer]() + + private func createMetadataObjectOverlayWithMetadataObject(_ metadataObject: AVMetadataObject) -> MetadataObjectLayer { + // Transform the metadata object so the bounds are updated to reflect those of the video preview layer. + let transformedMetadataObject = previewView.videoPreviewLayer.transformedMetadataObject(for: metadataObject) + + // Create the initial metadata object overlay layer that can be used for either machine readable codes or faces. + let metadataObjectOverlayLayer = MetadataObjectLayer() + metadataObjectOverlayLayer.metadataObject = transformedMetadataObject + metadataObjectOverlayLayer.lineJoin = kCALineJoinRound + metadataObjectOverlayLayer.lineWidth = 7.0 + metadataObjectOverlayLayer.strokeColor = view.tintColor.withAlphaComponent(0.7).cgColor + metadataObjectOverlayLayer.fillColor = view.tintColor.withAlphaComponent(0.3).cgColor + + if transformedMetadataObject is AVMetadataMachineReadableCodeObject { + let barcodeMetadataObject = transformedMetadataObject as! AVMetadataMachineReadableCodeObject + + let barcodeOverlayPath = barcodeOverlayPathWithCorners(barcodeMetadataObject.corners as! [CFDictionary]) + metadataObjectOverlayLayer.path = barcodeOverlayPath + + // If the metadata object has a string value, display it. + if barcodeMetadataObject.stringValue.characters.count > 0 { + let barcodeOverlayBoundingBox = barcodeOverlayPath.boundingBox + + let textLayer = CATextLayer() + textLayer.alignmentMode = kCAAlignmentCenter + textLayer.bounds = CGRect(x: 0.0, y: 0.0, width: barcodeOverlayBoundingBox.size.width, height: barcodeOverlayBoundingBox.size.height) + textLayer.contentsScale = UIScreen.main.scale + textLayer.font = UIFont.boldSystemFont(ofSize: 19).fontName as CFString + textLayer.position = CGPoint(x: barcodeOverlayBoundingBox.midX, y: barcodeOverlayBoundingBox.midY) + textLayer.string = NSAttributedString(string: barcodeMetadataObject.stringValue, attributes: [ + NSFontAttributeName : UIFont.boldSystemFont(ofSize: 19), + kCTForegroundColorAttributeName as String : UIColor.white.cgColor, + kCTStrokeWidthAttributeName as String : -5.0, + kCTStrokeColorAttributeName as String : UIColor.black.cgColor]) + textLayer.isWrapped = true + + // Invert the effect of transform of the video preview so the text is orientated with the interface orientation. + textLayer.transform = CATransform3DInvert(CATransform3DMakeAffineTransform(previewView.transform)) + + metadataObjectOverlayLayer.addSublayer(textLayer) + } + } + else if transformedMetadataObject is AVMetadataFaceObject { + metadataObjectOverlayLayer.path = CGPath(rect: transformedMetadataObject!.bounds, transform: nil) + } + + return metadataObjectOverlayLayer + } + + private func barcodeOverlayPathWithCorners(_ corners: [CFDictionary]) -> CGMutablePath { + let path = CGMutablePath() + + if !corners.isEmpty { + guard let corner = CGPoint(dictionaryRepresentation: corners[0]) else { return path } + path.move(to: corner, transform: .identity) + + for cornerDictionary in corners { + guard let corner = CGPoint(dictionaryRepresentation: cornerDictionary) else { return path } + path.addLine(to: corner) + } + + path.closeSubpath() + } + + return path + } + + private var removeMetadataObjectOverlayLayersTimer: Timer? + + @objc private func removeMetadataObjectOverlayLayers() { + for sublayer in metadataObjectOverlayLayers { + sublayer.removeFromSuperlayer() + } + metadataObjectOverlayLayers = [] + + removeMetadataObjectOverlayLayersTimer?.invalidate() + removeMetadataObjectOverlayLayersTimer = nil + } + + private func addMetadataObjectOverlayLayersToVideoPreviewView(_ metadataObjectOverlayLayers: [MetadataObjectLayer]) { + // Add the metadata object overlays as sublayers of the video preview layer. We disable actions to allow for fast drawing. + CATransaction.begin() + CATransaction.setDisableActions(true) + for metadataObjectOverlayLayer in metadataObjectOverlayLayers { + previewView.videoPreviewLayer.addSublayer(metadataObjectOverlayLayer) + } + CATransaction.commit() + + // Save the new metadata object overlays. + self.metadataObjectOverlayLayers = metadataObjectOverlayLayers + + // Create a timer to destroy the metadata object overlays. + removeMetadataObjectOverlayLayersTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(removeMetadataObjectOverlayLayers), userInfo: nil, repeats: false) + } + + private lazy var openBarcodeURLGestureRecognizer: UITapGestureRecognizer = { + UITapGestureRecognizer(target: self, action: #selector(CameraViewController.openBarcodeURL(with:))) + }() + + @objc private func openBarcodeURL(with openBarcodeURLGestureRecognizer: UITapGestureRecognizer) { + for metadataObjectOverlayLayer in metadataObjectOverlayLayers { + if metadataObjectOverlayLayer.path!.contains(openBarcodeURLGestureRecognizer.location(in: previewView), using: .winding, transform: .identity) { + if let barcodeMetadataObject = metadataObjectOverlayLayer.metadataObject as? AVMetadataMachineReadableCodeObject { + if barcodeMetadataObject.stringValue != nil { + if let url = URL(string: barcodeMetadataObject.stringValue), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + } + } + } + } + + // MARK: AVCaptureMetadataOutputObjectsDelegate + + func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputMetadataObjects metadataObjects: [Any]!, from connection: AVCaptureConnection!) { + // wait() is used to drop new notifications if old ones are still processing, to avoid queueing up a bunch of stale data. + if metadataObjectsOverlayLayersDrawingSemaphore.wait(timeout: DispatchTime.now()) == .success { + DispatchQueue.main.async { [unowned self] in + self.removeMetadataObjectOverlayLayers() + + var metadataObjectOverlayLayers = [MetadataObjectLayer]() + for metadataObject in metadataObjects as! [AVMetadataObject] { + let metadataObjectOverlayLayer = self.createMetadataObjectOverlayWithMetadataObject(metadataObject) + metadataObjectOverlayLayers.append(metadataObjectOverlayLayer) + } + + self.addMetadataObjectOverlayLayersToVideoPreviewView(metadataObjectOverlayLayers) + + self.metadataObjectsOverlayLayersDrawingSemaphore.signal() + } + } + } + + // MARK: ItemSelectionViewControllerDelegate + + let metadataObjectTypeItemSelectionIdentifier = "MetadataObjectTypes" + + let sessionPresetItemSelectionIdentifier = "SessionPreset" + + func itemSelectionViewController(_ itemSelectionViewController: ItemSelectionViewController, didFinishSelectingItems selectedItems: [String]) { + let identifier = itemSelectionViewController.identifier + + if identifier == metadataObjectTypeItemSelectionIdentifier { + sessionQueue.async { [unowned self] in + self.metadataOutput.metadataObjectTypes = selectedItems + } + } + else if identifier == sessionPresetItemSelectionIdentifier { + sessionQueue.async { [unowned self] in + self.session.sessionPreset = selectedItems.first + } + } + } +} + +extension AVCaptureDeviceDiscoverySession +{ + func uniqueDevicePositionsCount() -> Int { + var uniqueDevicePositions = [AVCaptureDevicePosition]() + + for device in devices { + if !uniqueDevicePositions.contains(device.position) { + uniqueDevicePositions.append(device.position) + } + } + + return uniqueDevicePositions.count + } +} + +extension UIDeviceOrientation { + var videoOrientation: AVCaptureVideoOrientation? { + switch self { + case .portrait: return .portrait + case .portraitUpsideDown: return .portraitUpsideDown + case .landscapeLeft: return .landscapeRight + case .landscapeRight: return .landscapeLeft + default: return nil + } + } +} + +extension UIInterfaceOrientation { + var videoOrientation: AVCaptureVideoOrientation? { + switch self { + case .portrait: return .portrait + case .portraitUpsideDown: return .portraitUpsideDown + case .landscapeLeft: return .landscapeLeft + case .landscapeRight: return .landscapeRight + default: return nil + } + } +} diff --git a/AVCamBarcode/AVCamBarcode/Info.plist b/AVCamBarcode/AVCamBarcode/Info.plist new file mode 100644 index 00000000..cf472858 --- /dev/null +++ b/AVCamBarcode/AVCamBarcode/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSCameraUsageDescription + to scan barcodes and faces + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/AVCamBarcode/AVCamBarcode/ItemSelectionViewController.swift b/AVCamBarcode/AVCamBarcode/ItemSelectionViewController.swift new file mode 100644 index 00000000..8e0992ea --- /dev/null +++ b/AVCamBarcode/AVCamBarcode/ItemSelectionViewController.swift @@ -0,0 +1,89 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller for selecting items. +*/ + +import UIKit + +protocol ItemSelectionViewControllerDelegate: class { + func itemSelectionViewController(_ itemSelectionViewController: ItemSelectionViewController, didFinishSelectingItems selectedItems: [String]) +} + +class ItemSelectionViewController: UITableViewController { + weak var delegate: ItemSelectionViewControllerDelegate? + + var identifier = "" + + var allItems = [String]() + + var selectedItems = [String]() + + var allowsMultipleSelection = false + + @IBAction private func done() { + // Notify the delegate that selecting items is finished. + delegate?.itemSelectionViewController(self, didFinishSelectingItems: selectedItems) + + // Dismiss the view controller. + dismiss(animated: true, completion: nil) + } + + // MARK: UITableViewDataSource + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let item = allItems[indexPath.row] + + let cell = tableView.dequeueReusableCell(withIdentifier: "Item", for: indexPath) + cell.tintColor = UIColor.black + cell.textLabel?.text = item + + if selectedItems.contains(item) { + cell.accessoryType = .checkmark + } + else { + cell.accessoryType = .none + } + + return cell + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return allItems.count + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if allowsMultipleSelection { + let item = allItems[indexPath.row] + + if selectedItems.contains(item) { + selectedItems = selectedItems.filter({ $0 != item }) + } + else { + selectedItems.append(item) + } + + tableView.deselectRow(at: indexPath, animated: true) + tableView.reloadRows(at: [indexPath], with: .automatic) + } + else { + let indexPathsToReload: [IndexPath] + if selectedItems.count > 0 { + indexPathsToReload = [indexPath, IndexPath(row: allItems.index(of: selectedItems[0])!, section: 0)] + } + else { + indexPathsToReload = [indexPath] + } + + selectedItems = [allItems[indexPath.row]] + + // Deselect the selected row & reload the table view cells for the old and new items to swap checkmarks. + tableView.deselectRow(at: indexPath, animated: true) + tableView.reloadRows(at: indexPathsToReload, with: .automatic) + } + } +} diff --git a/AVCamBarcode/AVCamBarcode/PreviewView.swift b/AVCamBarcode/AVCamBarcode/PreviewView.swift new file mode 100644 index 00000000..54e04cfc --- /dev/null +++ b/AVCamBarcode/AVCamBarcode/PreviewView.swift @@ -0,0 +1,449 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application preview view. +*/ + +import UIKit +import AVFoundation + +class PreviewView: UIView, UIGestureRecognizerDelegate { + // MARK: Types + private enum ControlCorner { + case none + case topLeft + case topRight + case bottomLeft + case bottomRight + } + + // MARK: Initialization + + override init(frame: CGRect) { + super.init(frame: frame) + + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + commonInit() + } + + private func commonInit() { + maskLayer.fillRule = kCAFillRuleEvenOdd + maskLayer.fillColor = UIColor.black.cgColor + maskLayer.opacity = 0.6 + layer.addSublayer(maskLayer) + + regionOfInterestOutline.path = UIBezierPath(rect: regionOfInterest).cgPath + regionOfInterestOutline.fillColor = UIColor.clear.cgColor + regionOfInterestOutline.strokeColor = UIColor.yellow.cgColor + layer.addSublayer(regionOfInterestOutline) + + topLeftControl.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: regionOfInterestControlDiameter, height: regionOfInterestControlDiameter)).cgPath + topLeftControl.fillColor = UIColor.white.cgColor + layer.addSublayer(topLeftControl) + + topRightControl.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: regionOfInterestControlDiameter, height: regionOfInterestControlDiameter)).cgPath + topRightControl.fillColor = UIColor.white.cgColor + layer.addSublayer(topRightControl) + + bottomLeftControl.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: regionOfInterestControlDiameter, height: regionOfInterestControlDiameter)).cgPath + bottomLeftControl.fillColor = UIColor.white.cgColor + layer.addSublayer(bottomLeftControl) + + bottomRightControl.path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: regionOfInterestControlDiameter, height: regionOfInterestControlDiameter)).cgPath + bottomRightControl.fillColor = UIColor.white.cgColor + layer.addSublayer(bottomRightControl) + + /* + Add the region of interest gesture recognizer to the region of interest + view so that the region of interest can be resized and moved. If you + would like to have a fixed region of interest that cannot be resized + or moved, do not add the following gesture recognizer. You will simply + need to set the region of interest once in + `observeValue(forKeyPath:, of:, change:, context:)`. + */ + resizeRegionOfInterestGestureRecognizer.delegate = self + addGestureRecognizer(resizeRegionOfInterestGestureRecognizer) + } + + deinit { + session?.removeObserver(self, forKeyPath: "running", context: &sessionRunningObserveContext) + } + + // MARK: AV capture properties + + var videoPreviewLayer: AVCaptureVideoPreviewLayer { + return layer as! AVCaptureVideoPreviewLayer + } + + var session: AVCaptureSession? { + get { + return videoPreviewLayer.session + } + + set{ + if let newValue = newValue { + newValue.addObserver(self, forKeyPath: "running", options: .new, context: &sessionRunningObserveContext) + } + else { + session?.removeObserver(self, forKeyPath: "running", context: &sessionRunningObserveContext) + } + + videoPreviewLayer.session = newValue + } + } + + // MARK: Region of Interest + + private let regionOfInterestCornerTouchThreshold: CGFloat = 50 + + /** + The minimum region of interest's size cannot be smaller than the corner + touch threshold as to avoid control selection conflicts when a user tries + to resize the region of interest. + */ + private var minimumRegionOfInterestSize: CGFloat { + return regionOfInterestCornerTouchThreshold + } + + private let regionOfInterestControlDiameter: CGFloat = 12.0 + + private var regionOfInterestControlRadius: CGFloat { + return regionOfInterestControlDiameter / 2.0 + } + + private let maskLayer = CAShapeLayer() + + private let regionOfInterestOutline = CAShapeLayer() + + /** + Saves a reference to the control corner that the user is using to resize + the region of interest in `resizeRegionOfInterestWithGestureRecognizer()`. + */ + private var currentControlCorner: ControlCorner = .none + + /// White dot on the top left of the region of interest. + private let topLeftControl = CAShapeLayer() + + /// White dot on the top right of the region of interest. + private let topRightControl = CAShapeLayer() + + /// White dot on the bottom left of the region of interest. + private let bottomLeftControl = CAShapeLayer() + + /// White dot on the bottom right of the region of interest. + private let bottomRightControl = CAShapeLayer() + + /** + This property is set only in `setRegionOfInterestWithProposedRegionOfInterest()`. + When a user is resizing the region of interest in `resizeRegionOfInterestWithGestureRecognizer()`, + the KVO notification will be triggered when the resizing is finished. + */ + private(set) var regionOfInterest = CGRect.null + + /** + Updates the region of interest with a proposed region of interest ensuring + the new region of interest is within the bounds of the video preview. When + a new region of interest is set, the region of interest is redrawn. + */ + func setRegionOfInterestWithProposedRegionOfInterest(_ proposedRegionOfInterest: CGRect) + { + // We standardize to ensure we have positive widths and heights with an origin at the top left. + let videoPreviewRect = videoPreviewLayer.rectForMetadataOutputRect(ofInterest: CGRect(x: 0, y: 0, width: 1, height: 1)).standardized + + /* + Intersect the video preview view with the view's frame to only get + the visible portions of the video preview view. + */ + let visibleVideoPreviewRect = videoPreviewRect.intersection(frame) + let oldRegionOfInterest = regionOfInterest + var newRegionOfInterest = proposedRegionOfInterest.standardized + + // Move the region of interest in bounds. + if currentControlCorner == .none { + var xOffset: CGFloat = 0 + var yOffset: CGFloat = 0 + + if !visibleVideoPreviewRect.contains(newRegionOfInterest.origin) { + xOffset = max(visibleVideoPreviewRect.minX - newRegionOfInterest.minX, CGFloat(0)) + yOffset = max(visibleVideoPreviewRect.minY - newRegionOfInterest.minY, CGFloat(0)) + } + + if !visibleVideoPreviewRect.contains(CGPoint(x: visibleVideoPreviewRect.maxX, y: visibleVideoPreviewRect.maxY)) { + xOffset = min(visibleVideoPreviewRect.maxX - newRegionOfInterest.maxX, xOffset) + yOffset = min(visibleVideoPreviewRect.maxY - newRegionOfInterest.maxY, yOffset) + } + + newRegionOfInterest = newRegionOfInterest.offsetBy(dx: xOffset, dy: yOffset) + } + + // Clamp the size when the region of interest is being resized. + newRegionOfInterest = visibleVideoPreviewRect.intersection(newRegionOfInterest) + + // Fix a minimum width of the region of interest. + if proposedRegionOfInterest.size.width < minimumRegionOfInterestSize { + switch currentControlCorner { + case .topLeft, .bottomLeft: + newRegionOfInterest.origin.x = oldRegionOfInterest.origin.x + oldRegionOfInterest.size.width - minimumRegionOfInterestSize + newRegionOfInterest.size.width = minimumRegionOfInterestSize + + case .topRight: + newRegionOfInterest.origin.x = oldRegionOfInterest.origin.x + newRegionOfInterest.size.width = minimumRegionOfInterestSize + + default: + newRegionOfInterest.origin = oldRegionOfInterest.origin + newRegionOfInterest.size.width = minimumRegionOfInterestSize + } + } + + // Fix a minimum height of the region of interest. + if proposedRegionOfInterest.size.height < minimumRegionOfInterestSize { + switch currentControlCorner { + case .topLeft, .topRight: + newRegionOfInterest.origin.y = oldRegionOfInterest.origin.y + oldRegionOfInterest.size.height - minimumRegionOfInterestSize + newRegionOfInterest.size.height = minimumRegionOfInterestSize + + case .bottomLeft: + newRegionOfInterest.origin.y = oldRegionOfInterest.origin.y + newRegionOfInterest.size.height = minimumRegionOfInterestSize + + default: + newRegionOfInterest.origin = oldRegionOfInterest.origin + newRegionOfInterest.size.height = minimumRegionOfInterestSize + } + } + + regionOfInterest = newRegionOfInterest + setNeedsLayout() + } + + var isResizingRegionOfInterest: Bool { + return resizeRegionOfInterestGestureRecognizer.state == .changed + } + + private lazy var resizeRegionOfInterestGestureRecognizer: UIPanGestureRecognizer = { + UIPanGestureRecognizer(target: self, action: #selector(PreviewView.resizeRegionOfInterestWithGestureRecognizer(_:))) + }() + + @objc func resizeRegionOfInterestWithGestureRecognizer(_ resizeRegionOfInterestGestureRecognizer: UIPanGestureRecognizer) { + let touchLocation = resizeRegionOfInterestGestureRecognizer.location(in: resizeRegionOfInterestGestureRecognizer.view) + let oldRegionOfInterest = regionOfInterest + + switch resizeRegionOfInterestGestureRecognizer.state { + case .began: + willChangeValue(forKey: "regionOfInterest") + + /* + When the gesture begins, save the corner that is closes to + the resize region of interest gesture recognizer's touch location. + */ + currentControlCorner = cornerOfRect(oldRegionOfInterest, closestToPointWithinTouchThreshold: touchLocation) + + case .changed: + var newRegionOfInterest = oldRegionOfInterest + + switch currentControlCorner { + case .none: + // Update the new region of interest with the gesture recognizer's translation. + let translation = resizeRegionOfInterestGestureRecognizer.translation(in: resizeRegionOfInterestGestureRecognizer.view) + + // Move the region of interest with the gesture recognizer's translation. + if regionOfInterest.contains(touchLocation) { + newRegionOfInterest.origin.x += translation.x + newRegionOfInterest.origin.y += translation.y + } + + /* + If the touch location goes outside the preview layer, + we will only translate the region of interest in the + plane that is not out of bounds. + */ + let normalizedRect = CGRect(x: 0, y: 0, width: 1, height: 1) + if !normalizedRect.contains(videoPreviewLayer.captureDevicePointOfInterest(for: touchLocation)) { + if touchLocation.x < regionOfInterest.minX || touchLocation.x > regionOfInterest.maxX { + newRegionOfInterest.origin.y += translation.y + } + else if touchLocation.y < regionOfInterest.minY || touchLocation.y > regionOfInterest.maxY { + newRegionOfInterest.origin.x += translation.x + } + } + + /* + Set the translation to be zero so that the new gesture + recognizer's translation is in respect to the region of + interest's new position. + */ + resizeRegionOfInterestGestureRecognizer.setTranslation(CGPoint.zero, in: resizeRegionOfInterestGestureRecognizer.view) + + case .topLeft: + newRegionOfInterest = CGRect(x: touchLocation.x, + y: touchLocation.y, + width: oldRegionOfInterest.size.width + oldRegionOfInterest.origin.x - touchLocation.x, + height: oldRegionOfInterest.size.height + oldRegionOfInterest.origin.y - touchLocation.y) + + case .topRight: + newRegionOfInterest = CGRect(x: newRegionOfInterest.origin.x, + y: touchLocation.y, + width: touchLocation.x - newRegionOfInterest.origin.x, + height: oldRegionOfInterest.size.height + newRegionOfInterest.origin.y - touchLocation.y) + + case .bottomLeft: + newRegionOfInterest = CGRect(x: touchLocation.x, + y: oldRegionOfInterest.origin.y, + width: oldRegionOfInterest.size.width + oldRegionOfInterest.origin.x - touchLocation.x, + height: touchLocation.y - oldRegionOfInterest.origin.y) + + case .bottomRight: + newRegionOfInterest = CGRect(x: oldRegionOfInterest.origin.x, + y: oldRegionOfInterest.origin.y, + width: touchLocation.x - oldRegionOfInterest.origin.x, + height: touchLocation.y - oldRegionOfInterest.origin.y) + } + + // Update the region of intresest with a valid CGRect. + setRegionOfInterestWithProposedRegionOfInterest(newRegionOfInterest) + + case .ended: + didChangeValue(forKey: "regionOfInterest") + + /* + Reset the current corner reference to none now that the resize. + gesture recognizer has ended. + */ + currentControlCorner = .none + + default: + return + } + } + + private func cornerOfRect(_ rect: CGRect, closestToPointWithinTouchThreshold point: CGPoint) -> ControlCorner { + var closestDistance = CGFloat.greatestFiniteMagnitude + var closestCorner: ControlCorner = .none + let corners: [(ControlCorner, CGPoint)] = [(.topLeft, rect.origin), + (.topRight, CGPoint(x: rect.maxX, y: rect.minY)), + (.bottomLeft, CGPoint(x: rect.minX, y: rect.maxY)), + (.bottomRight, CGPoint(x: rect.maxX, y: rect.maxY))] + + for (corner, cornerPoint) in corners { + let dX = point.x - cornerPoint.x + let dY = point.y - cornerPoint.y + let distance = sqrt((dX * dX) + (dY * dY)) + + if distance < closestDistance { + closestDistance = distance + closestCorner = corner + } + } + + if closestDistance > regionOfInterestCornerTouchThreshold { + closestCorner = .none + } + + return closestCorner + } + + // MARK: KVO + + var sessionRunningObserveContext = 0 + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if context == &sessionRunningObserveContext { + let newValue = change?[.newKey] as AnyObject? + guard let isSessionRunning = newValue?.boolValue else { return } + + DispatchQueue.main.async { [unowned self] in + /* + If the region of interest view's region of interest has not + been initialized yet, let's set an inital region of interest + that is 80% of the shortest side by 25% of the longest side + and centered in the root view. + */ + if self.regionOfInterest.isNull { + let width = min(self.frame.width, self.frame.height) * 0.8 + let height = max(self.frame.width, self.frame.height) * 0.25 + + let newRegionOfInterest = self.frame.insetBy(dx: self.frame.midX - width / 2.0, dy: self.frame.midY - height / 2.0) + self.setRegionOfInterestWithProposedRegionOfInterest(newRegionOfInterest) + } + + /* + If the region of interest view's region of interest has not + been initialized yet, let's set an inital region of interest + that is 80% of the shortest side by 25% of the longest side + and centered in the root view. + */ + if isSessionRunning { + self.setRegionOfInterestWithProposedRegionOfInterest(self.regionOfInterest) + } + } + } + else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + } + } + + // MARK: UIView + + override class var layerClass: AnyClass { + return AVCaptureVideoPreviewLayer.self + } + + override func layoutSubviews() { + super.layoutSubviews() + + // Disable CoreAnimation actions so that the positions of the sublayers immediately move to their new position. + CATransaction.begin() + CATransaction.setDisableActions(true) + + // Create the path for the mask layer. We use the even odd fill rule so that the region of interest does not have a fill color. + let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)) + path.append(UIBezierPath(rect: regionOfInterest)) + path.usesEvenOddFillRule = true + maskLayer.path = path.cgPath + + regionOfInterestOutline.path = CGPath(rect: regionOfInterest, transform: nil) + + topLeftControl.position = CGPoint(x: regionOfInterest.origin.x - regionOfInterestControlRadius, y: regionOfInterest.origin.y - regionOfInterestControlRadius) + topRightControl.position = CGPoint(x: regionOfInterest.origin.x + regionOfInterest.size.width - regionOfInterestControlRadius, y: regionOfInterest.origin.y - regionOfInterestControlRadius) + bottomLeftControl.position = CGPoint(x: regionOfInterest.origin.x - regionOfInterestControlRadius, y: regionOfInterest.origin.y + regionOfInterest.size.height - regionOfInterestControlRadius) + bottomRightControl.position = CGPoint(x: regionOfInterest.origin.x + regionOfInterest.size.width - regionOfInterestControlRadius, y: regionOfInterest.origin.y + regionOfInterest.size.height - regionOfInterestControlRadius) + + CATransaction.commit() + } + + // MARK: UIGestureRecognizerDelegate + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + // Ignore drags outside of the region of interest (plus some padding). + if gestureRecognizer == resizeRegionOfInterestGestureRecognizer { + let touchLocation = touch.location(in: gestureRecognizer.view) + + let paddedRegionOfInterest = regionOfInterest.insetBy(dx: -regionOfInterestCornerTouchThreshold, dy: -regionOfInterestCornerTouchThreshold) + if !paddedRegionOfInterest.contains(touchLocation) { + return false + } + } + + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + // Allow multiple gesture recognizers to be recognized simultaneously if and only if the touch location is not within the touch threshold. + if gestureRecognizer == resizeRegionOfInterestGestureRecognizer { + let touchLocation = gestureRecognizer.location(in: gestureRecognizer.view) + + let closestCorner = cornerOfRect(regionOfInterest, closestToPointWithinTouchThreshold: touchLocation) + return closestCorner == .none + } + + return false + } +} diff --git a/AVCamBarcode/LICENSE.txt b/AVCamBarcode/LICENSE.txt new file mode 100644 index 00000000..8052ac9e --- /dev/null +++ b/AVCamBarcode/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: AVCamBarcode: Using AVFoundation to Detect Barcodes and Faces +Version: 1.1 + +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) 2016 Apple Inc. All Rights Reserved. diff --git a/AVCamBarcode/README.md b/AVCamBarcode/README.md new file mode 100644 index 00000000..6729cb1c --- /dev/null +++ b/AVCamBarcode/README.md @@ -0,0 +1,15 @@ +# AVCamBarcode: Using AVFoundation to Detect Barcodes and Faces + +AVCamBarcode demonstrates how to use the AVFoundation capture API to detect barcodes and faces. + +## Requirements + +### Build + +Xcode 8.0, iOS 10.0 SDK + +### Runtime + +iOS 10.0 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/AVFoundationExporter/LICENSE.txt b/AVFoundationExporter/LICENSE.txt new file mode 100644 index 00000000..d5df14fe --- /dev/null +++ b/AVFoundationExporter/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: AVFoundationExporter: Exporting and Transcoding Movies +Version: 3.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) 2016 Apple Inc. All Rights Reserved. diff --git a/AVFoundationExporter/Objective-C/AVFoundationExporter.xcodeproj/project.pbxproj b/AVFoundationExporter/Objective-C/AVFoundationExporter.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ff6310dd --- /dev/null +++ b/AVFoundationExporter/Objective-C/AVFoundationExporter.xcodeproj/project.pbxproj @@ -0,0 +1,237 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1E1D58BC1368D74F00D93743 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E1D58BB1368D74F00D93743 /* Foundation.framework */; }; + 1E1D58BF1368D74F00D93743 /* AVFoundationExporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E1D58BE1368D74F00D93743 /* AVFoundationExporter.m */; }; + 1E1D58CF1368D7E600D93743 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E1D58CD1368D7E600D93743 /* CoreMedia.framework */; }; + 1E1D58D01368D7E600D93743 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E1D58CE1368D7E600D93743 /* AVFoundation.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 1E1D58B51368D74F00D93743 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1E1D58B71368D74F00D93743 /* AVFoundationExporter */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = AVFoundationExporter; sourceTree = BUILT_PRODUCTS_DIR; }; + 1E1D58BB1368D74F00D93743 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 1E1D58BE1368D74F00D93743 /* AVFoundationExporter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AVFoundationExporter.m; sourceTree = ""; }; + 1E1D58CD1368D7E600D93743 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = ""; }; + 1E1D58CE1368D7E600D93743 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = ""; }; + 3EAA11C51B1B895500EC0006 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1E1D58B41368D74F00D93743 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1E1D58BC1368D74F00D93743 /* Foundation.framework in Frameworks */, + 1E1D58CF1368D7E600D93743 /* CoreMedia.framework in Frameworks */, + 1E1D58D01368D7E600D93743 /* AVFoundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1E1D58AC1368D74F00D93743 = { + isa = PBXGroup; + children = ( + 3EAA11C51B1B895500EC0006 /* README.md */, + 1E1D58BD1368D74F00D93743 /* AVFoundationExporter */, + 1E1D58BA1368D74F00D93743 /* Frameworks */, + 1E1D58B81368D74F00D93743 /* Products */, + ); + sourceTree = ""; + }; + 1E1D58B81368D74F00D93743 /* Products */ = { + isa = PBXGroup; + children = ( + 1E1D58B71368D74F00D93743 /* AVFoundationExporter */, + ); + name = Products; + sourceTree = ""; + }; + 1E1D58BA1368D74F00D93743 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1E1D58BB1368D74F00D93743 /* Foundation.framework */, + 1E1D58CD1368D7E600D93743 /* CoreMedia.framework */, + 1E1D58CE1368D7E600D93743 /* AVFoundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1E1D58BD1368D74F00D93743 /* AVFoundationExporter */ = { + isa = PBXGroup; + children = ( + 1E1D58BE1368D74F00D93743 /* AVFoundationExporter.m */, + ); + path = AVFoundationExporter; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1E1D58B61368D74F00D93743 /* AVFoundationExporter */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1E1D58C61368D74F00D93743 /* Build configuration list for PBXNativeTarget "AVFoundationExporter" */; + buildPhases = ( + 1E1D58B31368D74F00D93743 /* Sources */, + 1E1D58B41368D74F00D93743 /* Frameworks */, + 1E1D58B51368D74F00D93743 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AVFoundationExporter; + productName = AVFoundationExporter; + productReference = 1E1D58B71368D74F00D93743 /* AVFoundationExporter */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1E1D58AE1368D74F00D93743 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = "Apple, Inc"; + }; + buildConfigurationList = 1E1D58B11368D74F00D93743 /* Build configuration list for PBXProject "AVFoundationExporter" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 1E1D58AC1368D74F00D93743; + productRefGroup = 1E1D58B81368D74F00D93743 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1E1D58B61368D74F00D93743 /* AVFoundationExporter */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 1E1D58B31368D74F00D93743 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1E1D58BF1368D74F00D93743 /* AVFoundationExporter.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 1E1D58C41368D74F00D93743 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_VERSION = ""; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.7; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + 1E1D58C51368D74F00D93743 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_VERSION = ""; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.7; + SDKROOT = macosx; + }; + name = Release; + }; + 1E1D58C71368D74F00D93743 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + COPY_PHASE_STRIP = NO; + DEFINES_MODULE = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = NO; + GCC_PREFIX_HEADER = ""; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = ""; + }; + name = Debug; + }; + 1E1D58C81368D74F00D93743 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = NO; + GCC_PREFIX_HEADER = ""; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1E1D58B11368D74F00D93743 /* Build configuration list for PBXProject "AVFoundationExporter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1E1D58C41368D74F00D93743 /* Debug */, + 1E1D58C51368D74F00D93743 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1E1D58C61368D74F00D93743 /* Build configuration list for PBXNativeTarget "AVFoundationExporter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1E1D58C71368D74F00D93743 /* Debug */, + 1E1D58C81368D74F00D93743 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 1E1D58AE1368D74F00D93743 /* Project object */; +} diff --git a/AVFoundationExporter/Objective-C/AVFoundationExporter/AVFoundationExporter.m b/AVFoundationExporter/Objective-C/AVFoundationExporter/AVFoundationExporter.m new file mode 100644 index 00000000..2a37e38f --- /dev/null +++ b/AVFoundationExporter/Objective-C/AVFoundationExporter/AVFoundationExporter.m @@ -0,0 +1,534 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This file shows an example of using the export and metadata functions in AVFoundation as a part of a command line tool for simple exports. +*/ + +@import Foundation; +@import AVFoundation; + +// --------------------------------------------------------------------------- +// Convenience Functions +// --------------------------------------------------------------------------- + +static void printNSString(NSString *string); +static void printArgs(int argc, const char **argv); + + +// --------------------------------------------------------------------------- +// AAPLExporter Class Interface +// --------------------------------------------------------------------------- +@interface AAPLExporter: NSObject { + NSString *programName; + NSString *exportType; + NSString *preset; + NSString *sourcePath; + NSString *destinationPath; + NSString *fileType; + NSNumber *progress; + NSNumber *startSeconds; + NSNumber *durationSeconds; + BOOL showProgress; + BOOL verbose; + BOOL exportFailed; + BOOL exportComplete; + BOOL listTracks; + BOOL listMetadata; + BOOL removePreExistingFiles; +} + +@property (copy) NSString *programName; +@property (copy) NSString *exportType; +@property (copy) NSString *preset; +@property (copy) NSString *sourcePath; +@property (copy) NSString *destinationPath; +@property (copy) NSString *fileType; +@property (strong) NSNumber *progress; +@property (strong) NSNumber *startSeconds; +@property (strong) NSNumber *durationSeconds; +@property (getter=isVerbose) BOOL verbose; +@property BOOL showProgress; +@property BOOL exportFailed; +@property BOOL exportComplete; +@property BOOL listTracks; +@property BOOL listMetadata; +@property BOOL removePreExistingFiles; + +- (id)initWithArgs:(int)argc argv:(const char **)argv environ:(const char **)environ; +- (void)printUsage; + +- (int)run; + +- (NSArray *)addNewMetadata:(NSArray *)sourceMetadataList presetName:(NSString *)presetName; + ++ (void)doListPresets; +- (void)doListTracks:(NSString *)assetPath; +- (void)doListMetadata:(NSString *)assetPath; + + +@end + + +// --------------------------------------------------------------------------- +// AAPLExporter Class Implementation +// --------------------------------------------------------------------------- + +@implementation AAPLExporter + +@synthesize programName, exportType, preset; +@synthesize sourcePath, destinationPath, progress, fileType; +@synthesize startSeconds, durationSeconds; +@synthesize verbose, showProgress, exportComplete, exportFailed; +@synthesize listTracks, listMetadata; +@synthesize removePreExistingFiles; + +-(id) initWithArgs: (int) argc argv: (const char **) argv environ: (const char **) environ +{ + self = [super init]; + + if (self == nil) { + return nil; + } + + printArgs(argc,argv); + + BOOL gotpreset = NO; + BOOL gotsource = NO; + BOOL gotout = NO; + BOOL parseOK = NO; + BOOL listPresets = NO; + [self setProgramName:[NSString stringWithUTF8String: *argv++]]; + argc--; + while ( argc > 0 && **argv == '-' ) + { + const char* args = &(*argv)[1]; + + argc--; + argv++; + + if ( ! strcmp ( args, "source" ) ) + { + [self setSourcePath: [NSString stringWithUTF8String: *argv++] ]; + gotsource = YES; + argc--; + } + else if (( ! strcmp ( args, "dest" )) || ( ! strcmp ( args, "destination" )) ) + { + [self setDestinationPath: [NSString stringWithUTF8String: *argv++]]; + gotout = YES; + argc--; + } + else if ( ! strcmp ( args, "preset" ) ) + { + [self setPreset: [NSString stringWithUTF8String: *argv++]]; + gotpreset = YES; + argc--; + } + else if ( ! strcmp ( args, "replace" ) ) + { + [self setRemovePreExistingFiles: YES]; + } + else if ( ! strcmp ( args, "filetype" ) ) + { + [self setFileType: [NSString stringWithUTF8String: *argv++]]; + argc--; + } + else if ( ! strcmp ( args, "verbose" ) ) + { + [self setVerbose:YES]; + } + else if ( ! strcmp ( args, "progress" ) ) + { + [self setShowProgress: YES]; + } + else if ( ! strcmp ( args, "start" ) ) + { + [self setStartSeconds: [NSNumber numberWithFloat:[[NSString stringWithUTF8String: *argv++] floatValue]]]; + argc--; + } + else if ( ! strcmp ( args, "duration" ) ) + { + [self setDurationSeconds: [NSNumber numberWithFloat:[[NSString stringWithUTF8String: *argv++] floatValue]]]; + argc--; + } + else if ( ! strcmp ( args, "listpresets" ) ) + { + listPresets = YES; + parseOK = YES; + } + else if ( ! strcmp ( args, "listtracks" ) ) + { + [self setListTracks: YES]; + parseOK = YES; + } + else if ( ! strcmp ( args, "listmetadata" ) ) + { + [self setListMetadata: YES]; + parseOK = YES; + } + else if ( ! strcmp ( args, "help" ) ) + { + [self printUsage]; + } + else { + printf("Invalid input parameter: %s\n", args ); + [self printUsage]; + return nil; + } + } + [self setProgress: [NSNumber numberWithFloat:(float)0.0]]; + [self setExportFailed: NO]; + [self setExportComplete: NO]; + + if (listPresets) { + [AAPLExporter doListPresets]; + } + + if ([self isVerbose]) { + printNSString([NSString stringWithFormat:@"Running: %@\n", [self programName]]); + } + + // There must be a source and either a preset and output (the normal case) or parseOK set for a listing + if ((gotsource == NO) || ((parseOK == NO) && ((gotpreset == NO) || (gotout == NO)))) { + [self printUsage]; + return nil; + } + return self; +} + + +-(void) printUsage +{ + printf("AVFoundationExporter - usage:\n"); + printf(" ./AVFoundationExporter [-parameter ...]\n"); + printf(" parameters are all preceded by a -. The order of the parameters is unimportant.\n"); + printf(" Required parameters are -preset -source -dest \n"); + printf(" Source and destination URL strings cannot contain spaces.\n"); + printf(" Available parameters are:\n"); + printf(" -preset . The preset name eg: AVAssetExportPreset640x480 AVAssetExportPresetAppleM4VWiFi. Use -listpresets to see a full list.\n"); + printf(" -destination (or -dest) \n"); + printf(" -source \n"); + printf(" -replace If there is a preexisting file at the destination location, remove it before exporting."); + printf(" -filetype The file type (eg com.apple.m4v-video) for the output file. If not specified, the first supported type will be used.\n"); + printf(" -start time in seconds (decimal are OK). Removes the startClip time from the beginning of the movie before exporting.\n"); + printf(" -duration time in seconds (decimal are OK). Trims the movie to this duration before exporting. \n"); + printf(" Also available are some setup options:\n"); + printf(" -verbose Print more information about the execution.\n"); + printf(" -progress Show progress information.\n"); + printf(" -listpresets For sourceMovieURL sources only, lists the tracks in the source movie before the export. \n"); + printf(" -listtracks For sourceMovieURL sources only, lists the tracks in the source movie before the export. \n"); + printf(" Always lists the tracks in the destination asset at the end of the export.\n"); + printf(" -listmetadata Lists the metadata in the source movie before the export. \n"); + printf(" Also lists the metadata in the destination asset at the end of the export.\n"); + printf(" Sample export lines:\n"); + printf(" ./AVFoundationExporter -dest /tmp/testOut.m4v -replace -preset AVAssetExportPresetAppleM4ViPod -listmetadata -source /path/to/myTestMovie.m4v\n"); + printf(" ./AVFoundationExporter -destination /tmp/testOut.mov -preset AVAssetExportPreset640x480 -listmetadata -listtracks -source /path/to/myTestMovie.mov\n"); +} + + +static dispatch_time_t getDispatchTimeFromSeconds(float seconds) { + long long milliseconds = seconds * 1000.0; + dispatch_time_t waitTime = dispatch_time( DISPATCH_TIME_NOW, 1000000LL * milliseconds ); + return waitTime; +} + +- (int)run +{ + NSURL *sourceURL = nil; + AVAssetExportSession *avsession = nil; + NSURL *destinationURL = nil; + BOOL success = YES; + + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + NSParameterAssert( [self sourcePath] != nil ); + + if ([self listTracks] && [self sourcePath]) { + [self doListTracks:[self sourcePath]]; + } + if ([self listMetadata] && [self sourcePath]) { + [self doListMetadata:[self sourcePath]]; + } + if ([self destinationPath] == nil) { + NSLog(@"No output path specified, only listing tracks and/or metadata, export was not performed."); + goto bail; + } + if ([self preset] == nil) { + NSLog(@"No preset specified, only listing tracks and/or metadata, export was not performed."); + goto bail; + } + + if ( [self isVerbose] && [self sourcePath] ) { + printNSString([NSString stringWithFormat:@"all av asset presets:%@", [AVAssetExportSession allExportPresets]]); + } + + if ([self sourcePath] != nil) { + sourceURL = [[NSURL fileURLWithPath: [self sourcePath] isDirectory: NO] retain]; + } + + AVAsset *sourceAsset = nil; + NSError* error = nil; + + if ([self isVerbose]) { + printNSString([NSString stringWithFormat:@"AVAssetExport for preset:%@ to with source:%@", [self preset], [destinationURL path]]); + } + + destinationURL = [NSURL fileURLWithPath: [self destinationPath] isDirectory: NO]; + if ([self removePreExistingFiles] && [[NSFileManager defaultManager] fileExistsAtPath:[self destinationPath]]) { + if ([self isVerbose]) { + printNSString([NSString stringWithFormat:@"Removing re-existing destination file at:%@", destinationURL]); + } + [[NSFileManager defaultManager] removeItemAtURL:destinationURL error:&error]; + } + + sourceAsset = [[[AVURLAsset alloc] initWithURL:sourceURL options:nil] autorelease]; + + if ([self isVerbose]) { + printNSString([NSString stringWithFormat:@"Compatible av asset presets:%@", [AVAssetExportSession exportPresetsCompatibleWithAsset:sourceAsset]]); + } + avsession = [[AVAssetExportSession alloc] initWithAsset:sourceAsset presetName:[self preset]]; + + [avsession setOutputURL:destinationURL]; + + if ([self fileType] != nil) { + [avsession setOutputFileType:[self fileType]]; + } + else { + [avsession setOutputFileType:[[avsession supportedFileTypes] objectAtIndex:0]]; + } + + if ([self isVerbose]) { + printNSString([NSString stringWithFormat:@"Created AVAssetExportSession: %p", avsession]); + printNSString([NSString stringWithFormat:@"presetName:%@", [avsession presetName]]); + printNSString([NSString stringWithFormat:@"source URL:%@", [sourceURL path]]); + printNSString([NSString stringWithFormat:@"destination URL:%@", [[avsession outputURL] path]]); + printNSString([NSString stringWithFormat:@"output file type:%@", [avsession outputFileType]]); + } + + // Add a metadata item to indicate how thie destination file was created. + NSArray *sourceMetadataList = [avsession metadata]; + sourceMetadataList = [self addNewMetadata: sourceMetadataList presetName:[self preset]]; + [avsession setMetadata:sourceMetadataList]; + + // Set up the time range + CMTime startTime = kCMTimeZero; + CMTime durationTime = kCMTimePositiveInfinity; + + if ([self startSeconds] != nil) { + startTime = CMTimeMake([[self startSeconds] floatValue] * 1000, 1000); + } + if ([self durationSeconds] != nil) { + durationTime = CMTimeMake([[self durationSeconds] floatValue] * 1000, 1000); + } + CMTimeRange exportTimeRange = CMTimeRangeMake(startTime, durationTime); + [avsession setTimeRange:exportTimeRange]; + + // start a fresh pool for the export. + [pool drain]; + pool = [[NSAutoreleasePool alloc] init]; + + // Set up a semaphore for the completion handler and progress timer + dispatch_semaphore_t sessionWaitSemaphore = dispatch_semaphore_create( 0 ); + + void (^completionHandler)(void) = ^(void) + { + dispatch_semaphore_signal(sessionWaitSemaphore); + }; + + // do it. + [avsession exportAsynchronouslyWithCompletionHandler:completionHandler]; + + do { + dispatch_time_t dispatchTime = DISPATCH_TIME_FOREVER; // if we dont want progress, we will wait until it finishes. + if ([self showProgress]) { + dispatchTime = getDispatchTimeFromSeconds((float)1.0); + printNSString([NSString stringWithFormat:@"AVAssetExport running progress=%3.2f%%", [avsession progress]*100]); + } + dispatch_semaphore_wait(sessionWaitSemaphore, dispatchTime); + } while( [avsession status] < AVAssetExportSessionStatusCompleted ); + + if ([self showProgress]) { + printNSString([NSString stringWithFormat:@"AVAssetExport finished progress=%3.2f", [avsession progress]*100]); + } + + [avsession release]; + avsession = nil; + + if ([self listMetadata] && [self destinationPath]) { + [self doListMetadata:[self destinationPath]]; + } + if ([self listTracks] && [self destinationPath]) { + [self doListTracks:[self destinationPath]]; + } + + printNSString([NSString stringWithFormat:@"Finished export of %@ to %@ using preset:%@ success=%s\n", [self sourcePath], [self destinationPath], [self preset], (success ? "YES" : "NO")]); + +bail: + [sourceURL release]; + + [pool drain]; + + return success; +} + + +- (NSArray *) addNewMetadata: (NSArray *)sourceMetadataList presetName:(NSString *)presetName +{ + // This method creates a few new metadata items in different keySpaces to be inserted into the exported file along with the metadata that + // was in the original source. + // Depending on the output file format, not all of these items will be valid and not all of them will come through to the destination. + + AVMutableMetadataItem *newUserDataCommentItem = [[[AVMutableMetadataItem alloc] init] autorelease]; + [newUserDataCommentItem setKeySpace:AVMetadataKeySpaceQuickTimeUserData]; + [newUserDataCommentItem setKey:AVMetadataQuickTimeUserDataKeyComment]; + [newUserDataCommentItem setValue:[NSString stringWithFormat:@"QuickTime userdata: Exported to preset %@ using AVFoundationExporter at: %@", presetName, + [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterMediumStyle timeStyle: NSDateFormatterShortStyle]]]; + + AVMutableMetadataItem *newMetaDataCommentItem = [[[AVMutableMetadataItem alloc] init] autorelease]; + [newMetaDataCommentItem setKeySpace:AVMetadataKeySpaceQuickTimeMetadata]; + [newMetaDataCommentItem setKey:AVMetadataQuickTimeMetadataKeyComment]; + [newMetaDataCommentItem setValue:[NSString stringWithFormat:@"QuickTime metadata: Exported to preset %@ using AVFoundationExporter at: %@", presetName, + [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterMediumStyle timeStyle: NSDateFormatterShortStyle]]]; + + AVMutableMetadataItem *newiTunesCommentItem = [[[AVMutableMetadataItem alloc] init] autorelease]; + [newiTunesCommentItem setKeySpace:AVMetadataKeySpaceiTunes]; + [newiTunesCommentItem setKey:AVMetadataiTunesMetadataKeyUserComment]; + [newiTunesCommentItem setValue:[NSString stringWithFormat:@"iTunes metadata: Exported to preset %@ using AVFoundationExporter at: %@", presetName, + [NSDateFormatter localizedStringFromDate:[NSDate date] dateStyle:NSDateFormatterMediumStyle timeStyle: NSDateFormatterShortStyle]]]; + + NSArray *newMetadata = [NSArray arrayWithObjects:newUserDataCommentItem, newMetaDataCommentItem, newiTunesCommentItem, nil]; + NSArray *newMetadataList = (sourceMetadataList == nil ? newMetadata : [sourceMetadataList arrayByAddingObjectsFromArray:newMetadata]); + return newMetadataList; +} + + ++ (void) doListPresets +{ + // A simple listing of the presets available for export + printNSString(@""); + printNSString(@"Presets available for AVFoundation export:"); + printNSString(@" QuickTime movie presets:"); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPreset640x480]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPreset960x540]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPreset1280x720]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPreset1920x1080]); + printNSString(@" Audio only preset:"); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4A]); + printNSString(@" Apple device presets:"); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4VCellular]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4ViPod]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4V480pSD]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4VAppleTV]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4VWiFi]); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleM4V720pHD]); + printNSString(@" Interim format (QuickTime movie) preset:"); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetAppleProRes422LPCM]); + printNSString(@" Passthrough preset:"); + printNSString([NSString stringWithFormat:@" %@", AVAssetExportPresetPassthrough]); + printNSString(@""); +} + + +- (void)doListTracks:(NSString *)assetPath +{ + // A simple listing of the tracks in the asset provided + NSURL *sourceURL = [NSURL fileURLWithPath: assetPath isDirectory: NO]; + if (sourceURL) { + AVURLAsset *sourceAsset = [[[AVURLAsset alloc] initWithURL:sourceURL options:nil] autorelease]; + printNSString([NSString stringWithFormat:@"Listing tracks for asset from url:%@", [sourceURL path]]); + NSInteger index = 0; + for (AVAssetTrack *track in [sourceAsset tracks]) { + [track retain]; + printNSString([ NSString stringWithFormat:@" Track index:%ld, trackID:%d, mediaType:%@, enabled:%d, isSelfContained:%d", index, [track trackID], [track mediaType], [track isEnabled], [track isSelfContained] ] ); + index++; + [track release]; + } + } +} + +enum { + kMaxMetadataValueLength = 80, +}; + +- (void)doListMetadata:(NSString *)assetPath +{ + // A simple listing of the metadata in the asset provided + NSURL *sourceURL = [NSURL fileURLWithPath: assetPath isDirectory: NO]; + if (sourceURL) { + AVURLAsset *sourceAsset = [[[AVURLAsset alloc] initWithURL:sourceURL options:nil] autorelease]; + NSLog(@"Listing metadata for asset from url:%@", [sourceURL path]); + for (NSString *format in [sourceAsset availableMetadataFormats]) { + NSLog(@"Metadata for format:%@", format); + for (AVMetadataItem *item in [sourceAsset metadataForFormat:format]) { + NSObject *key = [item key]; + NSString *itemValue = [[item value] description]; + if ([itemValue length] > kMaxMetadataValueLength) { + itemValue = [NSString stringWithFormat:@"%@ ...", [itemValue substringToIndex:kMaxMetadataValueLength-4]]; + } + if ([key isKindOfClass: [NSNumber class]]) { + NSInteger longValue = [(NSNumber *)key longValue]; + char *charSource = (char *)&longValue; + char charValue[5] = {0}; + charValue[0] = charSource[3]; + charValue[1] = charSource[2]; + charValue[2] = charSource[1]; + charValue[3] = charSource[0]; + NSString *stringKey = [[[NSString alloc] initWithBytes: charValue length:4 encoding:NSMacOSRomanStringEncoding] autorelease]; + printNSString([NSString stringWithFormat:@" metadata item key:%@ (%ld), keySpace:%@ commonKey:%@ value:%@", stringKey, longValue, [item keySpace], [item commonKey], itemValue]); + } + else { + printNSString([NSString stringWithFormat:@" metadata item key:%@, keySpace:%@ commonKey:%@ value:%@", [item key], [item keySpace], [item commonKey], itemValue]); + } + } + } + } +} + + +@end + + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + + +int main (int argc, const char * argv[], const char* environ[]) +{ + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + AAPLExporter* exportObj = [[AAPLExporter alloc] initWithArgs:argc argv:argv environ:environ]; + BOOL success; + if (exportObj) + success = [exportObj run]; + else { + success = NO; + } + + [exportObj release]; + [pool release]; + + return ((success == YES) ? 0 : -1); +} + + +// --------------------------------------------------------------------------- +// printNSString +// --------------------------------------------------------------------------- +static void printNSString(NSString *string) +{ + printf("%s\n", [string cStringUsingEncoding:NSUTF8StringEncoding]); +} + +// --------------------------------------------------------------------------- +// printArgs +// --------------------------------------------------------------------------- +static void printArgs(int argc, const char **argv) +{ + int i; + for( i = 0; i < argc; i++ ) + printf("%s ", argv[i]); + printf("\n"); +} + diff --git a/AVFoundationExporter/README.md b/AVFoundationExporter/README.md new file mode 100644 index 00000000..e99daf9d --- /dev/null +++ b/AVFoundationExporter/README.md @@ -0,0 +1,39 @@ +# AVFoundationExporter + +## Description + +Demonstrates use of AVFoundation export APIs with a simple command line utility. The command line application will list some information about the asset, transcode the asset in accord with one of the AVAssetExportSession presets, and demonstrates simple manipulation of the metadata that is exported with the source. + +## Build Requirements + +Xcode 8.0, macOS 10.12 + +## Runtime Requirements + +OS X 10.11 + +## Structure + +The main files associates with this project are: + +Objective-C Version: + Source file: AVFoundationExporter.m + Project bundle: Objective-C/AVFoundationExporter.xcodeproj + +Swift Version: + Source files: main.swift, ArgumentParsing.swift + Project bundle: Swift/AVFoundationExporter.xcodeproj + +## Changes + +Version 1.0 +- First version. + +Version 2.0 +- Add Swift version. + +Version 3.0 +- Updated project for Swift 2.3. + + +Copyright (C) 2015, 2016 Apple Inc. All rights reserved. diff --git a/AVFoundationExporter/Swift/AVFoundationExporter.xcodeproj/project.pbxproj b/AVFoundationExporter/Swift/AVFoundationExporter.xcodeproj/project.pbxproj new file mode 100644 index 00000000..01d0e08b --- /dev/null +++ b/AVFoundationExporter/Swift/AVFoundationExporter.xcodeproj/project.pbxproj @@ -0,0 +1,261 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 004967D71AE9751900B10C98 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 004967D61AE9751900B10C98 /* main.swift */; }; + 00599BBF1B1CFCC20093572A /* ArgumentParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00599BBE1B1CFCC20093572A /* ArgumentParsing.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 004967CA1AE974A600B10C98 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 004967CC1AE974A600B10C98 /* AVFoundationExporter */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = AVFoundationExporter; sourceTree = BUILT_PRODUCTS_DIR; }; + 004967D61AE9751900B10C98 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + 00599BBE1B1CFCC20093572A /* ArgumentParsing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArgumentParsing.swift; sourceTree = ""; }; + 3EAA11C31B1B894900EC0006 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 004967C91AE974A600B10C98 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 004967C31AE974A600B10C98 = { + isa = PBXGroup; + children = ( + 3EAA11C31B1B894900EC0006 /* README.md */, + 004967CE1AE974A600B10C98 /* AVFoundationExporter */, + 004967CD1AE974A600B10C98 /* Products */, + ); + sourceTree = ""; + }; + 004967CD1AE974A600B10C98 /* Products */ = { + isa = PBXGroup; + children = ( + 004967CC1AE974A600B10C98 /* AVFoundationExporter */, + ); + name = Products; + sourceTree = ""; + }; + 004967CE1AE974A600B10C98 /* AVFoundationExporter */ = { + isa = PBXGroup; + children = ( + 004967D61AE9751900B10C98 /* main.swift */, + 00599BBE1B1CFCC20093572A /* ArgumentParsing.swift */, + ); + path = AVFoundationExporter; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 004967CB1AE974A600B10C98 /* AVFoundationExporter */ = { + isa = PBXNativeTarget; + buildConfigurationList = 004967D31AE974A600B10C98 /* Build configuration list for PBXNativeTarget "AVFoundationExporter" */; + buildPhases = ( + 004967C81AE974A600B10C98 /* Sources */, + 004967C91AE974A600B10C98 /* Frameworks */, + 004967CA1AE974A600B10C98 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AVFoundationExporter; + productName = AVFoundationExporter; + productReference = 004967CC1AE974A600B10C98 /* AVFoundationExporter */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 004967C41AE974A600B10C98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + TargetAttributes = { + 004967CB1AE974A600B10C98 = { + CreatedOnToolsVersion = 6.3; + }; + }; + }; + buildConfigurationList = 004967C71AE974A600B10C98 /* Build configuration list for PBXProject "AVFoundationExporter" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 004967C31AE974A600B10C98; + productRefGroup = 004967CD1AE974A600B10C98 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 004967CB1AE974A600B10C98 /* AVFoundationExporter */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 004967C81AE974A600B10C98 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00599BBF1B1CFCC20093572A /* ArgumentParsing.swift in Sources */, + 004967D71AE9751900B10C98 /* main.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 004967D11AE974A600B10C98 /* 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_VERSION = 2.3; + }; + name = Debug; + }; + 004967D21AE974A600B10C98 /* 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; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_VERSION = 2.3; + }; + name = Release; + }; + 004967D41AE974A600B10C98 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 2.3; + }; + name = Debug; + }; + 004967D51AE974A600B10C98 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 2.3; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 004967C71AE974A600B10C98 /* Build configuration list for PBXProject "AVFoundationExporter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 004967D11AE974A600B10C98 /* Debug */, + 004967D21AE974A600B10C98 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 004967D31AE974A600B10C98 /* Build configuration list for PBXNativeTarget "AVFoundationExporter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 004967D41AE974A600B10C98 /* Debug */, + 004967D51AE974A600B10C98 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 004967C41AE974A600B10C98 /* Project object */; +} diff --git a/AVFoundationExporter/Swift/AVFoundationExporter/ArgumentParsing.swift b/AVFoundationExporter/Swift/AVFoundationExporter/ArgumentParsing.swift new file mode 100644 index 00000000..499f0726 --- /dev/null +++ b/AVFoundationExporter/Swift/AVFoundationExporter/ArgumentParsing.swift @@ -0,0 +1,246 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Parses command-line arguments and invokes the appropriate command +*/ + +import CoreMedia +import AVFoundation + +// Use enums to enforce uniqueness of option labels. +enum LongLabel: String { + case FileType = "filetype" + case PresetName = "preset" + case DeleteExistingFile = "replace" + case LogEverything = "verbose" + case TrimStartTime = "trim-start-time" + case TrimEndTime = "trim-end-time" + case FilterMetadata = "filter-metadata" + case InjectMetadata = "inject-metadata" +} + +enum ShortLabel: String { + case FileType = "f" + case PresetName = "p" + case DeleteExistingFile = "r" + case LogEverything = "v" +} + +let executableName = NSString(string: Process.arguments.first!).pathComponents.last! + +func usage() { + print("Usage:") + print("\t\(executableName) [options]") + print("\t\(executableName) list-presets []") + print("") // newline + print("In the first form, \(executableName) performs an export of the file at , writing the result to a file at . If no options are given, a passthrough export to a QuickTime Movie file is performed.") + print("") + print("In the second form, \(executableName) lists the available parameters to the -preset option. If is specified, only the presets compatible with the file at will be listed.") + print("") + print("Options for first form:") + print("\t-f, -filetype ") + print("\t\tThe file type (e.g. com.apple.m4v-video) for the output file") + print("") + print("\t-p, -preset ") + print("\t\tThe preset name; use commmand list-presets to see available preset names") + print("") + print("\t-r, -replace YES") + print("\t\tIf there is a pre-existing file at the destination location, remove it before exporting") + print("") + print("\t-v, -verbose YES") + print("\t\tPrint more information about the execution") + print("") + print("\t-trim-start-time ") + print("\t\tWhen specified, all media before the start time will be trimmed out") + print("") + print("\t-trim-end-time ") + print("\t\tWhen specified, all media after the end time will be trimmed out") + print("") + print("\t-filter-metadata YES") + print("\t\tFilter out privacy-sensitive metadata") + print("") + print("\t-inject-metadata YES") + print("\t\tAdd simple metadata during export") +} + +// Errors that can occur during argument parsing. +enum CommandLineError: ErrorType, CustomStringConvertible { + case TooManyArguments + case TooFewArguments(descriptionOfRequiredArguments: String) + case InvalidArgument(reason: String) + + var description: String { + switch self { + case .TooManyArguments: + return "Too many arguments" + + case .TooFewArguments(let descriptionOfRequiredArguments): + return "Missing argument(s). Must specify \(descriptionOfRequiredArguments)." + + case .InvalidArgument(let reason): + return "Invalid argument. \(reason)." + } + } +} + +/// A set of convenience methods to use with our specific command line arguments. +extension NSUserDefaults { + func stringForLongLabel(longLabel: LongLabel) -> String? { + return stringForKey(longLabel.rawValue) + } + + func stringForShortLabel(shortLabel: ShortLabel) -> String? { + return stringForKey(shortLabel.rawValue) + } + + func boolForLongLabel(longLabel: LongLabel) -> Bool { + return boolForKey(longLabel.rawValue) + } + + func boolForShortLabel(shortLabel: ShortLabel) -> Bool { + return boolForKey(shortLabel.rawValue) + } + + func timeForLongLabel(longLabel: LongLabel) throws -> CMTime? { + if let timeAsString = stringForLongLabel(longLabel) { + guard let timeAsSeconds = Float64(timeAsString) else { + throw CommandLineError.InvalidArgument(reason: "Non-numeric time \"\(timeAsString)\".") + } + + return CMTimeMakeWithSeconds(timeAsSeconds, 600) + } + + return nil + } + + func timeForShortLabel(shortLabel: ShortLabel) throws -> CMTime? { + if let timeAsString = stringForShortLabel(shortLabel) { + guard let timeAsSeconds = Float64(timeAsString) else { + throw CommandLineError.InvalidArgument(reason: "Non-numeric time \"\(timeAsString)\".") + } + + return CMTimeMakeWithSeconds(timeAsSeconds, 600) + } + + return nil + } +} + +// Lists all presets, or the presets compatible with the file at the given path +func listPresets(sourcePath: String? = nil) { + let presets: [String] + + switch sourcePath { + case let sourcePath?: + print("Presets compatible with \(sourcePath):.") + + let sourceURL = NSURL(fileURLWithPath: sourcePath) + let asset = AVAsset(URL: sourceURL) + presets = AVAssetExportSession.exportPresetsCompatibleWithAsset(asset) + + case nil: + print("Available presets:") + presets = AVAssetExportSession.allExportPresets() + } + + let presetsDescription = presets.joinWithSeparator("\n\t") + + print("\t\(presetsDescription)") +} + +/// The main function that handles all of the command line argument parsing. +func actOnCommandLineArguments() { + let arguments = Process.arguments + let firstArgumentAfterExecutablePath: String? = (arguments.count >= 2) ? arguments[1] : nil + + if arguments.contains("-help") || arguments.contains("-h") { + usage() + exit(0) + } + + do { + switch firstArgumentAfterExecutablePath { + case nil, "help"?: + usage() + exit(0) + + case "list-presets"?: + if arguments.count == 3 { + listPresets(arguments[2]) + } + else if arguments.count > 3 { + throw CommandLineError.TooManyArguments + } + else { + listPresets() + } + + default: + guard arguments.count >= 3 else { + throw CommandLineError.TooFewArguments(descriptionOfRequiredArguments: "source and dest paths") + } + + let sourceURL = NSURL(fileURLWithPath: arguments[1]) + let destinationURL = NSURL(fileURLWithPath: arguments[2]) + + var exporter = Exporter(sourceURL: sourceURL, destinationURL: destinationURL) + + let options = NSUserDefaults.standardUserDefaults() + + if let fileType = options.stringForLongLabel(.FileType) ?? options.stringForShortLabel(.FileType) { + exporter.destinationFileType = fileType + } + + if let presetName = options.stringForLongLabel(.PresetName) ?? options.stringForShortLabel(.PresetName) { + exporter.presetName = presetName + } + + exporter.deleteExistingFile = options.boolForLongLabel(.DeleteExistingFile) || options.boolForShortLabel(.DeleteExistingFile) + + exporter.isVerbose = options.boolForLongLabel(.LogEverything) || options.boolForShortLabel(.LogEverything) + + let trimStartTime = try options.timeForLongLabel(.TrimStartTime) + let trimEndTime = try options.timeForLongLabel(.TrimEndTime) + + switch (trimStartTime, trimEndTime) { + case (nil, nil): + exporter.timeRange = nil + + case (let realStartTime?, nil): + exporter.timeRange = CMTimeRange(start: realStartTime, duration: kCMTimePositiveInfinity) + + case (nil, let realEndTime?): + exporter.timeRange = CMTimeRangeFromTimeToTime(kCMTimeZero, realEndTime) + + case (let realStartTime?, let realEndTime?): + exporter.timeRange = CMTimeRangeFromTimeToTime(realStartTime, realEndTime) + } + + exporter.filterMetadata = options.boolForLongLabel(.FilterMetadata) + + exporter.injectMetadata = options.boolForLongLabel(.InjectMetadata) + + try exporter.export() + } + } + catch let error as CommandLineError { + print("error parsing arguments: \(error).") + print("") // newline + usage() + exit(1) + } + catch let error as NSError { + let highLevelFailure = error.localizedDescription + var errorOutput = highLevelFailure + + if let detailedFailure = error.localizedRecoverySuggestion ?? error.localizedFailureReason { + errorOutput += ": \(detailedFailure)" + } + + print("error: \(errorOutput).") + + exit(1) + } +} \ No newline at end of file diff --git a/AVFoundationExporter/Swift/AVFoundationExporter/main.swift b/AVFoundationExporter/Swift/AVFoundationExporter/main.swift new file mode 100644 index 00000000..91d31a17 --- /dev/null +++ b/AVFoundationExporter/Swift/AVFoundationExporter/main.swift @@ -0,0 +1,228 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Demonstrates how to use AVAssetExportSession to export and transcode media files +*/ + +import AVFoundation + +/* + Perform all of the argument parsing / set up. The interesting AV exporting + code is done in the `Exporter` type. +*/ +actOnCommandLineArguments() + +/// The type that performs all of the asset exporting. +struct Exporter { + // MARK: Properties + + let sourceURL: NSURL + + let destinationURL: NSURL + + var destinationFileType = AVFileTypeQuickTimeMovie + + var presetName = AVAssetExportPresetPassthrough + + var timeRange: CMTimeRange? + + var filterMetadata = false + + var injectMetadata = false + + var deleteExistingFile = false + + var isVerbose = false + + // MARK: Initialization + + init(sourceURL: NSURL, destinationURL: NSURL) { + self.sourceURL = sourceURL + self.destinationURL = destinationURL + } + + func export() throws { + let asset = AVURLAsset(URL: sourceURL) + + printVerbose("Exporting \"\(sourceURL)\" to \"\(destinationURL)\" (file type \(destinationFileType)), using preset \(presetName).") + + // Set up export session. + let exportSession = try setUpExportSession(asset, destinationURL: destinationURL) + + // AVAssetExportSession will not overwrite existing files. + try deleteExistingFile(destinationURL) + + describeSourceFile(asset) + + // Kick off asynchronous export operation. + let group = dispatch_group_create() + dispatch_group_enter(group) + exportSession.exportAsynchronouslyWithCompletionHandler { + dispatch_group_leave(group) + } + + waitForExportToFinish(exportSession, group: group) + + if exportSession.status == .Failed { + // `error` is non-nil when in the "failed" status. + throw exportSession.error! + } + else { + describeDestFile(destinationURL) + } + + printVerbose("Export completed successfully.") + } + + func setUpExportSession(asset: AVAsset, destinationURL: NSURL) throws -> AVAssetExportSession { + guard let exportSession = AVAssetExportSession(asset: asset, presetName: presetName) else { + throw CommandLineError.InvalidArgument(reason: "Invalid preset \(presetName).") + } + + // Set required properties. + exportSession.outputURL = destinationURL + exportSession.outputFileType = destinationFileType + + if let timeRange = timeRange { + exportSession.timeRange = timeRange + + printVerbose("Trimming to time range \(CMTimeRangeCopyDescription(nil, timeRange)!).") + } + + if filterMetadata { + printVerbose("Filtering metadata.") + + exportSession.metadataItemFilter = AVMetadataItemFilter.metadataItemFilterForSharing() + } + + if injectMetadata { + printVerbose("Injecting metadata") + + let now = NSDate() + let currentDate = NSDateFormatter.localizedStringFromDate(now, dateStyle: .MediumStyle, timeStyle: .ShortStyle) + + let userDataCommentItem = AVMutableMetadataItem() + userDataCommentItem.identifier = AVMetadataIdentifierQuickTimeUserDataComment + userDataCommentItem.value = "QuickTime userdata: Exported to preset \(presetName) using AVFoundationExporter at: \(currentDate)." + + let metadataCommentItem = AVMutableMetadataItem() + metadataCommentItem.identifier = AVMetadataIdentifierQuickTimeMetadataComment + metadataCommentItem.value = "QuickTime metadata: Exported to preset \(presetName) using AVFoundationExporter at: \(currentDate)." + + let iTunesCommentItem = AVMutableMetadataItem() + iTunesCommentItem.identifier = AVMetadataIdentifieriTunesMetadataUserComment + iTunesCommentItem.value = "iTunes metadata: Exported to preset \(presetName) using AVFoundationExporter at: \(currentDate)." + + /* + To avoid replacing metadata from the asset: + 1. Fetch existing metadata from the asset. + 2. Combine it with the new metadata. + 3. Set the result on the export session. + */ + exportSession.metadata = asset.metadata + [ + userDataCommentItem, + metadataCommentItem, + iTunesCommentItem + ] + } + + return exportSession + } + + func deleteExistingFile(destinationURL: NSURL) throws { + let fileManager = NSFileManager() + + if let destinationPath = destinationURL.path { + if deleteExistingFile && fileManager.fileExistsAtPath(destinationPath) { + printVerbose("Removing pre-existing file at destination path \"\(destinationPath)\".") + + try fileManager.removeItemAtURL(destinationURL) + } + } + } + + func describeSourceFile(asset: AVAsset) { + guard isVerbose else { return } + + printVerbose("Tracks in source file:") + + let trackDescriptions = trackDescriptionsForAsset(asset) + let tracksDescription = trackDescriptions.joinWithSeparator("\n\t") + printVerbose("\t\(tracksDescription)") + + printVerbose("Metadata in source file:") + let metadataDescriptions = metadataDescriptionsForAsset(asset) + let metadataDescription = metadataDescriptions.joinWithSeparator("\n\t") + + printVerbose("\t\(metadataDescription)") + } + + // Periodically polls & prints export session progress while waiting for the export to finish. + func waitForExportToFinish(exportSession: AVAssetExportSession, group: dispatch_group_t) { + while exportSession.status == .Waiting || exportSession.status == .Exporting { + printVerbose("Progress: \(exportSession.progress * 100.0)%.") + + dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, Int64(500 * NSEC_PER_MSEC))) + } + + printVerbose("Progress: \(exportSession.progress * 100.0)%.") + } + + func describeDestFile(destinationURL: NSURL) { + guard isVerbose else { return } + + let destinationAsset = AVAsset(URL:destinationURL) + + printVerbose("Tracks in written file:") + + let trackDescriptions = trackDescriptionsForAsset(destinationAsset) + let tracksDescription = trackDescriptions.joinWithSeparator("\n\t") + printVerbose("\t\(tracksDescription)") + + printVerbose("Metadata in written file:") + + let metadataDescriptions = metadataDescriptionsForAsset(destinationAsset) + let metadataDescription = metadataDescriptions.joinWithSeparator("\n\t") + printVerbose("\t\(metadataDescription)") + } + + func trackDescriptionsForAsset(asset: AVAsset) -> [String] { + return asset.tracks.map { track in + let enabledString = track.enabled ? "YES" : "NO" + + let selfContainedString = track.selfContained ? "YES" : "NO" + + let formatDescriptions = track.formatDescriptions as! [CMFormatDescriptionRef] + + let formatStrings = formatDescriptions.map { formatDescription -> String in + let mediaSubType = CMFormatDescriptionGetMediaSubType(formatDescription) + + let mediaSubTypeString = NSFileTypeForHFSTypeCode(mediaSubType) + + return "'\(track.mediaType)'/\(mediaSubTypeString)" + } + + let formatString = !formatStrings.isEmpty ? formatStrings.joinWithSeparator(", ") : "'\(track.mediaType)'" + + return "Track ID \(track.trackID): \(formatString), data length: \(track.totalSampleDataLength), enabled: \(enabledString), self-contained: \(selfContainedString)" + } + } + + func metadataDescriptionsForAsset(asset: AVAsset) -> [String] { + return asset.metadata.map { item in + let identifier = item.identifier ?? "" + + let value = item.value?.description ?? "" + + return "metadata item \(identifier): \(value)" + } + } + + func printVerbose(string: String) { + if isVerbose { + print(string) + } + } +} diff --git a/AVFoundationQueuePlayer/Common/Drums.m4a b/AVFoundationQueuePlayer/Common/Drums.m4a new file mode 100644 index 00000000..636f1436 Binary files /dev/null and b/AVFoundationQueuePlayer/Common/Drums.m4a differ diff --git a/AVFoundationQueuePlayer/Common/ElephantSeals.mov b/AVFoundationQueuePlayer/Common/ElephantSeals.mov new file mode 100644 index 00000000..69641555 Binary files /dev/null and b/AVFoundationQueuePlayer/Common/ElephantSeals.mov differ diff --git a/AVFoundationQueuePlayer/Common/HLSThumb.png b/AVFoundationQueuePlayer/Common/HLSThumb.png new file mode 100644 index 00000000..3ddf1b2b Binary files /dev/null and b/AVFoundationQueuePlayer/Common/HLSThumb.png differ diff --git a/AVFoundationQueuePlayer/Common/LocalAudioThumb.png b/AVFoundationQueuePlayer/Common/LocalAudioThumb.png new file mode 100644 index 00000000..66b76ce0 Binary files /dev/null and b/AVFoundationQueuePlayer/Common/LocalAudioThumb.png differ diff --git a/AVFoundationQueuePlayer/Common/LocalVideoThumb.png b/AVFoundationQueuePlayer/Common/LocalVideoThumb.png new file mode 100644 index 00000000..ae3c7670 Binary files /dev/null and b/AVFoundationQueuePlayer/Common/LocalVideoThumb.png differ diff --git a/AVFoundationQueuePlayer/Common/MediaManifest.json b/AVFoundationQueuePlayer/Common/MediaManifest.json new file mode 100644 index 00000000..7d3a5088 --- /dev/null +++ b/AVFoundationQueuePlayer/Common/MediaManifest.json @@ -0,0 +1,22 @@ +[ + { + "title" : "Video File", + "mediaResourceName" : "ElephantSeals.mov", + "thumbnailResourceName" : "LocalVideoThumb.png" + }, + { + "title" : "Audio File", + "mediaResourceName" : "Drums.m4a", + "thumbnailResourceName" : "LocalAudioThumb.png" + }, + { + "title" : "HTTP Live Stream", + "mediaURL" : "https://devimages.apple.com.edgekey.net/samplecode/avfoundationMedia/AVFoundationQueuePlayer_HLS2/master.m3u8", + "thumbnailResourceName" : "HLSThumb.png" + }, + { + "title" : "Progressive Download", + "mediaURL" : "https://devimages.apple.com.edgekey.net/samplecode/avfoundationMedia/AVFoundationQueuePlayer_Progressive.mov", + "thumbnailResourceName" : "ProgressiveThumb.png" + } +] diff --git a/AVFoundationQueuePlayer/Common/ProgressiveThumb.png b/AVFoundationQueuePlayer/Common/ProgressiveThumb.png new file mode 100644 index 00000000..80715d3d Binary files /dev/null and b/AVFoundationQueuePlayer/Common/ProgressiveThumb.png differ diff --git a/AVFoundationQueuePlayer/LICENSE.txt b/AVFoundationQueuePlayer/LICENSE.txt new file mode 100644 index 00000000..a4f2db2d --- /dev/null +++ b/AVFoundationQueuePlayer/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: AVFoundationQueuePlayer-iOS: Using a Mixture of Local File Based Assets and HTTP Live Streaming Assets with AVFoundation +Version: 2.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) 2016 Apple Inc. All Rights Reserved. diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS.xcodeproj/project.pbxproj b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS.xcodeproj/project.pbxproj new file mode 100644 index 00000000..531f456b --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS.xcodeproj/project.pbxproj @@ -0,0 +1,383 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + D2385B461AF5181400DC8ADE /* AAPLAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D2385B451AF5181400DC8ADE /* AAPLAppDelegate.m */; }; + D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D2385B491AF5181400DC8ADE /* Main.storyboard */; }; + D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2385B4C1AF5181400DC8ADE /* Images.xcassets */; }; + D265BBB71B1792720005C539 /* AAPLPlayerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D265BBB61B1792720005C539 /* AAPLPlayerViewController.m */; }; + D274898E1B1CC58A0020D82A /* AAPLPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D274898B1B1CC58A0020D82A /* AAPLPlayerView.m */; }; + D274898F1B1CC58A0020D82A /* AAPLQueuedItemCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D274898D1B1CC58A0020D82A /* AAPLQueuedItemCollectionViewCell.m */; }; + D27489941B1CC6E00020D82A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D27489901B1CC6E00020D82A /* Localizable.strings */; }; + D27489951B1CC6E00020D82A /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D27489921B1CC6E00020D82A /* Localizable.stringsdict */; }; + D27755701B0D0A4100C9D649 /* MediaManifest.json in Resources */ = {isa = PBXBuildFile; fileRef = D277556F1B0D0A4100C9D649 /* MediaManifest.json */; }; + D27755771B0D0A5400C9D649 /* HLSThumb.png in Resources */ = {isa = PBXBuildFile; fileRef = D27755731B0D0A5400C9D649 /* HLSThumb.png */; }; + D27755781B0D0A5400C9D649 /* LocalAudioThumb.png in Resources */ = {isa = PBXBuildFile; fileRef = D27755741B0D0A5400C9D649 /* LocalAudioThumb.png */; }; + D27755791B0D0A5400C9D649 /* LocalVideoThumb.png in Resources */ = {isa = PBXBuildFile; fileRef = D27755751B0D0A5400C9D649 /* LocalVideoThumb.png */; }; + D277557A1B0D0A5400C9D649 /* ProgressiveThumb.png in Resources */ = {isa = PBXBuildFile; fileRef = D27755761B0D0A5400C9D649 /* ProgressiveThumb.png */; }; + D2C148E71B0FA816004F41DA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D2C148E61B0FA816004F41DA /* main.m */; }; + D2C148EC1B0FAD79004F41DA /* Drums.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D2C148EB1B0FAD79004F41DA /* Drums.m4a */; }; + D2C148EE1B0FAD83004F41DA /* ElephantSeals.mov in Resources */ = {isa = PBXBuildFile; fileRef = D2C148ED1B0FAD83004F41DA /* ElephantSeals.mov */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D2385B421AF5181400DC8ADE /* AVFoundationQueuePlayer-ObjC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AVFoundationQueuePlayer-ObjC.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + D2385B451AF5181400DC8ADE /* AAPLAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AAPLAppDelegate.m; sourceTree = ""; }; + D2385B4A1AF5181400DC8ADE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + D2385B4C1AF5181400DC8ADE /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + D2385B511AF5181400DC8ADE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D265BBB61B1792720005C539 /* AAPLPlayerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLPlayerViewController.m; sourceTree = ""; }; + D27328DB1B1E22FD004EE77D /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; + D274898A1B1CC58A0020D82A /* AAPLPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLPlayerView.h; sourceTree = ""; }; + D274898B1B1CC58A0020D82A /* AAPLPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLPlayerView.m; sourceTree = ""; }; + D274898C1B1CC58A0020D82A /* AAPLQueuedItemCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLQueuedItemCollectionViewCell.h; sourceTree = ""; }; + D274898D1B1CC58A0020D82A /* AAPLQueuedItemCollectionViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLQueuedItemCollectionViewCell.m; sourceTree = ""; }; + D27489911B1CC6E00020D82A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + D27489931B1CC6E00020D82A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; + D277556F1B0D0A4100C9D649 /* MediaManifest.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = MediaManifest.json; path = ../Common/MediaManifest.json; sourceTree = SOURCE_ROOT; }; + D27755731B0D0A5400C9D649 /* HLSThumb.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = HLSThumb.png; path = ../Common/HLSThumb.png; sourceTree = SOURCE_ROOT; }; + D27755741B0D0A5400C9D649 /* LocalAudioThumb.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = LocalAudioThumb.png; path = ../Common/LocalAudioThumb.png; sourceTree = SOURCE_ROOT; }; + D27755751B0D0A5400C9D649 /* LocalVideoThumb.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = LocalVideoThumb.png; path = ../Common/LocalVideoThumb.png; sourceTree = SOURCE_ROOT; }; + D27755761B0D0A5400C9D649 /* ProgressiveThumb.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = ProgressiveThumb.png; path = ../Common/ProgressiveThumb.png; sourceTree = SOURCE_ROOT; }; + D2C148E61B0FA816004F41DA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + D2C148E91B0FAAE1004F41DA /* AAPLAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLAppDelegate.h; sourceTree = ""; }; + D2C148EA1B0FAAF2004F41DA /* AAPLPlayerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLPlayerViewController.h; sourceTree = ""; }; + D2C148EB1B0FAD79004F41DA /* Drums.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; name = Drums.m4a; path = ../Common/Drums.m4a; sourceTree = SOURCE_ROOT; }; + D2C148ED1B0FAD83004F41DA /* ElephantSeals.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; name = ElephantSeals.mov; path = ../Common/ElephantSeals.mov; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D2385B3F1AF5181400DC8ADE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D2385B391AF5181400DC8ADE = { + isa = PBXGroup; + children = ( + D27328DB1B1E22FD004EE77D /* README.md */, + D2385B441AF5181400DC8ADE /* AVFoundationQueuePlayer-iOS */, + D2385B431AF5181400DC8ADE /* Products */, + ); + sourceTree = ""; + }; + D2385B431AF5181400DC8ADE /* Products */ = { + isa = PBXGroup; + children = ( + D2385B421AF5181400DC8ADE /* AVFoundationQueuePlayer-ObjC.app */, + ); + name = Products; + sourceTree = ""; + }; + D2385B441AF5181400DC8ADE /* AVFoundationQueuePlayer-iOS */ = { + isa = PBXGroup; + children = ( + D2C148E91B0FAAE1004F41DA /* AAPLAppDelegate.h */, + D2385B451AF5181400DC8ADE /* AAPLAppDelegate.m */, + D2C148EA1B0FAAF2004F41DA /* AAPLPlayerViewController.h */, + D265BBB61B1792720005C539 /* AAPLPlayerViewController.m */, + D274898A1B1CC58A0020D82A /* AAPLPlayerView.h */, + D274898B1B1CC58A0020D82A /* AAPLPlayerView.m */, + D274898C1B1CC58A0020D82A /* AAPLQueuedItemCollectionViewCell.h */, + D274898D1B1CC58A0020D82A /* AAPLQueuedItemCollectionViewCell.m */, + D2385B491AF5181400DC8ADE /* Main.storyboard */, + D2385B4C1AF5181400DC8ADE /* Images.xcassets */, + D2385B511AF5181400DC8ADE /* Info.plist */, + D27489901B1CC6E00020D82A /* Localizable.strings */, + D27489921B1CC6E00020D82A /* Localizable.stringsdict */, + D2C148E81B0FA81E004F41DA /* Supporting Files */, + D28778461B011B1900E31BDD /* Common */, + ); + path = "AVFoundationQueuePlayer-iOS"; + sourceTree = ""; + }; + D28778461B011B1900E31BDD /* Common */ = { + isa = PBXGroup; + children = ( + D2C148ED1B0FAD83004F41DA /* ElephantSeals.mov */, + D2C148EB1B0FAD79004F41DA /* Drums.m4a */, + D277556F1B0D0A4100C9D649 /* MediaManifest.json */, + D28778471B011B2800E31BDD /* thumbnails */, + ); + name = Common; + sourceTree = ""; + }; + D28778471B011B2800E31BDD /* thumbnails */ = { + isa = PBXGroup; + children = ( + D27755731B0D0A5400C9D649 /* HLSThumb.png */, + D27755741B0D0A5400C9D649 /* LocalAudioThumb.png */, + D27755751B0D0A5400C9D649 /* LocalVideoThumb.png */, + D27755761B0D0A5400C9D649 /* ProgressiveThumb.png */, + ); + name = thumbnails; + sourceTree = ""; + }; + D2C148E81B0FA81E004F41DA /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D2C148E61B0FA816004F41DA /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + D2385B411AF5181400DC8ADE /* AVFoundationQueuePlayer-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationQueuePlayer-iOS" */; + buildPhases = ( + D2385B3E1AF5181400DC8ADE /* Sources */, + D2385B3F1AF5181400DC8ADE /* Frameworks */, + D2385B401AF5181400DC8ADE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "AVFoundationQueuePlayer-iOS"; + productName = "AVFoundationQueuePlayer-iOS"; + productReference = D2385B421AF5181400DC8ADE /* AVFoundationQueuePlayer-ObjC.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D2385B3A1AF5181400DC8ADE /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = "Apple Inc."; + TargetAttributes = { + D2385B411AF5181400DC8ADE = { + CreatedOnToolsVersion = 7.0; + }; + }; + }; + buildConfigurationList = D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationQueuePlayer-iOS" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D2385B391AF5181400DC8ADE; + productRefGroup = D2385B431AF5181400DC8ADE /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D2385B411AF5181400DC8ADE /* AVFoundationQueuePlayer-iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D2385B401AF5181400DC8ADE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2C148EC1B0FAD79004F41DA /* Drums.m4a in Resources */, + D2C148EE1B0FAD83004F41DA /* ElephantSeals.mov in Resources */, + D27489941B1CC6E00020D82A /* Localizable.strings in Resources */, + D277557A1B0D0A5400C9D649 /* ProgressiveThumb.png in Resources */, + D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */, + D27489951B1CC6E00020D82A /* Localizable.stringsdict in Resources */, + D27755771B0D0A5400C9D649 /* HLSThumb.png in Resources */, + D27755701B0D0A4100C9D649 /* MediaManifest.json in Resources */, + D27755791B0D0A5400C9D649 /* LocalVideoThumb.png in Resources */, + D27755781B0D0A5400C9D649 /* LocalAudioThumb.png in Resources */, + D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D2385B3E1AF5181400DC8ADE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D274898F1B1CC58A0020D82A /* AAPLQueuedItemCollectionViewCell.m in Sources */, + D2385B461AF5181400DC8ADE /* AAPLAppDelegate.m in Sources */, + D274898E1B1CC58A0020D82A /* AAPLPlayerView.m in Sources */, + D265BBB71B1792720005C539 /* AAPLPlayerViewController.m in Sources */, + D2C148E71B0FA816004F41DA /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + D2385B491AF5181400DC8ADE /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D2385B4A1AF5181400DC8ADE /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + D27489901B1CC6E00020D82A /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D27489911B1CC6E00020D82A /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D27489921B1CC6E00020D82A /* Localizable.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + D27489931B1CC6E00020D82A /* en */, + ); + name = Localizable.stringsdict; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + D2385B5D1AF5181400DC8ADE /* 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 = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = 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_SYMBOLS_PRIVATE_EXTERN = NO; + 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; + }; + D2385B5E1AF5181400DC8ADE /* 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 = "iPhone Developer"; + "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; + }; + D2385B601AF5181400DC8ADE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = "AVFoundationQueuePlayer-iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "AVFoundationQueuePlayer-ObjC"; + PROVISIONING_PROFILE = ""; + }; + name = Debug; + }; + D2385B611AF5181400DC8ADE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = "AVFoundationQueuePlayer-iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "AVFoundationQueuePlayer-ObjC"; + PROVISIONING_PROFILE = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationQueuePlayer-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2385B5D1AF5181400DC8ADE /* Debug */, + D2385B5E1AF5181400DC8ADE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationQueuePlayer-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2385B601AF5181400DC8ADE /* Debug */, + D2385B611AF5181400DC8ADE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D2385B3A1AF5181400DC8ADE /* Project object */; +} diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLAppDelegate.h b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLAppDelegate.h new file mode 100644 index 00000000..f67edc63 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLAppDelegate.h @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate. +*/ + +@import UIKit; + +@interface AAPLAppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end + diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLAppDelegate.m b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLAppDelegate.m new file mode 100644 index 00000000..fd04e586 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLAppDelegate.m @@ -0,0 +1,12 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate. +*/ + +#import "AAPLAppDelegate.h" + +@implementation AAPLAppDelegate +@end diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerView.h b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerView.h new file mode 100644 index 00000000..a94b0c91 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerView.h @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View containing an AVPlayerLayer. +*/ + +@import UIKit; + +@class AVPlayer; + +@interface AAPLPlayerView : UIView +@property AVPlayer *player; +@property (readonly) AVPlayerLayer *playerLayer; +@end diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerView.m b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerView.m new file mode 100644 index 00000000..cfa02265 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerView.m @@ -0,0 +1,34 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View containing an AVPlayerLayer. +*/ + +@import Foundation; +@import AVFoundation; +#import "AAPLPlayerView.h" + + +@implementation AAPLPlayerView + +- (AVPlayer *)player { + return self.playerLayer.player; +} + +- (void)setPlayer:(AVPlayer *)player { + self.playerLayer.player = player; +} + +// override UIView ++ (Class)layerClass { + return [AVPlayerLayer class]; +} + +- (AVPlayerLayer *)playerLayer { + return (AVPlayerLayer *)self.layer; +} + +@end + diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerViewController.h b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerViewController.h new file mode 100644 index 00000000..02d5e21d --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerViewController.h @@ -0,0 +1,32 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller containing a player view and basic playback controls. +*/ + +@import UIKit; + + +@class AAPLPlayerView; + +@interface AAPLPlayerViewController : UIViewController + +@property (readonly) AVQueuePlayer *player; + +/* + @{ + NSURL(asset URL) : @{ + NSString(title) : NSString, + NSString(thumbnail) : UIImage + } + } +*/ +@property NSMutableDictionary *loadedAssets; + +@property CMTime currentTime; +@property (readonly) CMTime duration; +@property float rate; + +@end diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerViewController.m b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerViewController.m new file mode 100644 index 00000000..a64f4b1a --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLPlayerViewController.m @@ -0,0 +1,491 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller containing a player view and basic playback controls. +*/ + + +@import Foundation; +@import AVFoundation; +@import CoreMedia.CMTime; +#import "AAPLPlayerViewController.h" +#import "AAPLPlayerView.h" +#import "AAPLQueuedItemCollectionViewCell.h" + + +// Private properties +@interface AAPLPlayerViewController () +{ + AVQueuePlayer *_player; + AVURLAsset *_asset; + + /* + A token obtained from calling `player`'s `addPeriodicTimeObserverForInterval(_:queue:usingBlock:)` + method. + */ + id _timeObserverToken; + AVPlayerItem *_playerItem; +} + +@property (readonly) AVPlayerLayer *playerLayer; + +@property NSMutableDictionary *assetTitlesAndThumbnailsByURL; + +// Formatter to provide formatted value for seconds displayed in `startTimeLabel` and `durationLabel`. +@property (readonly) NSDateComponentsFormatter *timeRemainingFormatter; + +@property (weak) IBOutlet UISlider *timeSlider; +@property (weak) IBOutlet UILabel *startTimeLabel; +@property (weak) IBOutlet UILabel *durationLabel; +@property (weak) IBOutlet UIButton *rewindButton; +@property (weak) IBOutlet UIButton *playPauseButton; +@property (weak) IBOutlet UIButton *fastForwardButton; +@property (weak) IBOutlet UIButton *clearButton; +@property (weak) IBOutlet UICollectionView *collectionView; +@property (weak) IBOutlet UILabel *queueLabel; +@property (weak) IBOutlet AAPLPlayerView *playerView; + +@end + +@implementation AAPLPlayerViewController + +// MARK: - View Controller + +/* + KVO context used to differentiate KVO callbacks for this class versus other + classes in its class hierarchy. +*/ +static int AAPLPlayerViewControllerKVOContext = 0; + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + /* + Update the UI when these player properties change. + + Use the context parameter to distinguish KVO for our particular observers and not + those destined for a subclass that also happens to be observing these properties. + */ + [self addObserver:self forKeyPath:@"player.currentItem.duration" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext]; + [self addObserver:self forKeyPath:@"player.rate" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext]; + [self addObserver:self forKeyPath:@"player.currentItem.status" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext]; + [self addObserver:self forKeyPath:@"player.currentItem" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext]; + + self.playerView.playerLayer.player = self.player; + + /* + Read the list of assets we'll be using from a JSON file. + */ + [self asynchronouslyLoadURLAssetsWithManifestURL:[[NSBundle mainBundle] URLForResource:@"MediaManifest" withExtension:@"json"]]; + + // Use a weak self variable to avoid a retain cycle in the block. + AAPLPlayerViewController __weak *weakSelf = self; + _timeObserverToken = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { + double timeElapsed = CMTimeGetSeconds(time); + + weakSelf.timeSlider.value = timeElapsed; + weakSelf.startTimeLabel.text = [weakSelf createTimeString: timeElapsed]; + }]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + + if (_timeObserverToken) { + [self.player removeTimeObserver:_timeObserverToken]; + _timeObserverToken = nil; + } + + [self.player pause]; + + [self removeObserver:self forKeyPath:@"player.currentItem.duration" context:&AAPLPlayerViewControllerKVOContext]; + [self removeObserver:self forKeyPath:@"player.rate" context:&AAPLPlayerViewControllerKVOContext]; + [self removeObserver:self forKeyPath:@"player.currentItem.status" context:&AAPLPlayerViewControllerKVOContext]; + [self removeObserver:self forKeyPath:@"player.currentItem" context:&AAPLPlayerViewControllerKVOContext]; +} + + +// MARK: - Properties + +// Will attempt load and test these asset keys before playing ++ (NSArray *)assetKeysRequiredToPlay { + return @[@"playable", @"hasProtectedContent"]; +} + +- (AVQueuePlayer *)player { + if (!_player) { + _player = [[AVQueuePlayer alloc] init]; + } + return _player; +} + +- (CMTime)currentTime { + return self.player.currentTime; +} + +- (void)setCurrentTime:(CMTime)newCurrentTime { + [self.player seekToTime:newCurrentTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; +} + +- (CMTime)duration { + return self.player.currentItem ? self.player.currentItem.duration : kCMTimeZero; +} + +- (float)rate { + return self.player.rate; +} + +- (void)setRate:(float)newRate { + self.player.rate = newRate; +} + +- (AVPlayerLayer *)playerLayer { + return self.playerView.playerLayer; +} + +- (NSDateComponentsFormatter *)timeRemainingFormatter { + NSDateComponentsFormatter *formatter = [[NSDateComponentsFormatter alloc] init]; + formatter.zeroFormattingBehavior = NSDateComponentsFormatterZeroFormattingBehaviorPad; + formatter.allowedUnits = NSCalendarUnitMinute | NSCalendarUnitSecond; + + return formatter; +} + +// MARK: - Asset Loading + +/* + Prepare an AVAsset for use on a background thread. When the minimum set + of properties we require (`assetKeysRequiredToPlay`) are loaded then add + the asset to the `assetTitlesAndThumbnails` dictionary. We'll use that + dictionary to populate the "Add Item" button popover. +*/ +- (void)asynchronouslyLoadURLAsset:(AVURLAsset *)asset title:(NSString *)title thumbnailResourceName:(NSString *)thumbnailResourceName { + + /* + Using AVAsset now runs the risk of blocking the current thread (the + main UI thread) whilst I/O happens to populate the properties. It's + prudent to defer our work until the properties we need have been loaded. + */ + [asset loadValuesAsynchronouslyForKeys:AAPLPlayerViewController.assetKeysRequiredToPlay completionHandler:^{ + + /* + The asset invokes its completion handler on an arbitrary queue. + To avoid multiple threads using our internal state at the same time + we'll elect to use the main thread at all times, let's dispatch + our handler to the main queue. + */ + dispatch_async(dispatch_get_main_queue(), ^{ + + /* + This method is called when the `AVAsset` for our URL has + completed the loading of the values of the specified array + of keys. + */ + + /* + Test whether the values of each of the keys we need have been + successfully loaded. + */ + for (NSString *key in self.class.assetKeysRequiredToPlay) { + NSError *error = nil; + if ([asset statusOfValueForKey:key error:&error] == AVKeyValueStatusFailed) { + NSString *stringFormat = NSLocalizedString(@"error.asset_%@_key_%@_failed.description", @"Can't use this AVAsset because one of it's keys failed to load"); + + NSString *message = [NSString localizedStringWithFormat:stringFormat, title, key]; + + [self handleErrorWithMessage:message error:error]; + + return; + } + } + + // We can't play this asset. + if (!asset.playable || asset.hasProtectedContent) { + NSString *stringFormat = NSLocalizedString(@"error.asset_%@_not_playable.description", @"Can't use this AVAsset because it isn't playable or has protected content"); + + NSString *message = [NSString localizedStringWithFormat:stringFormat, title]; + + [self handleErrorWithMessage:message error:nil]; + + return; + } + + /* + We can play this asset. Create a new AVPlayerItem and make it + our player's current item. + */ + if (!self.loadedAssets) + self.loadedAssets = [NSMutableDictionary dictionary]; + self.loadedAssets[title] = asset; + + NSString *path = [[NSBundle mainBundle] pathForResource:[thumbnailResourceName stringByDeletingPathExtension] ofType:[thumbnailResourceName pathExtension]]; + UIImage *thumbnail = [[UIImage alloc] initWithContentsOfFile:path]; + if (!self.assetTitlesAndThumbnailsByURL) { + self.assetTitlesAndThumbnailsByURL = [NSMutableDictionary dictionary]; + } + self.assetTitlesAndThumbnailsByURL[asset.URL] = @{ @"title" : title, @"thumbnail" : thumbnail }; + }); + }]; +} + +/* + Read the asset URLs, titles and thumbnail resource names from a JSON manifest + file - then load each asset. +*/ +- (void)asynchronouslyLoadURLAssetsWithManifestURL:(NSURL *)jsonURL +{ + NSArray *assetsArray = nil; + + NSData *jsonData = [[NSData alloc] initWithContentsOfURL:jsonURL]; + if (jsonData) { + assetsArray = (NSArray *)[NSJSONSerialization JSONObjectWithData:jsonData options:0 error:nil]; + if (!assetsArray) { + [self handleErrorWithMessage:NSLocalizedString(@"error.json_parse_failed.description", @"Failed to parse the assets manifest JSON") error:nil]; + } + } + else { + [self handleErrorWithMessage:NSLocalizedString(@"error.json_open_failed.description", @"Failed to open the assets manifest JSON") error:nil]; + } + + for (NSDictionary *assetDict in assetsArray) { + + NSURL *mediaURL = nil; + NSString *optionalResourceName = assetDict[@"mediaResourceName"]; + NSString *optionalURLString = assetDict[@"mediaURL"]; + if (optionalResourceName) { + mediaURL = [[NSBundle mainBundle] URLForResource:[optionalResourceName stringByDeletingPathExtension] withExtension:optionalResourceName.pathExtension]; + } + else if (optionalURLString) { + mediaURL = [NSURL URLWithString:optionalURLString]; + } + + [self asynchronouslyLoadURLAsset:[AVURLAsset URLAssetWithURL:mediaURL options:nil] + title:assetDict[@"title"] + thumbnailResourceName:assetDict[@"thumbnailResourceName"]]; + } +} + +// MARK: - IBActions + +- (IBAction)playPauseButtonWasPressed:(UIButton *)sender { + if (self.player.rate != 1.0) { + // Not playing foward; so play. + if (CMTIME_COMPARE_INLINE(self.currentTime, ==, self.duration)) { + // At end; so got back to beginning. + self.currentTime = kCMTimeZero; + } + [self.player play]; + } else { + // Playing; so pause. + [self.player pause]; + } +} + +- (IBAction)rewindButtonWasPressed:(UIButton *)sender { + self.rate = MAX(self.player.rate - 2.0, -2.0); // rewind no faster than -2.0 +} + +- (IBAction)fastForwardButtonWasPressed:(UIButton *)sender { + self.rate = MIN(self.player.rate + 2.0, 2.0); // fast forward no faster than 2.0 +} + +- (IBAction)timeSliderDidChange:(UISlider *)sender { + self.currentTime = CMTimeMakeWithSeconds(sender.value, 1000); +} + +- (void)presentModalPopoverAlertController:(UIAlertController *)alertController sender:(UIButton *)sender { + alertController.modalPresentationStyle = UIModalPresentationPopover; + + alertController.popoverPresentationController.sourceView = sender; + alertController.popoverPresentationController.sourceRect = sender.bounds; + alertController.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionAny; + + [self presentViewController:alertController animated:true completion:nil]; +} + +- (IBAction)addItemToQueueButtonPressed:(UIButton *)sender { + + NSString *alertTitle = NSLocalizedString(@"popover.title.addItem", @"Title of popover that adds items to the queue"); + NSString *alertMessage = NSLocalizedString(@"popover.message.addItem", @"Message on popover that adds items to the queue"); + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:alertTitle message:alertMessage preferredStyle:UIAlertControllerStyleActionSheet]; + + // Populate the sheet with the titles of the assets we have loaded. + for (NSString *loadedAssetTitle in self.loadedAssets.allKeys) { + AVAsset *loadedAsset = self.loadedAssets[loadedAssetTitle]; + AAPLPlayerViewController __weak *weakSelf = self; + [alertController addAction:[UIAlertAction actionWithTitle:loadedAssetTitle style:UIAlertActionStyleDefault handler: + ^(UIAlertAction *action){ + NSArray *oldItemsArray = [weakSelf.player items]; + AVPlayerItem *newPlayerItem = [AVPlayerItem playerItemWithAsset:loadedAsset]; + [weakSelf.player insertItem:newPlayerItem afterItem:nil]; + [weakSelf queueDidChangeFromArray:oldItemsArray toArray:[self.player items]]; + }]]; + } + + NSString *cancelActionTitle = NSLocalizedString(@"popover.title.cancel", @"Title of popover cancel action"); + [alertController addAction:[UIAlertAction actionWithTitle:cancelActionTitle style:UIAlertActionStyleCancel handler:nil]]; + + [self presentModalPopoverAlertController:alertController sender:sender]; +} + +- (IBAction)clearQueueButtonWasPressed:(UIButton *)sender { + + NSString *alertTitle = NSLocalizedString(@"popover.title.clear", @"Title of popover that clears the queue"); + NSString *alertMessage = NSLocalizedString(@"popover.message.clear", @"Message on popover that clears the queue"); + UIAlertController *alertController = [UIAlertController alertControllerWithTitle:alertTitle message:alertMessage preferredStyle:UIAlertControllerStyleActionSheet]; + + AAPLPlayerViewController __weak *weakSelf = self; + [alertController addAction:[UIAlertAction actionWithTitle:@"Clear Queue" style:UIAlertActionStyleDestructive handler: + ^(UIAlertAction *action){ + NSArray *oldItemsArray = [weakSelf.player items]; + [weakSelf.player removeAllItems]; + [weakSelf queueDidChangeFromArray:oldItemsArray toArray:[self.player items]]; + }]]; + + NSString *cancelActionTitle = NSLocalizedString(@"popover.title.cancel", @"Title of popover cancel action"); + [alertController addAction:[UIAlertAction actionWithTitle:cancelActionTitle style:UIAlertActionStyleCancel handler:nil]]; + + [self presentModalPopoverAlertController:alertController sender:sender]; +} + +// MARK: - KV Observation + +// Update our UI when player or player.currentItem changes +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + + if (context != &AAPLPlayerViewControllerKVOContext) { + // KVO isn't for us. + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + return; + } + + if ([keyPath isEqualToString:@"player.currentItem"]) { + [self queueDidChangeFromArray:nil toArray:[self.player items]]; + + } + else if ([keyPath isEqualToString:@"player.currentItem.duration"]) { + // Update timeSlider and enable/disable controls when duration > 0.0 + + // Handle NSNull value for NSKeyValueChangeNewKey, i.e. when player.currentItem is nil + NSValue *newDurationAsValue = change[NSKeyValueChangeNewKey]; + CMTime newDuration = [newDurationAsValue isKindOfClass:[NSValue class]] ? newDurationAsValue.CMTimeValue : kCMTimeZero; + BOOL hasValidDuration = CMTIME_IS_NUMERIC(newDuration) && newDuration.value != 0; + double currentTime = hasValidDuration ? CMTimeGetSeconds(self.currentTime) : 0.0; + double newDurationSeconds = hasValidDuration ? CMTimeGetSeconds(newDuration) : 0.0; + + self.timeSlider.maximumValue = newDurationSeconds; + self.timeSlider.value = currentTime; + self.rewindButton.enabled = hasValidDuration; + self.playPauseButton.enabled = hasValidDuration; + self.fastForwardButton.enabled = hasValidDuration; + self.timeSlider.enabled = hasValidDuration; + self.startTimeLabel.enabled = hasValidDuration; + self.startTimeLabel.text = [self createTimeString:currentTime]; + self.durationLabel.enabled = hasValidDuration; + self.durationLabel.text = [self createTimeString:newDurationSeconds]; + } + else if ([keyPath isEqualToString:@"player.rate"]) { + // Update playPauseButton image + + double newRate = [change[NSKeyValueChangeNewKey] doubleValue]; + UIImage *buttonImage = (newRate == 1.0) ? [UIImage imageNamed:@"PauseButton"] : [UIImage imageNamed:@"PlayButton"]; + [self.playPauseButton setImage:buttonImage forState:UIControlStateNormal]; + + } + else if ([keyPath isEqualToString:@"player.currentItem.status"]) { + // Display an error if status becomes Failed + + // Handle NSNull value for NSKeyValueChangeNewKey, i.e. when player.currentItem is nil + NSNumber *newStatusAsNumber = change[NSKeyValueChangeNewKey]; + AVPlayerItemStatus newStatus = [newStatusAsNumber isKindOfClass:[NSNumber class]] ? newStatusAsNumber.integerValue : AVPlayerItemStatusUnknown; + + if (newStatus == AVPlayerItemStatusFailed) { + [self handleErrorWithMessage:self.player.currentItem.error.localizedDescription error:self.player.currentItem.error]; + } + + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +// Trigger KVO for anyone observing our properties affected by player and player.currentItem ++ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { + if ([key isEqualToString:@"duration"]) { + return [NSSet setWithArray:@[ @"player.currentItem.duration" ]]; + } else if ([key isEqualToString:@"currentTime"]) { + return [NSSet setWithArray:@[ @"player.currentItem.currentTime" ]]; + } else if ([key isEqualToString:@"rate"]) { + return [NSSet setWithArray:@[ @"player.rate" ]]; + } else { + return [super keyPathsForValuesAffectingValueForKey:key]; + } +} + +// player.items is not KV observable so we need to call this function every time the queue changes +- (void)queueDidChangeFromArray:(NSArray *)oldPlayerItems toArray:(NSArray *)newPlayerItems { + + if (newPlayerItems.count == 0) { + self.queueLabel.text = NSLocalizedString(@"label.queue.empty", @"Queue is empty"); + } + else { + NSString *stringFormat = NSLocalizedString(@"label.queue.%lu items", @"Queue of n item(s)"); + + self.queueLabel.text = [NSString localizedStringWithFormat:stringFormat, newPlayerItems.count]; + } + + BOOL isQueueEmpty = newPlayerItems.count == 0; + self.clearButton.enabled = !isQueueEmpty; + + [self.collectionView reloadData]; +} + +// MARK: - Error Handling + +- (void)handleErrorWithMessage:(NSString *)message error:(NSError *)error { + NSLog(@"Error occurred with message: %@, error: %@.", message, error); + + NSString *alertTitle = NSLocalizedString(@"alert.error.title", @"Alert title for errors"); + NSString *defaultAlertMessage = NSLocalizedString(@"error.default.description", @"Default error message when no NSError provided"); + UIAlertController *controller = [UIAlertController alertControllerWithTitle:alertTitle message:message ?: defaultAlertMessage preferredStyle:UIAlertControllerStyleAlert]; + + NSString *alertActionTitle = NSLocalizedString(@"alert.error.actions.OK", @"OK on error alert"); + UIAlertAction *action = [UIAlertAction actionWithTitle:alertActionTitle style:UIAlertActionStyleDefault handler:nil]; + [controller addAction:action]; + + [self presentViewController:controller animated:YES completion:nil]; +} + +// MARK: UICollectionViewDataSource + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + return [self.player items].count; +} + +- (NSDictionary *)titleAndThumbnailForPlayerItemAtIndexPath:(NSIndexPath *)indexPath { + AVPlayerItem *item = [self.player items][[indexPath indexAtPosition:1]]; + return self.assetTitlesAndThumbnailsByURL[[(AVURLAsset *)item.asset URL]]; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + + AAPLQueuedItemCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"ItemCell" forIndexPath:indexPath]; + + NSDictionary *titleAndThumbnail = [self titleAndThumbnailForPlayerItemAtIndexPath:indexPath]; + cell.label.text = titleAndThumbnail[@"title"]; + cell.backgroundView = [[UIImageView alloc] initWithImage:titleAndThumbnail[@"thumbnail"]]; + + return cell; +} + +// MARK: Convenience + +- (NSString *)createTimeString:(double)time { + NSDateComponents *components = [[NSDateComponents alloc] init]; + components.second = (NSInteger)fmax(0.0, time); + + return [self.timeRemainingFormatter stringFromDateComponents:components]; +} + +@end diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLQueuedItemCollectionViewCell.h b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLQueuedItemCollectionViewCell.h new file mode 100644 index 00000000..9017beb8 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLQueuedItemCollectionViewCell.h @@ -0,0 +1,14 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Collection view cell to represent an AVPlayerItem in an AVQueuePlayer's queue. +*/ + +@import UIKit; + + +@interface AAPLQueuedItemCollectionViewCell: UICollectionViewCell + @property (weak) IBOutlet UILabel *label; +@end diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLQueuedItemCollectionViewCell.m b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLQueuedItemCollectionViewCell.m new file mode 100644 index 00000000..fa166035 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/AAPLQueuedItemCollectionViewCell.m @@ -0,0 +1,13 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Collection view cell to represent an AVPlayerItem in an AVQueuePlayer's queue. +*/ + +#import "AAPLQueuedItemCollectionViewCell.h" + + +@implementation AAPLQueuedItemCollectionViewCell +@end diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Base.lproj/Main.storyboard b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000..7fbc457f --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Base.lproj/Main.storyboard @@ -0,0 +1,249 @@ + + + + + + + + + HelveticaNeue-Italic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json new file mode 100644 index 00000000..01a9d605 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "PauseButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PauseButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PauseButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png new file mode 100644 index 00000000..b812da3e Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png new file mode 100644 index 00000000..5fd2dacf Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png new file mode 100644 index 00000000..f1664dc6 Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json new file mode 100644 index 00000000..bdde07ab --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "PlayButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PlayButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PlayButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png new file mode 100644 index 00000000..1c9975a5 Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png new file mode 100644 index 00000000..9aa4bf37 Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png new file mode 100644 index 00000000..31046b7d Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json new file mode 100644 index 00000000..52413004 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ScanBackwardButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ScanBackwardButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ScanBackwardButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png new file mode 100644 index 00000000..6fbe0cd3 Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png new file mode 100644 index 00000000..46fd14ff Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png new file mode 100644 index 00000000..b1cca0f1 Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json new file mode 100644 index 00000000..8f07e000 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ScanForwardButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ScanForwardButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ScanForwardButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png new file mode 100644 index 00000000..43e251ec Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png new file mode 100644 index 00000000..bf28ce79 Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png new file mode 100644 index 00000000..f8ccaf93 Binary files /dev/null and b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png differ diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Info.plist b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Info.plist new file mode 100644 index 00000000..32ccc9a8 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/Info.plist @@ -0,0 +1,60 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + Main + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSExceptionDomains + + devimages.apple.com.edgekey.net + + NSExceptionRequiresForwardSecrecy + + + + + + diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.strings b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.strings new file mode 100644 index 00000000..3f7f9081 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.strings @@ -0,0 +1,52 @@ +/* + + + Localizable strings. + + +*/ + +// Alert title for errors. +"alert.error.title" = "Error"; + +// OK action on error alert +"alert.error.actions.OK" = "OK"; + +// Queue is empty. +"label.queue.empty" = "Media queue is empty"; + +// Queue of n item(s). +"label.queue.%lu items" = "Queue of %n media items"; + +// Title on button to clear the queue. +"button.title.clear" = "Clear Queue"; + +// Title of popover that clears the queue. +"popover.title.clear" = "Clear"; + +// Message on popover that clears the queue. +"popover.message.clear" = "Remove all media from the queue?"; + +// Title of popover cancel action. +"popover.title.cancel" = "Cancel"; + +// Title of popover that adds items to the queue. +"popover.title.addItem" = "Add Media"; + +// Message on popover that adds items to the queue. +"popover.message.addItem" = "Select a media type to add to the queue"; + +// Can't use this AVAsset because one of it's keys failed to load. +"error.asset_%@_key_%@_failed.description" = "Media \"%@\" failed to load key \"%@\""; + +// Can't use this AVAsset because it isn't playable or has protected content. +"error.asset_%@_not_playable.description" = "Media \"%@\" isn't playable or has protected content"; + +// Failed to parse the assets manifest JSON. +"error.json_parse_failed.description" = "Failed to parse the manifest"; + +// Failed to open the assets manifest JSON. +"error.json_open_failed.description" = "Failed to open the manifest"; + +// Default error message when no NSError provided. +"error.default.description" = "Unknown error"; diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.stringsdict b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.stringsdict new file mode 100644 index 00000000..22d035b9 --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + label.queue.%lu items + + NSStringLocalizedFormatKey + Queue of %#@lu_items@ + lu_items + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + lu + one + %lu item + other + %lu items + + + + diff --git a/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/main.m b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/main.m new file mode 100644 index 00000000..54cd040e --- /dev/null +++ b/AVFoundationQueuePlayer/Objective-C/AVFoundationQueuePlayer-iOS/main.m @@ -0,0 +1,59 @@ +/* + File: AAPLAppDelegate.m + + Abstract: Sample code for AVFoundationSimplePlayer-ObjC-iOS + + Version: 1.0 + + Disclaimer: 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) 2013-2015 Apple Inc. All Rights Reserved. +*/ + +#import +#import "AAPLAppDelegate.h" + + + + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AAPLAppDelegate class])); + } +} diff --git a/AVFoundationQueuePlayer/README.md b/AVFoundationQueuePlayer/README.md new file mode 100644 index 00000000..e26e5abb --- /dev/null +++ b/AVFoundationQueuePlayer/README.md @@ -0,0 +1,15 @@ +# AVFoundationQueuePlayer-iOS: Using a Mixture of Local File Based Assets and HTTP Live Streaming Assets with AVFoundation + +Demonstrates how to create a movie queue playback app using only the AVQueuePlayer and AVPlayerLayer classes of AVFoundation (not AVKit). You’ll find out how to manage a queue compromised of local, HTTP-live-streamed, and progressive-download movies. You’ll also see how to implement play, pause, skip, time slider updating, and scrubbing. + +## Requirements + +### Build + +Xcode 8.0, iOS 9.0 SDK + +### Runtime + +iOS 9.0 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS.xcodeproj/project.pbxproj b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS.xcodeproj/project.pbxproj new file mode 100644 index 00000000..fe5a7555 --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS.xcodeproj/project.pbxproj @@ -0,0 +1,374 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + D2010F001B0E8B0100BD0BDF /* ElephantSeals.mov in Resources */ = {isa = PBXBuildFile; fileRef = D2010EFE1B0E8B0100BD0BDF /* ElephantSeals.mov */; }; + D2010F011B0E8B0100BD0BDF /* Drums.m4a in Resources */ = {isa = PBXBuildFile; fileRef = D2010EFF1B0E8B0100BD0BDF /* Drums.m4a */; }; + D20EB3C21AF94FDE0059CD72 /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20EB3C11AF94FDE0059CD72 /* PlayerViewController.swift */; }; + D2385B461AF5181400DC8ADE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2385B451AF5181400DC8ADE /* AppDelegate.swift */; }; + D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D2385B491AF5181400DC8ADE /* Main.storyboard */; }; + D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2385B4C1AF5181400DC8ADE /* Images.xcassets */; }; + D27755701B0D0A4100C9D649 /* MediaManifest.json in Resources */ = {isa = PBXBuildFile; fileRef = D277556F1B0D0A4100C9D649 /* MediaManifest.json */; }; + D27755771B0D0A5400C9D649 /* HLSThumb.png in Resources */ = {isa = PBXBuildFile; fileRef = D27755731B0D0A5400C9D649 /* HLSThumb.png */; }; + D27755781B0D0A5400C9D649 /* LocalAudioThumb.png in Resources */ = {isa = PBXBuildFile; fileRef = D27755741B0D0A5400C9D649 /* LocalAudioThumb.png */; }; + D27755791B0D0A5400C9D649 /* LocalVideoThumb.png in Resources */ = {isa = PBXBuildFile; fileRef = D27755751B0D0A5400C9D649 /* LocalVideoThumb.png */; }; + D277557A1B0D0A5400C9D649 /* ProgressiveThumb.png in Resources */ = {isa = PBXBuildFile; fileRef = D27755761B0D0A5400C9D649 /* ProgressiveThumb.png */; }; + D2CE3DA81B18FFFD008088F1 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CE3DA61B18FFFD008088F1 /* PlayerView.swift */; }; + D2CE3DA91B18FFFD008088F1 /* QueuedItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CE3DA71B18FFFD008088F1 /* QueuedItemCollectionViewCell.swift */; }; + D2CE3DD71B192921008088F1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2CE3DD31B192921008088F1 /* Localizable.strings */; }; + D2CE3DD81B192921008088F1 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D2CE3DD51B192921008088F1 /* Localizable.stringsdict */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 3E67195F1B1D14C500E01959 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; + D2010EFE1B0E8B0100BD0BDF /* ElephantSeals.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; name = ElephantSeals.mov; path = ../Common/ElephantSeals.mov; sourceTree = SOURCE_ROOT; }; + D2010EFF1B0E8B0100BD0BDF /* Drums.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; name = Drums.m4a; path = ../Common/Drums.m4a; sourceTree = SOURCE_ROOT; }; + D20EB3C11AF94FDE0059CD72 /* PlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + D2385B421AF5181400DC8ADE /* AVFoundationQueuePlayer-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AVFoundationQueuePlayer-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + D2385B451AF5181400DC8ADE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D2385B4A1AF5181400DC8ADE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + D2385B4C1AF5181400DC8ADE /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + D2385B511AF5181400DC8ADE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D277556F1B0D0A4100C9D649 /* MediaManifest.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = MediaManifest.json; path = ../Common/MediaManifest.json; sourceTree = SOURCE_ROOT; }; + D27755731B0D0A5400C9D649 /* HLSThumb.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = HLSThumb.png; path = ../Common/HLSThumb.png; sourceTree = SOURCE_ROOT; }; + D27755741B0D0A5400C9D649 /* LocalAudioThumb.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = LocalAudioThumb.png; path = ../Common/LocalAudioThumb.png; sourceTree = SOURCE_ROOT; }; + D27755751B0D0A5400C9D649 /* LocalVideoThumb.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = LocalVideoThumb.png; path = ../Common/LocalVideoThumb.png; sourceTree = SOURCE_ROOT; }; + D27755761B0D0A5400C9D649 /* ProgressiveThumb.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = ProgressiveThumb.png; path = ../Common/ProgressiveThumb.png; sourceTree = SOURCE_ROOT; }; + D2CE3DA61B18FFFD008088F1 /* PlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; }; + D2CE3DA71B18FFFD008088F1 /* QueuedItemCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueuedItemCollectionViewCell.swift; sourceTree = ""; }; + D2CE3DD41B192921008088F1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + D2CE3DD61B192921008088F1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D2385B3F1AF5181400DC8ADE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D2385B391AF5181400DC8ADE = { + isa = PBXGroup; + children = ( + 3E67195F1B1D14C500E01959 /* README.md */, + D2385B441AF5181400DC8ADE /* AVFoundationQueuePlayer-iOS */, + D2385B431AF5181400DC8ADE /* Products */, + ); + sourceTree = ""; + }; + D2385B431AF5181400DC8ADE /* Products */ = { + isa = PBXGroup; + children = ( + D2385B421AF5181400DC8ADE /* AVFoundationQueuePlayer-Swift.app */, + ); + name = Products; + sourceTree = ""; + }; + D2385B441AF5181400DC8ADE /* AVFoundationQueuePlayer-iOS */ = { + isa = PBXGroup; + children = ( + D2385B451AF5181400DC8ADE /* AppDelegate.swift */, + D20EB3C11AF94FDE0059CD72 /* PlayerViewController.swift */, + D2CE3DA61B18FFFD008088F1 /* PlayerView.swift */, + D2CE3DA71B18FFFD008088F1 /* QueuedItemCollectionViewCell.swift */, + D2385B491AF5181400DC8ADE /* Main.storyboard */, + D2385B4C1AF5181400DC8ADE /* Images.xcassets */, + D2385B511AF5181400DC8ADE /* Info.plist */, + D2CE3DD31B192921008088F1 /* Localizable.strings */, + D2CE3DD51B192921008088F1 /* Localizable.stringsdict */, + D28778461B011B1900E31BDD /* Common */, + ); + path = "AVFoundationQueuePlayer-iOS"; + sourceTree = ""; + }; + D28778461B011B1900E31BDD /* Common */ = { + isa = PBXGroup; + children = ( + D2010EFF1B0E8B0100BD0BDF /* Drums.m4a */, + D2010EFE1B0E8B0100BD0BDF /* ElephantSeals.mov */, + D277556F1B0D0A4100C9D649 /* MediaManifest.json */, + D28778471B011B2800E31BDD /* Thumbnails */, + ); + name = Common; + sourceTree = ""; + }; + D28778471B011B2800E31BDD /* Thumbnails */ = { + isa = PBXGroup; + children = ( + D27755731B0D0A5400C9D649 /* HLSThumb.png */, + D27755741B0D0A5400C9D649 /* LocalAudioThumb.png */, + D27755751B0D0A5400C9D649 /* LocalVideoThumb.png */, + D27755761B0D0A5400C9D649 /* ProgressiveThumb.png */, + ); + name = Thumbnails; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + D2385B411AF5181400DC8ADE /* AVFoundationQueuePlayer-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationQueuePlayer-iOS" */; + buildPhases = ( + D2385B3E1AF5181400DC8ADE /* Sources */, + D2385B3F1AF5181400DC8ADE /* Frameworks */, + D2385B401AF5181400DC8ADE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "AVFoundationQueuePlayer-iOS"; + productName = "AVFoundationQueuePlayer-Swift-iOS"; + productReference = D2385B421AF5181400DC8ADE /* AVFoundationQueuePlayer-Swift.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D2385B3A1AF5181400DC8ADE /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Apple Inc."; + TargetAttributes = { + D2385B411AF5181400DC8ADE = { + CreatedOnToolsVersion = 7.0; + LastSwiftMigration = 0800; + }; + }; + }; + buildConfigurationList = D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationQueuePlayer-iOS" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D2385B391AF5181400DC8ADE; + productRefGroup = D2385B431AF5181400DC8ADE /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D2385B411AF5181400DC8ADE /* AVFoundationQueuePlayer-iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D2385B401AF5181400DC8ADE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2CE3DD71B192921008088F1 /* Localizable.strings in Resources */, + D277557A1B0D0A5400C9D649 /* ProgressiveThumb.png in Resources */, + D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */, + D27755771B0D0A5400C9D649 /* HLSThumb.png in Resources */, + D27755701B0D0A4100C9D649 /* MediaManifest.json in Resources */, + D27755791B0D0A5400C9D649 /* LocalVideoThumb.png in Resources */, + D27755781B0D0A5400C9D649 /* LocalAudioThumb.png in Resources */, + D2010F001B0E8B0100BD0BDF /* ElephantSeals.mov in Resources */, + D2CE3DD81B192921008088F1 /* Localizable.stringsdict in Resources */, + D2010F011B0E8B0100BD0BDF /* Drums.m4a in Resources */, + D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D2385B3E1AF5181400DC8ADE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D2CE3DA81B18FFFD008088F1 /* PlayerView.swift in Sources */, + D2385B461AF5181400DC8ADE /* AppDelegate.swift in Sources */, + D20EB3C21AF94FDE0059CD72 /* PlayerViewController.swift in Sources */, + D2CE3DA91B18FFFD008088F1 /* QueuedItemCollectionViewCell.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + D2385B491AF5181400DC8ADE /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D2385B4A1AF5181400DC8ADE /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + D2CE3DD31B192921008088F1 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D2CE3DD41B192921008088F1 /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + D2CE3DD51B192921008088F1 /* Localizable.stringsdict */ = { + isa = PBXVariantGroup; + children = ( + D2CE3DD61B192921008088F1 /* en */, + ); + name = Localizable.stringsdict; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + D2385B5D1AF5181400DC8ADE /* 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 = "iPhone Developer"; + "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_SYMBOLS_PRIVATE_EXTERN = NO; + 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; + }; + D2385B5E1AF5181400DC8ADE /* 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 = "iPhone Developer"; + "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; + }; + D2385B601AF5181400DC8ADE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = "$(SRCROOT)/AVFoundationQueuePlayer-iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "AVFoundationQueuePlayer-Swift"; + PROVISIONING_PROFILE = ""; + SDKROOT = iphoneos; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + D2385B611AF5181400DC8ADE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = "$(SRCROOT)/AVFoundationQueuePlayer-iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "AVFoundationQueuePlayer-Swift"; + PROVISIONING_PROFILE = ""; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationQueuePlayer-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2385B5D1AF5181400DC8ADE /* Debug */, + D2385B5E1AF5181400DC8ADE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationQueuePlayer-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2385B601AF5181400DC8ADE /* Debug */, + D2385B611AF5181400DC8ADE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D2385B3A1AF5181400DC8ADE /* Project object */; +} diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/AppDelegate.swift b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/AppDelegate.swift new file mode 100644 index 00000000..bf01b0f4 --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/AppDelegate.swift @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Main application entry point. +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + // MARK: Properties + + var window: UIWindow? +} diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Base.lproj/Main.storyboard b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000..cf0ffca4 --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Base.lproj/Main.storyboard @@ -0,0 +1,264 @@ + + + + + + + + + HelveticaNeue-Italic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json new file mode 100644 index 00000000..01a9d605 --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "PauseButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PauseButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PauseButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png new file mode 100644 index 00000000..b812da3e Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png new file mode 100644 index 00000000..5fd2dacf Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png new file mode 100644 index 00000000..f1664dc6 Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json new file mode 100644 index 00000000..bdde07ab --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "PlayButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PlayButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PlayButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png new file mode 100644 index 00000000..1c9975a5 Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png new file mode 100644 index 00000000..9aa4bf37 Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png new file mode 100644 index 00000000..31046b7d Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json new file mode 100644 index 00000000..52413004 --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ScanBackwardButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ScanBackwardButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ScanBackwardButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png new file mode 100644 index 00000000..6fbe0cd3 Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png new file mode 100644 index 00000000..46fd14ff Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png new file mode 100644 index 00000000..b1cca0f1 Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json new file mode 100644 index 00000000..8f07e000 --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ScanForwardButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ScanForwardButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ScanForwardButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png new file mode 100644 index 00000000..43e251ec Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png new file mode 100644 index 00000000..bf28ce79 Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png new file mode 100644 index 00000000..f8ccaf93 Binary files /dev/null and b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png differ diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Info.plist b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Info.plist new file mode 100644 index 00000000..e8c4cf85 --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/Info.plist @@ -0,0 +1,60 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSExceptionDomains + + devimages.apple.com.edgekey.net + + NSExceptionRequiresForwardSecrecy + + + + + UILaunchStoryboardName + Main + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/PlayerView.swift b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/PlayerView.swift new file mode 100644 index 00000000..61dd42ca --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/PlayerView.swift @@ -0,0 +1,31 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Player view backed by an AVPlayerLayer. +*/ + +import UIKit +import AVFoundation + +/// A simple `UIView` subclass that is backed by an `AVPlayerLayer` layer. +class PlayerView: UIView { + var player: AVPlayer? { + get { + return playerLayer.player + } + + set { + playerLayer.player = newValue + } + } + + var playerLayer: AVPlayerLayer { + return layer as! AVPlayerLayer + } + + override class var layerClass: AnyClass { + return AVPlayerLayer.self + } +} diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/PlayerViewController.swift b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/PlayerViewController.swift new file mode 100644 index 00000000..bf972baa --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/PlayerViewController.swift @@ -0,0 +1,532 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller containing a player view, collection view showing the AVQueuePlayer content and basic playback controls. +*/ + +import UIKit +import AVFoundation + +/* + KVO context used to differentiate KVO callbacks for this class versus other + classes in its class hierarchy. +*/ +private var playerViewControllerKVOContext = 0 + +class PlayerViewController: UIViewController, UICollectionViewDataSource { + // MARK: Properties + + // Attempt load and test these asset keys before playing. + static let assetKeysRequiredToPlay = [ + "playable", + "hasProtectedContent" + ] + + let player = AVQueuePlayer() + + var currentTime: Double { + get { + return CMTimeGetSeconds(player.currentTime()) + } + + set { + let newTime = CMTimeMakeWithSeconds(newValue, 1) + player.seek(to: newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) + } + } + + var duration: Double { + guard let currentItem = player.currentItem else { return 0.0 } + + return CMTimeGetSeconds(currentItem.duration) + } + + var rate: Float { + get { + return player.rate + } + + set { + player.rate = newValue + } + } + + var playerLayer: AVPlayerLayer? { + return playerView.playerLayer + } + + /* + A formatter for individual date components used to provide an appropriate + value for the `startTimeLabel` and `durationLabel`. + */ + let timeRemainingFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.zeroFormattingBehavior = .pad + formatter.allowedUnits = [.minute, .second] + + return formatter + }() + + /* + A token obtained from calling `player`'s `addPeriodicTimeObserverForInterval(_:queue:usingBlock:)` + method. + */ + var timeObserverToken: Any? + + var assetTitlesAndThumbnails: [URL: (title: String, thumbnail: UIImage)] = [:] + + var loadedAssets = [String: AVURLAsset]() + + // MARK: IBOutlets + + @IBOutlet weak var timeSlider: UISlider! + @IBOutlet weak var startTimeLabel: UILabel! + @IBOutlet weak var durationLabel: UILabel! + @IBOutlet weak var rewindButton: UIButton! + @IBOutlet weak var playPauseButton: UIButton! + @IBOutlet weak var fastForwardButton: UIButton! + @IBOutlet weak var clearButton: UIButton! + @IBOutlet weak var collectionView: UICollectionView! + @IBOutlet weak var queueLabel: UILabel! + @IBOutlet weak var playerView: PlayerView! + + // MARK: View Controller + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + /* + Update the UI when these player properties change. + + Use the context parameter to distinguish KVO for our particular observers + and not those destined for a subclass that also happens to be observing + these properties. + */ + addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.duration), options: [.new, .initial], context: &playerViewControllerKVOContext) + addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.rate), options: [.new, .initial], context: &playerViewControllerKVOContext) + addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.status), options: [.new, .initial], context: &playerViewControllerKVOContext) + addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem), options: [.new, .initial], context: &playerViewControllerKVOContext) + + playerView.playerLayer.player = player + + /* + Read the list of assets we'll be using from a JSON file. + */ + let manifestURL = Bundle.main.url(forResource: "MediaManifest", withExtension: "json")! + asynchronouslyLoadURLAssetsWithManifestURL(jsonURL: manifestURL) + + // Make sure we don't have a strong reference cycle by only capturing self as weak. + let interval = CMTimeMake(1, 1) + timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main) { [unowned self] time in + let timeElapsed = Float(CMTimeGetSeconds(time)) + + self.timeSlider.value = Float(timeElapsed) + self.startTimeLabel.text = self.createTimeString(time: timeElapsed) + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if let timeObserverToken = timeObserverToken { + player.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + + player.pause() + + removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.duration), context: &playerViewControllerKVOContext) + removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.rate), context: &playerViewControllerKVOContext) + removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.status), context: &playerViewControllerKVOContext) + removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem), context: &playerViewControllerKVOContext) + } + + // MARK: Asset Loading + + /* + Prepare an AVAsset for use on a background thread. When the minimum set + of properties we require (`assetKeysRequiredToPlay`) are loaded then add + the asset to the `assetTitlesAndThumbnails` dictionary. We'll use that + dictionary to populate the "Add Item" button popover. + */ + func asynchronouslyLoadURLAsset(asset: AVURLAsset, title: String, thumbnailResourceName: String) { + /* + Using AVAsset now runs the risk of blocking the current thread (the + main UI thread) whilst I/O happens to populate the properties. It's + prudent to defer our work until the properties we need have been loaded. + */ + asset.loadValuesAsynchronously(forKeys: PlayerViewController.assetKeysRequiredToPlay) { + + /* + The asset invokes its completion handler on an arbitrary queue. + To avoid multiple threads using our internal state at the same time + we'll elect to use the main thread at all times, let's dispatch + our handler to the main queue. + */ + DispatchQueue.main.async() { + /* + This method is called when the `AVAsset` for our URL has + completed the loading of the values of the specified array + of keys. + */ + + /* + Test whether the values of each of the keys we need have been + successfully loaded. + */ + for key in PlayerViewController.assetKeysRequiredToPlay { + var error: NSError? + + if asset.statusOfValue(forKey: key, error: &error) == .failed { + let stringFormat = NSLocalizedString("error.asset_%@_key_%@_failed.description", comment: "Can't use this AVAsset because one of it's keys failed to load") + + let message = String.localizedStringWithFormat(stringFormat, title, key) + + self.handleError(with: message, error: error) + + return + } + } + + // We can't play this asset. + if !asset.isPlayable || asset.hasProtectedContent { + let stringFormat = NSLocalizedString("error.asset_%@_not_playable.description", comment: "Can't use this AVAsset because it isn't playable or has protected content") + + let message = String.localizedStringWithFormat(stringFormat, title) + + self.handleError(with: message) + + return + } + + /* + We can play this asset. Create a new AVPlayerItem and make it + our player's current item. + */ + self.loadedAssets[title] = asset + + let name = (thumbnailResourceName as NSString).deletingPathExtension + let type = (thumbnailResourceName as NSString).pathExtension + let path = Bundle.main.path(forResource: name, ofType: type)! + + let thumbnail = UIImage(contentsOfFile: path)! + + self.assetTitlesAndThumbnails[asset.url] = (title, thumbnail) + } + } + } + + /* + Read the asset URLs, titles and thumbnail resource names from a JSON manifest + file - then load each asset. + */ + func asynchronouslyLoadURLAssetsWithManifestURL(jsonURL: URL!) { + var assetsJSON = [[String: AnyObject]]() + + if let jsonData = NSData(contentsOf: jsonURL as URL) { + do { + try assetsJSON = JSONSerialization.jsonObject(with: jsonData as Data, options: []) as! [[String: AnyObject]] + } + catch { + let message = NSLocalizedString("error.json_parse_failed.description", comment: "Failed to parse the assets manifest JSON") + + handleError(with: message) + } + } + else { + let message = NSLocalizedString("error.json_open_failed.description", comment: "Failed to open the assets manifest JSON") + + handleError(with: message) + } + + for assetJSON in assetsJSON { + let mediaURL: URL + + if let resourceName = assetJSON["mediaResourceName"] as! String? { + let name = (resourceName as NSString).deletingPathExtension + let type = (resourceName as NSString).pathExtension + mediaURL = Bundle.main.url(forResource: name, withExtension: type)! + } + else { + let URLString = assetJSON["mediaURL"] as! String + mediaURL = URL(string: URLString)! + } + + let title = assetJSON["title"] as! String + let thumbnailResourceName = assetJSON["thumbnailResourceName"] as! String + + let asset = AVURLAsset(url: mediaURL as URL, options: [:]) + asynchronouslyLoadURLAsset(asset: asset, title: title, thumbnailResourceName: thumbnailResourceName) + } + } + + // MARK: - IBActions + + @IBAction func playPauseButtonWasPressed(_ sender: UIButton) { + if player.rate != 1.0 { + // Not playing forward, so play. + if currentTime == duration { + // At end, so go back to beginning. + currentTime = 0.0 + } + + player.play() + } + else { + // Playing, so pause. + player.pause() + } + } + + @IBAction func rewindButtonWasPressed(_ sender: UIButton) { + // Rewind no faster than -2.0. + rate = max(player.rate - 2.0, -2.0) + } + + @IBAction func fastForwardButtonWasPressed(_ sender: UIButton) { + // Fast forward no faster than 2.0. + rate = min(player.rate + 2.0, 2.0) + } + + @IBAction func timeSliderDidChange(_ sender: UISlider) { + currentTime = Double(sender.value) + } + + private func presentModalPopoverAlertController(alertController: UIAlertController, sender: UIButton) { + alertController.modalPresentationStyle = .popover + + alertController.popoverPresentationController?.sourceView = sender + alertController.popoverPresentationController?.sourceRect = sender.bounds + alertController.popoverPresentationController?.permittedArrowDirections = .any + + present(alertController, animated: true, completion: nil) + } + + @IBAction func addItemToQueueButtonPressed(_ sender: UIButton) { + let alertTitle = NSLocalizedString("popover.title.addItem", comment: "Title of popover that adds items to the queue") + + let alertMessage = NSLocalizedString("popover.message.addItem", comment: "Message on popover that adds items to the queue") + + let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .actionSheet) + + // Populate the sheet with the titles of the assets we have loaded. + for (loadedAssetTitle, loadedAsset) in loadedAssets { + let alertAction = UIAlertAction(title:loadedAssetTitle, style: .default) { [unowned self] alertAction in + let oldItems = self.player.items() + + let newPlayerItem = AVPlayerItem(asset: loadedAsset) + + self.player.insert(newPlayerItem, after: nil) + + self.queueDidChangeWithOldPlayerItems(oldPlayerItems: oldItems, newPlayerItems: self.player.items()) + } + + alertController.addAction(alertAction) + } + + let cancelActionTitle = NSLocalizedString("popover.title.cancel", comment: "Title of popover cancel action") + + let cancelAction = UIAlertAction(title: cancelActionTitle, style: .cancel, handler: nil) + + alertController.addAction(cancelAction) + + presentModalPopoverAlertController(alertController: alertController, sender: sender) + } + + @IBAction func clearQueueButtonWasPressed(_ sender: UIButton) { + let alertTitle = NSLocalizedString("popover.title.clear", comment: "Title of popover that clears the queue") + + let alertMessage = NSLocalizedString("popover.message.clear", comment: "Message on popover that clears the queue") + + let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .actionSheet) + + let clearButtonTitle = NSLocalizedString("button.title.clear", comment: "Title on button to clear the queue") + + let clearQueueAction = UIAlertAction(title: clearButtonTitle, style: .destructive) { [unowned self] alertAction in + let oldItems = self.player.items() + + self.player.removeAllItems() + + self.queueDidChangeWithOldPlayerItems(oldPlayerItems: oldItems, newPlayerItems: self.player.items()) + } + + alertController.addAction(clearQueueAction) + + let cancelActionTitle = NSLocalizedString("popover.title.cancel", comment: "Title of popover cancel action") + + let cancelAction = UIAlertAction(title: cancelActionTitle, style: .cancel, handler: nil) + + alertController.addAction(cancelAction) + + presentModalPopoverAlertController(alertController: alertController, sender: sender) + } + + // MARK: KVO Observation + + // Update our UI when player or `player.currentItem` changes. + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + // Make sure the this KVO callback was intended for this view controller. + guard context == &playerViewControllerKVOContext else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + return + } + + if keyPath == #keyPath(PlayerViewController.player.currentItem) { + queueDidChangeWithOldPlayerItems(oldPlayerItems: [], newPlayerItems: player.items()) + } + else if keyPath == #keyPath(PlayerViewController.player.currentItem.duration) { + // Update `timeSlider` and enable / disable controls when `duration` > 0.0. + + /* + Handle `NSNull` value for `NSKeyValueChangeNewKey`, i.e. when + `player.currentItem` is nil. + */ + let newDuration: CMTime + if let newDurationAsValue = change?[NSKeyValueChangeKey.newKey] as? NSValue { + newDuration = newDurationAsValue.timeValue + } + else { + newDuration = kCMTimeZero + } + + let hasValidDuration = newDuration.isNumeric && newDuration.value != 0 + let newDurationSeconds = hasValidDuration ? CMTimeGetSeconds(newDuration) : 0.0 + let currentTime = hasValidDuration ? Float(CMTimeGetSeconds(player.currentTime())) : 0.0 + + timeSlider.maximumValue = Float(newDurationSeconds) + + timeSlider.value = currentTime + + rewindButton.isEnabled = hasValidDuration + + playPauseButton.isEnabled = hasValidDuration + + fastForwardButton.isEnabled = hasValidDuration + + timeSlider.isEnabled = hasValidDuration + + startTimeLabel.isEnabled = hasValidDuration + startTimeLabel.text = createTimeString(time: currentTime) + + durationLabel.isEnabled = hasValidDuration + durationLabel.text = createTimeString(time: Float(newDurationSeconds)) + } + else if keyPath == #keyPath(PlayerViewController.player.rate) { + // Update `playPauseButton` image. + + let newRate = (change?[NSKeyValueChangeKey.newKey] as! NSNumber).doubleValue + + let buttonImageName = newRate == 1.0 ? "PauseButton" : "PlayButton" + + let buttonImage = UIImage(named: buttonImageName) + + playPauseButton.setImage(buttonImage, for: .normal) + } + else if keyPath == #keyPath(PlayerViewController.player.currentItem.status) { + // Display an error if status becomes `.Failed`. + + /* + Handle `NSNull` value for `NSKeyValueChangeNewKey`, i.e. when + `player.currentItem` is nil. + */ + let newStatus: AVPlayerItemStatus + + if let newStatusAsNumber = change?[NSKeyValueChangeKey.newKey] as? NSNumber { + newStatus = AVPlayerItemStatus(rawValue: newStatusAsNumber.intValue)! + } + else { + newStatus = .unknown + } + + if newStatus == .failed { + handleError(with: player.currentItem?.error?.localizedDescription, error: player.currentItem?.error) + } + } + } + + /* + Trigger KVO for anyone observing our properties affected by `player` and + `player.currentItem`. + */ + override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set { + let affectedKeyPathsMappingByKey: [String: Set] = [ + "duration": [#keyPath(PlayerViewController.player.currentItem.duration)], + "rate": [#keyPath(PlayerViewController.player.rate)] + ] + + return affectedKeyPathsMappingByKey[key] ?? super.keyPathsForValuesAffectingValue(forKey: key) + } + + /* + `player.items` is not KVO observable so we need to call this function + every time the queue changes. + */ + private func queueDidChangeWithOldPlayerItems(oldPlayerItems: [AVPlayerItem], newPlayerItems: [AVPlayerItem]) { + if newPlayerItems.isEmpty { + queueLabel.text = NSLocalizedString("label.queue.empty", comment: "Queue is empty") + } + else { + let stringFormat = NSLocalizedString("label.queue.%lu items", comment: "Queue of n item(s)") + + queueLabel.text = String.localizedStringWithFormat(stringFormat, newPlayerItems.count) + } + + let isQueueEmpty = newPlayerItems.count == 0 + clearButton.isEnabled = !isQueueEmpty + + collectionView.reloadData() + } + + // MARK: Error Handling + + func handleError(with message: String?, error: Error? = nil) { + NSLog("Error occurred with message: \(message), error: \(error).") + + let alertTitle = NSLocalizedString("alert.error.title", comment: "Alert title for errors") + + let alertMessage = message ?? NSLocalizedString("error.default.description", comment: "Default error message when no NSError provided") + + let alert = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert) + + let alertActionTitle = NSLocalizedString("alert.error.actions.OK", comment: "OK on error alert") + let alertAction = UIAlertAction(title: alertActionTitle, style: .default, handler: nil) + + alert.addAction(alertAction) + + present(alert, animated: true, completion: nil) + } + + + // MARK: UICollectionViewDataSource + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return player.items().count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ItemCell", for: indexPath as IndexPath) as! QueuedItemCollectionViewCell + + let item = player.items()[indexPath.row] + + let urlAsset = item.asset as! AVURLAsset + + let titleAndThumbnail = assetTitlesAndThumbnails[urlAsset.url]! + + cell.label.text = titleAndThumbnail.title + + cell.backgroundView = UIImageView(image: titleAndThumbnail.thumbnail) + + return cell + } + + // MARK: Convenience + + func createTimeString(time: Float) -> String { + let components = NSDateComponents() + components.second = Int(max(0.0, time)) + + return timeRemainingFormatter.string(from: components as DateComponents)! + } +} diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/QueuedItemCollectionViewCell.swift b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/QueuedItemCollectionViewCell.swift new file mode 100644 index 00000000..655a4163 --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/QueuedItemCollectionViewCell.swift @@ -0,0 +1,14 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Collection view cell to represent an AVPlayerItem in an AVQueuePlayer's queue. +*/ + +import UIKit + +/// A simple `UICollectionViewCell` that displays text. +class QueuedItemCollectionViewCell: UICollectionViewCell { + @IBOutlet var label: UILabel! +} diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.strings b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.strings new file mode 100644 index 00000000..3f7f9081 --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.strings @@ -0,0 +1,52 @@ +/* + + + Localizable strings. + + +*/ + +// Alert title for errors. +"alert.error.title" = "Error"; + +// OK action on error alert +"alert.error.actions.OK" = "OK"; + +// Queue is empty. +"label.queue.empty" = "Media queue is empty"; + +// Queue of n item(s). +"label.queue.%lu items" = "Queue of %n media items"; + +// Title on button to clear the queue. +"button.title.clear" = "Clear Queue"; + +// Title of popover that clears the queue. +"popover.title.clear" = "Clear"; + +// Message on popover that clears the queue. +"popover.message.clear" = "Remove all media from the queue?"; + +// Title of popover cancel action. +"popover.title.cancel" = "Cancel"; + +// Title of popover that adds items to the queue. +"popover.title.addItem" = "Add Media"; + +// Message on popover that adds items to the queue. +"popover.message.addItem" = "Select a media type to add to the queue"; + +// Can't use this AVAsset because one of it's keys failed to load. +"error.asset_%@_key_%@_failed.description" = "Media \"%@\" failed to load key \"%@\""; + +// Can't use this AVAsset because it isn't playable or has protected content. +"error.asset_%@_not_playable.description" = "Media \"%@\" isn't playable or has protected content"; + +// Failed to parse the assets manifest JSON. +"error.json_parse_failed.description" = "Failed to parse the manifest"; + +// Failed to open the assets manifest JSON. +"error.json_open_failed.description" = "Failed to open the manifest"; + +// Default error message when no NSError provided. +"error.default.description" = "Unknown error"; diff --git a/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.stringsdict b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.stringsdict new file mode 100644 index 00000000..22d035b9 --- /dev/null +++ b/AVFoundationQueuePlayer/Swift/AVFoundationQueuePlayer-iOS/en.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ + + + + + label.queue.%lu items + + NSStringLocalizedFormatKey + Queue of %#@lu_items@ + lu_items + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + lu + one + %lu item + other + %lu items + + + + diff --git a/AVFoundationSimplePlayer/Common/ElephantSeals.mov b/AVFoundationSimplePlayer/Common/ElephantSeals.mov new file mode 100644 index 00000000..69641555 Binary files /dev/null and b/AVFoundationSimplePlayer/Common/ElephantSeals.mov differ diff --git a/AVFoundationSimplePlayer/LICENSE.txt b/AVFoundationSimplePlayer/LICENSE.txt new file mode 100644 index 00000000..4e443d77 --- /dev/null +++ b/AVFoundationSimplePlayer/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: AVFoundationSimplePlayer-iOS: Using AVFoundation to Play Media +Version: 2.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) 2016 Apple Inc. All Rights Reserved. diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS.xcodeproj/project.pbxproj b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS.xcodeproj/project.pbxproj new file mode 100644 index 00000000..2f9af4ff --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS.xcodeproj/project.pbxproj @@ -0,0 +1,330 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + D2010EFB1B0E8A8D00BD0BDF /* ElephantSeals.mov in Resources */ = {isa = PBXBuildFile; fileRef = D2010EFA1B0E8A8D00BD0BDF /* ElephantSeals.mov */; }; + D20EB3C21AF94FDE0059CD72 /* AAPLPlayerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D20EB3C11AF94FDE0059CD72 /* AAPLPlayerViewController.m */; }; + D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D2385B491AF5181400DC8ADE /* Main.storyboard */; }; + D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2385B4C1AF5181400DC8ADE /* Images.xcassets */; }; + D23E499F1B0D30AF0013F574 /* AAPLAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D23E499E1B0D30AF0013F574 /* AAPLAppDelegate.m */; }; + D27489A01B1CD92F0020D82A /* AAPLPlayerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D274899F1B1CD92F0020D82A /* AAPLPlayerView.m */; }; + D27489A31B1CD9370020D82A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D27489A11B1CD9370020D82A /* Localizable.strings */; }; + D2C148E51B0FA7AD004F41DA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = D2C148E41B0FA7AD004F41DA /* main.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D2010EFA1B0E8A8D00BD0BDF /* ElephantSeals.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; name = ElephantSeals.mov; path = ../Common/ElephantSeals.mov; sourceTree = SOURCE_ROOT; }; + D20EB3C11AF94FDE0059CD72 /* AAPLPlayerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLPlayerViewController.m; sourceTree = ""; }; + D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-ObjC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AVFoundationSimplePlayer-ObjC.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + D2385B4A1AF5181400DC8ADE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + D2385B4C1AF5181400DC8ADE /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + D2385B511AF5181400DC8ADE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D23E499D1B0D2DF90013F574 /* AAPLAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLAppDelegate.h; sourceTree = ""; }; + D23E499E1B0D30AF0013F574 /* AAPLAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLAppDelegate.m; sourceTree = ""; }; + D23E49A01B0D325D0013F574 /* AAPLPlayerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLPlayerViewController.h; sourceTree = ""; }; + D274899E1B1CD92F0020D82A /* AAPLPlayerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLPlayerView.h; sourceTree = ""; }; + D274899F1B1CD92F0020D82A /* AAPLPlayerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLPlayerView.m; sourceTree = ""; }; + D27489A21B1CD9370020D82A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + D2C148E41B0FA7AD004F41DA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + D2CF22161B1E5EAD0006C864 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D2385B3F1AF5181400DC8ADE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D2010EFD1B0E8AD600BD0BDF /* Common */ = { + isa = PBXGroup; + children = ( + D2010EFA1B0E8A8D00BD0BDF /* ElephantSeals.mov */, + ); + name = Common; + sourceTree = ""; + }; + D2385B391AF5181400DC8ADE = { + isa = PBXGroup; + children = ( + D2CF22161B1E5EAD0006C864 /* README.md */, + D2385B441AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */, + D2385B431AF5181400DC8ADE /* Products */, + ); + sourceTree = ""; + }; + D2385B431AF5181400DC8ADE /* Products */ = { + isa = PBXGroup; + children = ( + D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-ObjC.app */, + ); + name = Products; + sourceTree = ""; + }; + D2385B441AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */ = { + isa = PBXGroup; + children = ( + D23E499E1B0D30AF0013F574 /* AAPLAppDelegate.m */, + D23E499D1B0D2DF90013F574 /* AAPLAppDelegate.h */, + D20EB3C11AF94FDE0059CD72 /* AAPLPlayerViewController.m */, + D23E49A01B0D325D0013F574 /* AAPLPlayerViewController.h */, + D274899E1B1CD92F0020D82A /* AAPLPlayerView.h */, + D274899F1B1CD92F0020D82A /* AAPLPlayerView.m */, + D2385B491AF5181400DC8ADE /* Main.storyboard */, + D2385B4C1AF5181400DC8ADE /* Images.xcassets */, + D2385B511AF5181400DC8ADE /* Info.plist */, + D27489A11B1CD9370020D82A /* Localizable.strings */, + D2C148E31B0FA76B004F41DA /* Supporting Files */, + D2010EFD1B0E8AD600BD0BDF /* Common */, + ); + path = "AVFoundationSimplePlayer-iOS"; + sourceTree = ""; + }; + D2C148E31B0FA76B004F41DA /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D2C148E41B0FA7AD004F41DA /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + D2385B411AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationSimplePlayer-iOS" */; + buildPhases = ( + D2385B3E1AF5181400DC8ADE /* Sources */, + D2385B3F1AF5181400DC8ADE /* Frameworks */, + D2385B401AF5181400DC8ADE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "AVFoundationSimplePlayer-iOS"; + productName = "AVFoundationSimplePlayer-iOS"; + productReference = D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-ObjC.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D2385B3A1AF5181400DC8ADE /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = "Apple Inc."; + TargetAttributes = { + D2385B411AF5181400DC8ADE = { + CreatedOnToolsVersion = 7.0; + }; + }; + }; + buildConfigurationList = D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationSimplePlayer-iOS" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D2385B391AF5181400DC8ADE; + productRefGroup = D2385B431AF5181400DC8ADE /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D2385B411AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D2385B401AF5181400DC8ADE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D27489A31B1CD9370020D82A /* Localizable.strings in Resources */, + D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */, + D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */, + D2010EFB1B0E8A8D00BD0BDF /* ElephantSeals.mov in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D2385B3E1AF5181400DC8ADE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D23E499F1B0D30AF0013F574 /* AAPLAppDelegate.m in Sources */, + D27489A01B1CD92F0020D82A /* AAPLPlayerView.m in Sources */, + D2C148E51B0FA7AD004F41DA /* main.m in Sources */, + D20EB3C21AF94FDE0059CD72 /* AAPLPlayerViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + D2385B491AF5181400DC8ADE /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D2385B4A1AF5181400DC8ADE /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + D27489A11B1CD9370020D82A /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D27489A21B1CD9370020D82A /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + D2385B5D1AF5181400DC8ADE /* 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 = "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; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D2385B5E1AF5181400DC8ADE /* 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 = "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; + }; + D2385B601AF5181400DC8ADE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = "AVFoundationSimplePlayer-iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "AVFoundationSimplePlayer-ObjC"; + }; + name = Debug; + }; + D2385B611AF5181400DC8ADE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = "AVFoundationSimplePlayer-iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "AVFoundationSimplePlayer-ObjC"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationSimplePlayer-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2385B5D1AF5181400DC8ADE /* Debug */, + D2385B5E1AF5181400DC8ADE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationSimplePlayer-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2385B601AF5181400DC8ADE /* Debug */, + D2385B611AF5181400DC8ADE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D2385B3A1AF5181400DC8ADE /* Project object */; +} diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLAppDelegate.h b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLAppDelegate.h new file mode 100644 index 00000000..e5e2e991 --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLAppDelegate.h @@ -0,0 +1,17 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate. +*/ + +@import UIKit; + + +@interface AAPLAppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end + diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLAppDelegate.m b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLAppDelegate.m new file mode 100644 index 00000000..1e4f2f80 --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLAppDelegate.m @@ -0,0 +1,13 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate. +*/ + +#import "AAPLAppDelegate.h" + + +@implementation AAPLAppDelegate +@end diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerView.h b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerView.h new file mode 100644 index 00000000..a94b0c91 --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerView.h @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View containing an AVPlayerLayer. +*/ + +@import UIKit; + +@class AVPlayer; + +@interface AAPLPlayerView : UIView +@property AVPlayer *player; +@property (readonly) AVPlayerLayer *playerLayer; +@end diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerView.m b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerView.m new file mode 100644 index 00000000..cfa02265 --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerView.m @@ -0,0 +1,34 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View containing an AVPlayerLayer. +*/ + +@import Foundation; +@import AVFoundation; +#import "AAPLPlayerView.h" + + +@implementation AAPLPlayerView + +- (AVPlayer *)player { + return self.playerLayer.player; +} + +- (void)setPlayer:(AVPlayer *)player { + self.playerLayer.player = player; +} + +// override UIView ++ (Class)layerClass { + return [AVPlayerLayer class]; +} + +- (AVPlayerLayer *)playerLayer { + return (AVPlayerLayer *)self.layer; +} + +@end + diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerViewController.h b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerViewController.h new file mode 100644 index 00000000..91cff872 --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerViewController.h @@ -0,0 +1,32 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller containing a player view and basic playback controls. +*/ + +@import UIKit; + + +@class AAPLPlayerView; + +@interface AAPLPlayerViewController : UIViewController + +@property (readonly) AVPlayer *player; +@property AVURLAsset *asset; + +@property CMTime currentTime; +@property (readonly) CMTime duration; +@property float rate; + +@property (weak) IBOutlet UISlider *timeSlider; +@property (weak) IBOutlet UILabel *startTimeLabel; +@property (weak) IBOutlet UILabel *durationLabel; +@property (weak) IBOutlet UIButton *rewindButton; +@property (weak) IBOutlet UIButton *playPauseButton; +@property (weak) IBOutlet UIButton *fastForwardButton; +@property (weak) IBOutlet AAPLPlayerView *playerView; + +@end + diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerViewController.m b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerViewController.m new file mode 100644 index 00000000..881149c5 --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerViewController.m @@ -0,0 +1,316 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller containing a player view and basic playback controls. +*/ + + +@import Foundation; +@import AVFoundation; +@import CoreMedia.CMTime; +#import "AAPLPlayerViewController.h" +#import "AAPLPlayerView.h" + + +// Private properties +@interface AAPLPlayerViewController () +{ + AVPlayer *_player; + AVURLAsset *_asset; + id _timeObserverToken; + AVPlayerItem *_playerItem; +} + +@property AVPlayerItem *playerItem; + +@property (readonly) AVPlayerLayer *playerLayer; + +@end + +@implementation AAPLPlayerViewController + +// MARK: - View Handling + +/* + KVO context used to differentiate KVO callbacks for this class versus other + classes in its class hierarchy. +*/ +static int AAPLPlayerViewControllerKVOContext = 0; + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + /* + Update the UI when these player properties change. + + Use the context parameter to distinguish KVO for our particular observers and not + those destined for a subclass that also happens to be observing these properties. + */ + [self addObserver:self forKeyPath:@"asset" options:NSKeyValueObservingOptionNew context:&AAPLPlayerViewControllerKVOContext]; + [self addObserver:self forKeyPath:@"player.currentItem.duration" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext]; + [self addObserver:self forKeyPath:@"player.rate" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext]; + [self addObserver:self forKeyPath:@"player.currentItem.status" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext]; + + self.playerView.playerLayer.player = self.player; + + NSURL *movieURL = [[NSBundle mainBundle] URLForResource:@"ElephantSeals" withExtension:@"mov"]; + self.asset = [AVURLAsset assetWithURL:movieURL]; + + // Use a weak self variable to avoid a retain cycle in the block. + AAPLPlayerViewController __weak *weakSelf = self; + _timeObserverToken = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock: + ^(CMTime time) { + weakSelf.timeSlider.value = CMTimeGetSeconds(time); + }]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + + if (_timeObserverToken) { + [self.player removeTimeObserver:_timeObserverToken]; + _timeObserverToken = nil; + } + + [self.player pause]; + + [self removeObserver:self forKeyPath:@"asset" context:&AAPLPlayerViewControllerKVOContext]; + [self removeObserver:self forKeyPath:@"player.currentItem.duration" context:&AAPLPlayerViewControllerKVOContext]; + [self removeObserver:self forKeyPath:@"player.rate" context:&AAPLPlayerViewControllerKVOContext]; + [self removeObserver:self forKeyPath:@"player.currentItem.status" context:&AAPLPlayerViewControllerKVOContext]; +} + +// MARK: - Properties + +// Will attempt load and test these asset keys before playing ++ (NSArray *)assetKeysRequiredToPlay { + return @[ @"playable", @"hasProtectedContent" ]; +} + +- (AVPlayer *)player { + if (!_player) + _player = [[AVPlayer alloc] init]; + return _player; +} + +- (CMTime)currentTime { + return self.player.currentTime; +} +- (void)setCurrentTime:(CMTime)newCurrentTime { + [self.player seekToTime:newCurrentTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; +} + +- (CMTime)duration { + return self.player.currentItem ? self.player.currentItem.duration : kCMTimeZero; +} + +- (float)rate { + return self.player.rate; +} +- (void)setRate:(float)newRate { + self.player.rate = newRate; +} + +- (AVPlayerLayer *)playerLayer { + return self.playerView.playerLayer; +} + +- (AVPlayerItem *)playerItem { + return _playerItem; +} + +- (void)setPlayerItem:(AVPlayerItem *)newPlayerItem { + if (_playerItem != newPlayerItem) { + + _playerItem = newPlayerItem; + + // If needed, configure player item here before associating it with a player + // (example: adding outputs, setting text style rules, selecting media options) + [self.player replaceCurrentItemWithPlayerItem:_playerItem]; + } +} + +// MARK: - Asset Loading + +- (void)asynchronouslyLoadURLAsset:(AVURLAsset *)newAsset { + + /* + Using AVAsset now runs the risk of blocking the current thread + (the main UI thread) whilst I/O happens to populate the + properties. It's prudent to defer our work until the properties + we need have been loaded. + */ + [newAsset loadValuesAsynchronouslyForKeys:AAPLPlayerViewController.assetKeysRequiredToPlay completionHandler:^{ + + /* + The asset invokes its completion handler on an arbitrary queue. + To avoid multiple threads using our internal state at the same time + we'll elect to use the main thread at all times, let's dispatch + our handler to the main queue. + */ + dispatch_async(dispatch_get_main_queue(), ^{ + + if (newAsset != self.asset) { + /* + self.asset has already changed! No point continuing because + another newAsset will come along in a moment. + */ + return; + } + + /* + Test whether the values of each of the keys we need have been + successfully loaded. + */ + for (NSString *key in self.class.assetKeysRequiredToPlay) { + NSError *error = nil; + if ([newAsset statusOfValueForKey:key error:&error] == AVKeyValueStatusFailed) { + + NSString *message = [NSString localizedStringWithFormat:NSLocalizedString(@"error.asset_key_%@_failed.description", @"Can't use this AVAsset because one of it's keys failed to load"), key]; + + [self handleErrorWithMessage:message error:error]; + + return; + } + } + + // We can't play this asset. + if (!newAsset.playable || newAsset.hasProtectedContent) { + NSString *message = NSLocalizedString(@"error.asset_not_playable.description", @"Can't use this AVAsset because it isn't playable or has protected content"); + + [self handleErrorWithMessage:message error:nil]; + + return; + } + + /* + We can play this asset. Create a new AVPlayerItem and make it + our player's current item. + */ + self.playerItem = [AVPlayerItem playerItemWithAsset:newAsset]; + }); + }]; +} + +// MARK: - IBActions + +- (IBAction)playPauseButtonWasPressed:(UIButton *)sender { + if (self.player.rate != 1.0) { + // not playing foward so play + if (CMTIME_COMPARE_INLINE(self.currentTime, ==, self.duration)) { + // at end so got back to begining + self.currentTime = kCMTimeZero; + } + [self.player play]; + } else { + // playing so pause + [self.player pause]; + } +} + +- (IBAction)rewindButtonWasPressed:(UIButton *)sender { + self.rate = MAX(self.player.rate - 2.0, -2.0); // rewind no faster than -2.0 +} + +- (IBAction)fastForwardButtonWasPressed:(UIButton *)sender { + self.rate = MIN(self.player.rate + 2.0, 2.0); // fast forward no faster than 2.0 +} + +- (IBAction)timeSliderDidChange:(UISlider *)sender { + self.currentTime = CMTimeMakeWithSeconds(sender.value, 1000); +} + +// MARK: - KV Observation + +// Update our UI when player or player.currentItem changes +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + + if (context != &AAPLPlayerViewControllerKVOContext) { + // KVO isn't for us. + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + return; + } + + if ([keyPath isEqualToString:@"asset"]) { + if (self.asset) { + [self asynchronouslyLoadURLAsset:self.asset]; + } + } + else if ([keyPath isEqualToString:@"player.currentItem.duration"]) { + + // Update timeSlider and enable/disable controls when duration > 0.0 + + // Handle NSNull value for NSKeyValueChangeNewKey, i.e. when player.currentItem is nil + NSValue *newDurationAsValue = change[NSKeyValueChangeNewKey]; + CMTime newDuration = [newDurationAsValue isKindOfClass:[NSValue class]] ? newDurationAsValue.CMTimeValue : kCMTimeZero; + BOOL hasValidDuration = CMTIME_IS_NUMERIC(newDuration) && newDuration.value != 0; + double newDurationSeconds = hasValidDuration ? CMTimeGetSeconds(newDuration) : 0.0; + + self.timeSlider.maximumValue = newDurationSeconds; + self.timeSlider.value = hasValidDuration ? CMTimeGetSeconds(self.currentTime) : 0.0; + self.rewindButton.enabled = hasValidDuration; + self.playPauseButton.enabled = hasValidDuration; + self.fastForwardButton.enabled = hasValidDuration; + self.timeSlider.enabled = hasValidDuration; + self.startTimeLabel.enabled = hasValidDuration; + self.durationLabel.enabled = hasValidDuration; + int wholeMinutes = (int)trunc(newDurationSeconds / 60); + self.durationLabel.text = [NSString stringWithFormat:@"%d:%02d", wholeMinutes, (int)trunc(newDurationSeconds) - wholeMinutes * 60]; + + } + else if ([keyPath isEqualToString:@"player.rate"]) { + // Update playPauseButton image + + double newRate = [change[NSKeyValueChangeNewKey] doubleValue]; + UIImage *buttonImage = (newRate == 1.0) ? [UIImage imageNamed:@"PauseButton"] : [UIImage imageNamed:@"PlayButton"]; + [self.playPauseButton setImage:buttonImage forState:UIControlStateNormal]; + + } + else if ([keyPath isEqualToString:@"player.currentItem.status"]) { + // Display an error if status becomes Failed + + // Handle NSNull value for NSKeyValueChangeNewKey, i.e. when player.currentItem is nil + NSNumber *newStatusAsNumber = change[NSKeyValueChangeNewKey]; + AVPlayerItemStatus newStatus = [newStatusAsNumber isKindOfClass:[NSNumber class]] ? newStatusAsNumber.integerValue : AVPlayerItemStatusUnknown; + + if (newStatus == AVPlayerItemStatusFailed) { + [self handleErrorWithMessage:self.player.currentItem.error.localizedDescription error:self.player.currentItem.error]; + } + + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +// Trigger KVO for anyone observing our properties affected by player and player.currentItem ++ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { + if ([key isEqualToString:@"duration"]) { + return [NSSet setWithArray:@[ @"player.currentItem.duration" ]]; + } else if ([key isEqualToString:@"currentTime"]) { + return [NSSet setWithArray:@[ @"player.currentItem.currentTime" ]]; + } else if ([key isEqualToString:@"rate"]) { + return [NSSet setWithArray:@[ @"player.rate" ]]; + } else { + return [super keyPathsForValuesAffectingValueForKey:key]; + } +} + +// MARK: - Error Handling + +- (void)handleErrorWithMessage:(NSString *)message error:(NSError *)error { + NSLog(@"Error occured with message: %@, error: %@.", message, error); + + NSString *alertTitle = NSLocalizedString(@"alert.error.title", @"Alert title for errors"); + NSString *defaultAlertMesssage = NSLocalizedString(@"error.default.description", @"Default error message when no NSError provided"); + UIAlertController *controller = [UIAlertController alertControllerWithTitle:alertTitle message:message ?: defaultAlertMesssage preferredStyle:UIAlertControllerStyleAlert]; + + NSString *alertActionTitle = NSLocalizedString(@"alert.error.actions.OK", @"OK on error alert"); + UIAlertAction *action = [UIAlertAction actionWithTitle:alertActionTitle style:UIAlertActionStyleDefault handler:nil]; + [controller addAction:action]; + + [self presentViewController:controller animated:YES completion:nil]; +} + +@end diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Base.lproj/Main.storyboard b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000..33f0282f --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Base.lproj/Main.storyboard @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json new file mode 100644 index 00000000..01a9d605 --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "PauseButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PauseButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PauseButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png new file mode 100644 index 00000000..b812da3e Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png new file mode 100644 index 00000000..5fd2dacf Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png new file mode 100644 index 00000000..f1664dc6 Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json new file mode 100644 index 00000000..bdde07ab --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "PlayButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PlayButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PlayButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png new file mode 100644 index 00000000..1c9975a5 Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png new file mode 100644 index 00000000..9aa4bf37 Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png new file mode 100644 index 00000000..31046b7d Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json new file mode 100644 index 00000000..52413004 --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ScanBackwardButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ScanBackwardButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ScanBackwardButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png new file mode 100644 index 00000000..6fbe0cd3 Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png new file mode 100644 index 00000000..46fd14ff Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png new file mode 100644 index 00000000..b1cca0f1 Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json new file mode 100644 index 00000000..8f07e000 --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ScanForwardButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ScanForwardButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ScanForwardButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png new file mode 100644 index 00000000..43e251ec Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png new file mode 100644 index 00000000..bf28ce79 Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png new file mode 100644 index 00000000..f8ccaf93 Binary files /dev/null and b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png differ diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Info.plist b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Info.plist new file mode 100644 index 00000000..43c0659b --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + Main + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/en.lproj/Localizable.strings b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/en.lproj/Localizable.strings new file mode 100644 index 00000000..6df6625d --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/en.lproj/Localizable.strings @@ -0,0 +1,22 @@ +/* + + + Localizable strings. + + +*/ + +// Alert title for errors. +"alert.error.title" = "Error"; + +// OK action on error alert +"alert.error.actions.OK" = "OK on error alert"; + +// Can't use this AVAsset because one of it's keys failed to load. +"error.asset_key_%@_failed.description" = "Media failed to load key \"%@\""; + +// Can't use this AVAsset because it isn't playable or has protected content. +"error.asset_not_playable.description" = "Media isn't playable or has protected content"; + +// Default error message when no NSError provided. +"error.default.description" = "Unknown error"; diff --git a/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/main.m b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/main.m new file mode 100644 index 00000000..54cd040e --- /dev/null +++ b/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/main.m @@ -0,0 +1,59 @@ +/* + File: AAPLAppDelegate.m + + Abstract: Sample code for AVFoundationSimplePlayer-ObjC-iOS + + Version: 1.0 + + Disclaimer: 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) 2013-2015 Apple Inc. All Rights Reserved. +*/ + +#import +#import "AAPLAppDelegate.h" + + + + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AAPLAppDelegate class])); + } +} diff --git a/AVFoundationSimplePlayer/README.md b/AVFoundationSimplePlayer/README.md new file mode 100644 index 00000000..cee6bf3d --- /dev/null +++ b/AVFoundationSimplePlayer/README.md @@ -0,0 +1,15 @@ +# AVFoundationSimplePlayer-iOS: Using AVFoundation to Play Media + +Demonstrates how to create a simple movie playback app using only the AVPlayer and AVPlayerLayer classes from AVFoundation (not AVKit). You'll see how to load a movie file as an AVAsset and then how to implement various functionality including play, pause, fast forward, rewind, time slider updating, and scrubbing. + +## Requirements + +### Build + +Xcode 8.0, iOS 9.0 SDK + +### Runtime + +iOS 9.0 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS.xcodeproj/project.pbxproj b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS.xcodeproj/project.pbxproj new file mode 100644 index 00000000..04bf4dc2 --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS.xcodeproj/project.pbxproj @@ -0,0 +1,322 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + D2010EF91B0E8A8800BD0BDF /* ElephantSeals.mov in Resources */ = {isa = PBXBuildFile; fileRef = D2010EF81B0E8A8800BD0BDF /* ElephantSeals.mov */; }; + D20EB3C21AF94FDE0059CD72 /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20EB3C11AF94FDE0059CD72 /* PlayerViewController.swift */; }; + D2385B461AF5181400DC8ADE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2385B451AF5181400DC8ADE /* AppDelegate.swift */; }; + D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D2385B491AF5181400DC8ADE /* Main.storyboard */; }; + D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2385B4C1AF5181400DC8ADE /* Images.xcassets */; }; + D27489971B1CD4E00020D82A /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27489961B1CD4E00020D82A /* PlayerView.swift */; }; + D274899C1B1CD4EB0020D82A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D27489981B1CD4EB0020D82A /* Localizable.strings */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D2010EF81B0E8A8800BD0BDF /* ElephantSeals.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; name = ElephantSeals.mov; path = ../Common/ElephantSeals.mov; sourceTree = SOURCE_ROOT; }; + D20EB3C11AF94FDE0059CD72 /* PlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; + D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AVFoundationSimplePlayer-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + D2385B451AF5181400DC8ADE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D2385B4A1AF5181400DC8ADE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + D2385B4C1AF5181400DC8ADE /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + D2385B511AF5181400DC8ADE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D27489961B1CD4E00020D82A /* PlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; }; + D27489991B1CD4EB0020D82A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + D2CF22151B1E5E970006C864 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D2385B3F1AF5181400DC8ADE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D2010EFC1B0E8ACB00BD0BDF /* Common */ = { + isa = PBXGroup; + children = ( + D2010EF81B0E8A8800BD0BDF /* ElephantSeals.mov */, + ); + name = Common; + sourceTree = ""; + }; + D2385B391AF5181400DC8ADE = { + isa = PBXGroup; + children = ( + D2CF22151B1E5E970006C864 /* README.md */, + D2385B441AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */, + D2385B431AF5181400DC8ADE /* Products */, + ); + sourceTree = ""; + }; + D2385B431AF5181400DC8ADE /* Products */ = { + isa = PBXGroup; + children = ( + D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-Swift.app */, + ); + name = Products; + sourceTree = ""; + }; + D2385B441AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */ = { + isa = PBXGroup; + children = ( + D2385B451AF5181400DC8ADE /* AppDelegate.swift */, + D20EB3C11AF94FDE0059CD72 /* PlayerViewController.swift */, + D27489961B1CD4E00020D82A /* PlayerView.swift */, + D2385B491AF5181400DC8ADE /* Main.storyboard */, + D2385B4C1AF5181400DC8ADE /* Images.xcassets */, + D2385B511AF5181400DC8ADE /* Info.plist */, + D27489981B1CD4EB0020D82A /* Localizable.strings */, + D2010EFC1B0E8ACB00BD0BDF /* Common */, + ); + path = "AVFoundationSimplePlayer-iOS"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + D2385B411AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationSimplePlayer-iOS" */; + buildPhases = ( + D2385B3E1AF5181400DC8ADE /* Sources */, + D2385B3F1AF5181400DC8ADE /* Frameworks */, + D2385B401AF5181400DC8ADE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "AVFoundationSimplePlayer-iOS"; + productName = "AVFoundationSimplePlayer-iOS"; + productReference = D2385B421AF5181400DC8ADE /* AVFoundationSimplePlayer-Swift.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D2385B3A1AF5181400DC8ADE /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Apple Inc."; + TargetAttributes = { + D2385B411AF5181400DC8ADE = { + CreatedOnToolsVersion = 7.0; + LastSwiftMigration = 0800; + }; + }; + }; + buildConfigurationList = D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationSimplePlayer-iOS" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D2385B391AF5181400DC8ADE; + productRefGroup = D2385B431AF5181400DC8ADE /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D2385B411AF5181400DC8ADE /* AVFoundationSimplePlayer-iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D2385B401AF5181400DC8ADE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D274899C1B1CD4EB0020D82A /* Localizable.strings in Resources */, + D2010EF91B0E8A8800BD0BDF /* ElephantSeals.mov in Resources */, + D2385B4B1AF5181400DC8ADE /* Main.storyboard in Resources */, + D2385B4D1AF5181400DC8ADE /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D2385B3E1AF5181400DC8ADE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D27489971B1CD4E00020D82A /* PlayerView.swift in Sources */, + D2385B461AF5181400DC8ADE /* AppDelegate.swift in Sources */, + D20EB3C21AF94FDE0059CD72 /* PlayerViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + D2385B491AF5181400DC8ADE /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + D2385B4A1AF5181400DC8ADE /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + D27489981B1CD4EB0020D82A /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + D27489991B1CD4EB0020D82A /* en */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + D2385B5D1AF5181400DC8ADE /* 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 = "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_SYMBOLS_PRIVATE_EXTERN = NO; + 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; + }; + D2385B5E1AF5181400DC8ADE /* 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 = "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; + }; + D2385B601AF5181400DC8ADE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "AVFoundationSimplePlayer-iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "AVFoundationSimplePlayer-Swift"; + PROVISIONING_PROFILE = ""; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + D2385B611AF5181400DC8ADE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "AVFoundationSimplePlayer-iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "AVFoundationSimplePlayer-Swift"; + PROVISIONING_PROFILE = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D2385B3D1AF5181400DC8ADE /* Build configuration list for PBXProject "AVFoundationSimplePlayer-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2385B5D1AF5181400DC8ADE /* Debug */, + D2385B5E1AF5181400DC8ADE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2385B5F1AF5181400DC8ADE /* Build configuration list for PBXNativeTarget "AVFoundationSimplePlayer-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2385B601AF5181400DC8ADE /* Debug */, + D2385B611AF5181400DC8ADE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D2385B3A1AF5181400DC8ADE /* Project object */; +} diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/AppDelegate.swift b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/AppDelegate.swift new file mode 100644 index 00000000..de287ffa --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/AppDelegate.swift @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Main application entry point. +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + // MARK: Properties + + var window: UIWindow? +} diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Base.lproj/Main.storyboard b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000..75bf12a8 --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Base.lproj/Main.storyboard @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json new file mode 100644 index 00000000..01a9d605 --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "PauseButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PauseButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PauseButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png new file mode 100644 index 00000000..b812da3e Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png new file mode 100644 index 00000000..5fd2dacf Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@2x.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png new file mode 100644 index 00000000..f1664dc6 Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PauseButton.imageset/PauseButton@3x.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json new file mode 100644 index 00000000..bdde07ab --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "PlayButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "PlayButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "PlayButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png new file mode 100644 index 00000000..1c9975a5 Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png new file mode 100644 index 00000000..9aa4bf37 Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@2x.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png new file mode 100644 index 00000000..31046b7d Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/PlayButton.imageset/PlayButton@3x.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json new file mode 100644 index 00000000..52413004 --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ScanBackwardButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ScanBackwardButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ScanBackwardButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png new file mode 100644 index 00000000..6fbe0cd3 Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png new file mode 100644 index 00000000..46fd14ff Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@2x.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png new file mode 100644 index 00000000..b1cca0f1 Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanBackwardButton.imageset/ScanBackwardButton@3x.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json new file mode 100644 index 00000000..8f07e000 --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ScanForwardButton.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ScanForwardButton@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ScanForwardButton@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png new file mode 100644 index 00000000..43e251ec Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png new file mode 100644 index 00000000..bf28ce79 Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@2x.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png new file mode 100644 index 00000000..f8ccaf93 Binary files /dev/null and b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Images.xcassets/ScanForwardButton.imageset/ScanForwardButton@3x.png differ diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Info.plist b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Info.plist new file mode 100644 index 00000000..a1853be2 --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + Main + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/PlayerView.swift b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/PlayerView.swift new file mode 100644 index 00000000..61dd42ca --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/PlayerView.swift @@ -0,0 +1,31 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Player view backed by an AVPlayerLayer. +*/ + +import UIKit +import AVFoundation + +/// A simple `UIView` subclass that is backed by an `AVPlayerLayer` layer. +class PlayerView: UIView { + var player: AVPlayer? { + get { + return playerLayer.player + } + + set { + playerLayer.player = newValue + } + } + + var playerLayer: AVPlayerLayer { + return layer as! AVPlayerLayer + } + + override class var layerClass: AnyClass { + return AVPlayerLayer.self + } +} diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/PlayerViewController.swift b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/PlayerViewController.swift new file mode 100644 index 00000000..8e8870e8 --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/PlayerViewController.swift @@ -0,0 +1,359 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller containing a player view and basic playback controls. +*/ + +import Foundation +import AVFoundation +import UIKit + +/* + KVO context used to differentiate KVO callbacks for this class versus other + classes in its class hierarchy. +*/ +private var playerViewControllerKVOContext = 0 + +class PlayerViewController: UIViewController { + // MARK: Properties + + // Attempt load and test these asset keys before playing. + static let assetKeysRequiredToPlay = [ + "playable", + "hasProtectedContent" + ] + + let player = AVPlayer() + + var currentTime: Double { + get { + return CMTimeGetSeconds(player.currentTime()) + } + set { + let newTime = CMTimeMakeWithSeconds(newValue, 1) + player.seek(to: newTime, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) + } + } + + var duration: Double { + guard let currentItem = player.currentItem else { return 0.0 } + + return CMTimeGetSeconds(currentItem.duration) + } + + var rate: Float { + get { + return player.rate + } + + set { + player.rate = newValue + } + } + + var asset: AVURLAsset? { + didSet { + guard let newAsset = asset else { return } + + asynchronouslyLoadURLAsset(newAsset) + } + } + + private var playerLayer: AVPlayerLayer? { + return playerView.playerLayer + } + + /* + A formatter for individual date components used to provide an appropriate + value for the `startTimeLabel` and `durationLabel`. + */ + let timeRemainingFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.zeroFormattingBehavior = .pad + formatter.allowedUnits = [.minute, .second] + + return formatter + }() + + /* + A token obtained from calling `player`'s `addPeriodicTimeObserverForInterval(_:queue:usingBlock:)` + method. + */ + private var timeObserverToken: Any? + + private var playerItem: AVPlayerItem? = nil { + didSet { + /* + If needed, configure player item here before associating it with a player. + (example: adding outputs, setting text style rules, selecting media options) + */ + player.replaceCurrentItem(with: self.playerItem) + } + } + + // MARK: - IBOutlets + + @IBOutlet weak var timeSlider: UISlider! + @IBOutlet weak var startTimeLabel: UILabel! + @IBOutlet weak var durationLabel: UILabel! + @IBOutlet weak var rewindButton: UIButton! + @IBOutlet weak var playPauseButton: UIButton! + @IBOutlet weak var fastForwardButton: UIButton! + @IBOutlet weak var playerView: PlayerView! + + // MARK: - View Controller + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + /* + Update the UI when these player properties change. + + Use the context parameter to distinguish KVO for our particular observers + and not those destined for a subclass that also happens to be observing + these properties. + */ + addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.duration), options: [.new, .initial], context: &playerViewControllerKVOContext) + addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.rate), options: [.new, .initial], context: &playerViewControllerKVOContext) + addObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.status), options: [.new, .initial], context: &playerViewControllerKVOContext) + + playerView.playerLayer.player = player + + let movieURL = Bundle.main.url(forResource: "ElephantSeals", withExtension: "mov")! + asset = AVURLAsset(url: movieURL, options: nil) + + // Make sure we don't have a strong reference cycle by only capturing self as weak. + let interval = CMTimeMake(1, 1) + timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main) { [unowned self] time in + let timeElapsed = Float(CMTimeGetSeconds(time)) + + self.timeSlider.value = Float(timeElapsed) + self.startTimeLabel.text = self.createTimeString(time: timeElapsed) + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + if let timeObserverToken = timeObserverToken { + player.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + + player.pause() + + removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.duration), context: &playerViewControllerKVOContext) + removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.rate), context: &playerViewControllerKVOContext) + removeObserver(self, forKeyPath: #keyPath(PlayerViewController.player.currentItem.status), context: &playerViewControllerKVOContext) + } + + // MARK: - Asset Loading + + func asynchronouslyLoadURLAsset(_ newAsset: AVURLAsset) { + /* + Using AVAsset now runs the risk of blocking the current thread (the + main UI thread) whilst I/O happens to populate the properties. It's + prudent to defer our work until the properties we need have been loaded. + */ + newAsset.loadValuesAsynchronously(forKeys: PlayerViewController.assetKeysRequiredToPlay) { + /* + The asset invokes its completion handler on an arbitrary queue. + To avoid multiple threads using our internal state at the same time + we'll elect to use the main thread at all times, let's dispatch + our handler to the main queue. + */ + DispatchQueue.main.async { + /* + `self.asset` has already changed! No point continuing because + another `newAsset` will come along in a moment. + */ + guard newAsset == self.asset else { return } + + /* + Test whether the values of each of the keys we need have been + successfully loaded. + */ + for key in PlayerViewController.assetKeysRequiredToPlay { + var error: NSError? + + if newAsset.statusOfValue(forKey: key, error: &error) == .failed { + let stringFormat = NSLocalizedString("error.asset_key_%@_failed.description", comment: "Can't use this AVAsset because one of it's keys failed to load") + + let message = String.localizedStringWithFormat(stringFormat, key) + + self.handleErrorWithMessage(message, error: error) + + return + } + } + + // We can't play this asset. + if !newAsset.isPlayable || newAsset.hasProtectedContent { + let message = NSLocalizedString("error.asset_not_playable.description", comment: "Can't use this AVAsset because it isn't playable or has protected content") + + self.handleErrorWithMessage(message) + + return + } + + /* + We can play this asset. Create a new `AVPlayerItem` and make + it our player's current item. + */ + self.playerItem = AVPlayerItem(asset: newAsset) + } + } + } + + // MARK: - IBActions + + @IBAction func playPauseButtonWasPressed(_ sender: UIButton) { + if player.rate != 1.0 { + // Not playing forward, so play. + if currentTime == duration { + // At end, so got back to begining. + currentTime = 0.0 + } + + player.play() + } + else { + // Playing, so pause. + player.pause() + } + } + + @IBAction func rewindButtonWasPressed(_ sender: UIButton) { + // Rewind no faster than -2.0. + rate = max(player.rate - 2.0, -2.0) + } + + @IBAction func fastForwardButtonWasPressed(_ sender: UIButton) { + // Fast forward no faster than 2.0. + rate = min(player.rate + 2.0, 2.0) + } + + @IBAction func timeSliderDidChange(_ sender: UISlider) { + currentTime = Double(sender.value) + } + + // MARK: - KVO Observation + + // Update our UI when player or `player.currentItem` changes. + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { + // Make sure the this KVO callback was intended for this view controller. + guard context == &playerViewControllerKVOContext else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + return + } + + if keyPath == #keyPath(PlayerViewController.player.currentItem.duration) { + // Update timeSlider and enable/disable controls when duration > 0.0 + + /* + Handle `NSNull` value for `NSKeyValueChangeNewKey`, i.e. when + `player.currentItem` is nil. + */ + let newDuration: CMTime + if let newDurationAsValue = change?[NSKeyValueChangeKey.newKey] as? NSValue { + newDuration = newDurationAsValue.timeValue + } + else { + newDuration = kCMTimeZero + } + + let hasValidDuration = newDuration.isNumeric && newDuration.value != 0 + let newDurationSeconds = hasValidDuration ? CMTimeGetSeconds(newDuration) : 0.0 + let currentTime = hasValidDuration ? Float(CMTimeGetSeconds(player.currentTime())) : 0.0 + + timeSlider.maximumValue = Float(newDurationSeconds) + + timeSlider.value = currentTime + + rewindButton.isEnabled = hasValidDuration + + playPauseButton.isEnabled = hasValidDuration + + fastForwardButton.isEnabled = hasValidDuration + + timeSlider.isEnabled = hasValidDuration + + startTimeLabel.isEnabled = hasValidDuration + startTimeLabel.text = createTimeString(time: currentTime) + + durationLabel.isEnabled = hasValidDuration + durationLabel.text = createTimeString(time: Float(newDurationSeconds)) + } + else if keyPath == #keyPath(PlayerViewController.player.rate) { + // Update `playPauseButton` image. + + let newRate = (change?[NSKeyValueChangeKey.newKey] as! NSNumber).doubleValue + + let buttonImageName = newRate == 1.0 ? "PauseButton" : "PlayButton" + + let buttonImage = UIImage(named: buttonImageName) + + playPauseButton.setImage(buttonImage, for: UIControlState()) + } + else if keyPath == #keyPath(PlayerViewController.player.currentItem.status) { + // Display an error if status becomes `.Failed`. + + /* + Handle `NSNull` value for `NSKeyValueChangeNewKey`, i.e. when + `player.currentItem` is nil. + */ + let newStatus: AVPlayerItemStatus + + if let newStatusAsNumber = change?[NSKeyValueChangeKey.newKey] as? NSNumber { + newStatus = AVPlayerItemStatus(rawValue: newStatusAsNumber.intValue)! + } + else { + newStatus = .unknown + } + + if newStatus == .failed { + handleErrorWithMessage(player.currentItem?.error?.localizedDescription, error:player.currentItem?.error) + } + } + } + + // Trigger KVO for anyone observing our properties affected by player and player.currentItem + override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set { + let affectedKeyPathsMappingByKey: [String: Set] = [ + "duration": [#keyPath(PlayerViewController.player.currentItem.duration)], + "rate": [#keyPath(PlayerViewController.player.rate)] + ] + + return affectedKeyPathsMappingByKey[key] ?? super.keyPathsForValuesAffectingValue(forKey: key) + } + + // MARK: - Error Handling + + func handleErrorWithMessage(_ message: String?, error: Error? = nil) { + NSLog("Error occured with message: \(message), error: \(error).") + + let alertTitle = NSLocalizedString("alert.error.title", comment: "Alert title for errors") + let defaultAlertMessage = NSLocalizedString("error.default.description", comment: "Default error message when no NSError provided") + + let alert = UIAlertController(title: alertTitle, message: message == nil ? defaultAlertMessage : message, preferredStyle: UIAlertControllerStyle.alert) + + let alertActionTitle = NSLocalizedString("alert.error.actions.OK", comment: "OK on error alert") + + let alertAction = UIAlertAction(title: alertActionTitle, style: .default, handler: nil) + + alert.addAction(alertAction) + + present(alert, animated: true, completion: nil) + } + + // MARK: Convenience + + func createTimeString(time: Float) -> String { + let components = NSDateComponents() + components.second = Int(max(0.0, time)) + + return timeRemainingFormatter.string(from: components as DateComponents)! + } +} diff --git a/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/en.lproj/Localizable.strings b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/en.lproj/Localizable.strings new file mode 100644 index 00000000..6df6625d --- /dev/null +++ b/AVFoundationSimplePlayer/Swift/AVFoundationSimplePlayer-iOS/en.lproj/Localizable.strings @@ -0,0 +1,22 @@ +/* + + + Localizable strings. + + +*/ + +// Alert title for errors. +"alert.error.title" = "Error"; + +// OK action on error alert +"alert.error.actions.OK" = "OK on error alert"; + +// Can't use this AVAsset because one of it's keys failed to load. +"error.asset_key_%@_failed.description" = "Media failed to load key \"%@\""; + +// Can't use this AVAsset because it isn't playable or has protected content. +"error.asset_not_playable.description" = "Media isn't playable or has protected content"; + +// Default error message when no NSError provided. +"error.default.description" = "Unknown error"; diff --git a/AVReaderWriter/LICENSE.txt b/AVReaderWriter/LICENSE.txt new file mode 100644 index 00000000..a2330a4c --- /dev/null +++ b/AVReaderWriter/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: AVReaderWriter: Offline Audio / Video Processing +Version: 3.1 + +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) 2016 Apple Inc. All Rights Reserved. diff --git a/AVReaderWriter/Objective-C/AVReaderWriter.xcodeproj/project.pbxproj b/AVReaderWriter/Objective-C/AVReaderWriter.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ce1b4318 --- /dev/null +++ b/AVReaderWriter/Objective-C/AVReaderWriter.xcodeproj/project.pbxproj @@ -0,0 +1,319 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + EE33EA6D1370DF3100F77FCE /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = EE33EA6B1370DF3100F77FCE /* InfoPlist.strings */; }; + EE33EA701370DF3100F77FCE /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = EE33EA6F1370DF3100F77FCE /* main.m */; }; + EE33EA761370DF3100F77FCE /* AAPLDocument.m in Sources */ = {isa = PBXBuildFile; fileRef = EE33EA751370DF3100F77FCE /* AAPLDocument.m */; }; + EE33EA791370DF3100F77FCE /* AAPLDocument.xib in Resources */ = {isa = PBXBuildFile; fileRef = EE33EA771370DF3100F77FCE /* AAPLDocument.xib */; }; + EE33EA7C1370DF3100F77FCE /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = EE33EA7A1370DF3100F77FCE /* MainMenu.xib */; }; + EEB8F96C1399AF67004FEB2E /* AAPLProgressPanelController.m in Sources */ = {isa = PBXBuildFile; fileRef = EEB8F96B1399AF67004FEB2E /* AAPLProgressPanelController.m */; }; + EEB8F96F1399AFA0004FEB2E /* AAPLProgressPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = EEB8F96E1399AFA0004FEB2E /* AAPLProgressPanel.xib */; }; + EEDD0D04139D9946008A5566 /* AudioOnly2x.png in Resources */ = {isa = PBXBuildFile; fileRef = EEDD0D03139D9946008A5566 /* AudioOnly2x.png */; }; + EEDD0D07139D9A42008A5566 /* ErrorLoading2x.png in Resources */ = {isa = PBXBuildFile; fileRef = EEDD0D06139D9A42008A5566 /* ErrorLoading2x.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 3E7D60AB1B1FA25E001EAE8D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; + EE33EA5E1370DF3100F77FCE /* AVReaderWriter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AVReaderWriter.app; sourceTree = BUILT_PRODUCTS_DIR; }; + EE33EA6A1370DF3100F77FCE /* AVReaderWriterOSX-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "AVReaderWriterOSX-Info.plist"; sourceTree = ""; }; + EE33EA6C1370DF3100F77FCE /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + EE33EA6F1370DF3100F77FCE /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + EE33EA741370DF3100F77FCE /* AAPLDocument.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AAPLDocument.h; sourceTree = ""; }; + EE33EA751370DF3100F77FCE /* AAPLDocument.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AAPLDocument.m; sourceTree = ""; }; + EE33EA781370DF3100F77FCE /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/AAPLDocument.xib; sourceTree = ""; }; + EE33EA7B1370DF3100F77FCE /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; + EEB8F96A1399AF67004FEB2E /* AAPLProgressPanelController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLProgressPanelController.h; sourceTree = ""; }; + EEB8F96B1399AF67004FEB2E /* AAPLProgressPanelController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLProgressPanelController.m; sourceTree = ""; }; + EEB8F96E1399AFA0004FEB2E /* AAPLProgressPanel.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AAPLProgressPanel.xib; sourceTree = ""; }; + EEDD0D03139D9946008A5566 /* AudioOnly2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = AudioOnly2x.png; sourceTree = ""; }; + EEDD0D06139D9A42008A5566 /* ErrorLoading2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ErrorLoading2x.png; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + EE33EA5B1370DF3100F77FCE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + EE33EA531370DF3100F77FCE = { + isa = PBXGroup; + children = ( + 3E7D60AB1B1FA25E001EAE8D /* README.md */, + EE33EA681370DF3100F77FCE /* AVReaderWriterOSX */, + EE33EA5F1370DF3100F77FCE /* Products */, + ); + sourceTree = ""; + }; + EE33EA5F1370DF3100F77FCE /* Products */ = { + isa = PBXGroup; + children = ( + EE33EA5E1370DF3100F77FCE /* AVReaderWriter.app */, + ); + name = Products; + sourceTree = ""; + }; + EE33EA681370DF3100F77FCE /* AVReaderWriterOSX */ = { + isa = PBXGroup; + children = ( + EE33EA741370DF3100F77FCE /* AAPLDocument.h */, + EE33EA751370DF3100F77FCE /* AAPLDocument.m */, + EEB8F96A1399AF67004FEB2E /* AAPLProgressPanelController.h */, + EEB8F96B1399AF67004FEB2E /* AAPLProgressPanelController.m */, + EEDD0D64139DA9A3008A5566 /* Resources */, + EE33EA691370DF3100F77FCE /* Supporting Files */, + ); + path = AVReaderWriterOSX; + sourceTree = ""; + }; + EE33EA691370DF3100F77FCE /* Supporting Files */ = { + isa = PBXGroup; + children = ( + EE33EA6A1370DF3100F77FCE /* AVReaderWriterOSX-Info.plist */, + EE33EA6B1370DF3100F77FCE /* InfoPlist.strings */, + EE33EA6F1370DF3100F77FCE /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + EEDD0D64139DA9A3008A5566 /* Resources */ = { + isa = PBXGroup; + children = ( + EE33EA771370DF3100F77FCE /* AAPLDocument.xib */, + EEB8F96E1399AFA0004FEB2E /* AAPLProgressPanel.xib */, + EE33EA7A1370DF3100F77FCE /* MainMenu.xib */, + EEDD0D03139D9946008A5566 /* AudioOnly2x.png */, + EEDD0D06139D9A42008A5566 /* ErrorLoading2x.png */, + ); + name = Resources; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + EE33EA5D1370DF3100F77FCE /* AVReaderWriterOSX */ = { + isa = PBXNativeTarget; + buildConfigurationList = EE33EA7F1370DF3100F77FCE /* Build configuration list for PBXNativeTarget "AVReaderWriterOSX" */; + buildPhases = ( + EE33EA5A1370DF3100F77FCE /* Sources */, + EE33EA5B1370DF3100F77FCE /* Frameworks */, + EE33EA5C1370DF3100F77FCE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AVReaderWriterOSX; + productName = ReaderWriter; + productReference = EE33EA5E1370DF3100F77FCE /* AVReaderWriter.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + EE33EA551370DF3100F77FCE /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0700; + }; + buildConfigurationList = EE33EA581370DF3100F77FCE /* Build configuration list for PBXProject "AVReaderWriter" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = EE33EA531370DF3100F77FCE; + productRefGroup = EE33EA5F1370DF3100F77FCE /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + EE33EA5D1370DF3100F77FCE /* AVReaderWriterOSX */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + EE33EA5C1370DF3100F77FCE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE33EA6D1370DF3100F77FCE /* InfoPlist.strings in Resources */, + EE33EA791370DF3100F77FCE /* AAPLDocument.xib in Resources */, + EE33EA7C1370DF3100F77FCE /* MainMenu.xib in Resources */, + EEB8F96F1399AFA0004FEB2E /* AAPLProgressPanel.xib in Resources */, + EEDD0D04139D9946008A5566 /* AudioOnly2x.png in Resources */, + EEDD0D07139D9A42008A5566 /* ErrorLoading2x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + EE33EA5A1370DF3100F77FCE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EE33EA701370DF3100F77FCE /* main.m in Sources */, + EE33EA761370DF3100F77FCE /* AAPLDocument.m in Sources */, + EEB8F96C1399AF67004FEB2E /* AAPLProgressPanelController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + EE33EA6B1370DF3100F77FCE /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + EE33EA6C1370DF3100F77FCE /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + EE33EA771370DF3100F77FCE /* AAPLDocument.xib */ = { + isa = PBXVariantGroup; + children = ( + EE33EA781370DF3100F77FCE /* en */, + ); + name = AAPLDocument.xib; + sourceTree = ""; + }; + EE33EA7A1370DF3100F77FCE /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + EE33EA7B1370DF3100F77FCE /* en */, + ); + name = MainMenu.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + EE33EA7D1370DF3100F77FCE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.7; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + EE33EA7E1370DF3100F77FCE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.7; + SDKROOT = macosx; + }; + name = Release; + }; + EE33EA801370DF3100F77FCE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = NO; + INFOPLIST_FILE = "AVReaderWriterOSX/AVReaderWriterOSX-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = AVReaderWriter; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + EE33EA811370DF3100F77FCE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = NO; + INFOPLIST_FILE = "AVReaderWriterOSX/AVReaderWriterOSX-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = AVReaderWriter; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + EE33EA581370DF3100F77FCE /* Build configuration list for PBXProject "AVReaderWriter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE33EA7D1370DF3100F77FCE /* Debug */, + EE33EA7E1370DF3100F77FCE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EE33EA7F1370DF3100F77FCE /* Build configuration list for PBXNativeTarget "AVReaderWriterOSX" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EE33EA801370DF3100F77FCE /* Debug */, + EE33EA811370DF3100F77FCE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = EE33EA551370DF3100F77FCE /* Project object */; +} diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLDocument.h b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLDocument.h new file mode 100644 index 00000000..7e972a06 --- /dev/null +++ b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLDocument.h @@ -0,0 +1,52 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Main class used to demonstrate reading/writing of assets. + */ + +@import AppKit; +@import CoreMedia; +@import AVFoundation; + +@class AAPLSampleBufferChannel; +@class AAPLProgressPanelController; + +@interface AAPLDocument : NSDocument +{ +@private + IBOutlet NSView *frameView; + IBOutlet NSPopUpButton *filterPopUpButton; + + AVAsset *asset; + AVAssetImageGenerator *imageGenerator; + CMTimeRange timeRange; + NSInteger filterTag; + dispatch_queue_t serializationQueue; + + // Only accessed on the main thread + NSURL *outputURL; + BOOL writingSamples; + AAPLProgressPanelController *progressPanelController; + + // All of these are createed, accessed, and torn down exclusively on the serializaton queue + AVAssetReader *assetReader; + AVAssetWriter *assetWriter; + AAPLSampleBufferChannel *audioSampleBufferChannel; + AAPLSampleBufferChannel *videoSampleBufferChannel; + BOOL cancelled; +} + +@property (nonatomic, retain) AVAsset *asset; +@property (nonatomic) CMTimeRange timeRange; +@property (nonatomic, copy) NSURL *outputURL; + +@property (nonatomic, retain) IBOutlet NSView *frameView; +@property (nonatomic, retain) IBOutlet NSPopUpButton *filterPopUpButton; + +- (IBAction)start:(id)sender; +- (IBAction)cancel:(id)sender; +@property (nonatomic, getter=isWritingSamples) BOOL writingSamples; + +@end diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLDocument.m b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLDocument.m new file mode 100644 index 00000000..cdf5638c --- /dev/null +++ b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLDocument.m @@ -0,0 +1,697 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Main class used to demonstrate reading/writing of assets. + */ + +#import "AAPLDocument.h" +#import "AAPLProgressPanelController.h" + +@protocol AAPLSampleBufferChannelDelegate; + +@interface AAPLSampleBufferChannel : NSObject +{ +@private + AVAssetReaderOutput *assetReaderOutput; + AVAssetWriterInput *assetWriterInput; + + dispatch_block_t completionHandler; + dispatch_queue_t serializationQueue; + BOOL finished; // only accessed on serialization queue +} +- (id)initWithAssetReaderOutput:(AVAssetReaderOutput *)assetReaderOutput assetWriterInput:(AVAssetWriterInput *)assetWriterInput; +@property (nonatomic, readonly) NSString *mediaType; +- (void)startWithDelegate:(id )delegate completionHandler:(dispatch_block_t)completionHandler; // delegate is retained until completion handler is called. Completion handler is guaranteed to be called exactly once, whether reading/writing finishes, fails, or is cancelled. Delegate may be nil. +- (void)cancel; +@end + + +@protocol AAPLSampleBufferChannelDelegate +@required +- (void)sampleBufferChannel:(AAPLSampleBufferChannel *)sampleBufferChannel didReadSampleBuffer:(CMSampleBufferRef)sampleBuffer; +@end + + +@interface AAPLDocument () +- (void)setPreviewLayerContents:(id)contents gravity:(NSString *)gravity; +// These three methods are always called on the serialization dispatch queue +- (BOOL)setUpReaderAndWriterReturningError:(NSError **)outError; // make sure "tracks" key of asset is loaded before calling this +- (BOOL)startReadingAndWritingReturningError:(NSError **)outError; +- (void)readingAndWritingDidFinishSuccessfully:(BOOL)success withError:(NSError *)error; +@end + + +@implementation AAPLDocument + ++ (NSArray *)readableTypes +{ + return [AVURLAsset audiovisualTypes]; +} + ++ (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName +{ + return YES; +} + +- (id)init +{ + self = [super init]; + + if (self) + { + NSString *serializationQueueDescription = [NSString stringWithFormat:@"%@ serialization queue", self]; + serializationQueue = dispatch_queue_create([serializationQueueDescription UTF8String], NULL); + } + + return self; +} + +- (void)dealloc +{ + [asset release]; + [imageGenerator release]; + [outputURL release]; + [progressPanelController setDelegate:nil]; + [progressPanelController release]; + [frameView release]; + [filterPopUpButton release]; + + [assetReader release]; + [assetWriter release]; + [audioSampleBufferChannel release]; + [videoSampleBufferChannel release]; + if (serializationQueue) + dispatch_release(serializationQueue); + + [super dealloc]; +} + +- (NSString *)windowNibName +{ + return @"AAPLDocument"; +} + +@synthesize frameView=frameView; +@synthesize filterPopUpButton=filterPopUpButton; + +- (void)windowControllerDidLoadNib:(NSWindowController *)aController +{ + [super windowControllerDidLoadNib:aController]; + + // Create a layer and set it on the view. We will display video frames by setting the contents of the layer. + CALayer *localFrameLayer = [CALayer layer]; + NSView *localFrameView = [self frameView]; + [localFrameView setLayer:localFrameLayer]; + [localFrameView setWantsLayer:YES]; + + // Generate an image of some sort to use as a preview + AVAsset *localAsset = [self asset]; + [localAsset loadValuesAsynchronouslyForKeys:[NSArray arrayWithObject:@"tracks"] completionHandler:^{ + if ([localAsset statusOfValueForKey:@"tracks" error:NULL] != AVKeyValueStatusLoaded) + return; + + NSArray *visualTracks = [localAsset tracksWithMediaCharacteristic:AVMediaCharacteristicVisual]; + NSArray *audibleTracks = [localAsset tracksWithMediaCharacteristic:AVMediaCharacteristicAudible]; + if ([visualTracks count] > 0) + { + // Grab the first frame from the asset and display it + [imageGenerator generateCGImagesAsynchronouslyForTimes:[NSArray arrayWithObject:[NSValue valueWithCMTime:kCMTimeZero]] completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error) { + if (result == AVAssetImageGeneratorSucceeded) + [self setPreviewLayerContents:(id)image gravity:kCAGravityResizeAspect]; + else + [self setPreviewLayerContents:[NSImage imageNamed:@"ErrorLoading2x"] gravity:kCAGravityCenter]; + }]; + } + else if ([audibleTracks count] > 0) + { + [self setPreviewLayerContents:[NSImage imageNamed:@"AudioOnly2x"] gravity:kCAGravityCenter]; + } + else + { + [self setPreviewLayerContents:[NSImage imageNamed:@"ErrorLoading2x"] gravity:kCAGravityCenter]; + } + }]; +} + +- (void)setPreviewLayerContents:(id)contents gravity:(NSString *)gravity +{ + CALayer *localFrameLayer = [[self frameView] layer]; + + [CATransaction begin]; // need a transaction since we are not executing on the main thread + { + [localFrameLayer setContents:contents]; + [localFrameLayer setContentsGravity:gravity]; + } + [CATransaction commit]; +} + +- (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError +{ + NSDictionary *assetOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:AVURLAssetPreferPreciseDurationAndTimingKey]; + AVAsset *localAsset = [AVURLAsset URLAssetWithURL:url options:assetOptions]; + [self setAsset:localAsset]; + if (localAsset) + imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:localAsset]; + + return (localAsset != nil); +} + +- (void)close +{ + [self cancel:self]; + + [super close]; +} + +@synthesize asset=asset; +@synthesize timeRange=timeRange; +@synthesize writingSamples=writingSamples; +@synthesize outputURL=outputURL; + +- (IBAction)start:(id)sender +{ + cancelled = NO; + + // Let the user choose an output file, then start the process of writing samples + NSSavePanel *savePanel = [NSSavePanel savePanel]; + [savePanel setAllowedFileTypes:[NSArray arrayWithObject:AVFileTypeQuickTimeMovie]]; + [savePanel setCanSelectHiddenExtension:YES]; + [savePanel beginSheetModalForWindow:[self windowForSheet] completionHandler:^(NSInteger result) { + if (result == NSFileHandlingPanelOKButton) + [self performSelector:@selector(startProgressSheetWithURL:) withObject:[savePanel URL] afterDelay:0.0]; // avoid starting a new sheet while in the old sheet's completion handler + }]; +} + +- (void)startProgressSheetWithURL:(NSURL *)localOutputURL +{ + [self setOutputURL:localOutputURL]; + [self setWritingSamples:YES]; + filterTag = [[self filterPopUpButton] selectedTag]; + + progressPanelController = [[AAPLProgressPanelController alloc] initWithWindowNibName:@"AAPLProgressPanel"]; + [progressPanelController setDelegate:self]; + + [NSApp beginSheet:[progressPanelController window] modalForWindow:[self windowForSheet] modalDelegate:self didEndSelector:@selector(progressPanelDidEnd:returnCode:contextInfo:) contextInfo:NULL]; + + AVAsset *localAsset = [self asset]; + [localAsset loadValuesAsynchronouslyForKeys:[NSArray arrayWithObjects:@"tracks", @"duration", nil] completionHandler:^{ + // Dispatch the setup work to the serialization queue, to ensure this work is serialized with potential cancellation + dispatch_async(serializationQueue, ^{ + // Since we are doing these things asynchronously, the user may have already cancelled on the main thread. In that case, simply return from this block + if (cancelled) + return; + + BOOL success = YES; + NSError *localError = nil; + + success = ([localAsset statusOfValueForKey:@"tracks" error:&localError] == AVKeyValueStatusLoaded); + if (success) + success = ([localAsset statusOfValueForKey:@"duration" error:&localError] == AVKeyValueStatusLoaded); + + if (success) + { + [self setTimeRange:CMTimeRangeMake(kCMTimeZero, [localAsset duration])]; + + // AVAssetWriter does not overwrite files for us, so remove the destination file if it already exists + NSFileManager *fm = [NSFileManager defaultManager]; + NSString *localOutputPath = [localOutputURL path]; + if ([fm fileExistsAtPath:localOutputPath]) + success = [fm removeItemAtPath:localOutputPath error:&localError]; + } + + // Set up the AVAssetReader and AVAssetWriter, then begin writing samples or flag an error + if (success) + success = [self setUpReaderAndWriterReturningError:&localError]; + if (success) + success = [self startReadingAndWritingReturningError:&localError]; + if (!success) + [self readingAndWritingDidFinishSuccessfully:success withError:localError]; + }); + }]; +} + +- (void)progressPanelControllerDidCancel:(AAPLProgressPanelController *)localProgressPanelController +{ + [self cancel:nil]; +} + +- (void)progressPanelDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo +{ + [progressPanelController setDelegate:nil]; + [progressPanelController release]; + progressPanelController = nil; +} + +- (BOOL)setUpReaderAndWriterReturningError:(NSError **)outError +{ + BOOL success = YES; + NSError *localError = nil; + AVAsset *localAsset = [self asset]; + NSURL *localOutputURL = [self outputURL]; + + // Create asset reader and asset writer + assetReader = [[AVAssetReader alloc] initWithAsset:asset error:&localError]; + success = (assetReader != nil); + if (success) + { + assetWriter = [[AVAssetWriter alloc] initWithURL:localOutputURL fileType:AVFileTypeQuickTimeMovie error:&localError]; + success = (assetWriter != nil); + } + + // Create asset reader outputs and asset writer inputs for the first audio track and first video track of the asset + if (success) + { + AVAssetTrack *audioTrack = nil, *videoTrack = nil; + + // Grab first audio track and first video track, if the asset has them + NSArray *audioTracks = [localAsset tracksWithMediaType:AVMediaTypeAudio]; + if ([audioTracks count] > 0) + audioTrack = [audioTracks objectAtIndex:0]; + NSArray *videoTracks = [localAsset tracksWithMediaType:AVMediaTypeVideo]; + if ([videoTracks count] > 0) + videoTrack = [videoTracks objectAtIndex:0]; + + if (audioTrack) + { + // Decompress to Linear PCM with the asset reader + NSDictionary *decompressionAudioSettings = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM], AVFormatIDKey, + nil]; + AVAssetReaderOutput *output = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:audioTrack outputSettings:decompressionAudioSettings]; + [assetReader addOutput:output]; + + AudioChannelLayout stereoChannelLayout = { + .mChannelLayoutTag = kAudioChannelLayoutTag_Stereo, + .mChannelBitmap = 0, + .mNumberChannelDescriptions = 0 + }; + NSData *channelLayoutAsData = [NSData dataWithBytes:&stereoChannelLayout length:offsetof(AudioChannelLayout, mChannelDescriptions)]; + + // Compress to 128kbps AAC with the asset writer + NSDictionary *compressionAudioSettings = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithUnsignedInt:kAudioFormatMPEG4AAC], AVFormatIDKey, + [NSNumber numberWithInteger:128000], AVEncoderBitRateKey, + [NSNumber numberWithInteger:44100], AVSampleRateKey, + channelLayoutAsData, AVChannelLayoutKey, + [NSNumber numberWithUnsignedInteger:2], AVNumberOfChannelsKey, + nil]; + AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:[audioTrack mediaType] outputSettings:compressionAudioSettings]; + [assetWriter addInput:input]; + + // Create and save an instance of AAPLSampleBufferChannel, which will coordinate the work of reading and writing sample buffers + audioSampleBufferChannel = [[AAPLSampleBufferChannel alloc] initWithAssetReaderOutput:output assetWriterInput:input]; + } + + if (videoTrack) + { + // Decompress to ARGB with the asset reader + NSDictionary *decompressionVideoSettings = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithUnsignedInt:kCVPixelFormatType_32ARGB], (id)kCVPixelBufferPixelFormatTypeKey, + [NSDictionary dictionary], (id)kCVPixelBufferIOSurfacePropertiesKey, + nil]; + AVAssetReaderOutput *output = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:videoTrack outputSettings:decompressionVideoSettings]; + [assetReader addOutput:output]; + + // Get the format description of the track, to fill in attributes of the video stream that we don't want to change + CMFormatDescriptionRef formatDescription = NULL; + NSArray *formatDescriptions = [videoTrack formatDescriptions]; + if ([formatDescriptions count] > 0) + formatDescription = (CMFormatDescriptionRef)[formatDescriptions objectAtIndex:0]; + + // Grab track dimensions from format description + CGSize trackDimensions = { + .width = 0.0, + .height = 0.0, + }; + if (formatDescription) + trackDimensions = CMVideoFormatDescriptionGetPresentationDimensions(formatDescription, false, false); + else + trackDimensions = [videoTrack naturalSize]; + + // Grab clean aperture, pixel aspect ratio from format description + NSDictionary *compressionSettings = nil; + if (formatDescription) + { + NSDictionary *cleanAperture = nil; + NSDictionary *pixelAspectRatio = nil; + CFDictionaryRef cleanApertureFromCMFormatDescription = CMFormatDescriptionGetExtension(formatDescription, kCMFormatDescriptionExtension_CleanAperture); + if (cleanApertureFromCMFormatDescription) + { + cleanAperture = [NSDictionary dictionaryWithObjectsAndKeys: + CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureWidth), AVVideoCleanApertureWidthKey, + CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureHeight), AVVideoCleanApertureHeightKey, + CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureHorizontalOffset), AVVideoCleanApertureHorizontalOffsetKey, + CFDictionaryGetValue(cleanApertureFromCMFormatDescription, kCMFormatDescriptionKey_CleanApertureVerticalOffset), AVVideoCleanApertureVerticalOffsetKey, + nil]; + } + CFDictionaryRef pixelAspectRatioFromCMFormatDescription = CMFormatDescriptionGetExtension(formatDescription, kCMFormatDescriptionExtension_PixelAspectRatio); + if (pixelAspectRatioFromCMFormatDescription) + { + pixelAspectRatio = [NSDictionary dictionaryWithObjectsAndKeys: + CFDictionaryGetValue(pixelAspectRatioFromCMFormatDescription, kCMFormatDescriptionKey_PixelAspectRatioHorizontalSpacing), AVVideoPixelAspectRatioHorizontalSpacingKey, + CFDictionaryGetValue(pixelAspectRatioFromCMFormatDescription, kCMFormatDescriptionKey_PixelAspectRatioVerticalSpacing), AVVideoPixelAspectRatioVerticalSpacingKey, + nil]; + } + + if (cleanAperture || pixelAspectRatio) + { + NSMutableDictionary *mutableCompressionSettings = [NSMutableDictionary dictionary]; + if (cleanAperture) + [mutableCompressionSettings setObject:cleanAperture forKey:AVVideoCleanApertureKey]; + if (pixelAspectRatio) + [mutableCompressionSettings setObject:pixelAspectRatio forKey:AVVideoPixelAspectRatioKey]; + compressionSettings = mutableCompressionSettings; + } + } + + // Compress to H.264 with the asset writer + NSMutableDictionary *videoSettings = [NSMutableDictionary dictionaryWithObjectsAndKeys: + AVVideoCodecH264, AVVideoCodecKey, + [NSNumber numberWithDouble:trackDimensions.width], AVVideoWidthKey, + [NSNumber numberWithDouble:trackDimensions.height], AVVideoHeightKey, + nil]; + if (compressionSettings) + [videoSettings setObject:compressionSettings forKey:AVVideoCompressionPropertiesKey]; + + AVAssetWriterInput *input = [AVAssetWriterInput assetWriterInputWithMediaType:[videoTrack mediaType] outputSettings:videoSettings]; + [assetWriter addInput:input]; + + // Create and save an instance of AAPLSampleBufferChannel, which will coordinate the work of reading and writing sample buffers + videoSampleBufferChannel = [[AAPLSampleBufferChannel alloc] initWithAssetReaderOutput:output assetWriterInput:input]; + } + } + + if (outError) + *outError = localError; + + return success; +} + +- (BOOL)startReadingAndWritingReturningError:(NSError **)outError +{ + BOOL success = YES; + NSError *localError = nil; + + // Instruct the asset reader and asset writer to get ready to do work + success = [assetReader startReading]; + if (!success) + localError = [assetReader error]; + if (success) + { + success = [assetWriter startWriting]; + if (!success) + localError = [assetWriter error]; + } + + if (success) + { + dispatch_group_t dispatchGroup = dispatch_group_create(); + + // Start a sample-writing session + [assetWriter startSessionAtSourceTime:[self timeRange].start]; + + // Start reading and writing samples + if (audioSampleBufferChannel) + { + // Only set audio delegate for audio-only assets, else let the video channel drive progress + id delegate = nil; + if (!videoSampleBufferChannel) + delegate = self; + + dispatch_group_enter(dispatchGroup); + [audioSampleBufferChannel startWithDelegate:delegate completionHandler:^{ + dispatch_group_leave(dispatchGroup); + }]; + } + if (videoSampleBufferChannel) + { + dispatch_group_enter(dispatchGroup); + [videoSampleBufferChannel startWithDelegate:self completionHandler:^{ + dispatch_group_leave(dispatchGroup); + }]; + } + + // Set up a callback for when the sample writing is finished + dispatch_group_notify(dispatchGroup, serializationQueue, ^{ + BOOL finalSuccess = YES; + NSError *finalError = nil; + + if (cancelled) + { + [assetReader cancelReading]; + [assetWriter cancelWriting]; + } + else + { + if ([assetReader status] == AVAssetReaderStatusFailed) + { + finalSuccess = NO; + finalError = [assetReader error]; + } + + if (finalSuccess) + { + finalSuccess = [assetWriter finishWriting]; + if (!finalSuccess) + finalError = [assetWriter error]; + } + } + + [self readingAndWritingDidFinishSuccessfully:finalSuccess withError:finalError]; + }); + + dispatch_release(dispatchGroup); + } + + if (outError) + *outError = localError; + + return success; +} + +- (void)cancel:(id)sender +{ + // Dispatch cancellation tasks to the serialization queue to avoid races with setup and teardown + dispatch_async(serializationQueue, ^{ + [audioSampleBufferChannel cancel]; + [videoSampleBufferChannel cancel]; + cancelled = YES; + }); +} + +- (void)readingAndWritingDidFinishSuccessfully:(BOOL)success withError:(NSError *)error +{ + if (!success) + { + [assetReader cancelReading]; + [assetWriter cancelWriting]; + } + + // Tear down ivars + [assetReader release]; + assetReader = nil; + [assetWriter release]; + assetWriter = nil; + [audioSampleBufferChannel release]; + audioSampleBufferChannel = nil; + [videoSampleBufferChannel release]; + videoSampleBufferChannel = nil; + cancelled = NO; + + // Dispatch UI-related tasks to the main queue + dispatch_async(dispatch_get_main_queue(), ^{ + // Order out and end the progress panel + NSWindow *progressPanel = [progressPanelController window]; + [progressPanel orderOut:self]; + [NSApp endSheet:progressPanel]; + + if (!success) + { + NSAlert *alert = [[NSAlert alloc] init]; + [alert setAlertStyle:NSCriticalAlertStyle]; + [alert setMessageText:[error localizedDescription]]; + NSString *informativeText = [error localizedRecoverySuggestion]; + informativeText = informativeText ? informativeText : [error localizedFailureReason]; // No recovery suggestion, then at least tell the user why it failed. + [alert setInformativeText:informativeText]; + + [alert beginSheetModalForWindow:[self windowForSheet] + modalDelegate:self + didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) + contextInfo:NULL]; + [alert release]; + } + [self setWritingSamples:NO]; + }); +} + +- (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo +{ + // Do nothing +} + +static double progressOfSampleBufferInTimeRange(CMSampleBufferRef sampleBuffer, CMTimeRange timeRange) +{ + CMTime progressTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); + progressTime = CMTimeSubtract(progressTime, timeRange.start); + CMTime sampleDuration = CMSampleBufferGetDuration(sampleBuffer); + if (CMTIME_IS_NUMERIC(sampleDuration)) + progressTime= CMTimeAdd(progressTime, sampleDuration); + return CMTimeGetSeconds(progressTime) / CMTimeGetSeconds(timeRange.duration); +} + +static void removeARGBColorComponentOfPixelBuffer(CVPixelBufferRef pixelBuffer, size_t componentIndex) +{ + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + + size_t bufferHeight = CVPixelBufferGetHeight(pixelBuffer); + size_t bufferWidth = CVPixelBufferGetWidth(pixelBuffer); + size_t bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer); + static const size_t bytesPerPixel = 4; // constant for ARGB pixel format + unsigned char *base = (unsigned char *)CVPixelBufferGetBaseAddress(pixelBuffer); + + for (size_t row = 0; row < bufferHeight; ++row) + { + for (size_t column = 0; column < bufferWidth; ++column) + { + unsigned char *pixel = base + (row * bytesPerRow) + (column * bytesPerPixel); + pixel[componentIndex] = 0; + } + } + + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); +} + ++ (size_t)componentIndexFromFilterTag:(NSInteger)filterTag +{ + return (size_t)filterTag; // we set up the tags in the popup button to correspond directly with the index they modify +} + +- (void)sampleBufferChannel:(AAPLSampleBufferChannel *)sampleBufferChannel didReadSampleBuffer:(CMSampleBufferRef)sampleBuffer +{ + CVPixelBufferRef pixelBuffer = NULL; + + // Calculate progress (scale of 0.0 to 1.0) + double progress = progressOfSampleBufferInTimeRange(sampleBuffer, [self timeRange]); + + // Grab the pixel buffer from the sample buffer, if possible + CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + if (imageBuffer && (CFGetTypeID(imageBuffer) == CVPixelBufferGetTypeID())) + { + pixelBuffer = (CVPixelBufferRef)imageBuffer; + if (filterTag >= 0) // -1 means "no filtering, please" + removeARGBColorComponentOfPixelBuffer(pixelBuffer, [[self class] componentIndexFromFilterTag:filterTag]); + } + + [progressPanelController setPixelBuffer:pixelBuffer forProgress:progress]; +} + +@end + + +@interface AAPLSampleBufferChannel () +- (void)callCompletionHandlerIfNecessary; // always called on the serialization queue +@end + +@implementation AAPLSampleBufferChannel + +- (id)initWithAssetReaderOutput:(AVAssetReaderOutput *)localAssetReaderOutput assetWriterInput:(AVAssetWriterInput *)localAssetWriterInput +{ + self = [super init]; + + if (self) + { + assetReaderOutput = [localAssetReaderOutput retain]; + assetWriterInput = [localAssetWriterInput retain]; + + finished = NO; + NSString *serializationQueueDescription = [NSString stringWithFormat:@"%@ serialization queue", self]; + serializationQueue = dispatch_queue_create([serializationQueueDescription UTF8String], NULL); + } + + return self; +} + +- (void)dealloc +{ + [assetReaderOutput release]; + [assetWriterInput release]; + if (serializationQueue) + dispatch_release(serializationQueue); + [completionHandler release]; + + [super dealloc]; +} + +- (NSString *)mediaType +{ + return [assetReaderOutput mediaType]; +} + +- (void)startWithDelegate:(id )delegate completionHandler:(dispatch_block_t)localCompletionHandler +{ + completionHandler = [localCompletionHandler copy]; // released in -callCompletionHandlerIfNecessary + + [assetWriterInput requestMediaDataWhenReadyOnQueue:serializationQueue usingBlock:^{ + if (finished) + return; + + BOOL completedOrFailed = NO; + + // Read samples in a loop as long as the asset writer input is ready + while ([assetWriterInput isReadyForMoreMediaData] && !completedOrFailed) + { + CMSampleBufferRef sampleBuffer = [assetReaderOutput copyNextSampleBuffer]; + if (sampleBuffer != NULL) + { + if ([delegate respondsToSelector:@selector(sampleBufferChannel:didReadSampleBuffer:)]) + [delegate sampleBufferChannel:self didReadSampleBuffer:sampleBuffer]; + + BOOL success = [assetWriterInput appendSampleBuffer:sampleBuffer]; + CFRelease(sampleBuffer); + sampleBuffer = NULL; + + completedOrFailed = !success; + } + else + { + completedOrFailed = YES; + } + } + + if (completedOrFailed) + [self callCompletionHandlerIfNecessary]; + }]; +} + +- (void)cancel +{ + dispatch_async(serializationQueue, ^{ + [self callCompletionHandlerIfNecessary]; + }); +} + +- (void)callCompletionHandlerIfNecessary +{ + // Set state to mark that we no longer need to call the completion handler, grab the completion handler, and clear out the ivar + BOOL oldFinished = finished; + finished = YES; + + if (oldFinished == NO) + { + [assetWriterInput markAsFinished]; // let the asset writer know that we will not be appending any more samples to this input + + dispatch_block_t localCompletionHandler = [completionHandler retain]; + [completionHandler release]; + completionHandler = nil; + + if (localCompletionHandler) + { + localCompletionHandler(); + [localCompletionHandler release]; + } + } +} + +@end diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLProgressPanel.xib b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLProgressPanel.xib new file mode 100644 index 00000000..f6b2c14c --- /dev/null +++ b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLProgressPanel.xib @@ -0,0 +1,304 @@ + + + + 101000 + + 8121.17 + 1382.14 + 788.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 8121.17 + + + NSBox + NSButton + NSButtonCell + NSCustomObject + NSCustomView + NSProgressIndicator + NSView + NSWindowTemplate + + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + AAPLProgressPanelController + + + FirstResponder + + + NSApplication + + + 7 + 2 + {{196, 240}, {572, 209}} + 544735232 + Window + NSWindow + + + + + 256 + + + + 293 + {{245, 12}, {82, 32}} + + _NS:161 + YES + + 67108864 + 134217728 + Cancel + + YES + 13 + 1044 + + _NS:161 + + -2038284288 + 129 + + Gw + 200 + 25 + + NO + + + + 12 + + + + 274 + + + + 274 + {{18, 38}, {500, 76}} + + + _NS:490 + NSView + + + + 1292 + {{16, 10}, {504, 20}} + + + 16392 + 1 + + + {{1, 1}, {536, 124}} + + + _NS:631 + + + {{17, 56}, {538, 140}} + + + _NS:629 + {0, 0} + + 67108864 + 0 + Progress + + YES + 11 + 3100 + + + 6 + System + textBackgroundColor + + 3 + MQA + + + + 6 + System + labelColor + + 3 + MAA + + + + + 1 + 0 + 2 + NO + + + {572, 209} + + + {{0, 0}, {1440, 878}} + {10000000000000, 10000000000000} + YES + + + + + + + cancel: + + + + 5 + + + + window + + + + 6 + + + + frameView + + + + 8 + + + + progressIndicator + + + + 13 + + + + + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 1 + + + + + + + + 2 + + + + + + + + + 3 + + + + + + + + 4 + + + + + 10 + + + + + + + + + 7 + + + + + 11 + + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + {{357, 418}, {480, 270}} + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + + + + + + + + + 13 + + + 0 + IBCocoaFramework + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + 3 + + diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLProgressPanelController.h b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLProgressPanelController.h new file mode 100644 index 00000000..bfe4655a --- /dev/null +++ b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLProgressPanelController.h @@ -0,0 +1,39 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Main window controller for the sample app. + */ + +@import AppKit; +@import CoreMedia; + +@protocol AAPLProgressPanelControllerDelegate; + +@interface AAPLProgressPanelController : NSWindowController +{ +@private + id delegate; + + IBOutlet NSView *frameView; + IBOutlet NSProgressIndicator *progressIndicator; + CALayer *frameLayer; + + NSMutableArray *interestingProgressValues; +} + +@property (nonatomic, retain) IBOutlet NSView *frameView; +@property (nonatomic, retain) IBOutlet NSProgressIndicator *progressIndicator; +@property (nonatomic, assign) id delegate; + +- (void)setPixelBuffer:(CVPixelBufferRef)pixelBuffer forProgress:(double)progress; // progress should be in the range 0.0 to 1.0 +- (IBAction)cancel:(id)sender; + +@end + + +@protocol AAPLProgressPanelControllerDelegate +@optional +- (void)progressPanelControllerDidCancel:(AAPLProgressPanelController *)progressPanelController; +@end diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLProgressPanelController.m b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLProgressPanelController.m new file mode 100644 index 00000000..34c7c5a9 --- /dev/null +++ b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AAPLProgressPanelController.m @@ -0,0 +1,142 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Main window controller for the sample app. + */ + +#import "AAPLProgressPanelController.h" + +@import QuartzCore; + +@interface AAPLProgressPanelController () +@property (nonatomic, retain) CALayer *frameLayer; +@end + +@implementation AAPLProgressPanelController + +- (void)dealloc +{ + [frameLayer release]; + [frameView release]; + [progressIndicator release]; + [interestingProgressValues release]; + + [super dealloc]; +} + +- (void)windowDidLoad +{ + [super windowDidLoad]; + + // Create a layer and set it on the view. We will display video frames by adding sublayers as we go + CALayer *localFrameLayer = [CALayer layer]; + [self setFrameLayer:localFrameLayer]; + NSView *localFrameView = [self frameView]; + [localFrameView setLayer:localFrameLayer]; + [localFrameView setWantsLayer:YES]; +} + +@synthesize progressIndicator=progressIndicator; +@synthesize frameView=frameView; +@synthesize frameLayer=frameLayer; +@synthesize delegate=delegate; + +- (void)setPixelBuffer:(CVPixelBufferRef)pixelBuffer forProgress:(double)progress +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [[self progressIndicator] setDoubleValue:progress]; + }); + + if (pixelBuffer == NULL) + return; + + CALayer *localFrameLayer = [self frameLayer]; + + // Calculate size of image + CGSize frameLayerSize = [localFrameLayer frame].size; + double imageLayerWidth = (double)CVPixelBufferGetWidth(pixelBuffer) * frameLayerSize.height / (double)CVPixelBufferGetHeight(pixelBuffer); + double imageLayerHeight = frameLayerSize.height; + + // Calculate position of image in the progress bar + double expectedImageCount = ceil(frameLayerSize.width / imageLayerWidth) + 2.0; + double progressValueForFinalImage = (expectedImageCount - 1) / expectedImageCount; + double imageLayerXPos = progress * (frameLayerSize.width - imageLayerWidth) / progressValueForFinalImage; + double imageLayerYPos = 0.0; + + // If we haven't already done so, decide the set of progress values for which we will display an image + if (!interestingProgressValues) + { + interestingProgressValues = [[NSMutableArray alloc] init]; + + double progressDisplayInterval = 1.0 / expectedImageCount; + for (NSInteger i = 0; i < (NSInteger)expectedImageCount; ++i) + [interestingProgressValues addObject:[NSNumber numberWithDouble:((double)i * progressDisplayInterval)]]; + } + + // Determine whether we will display this frame + BOOL displayThisFrame = NO; + if ([interestingProgressValues count] > 0) + { + NSNumber *nextInterestingProgressValue = [interestingProgressValues objectAtIndex:0]; + // If we have progressed beyond the next progress value, make a note that we should display this one + if (progress >= [nextInterestingProgressValue doubleValue]) + { + displayThisFrame = YES; + [interestingProgressValues removeObjectAtIndex:0]; + } + } + + // If so, add a sublayer to the frame layer with the pixel buffer as its contents + if (displayThisFrame) + { + CALayer *imageLayer = [[CALayer alloc] init]; + + // Make contents for this layer + NSImage *image = nil; + id contents = (id)CVPixelBufferGetIOSurface(pixelBuffer); // try IOSurface first + if (!contents) + { + // Fall back to creating an NSImage from the image buffer, via CIImage + CIImage *ciImage = [[CIImage alloc] initWithCVImageBuffer:pixelBuffer]; + NSCIImageRep *imageRep = [[NSCIImageRep alloc] initWithCIImage:ciImage]; + [ciImage release]; + image = [[NSImage alloc] initWithSize:[imageRep size]]; + [image addRepresentation:imageRep]; + [imageRep release]; + + contents = image; + } + + // Set contents, frame, and initial opacity + [CATransaction begin]; // need an explicit transaction since we may not be executing on the main thread + { + [imageLayer setContents:contents]; + [imageLayer setFrame:CGRectMake(imageLayerXPos, imageLayerYPos, imageLayerWidth, imageLayerHeight)]; + [imageLayer setOpacity:0.0]; + [localFrameLayer addSublayer:imageLayer]; + } + [CATransaction commit]; + + // Animate opacity from 0.0 -> 1.0 + [CATransaction begin]; + { + [CATransaction setAnimationDuration:1.5]; + [imageLayer setOpacity:1.0]; + } + [CATransaction commit]; + + [image release]; + [imageLayer release]; + } +} + +- (IBAction)cancel:(id)sender +{ + id localDelegate = [self delegate]; + if (localDelegate && [localDelegate respondsToSelector:@selector(progressPanelControllerDidCancel:)]) + [localDelegate progressPanelControllerDidCancel:self]; +} + +@end diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/AVReaderWriterOSX-Info.plist b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AVReaderWriterOSX-Info.plist new file mode 100644 index 00000000..b06433b3 --- /dev/null +++ b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AVReaderWriterOSX-Info.plist @@ -0,0 +1,57 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDocumentTypes + + + CFBundleTypeName + Audiovisual Assets + CFBundleTypeRole + Viewer + LSItemContentTypes + + public.audiovisual-content + + NSDocumentClass + AAPLDocument + + + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleURLTypes + + CFBundleVersion + 1 + LSMinimumSystemVersion + ${MACOSX_DEPLOYMENT_TARGET} + NSHumanReadableCopyright + Copyright © 2011 __MyCompanyName__. All rights reserved. + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + NSServices + + UTExportedTypeDeclarations + + UTImportedTypeDeclarations + + + diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/AudioOnly2x.png b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AudioOnly2x.png new file mode 100644 index 00000000..e54ffd78 Binary files /dev/null and b/AVReaderWriter/Objective-C/AVReaderWriterOSX/AudioOnly2x.png differ diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/ErrorLoading2x.png b/AVReaderWriter/Objective-C/AVReaderWriterOSX/ErrorLoading2x.png new file mode 100644 index 00000000..be61eaad Binary files /dev/null and b/AVReaderWriter/Objective-C/AVReaderWriterOSX/ErrorLoading2x.png differ diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/en.lproj/AAPLDocument.xib b/AVReaderWriter/Objective-C/AVReaderWriterOSX/en.lproj/AAPLDocument.xib new file mode 100644 index 00000000..e7b545df --- /dev/null +++ b/AVReaderWriter/Objective-C/AVReaderWriterOSX/en.lproj/AAPLDocument.xib @@ -0,0 +1,438 @@ + + + + 101000 + + 8121.17 + 1382.14 + 788.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 8121.17 + + + NSButton + NSButtonCell + NSCustomObject + NSCustomView + NSMenu + NSMenuItem + NSPopUpButton + NSPopUpButtonCell + NSTextField + NSTextFieldCell + NSView + NSWindowTemplate + + + com.apple.InterfaceBuilder.CocoaPlugin + + + + + AAPLDocument + + + FirstResponder + + + 15 + 2 + {{133, 235}, {507, 413}} + 1886912512 + Window + NSWindow + View + + {350, 300} + + + 256 + + + + 292 + {{196, 12}, {125, 32}} + + _NS:164 + YES + + 67108864 + 134217728 + Write to file... + + YES + 13 + 1044 + + _NS:164 + + -2038284288 + 129 + + + 200 + 25 + + NO + + + + 274 + {{20, 60}, {467, 333}} + + + _NS:498 + NSView + + + + 292 + {{97, 16}, {100, 26}} + + + _NS:179 + YES + + -2076180416 + 2048 + + _NS:179 + + 109199360 + 129 + + + 400 + 75 + + + None + + 1048576 + 2147483647 + 1 + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + _popUpItemAction: + -1 + + + YES + + OtherViews + + + + + Aquafy + + 1048576 + 2147483647 + + + _popUpItemAction: + 1 + + + + + Rosify + + 1048576 + 2147483647 + + + _popUpItemAction: + 2 + + + + + + 1 + YES + YES + 2 + + NO + + + + 292 + {{17, 22}, {78, 17}} + + + _NS:3945 + YES + + 68157504 + 272630784 + Video filter: + + _NS:3945 + + + 6 + System + controlColor + + 3 + MC42NjY2NjY2NjY3AA + + + + 6 + System + controlTextColor + + 3 + MAA + + + + NO + 2 + + + {507, 413} + + + {{0, 0}, {1440, 878}} + {350, 322} + {10000000000000, 10000000000000} + YES + + + NSApplication + + + + + + + window + + + + 18 + + + + start: + + + + 100034 + + + + frameView + + + + 100045 + + + + filterPopUpButton + + + + 100052 + + + + delegate + + + + 17 + + + + enabled: writingSamples + + + + + + enabled: writingSamples + enabled + writingSamples + + NSValueTransformerName + NSNegateBoolean + + 2 + + + 100039 + + + + + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + 5 + + + + + + Window + + + 6 + + + + + + + + + + + -3 + + + Application + + + 100021 + + + + + + + + 100022 + + + + + 100044 + + + + + 100046 + + + + + + + + 100047 + + + + + + + + 100048 + + + + + + + + + + 100049 + + + + + 100050 + + + + + 100051 + + + + + 100053 + + + + + + + + 100054 + + + + + + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + {{133, 170}, {507, 413}} + com.apple.InterfaceBuilder.CocoaPlugin + + + + + + 100054 + + + 0 + IBCocoaFramework + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + 3 + + {12, 12} + {10, 2} + + + diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/en.lproj/InfoPlist.strings b/AVReaderWriter/Objective-C/AVReaderWriterOSX/en.lproj/InfoPlist.strings new file mode 100644 index 00000000..61dd0cd1 --- /dev/null +++ b/AVReaderWriter/Objective-C/AVReaderWriterOSX/en.lproj/InfoPlist.strings @@ -0,0 +1,4 @@ +/* Localized versions of Info.plist keys */ + +CFBundleGetInfoString = "v1.0, Copyright 2011, Apple Inc."; +NSHumanReadableCopyright = "Copyright © 2011, Apple Inc."; diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/en.lproj/MainMenu.xib b/AVReaderWriter/Objective-C/AVReaderWriterOSX/en.lproj/MainMenu.xib new file mode 100644 index 00000000..d66d8cbb --- /dev/null +++ b/AVReaderWriter/Objective-C/AVReaderWriterOSX/en.lproj/MainMenu.xib @@ -0,0 +1,1769 @@ + + + + 1060 + 11A480b + 1576 + 1127 + 561.00 + + com.apple.InterfaceBuilder.CocoaPlugin + 1576 + + + YES + NSMenu + NSMenuItem + NSCustomObject + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + + + YES + + YES + + + + + YES + + NSApplication + + + FirstResponder + + + NSApplication + + + AMainMenu + + YES + + + AVReaderWriter + + 1048576 + 2147483647 + + NSImage + NSMenuCheckmark + + + NSImage + NSMenuMixedState + + submenuAction: + + AVReaderWriter + + YES + + + About AVReaderWriter + + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Services + + 1048576 + 2147483647 + + + submenuAction: + + Services + + YES + + _NSServicesMenu + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Hide AVReaderWriter + h + 1048576 + 2147483647 + + + + + + Hide Others + h + 1572864 + 2147483647 + + + + + + Show All + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Quit AVReaderWriter + q + 1048576 + 2147483647 + + + + + _NSAppleMenu + + + + + File + + 1048576 + 2147483647 + + + submenuAction: + + File + + YES + + + Open… + o + 1048576 + 2147483647 + + + + + + Open Recent + + 1048576 + 2147483647 + + + submenuAction: + + Open Recent + + YES + + + Clear Menu + + 1048576 + 2147483647 + + + + + _NSRecentDocumentsMenu + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Close + w + 1048576 + 2147483647 + + + + + + + + + Edit + + 1048576 + 2147483647 + + + submenuAction: + + Edit + + YES + + + Undo + z + 1048576 + 2147483647 + + + + + + Redo + Z + 1179648 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Cut + x + 1048576 + 2147483647 + + + + + + Copy + c + 1048576 + 2147483647 + + + + + + Paste + v + 1048576 + 2147483647 + + + + + + Paste and Match Style + V + 1572864 + 2147483647 + + + + + + Delete + + 1048576 + 2147483647 + + + + + + Select All + a + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Find + + 1048576 + 2147483647 + + + submenuAction: + + Find + + YES + + + Find… + f + 1048576 + 2147483647 + + + 1 + + + + Find Next + g + 1048576 + 2147483647 + + + 2 + + + + Find Previous + G + 1179648 + 2147483647 + + + 3 + + + + Use Selection for Find + e + 1048576 + 2147483647 + + + 7 + + + + Jump to Selection + j + 1048576 + 2147483647 + + + + + + + + + Spelling and Grammar + + 1048576 + 2147483647 + + + submenuAction: + + Spelling and Grammar + + YES + + + Show Spelling and Grammar + : + 1048576 + 2147483647 + + + + + + Check Document Now + ; + 1048576 + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Check Spelling While Typing + + 1048576 + 2147483647 + + + + + + Check Grammar With Spelling + + 1048576 + 2147483647 + + + + + + Correct Spelling Automatically + + 2147483647 + + + + + + + + + Substitutions + + 1048576 + 2147483647 + + + submenuAction: + + Substitutions + + YES + + + Show Substitutions + + 2147483647 + + + + + + YES + YES + + + 2147483647 + + + + + + Smart Copy/Paste + f + 1048576 + 2147483647 + + + 1 + + + + Smart Quotes + g + 1048576 + 2147483647 + + + 2 + + + + Smart Dashes + + 2147483647 + + + + + + Smart Links + G + 1179648 + 2147483647 + + + 3 + + + + Text Replacement + + 2147483647 + + + + + + + + + Transformations + + 2147483647 + + + submenuAction: + + Transformations + + YES + + + Make Upper Case + + 2147483647 + + + + + + Make Lower Case + + 2147483647 + + + + + + Capitalize + + 2147483647 + + + + + + + + + Speech + + 1048576 + 2147483647 + + + submenuAction: + + Speech + + YES + + + Start Speaking + + 1048576 + 2147483647 + + + + + + Stop Speaking + + 1048576 + 2147483647 + + + + + + + + + + + + Window + + 1048576 + 2147483647 + + + submenuAction: + + Window + + YES + + + Minimize + m + 1048576 + 2147483647 + + + + + + Zoom + + 1048576 + 2147483647 + + + + + + YES + YES + + + 1048576 + 2147483647 + + + + + + Bring All to Front + + 1048576 + 2147483647 + + + + + _NSWindowsMenu + + + + + Help + + 2147483647 + + + + + _NSMainMenu + + + NSFontManager + + + + + YES + + + performMiniaturize: + + + + 37 + + + + arrangeInFront: + + + + 39 + + + + clearRecentDocuments: + + + + 127 + + + + orderFrontStandardAboutPanel: + + + + 142 + + + + performClose: + + + + 193 + + + + toggleContinuousSpellChecking: + + + + 222 + + + + undo: + + + + 223 + + + + copy: + + + + 224 + + + + checkSpelling: + + + + 225 + + + + paste: + + + + 226 + + + + stopSpeaking: + + + + 227 + + + + cut: + + + + 228 + + + + showGuessPanel: + + + + 230 + + + + redo: + + + + 231 + + + + selectAll: + + + + 232 + + + + startSpeaking: + + + + 233 + + + + delete: + + + + 235 + + + + performZoom: + + + + 240 + + + + performFindPanelAction: + + + + 241 + + + + centerSelectionInVisibleArea: + + + + 245 + + + + toggleGrammarChecking: + + + + 347 + + + + toggleSmartInsertDelete: + + + + 355 + + + + toggleAutomaticQuoteSubstitution: + + + + 356 + + + + toggleAutomaticLinkDetection: + + + + 357 + + + + hide: + + + + 367 + + + + hideOtherApplications: + + + + 368 + + + + unhideAllApplications: + + + + 370 + + + + openDocument: + + + + 372 + + + + terminate: + + + + 448 + + + + capitalizeWord: + + + + 454 + + + + lowercaseWord: + + + + 455 + + + + uppercaseWord: + + + + 456 + + + + toggleAutomaticDashSubstitution: + + + + 460 + + + + orderFrontSubstitutionsPanel: + + + + 461 + + + + toggleAutomaticTextReplacement: + + + + 463 + + + + toggleAutomaticSpellingCorrection: + + + + 466 + + + + performFindPanelAction: + + + + 467 + + + + performFindPanelAction: + + + + 468 + + + + performFindPanelAction: + + + + 469 + + + + pasteAsPlainText: + + + + 471 + + + + + YES + + 0 + + + + + + -2 + + + File's Owner + + + -1 + + + First Responder + + + -3 + + + Application + + + 29 + + + YES + + + + + + + + + + 19 + + + YES + + + + + + 56 + + + YES + + + + + + 217 + + + YES + + + + + + 83 + + + YES + + + + + + 81 + + + YES + + + + + + + + + 72 + + + + + 124 + + + YES + + + + + + 73 + + + + + 79 + + + + + 125 + + + YES + + + + + + 126 + + + + + 205 + + + YES + + + + + + + + + + + + + + + + + + + + 202 + + + + + 198 + + + + + 207 + + + + + 214 + + + + + 199 + + + + + 203 + + + + + 197 + + + + + 206 + + + + + 215 + + + + + 218 + + + YES + + + + + + 216 + + + YES + + + + + + 200 + + + YES + + + + + + + + + + + 219 + + + + + 201 + + + + + 204 + + + + + 220 + + + YES + + + + + + + + + + 213 + + + + + 210 + + + + + 221 + + + + + 208 + + + + + 209 + + + + + 57 + + + YES + + + + + + + + + + + + + + 58 + + + + + 134 + + + + + 150 + + + + + 136 + + + + + 144 + + + + + 236 + + + + + 131 + + + YES + + + + + + 149 + + + + + 145 + + + + + 130 + + + + + 24 + + + YES + + + + + + + + + 92 + + + + + 5 + + + + + 239 + + + + + 23 + + + + + 211 + + + YES + + + + + + 212 + + + YES + + + + + + + 195 + + + + + 196 + + + + + 346 + + + + + 348 + + + YES + + + + + + 349 + + + YES + + + + + + + + + + + + 350 + + + + + 351 + + + + + 354 + + + + + 419 + + + + + 449 + + + YES + + + + + + 450 + + + YES + + + + + + + + 451 + + + + + 452 + + + + + 453 + + + + + 457 + + + + + 458 + + + + + 459 + + + + + 462 + + + + + 464 + + + + + 465 + + + + + 470 + + + + + 491 + + + YES + + + + + + + YES + + YES + -1.IBPluginDependency + -2.IBPluginDependency + -3.IBPluginDependency + 124.IBPluginDependency + 125.IBPluginDependency + 126.IBPluginDependency + 130.IBPluginDependency + 131.IBPluginDependency + 134.IBPluginDependency + 136.IBPluginDependency + 144.IBPluginDependency + 145.IBPluginDependency + 149.IBPluginDependency + 150.IBPluginDependency + 19.IBPluginDependency + 195.IBPluginDependency + 196.IBPluginDependency + 197.IBPluginDependency + 198.IBPluginDependency + 199.IBPluginDependency + 200.IBPluginDependency + 201.IBPluginDependency + 202.IBPluginDependency + 203.IBPluginDependency + 204.IBPluginDependency + 205.IBPluginDependency + 206.IBPluginDependency + 207.IBPluginDependency + 208.IBPluginDependency + 209.IBPluginDependency + 210.IBPluginDependency + 211.IBPluginDependency + 212.IBPluginDependency + 213.IBPluginDependency + 214.IBPluginDependency + 215.IBPluginDependency + 216.IBPluginDependency + 217.IBPluginDependency + 218.IBPluginDependency + 219.IBPluginDependency + 220.IBPluginDependency + 221.IBPluginDependency + 23.IBPluginDependency + 236.IBPluginDependency + 239.IBPluginDependency + 24.IBPluginDependency + 29.IBPluginDependency + 346.IBPluginDependency + 348.IBPluginDependency + 349.IBPluginDependency + 350.IBPluginDependency + 351.IBPluginDependency + 354.IBPluginDependency + 419.IBPluginDependency + 449.IBPluginDependency + 450.IBPluginDependency + 451.IBPluginDependency + 452.IBPluginDependency + 453.IBPluginDependency + 457.IBPluginDependency + 458.IBPluginDependency + 459.IBPluginDependency + 462.IBPluginDependency + 464.IBPluginDependency + 465.IBPluginDependency + 470.IBPluginDependency + 491.IBPluginDependency + 5.IBPluginDependency + 56.IBPluginDependency + 57.IBPluginDependency + 58.IBPluginDependency + 72.IBPluginDependency + 73.IBPluginDependency + 79.IBPluginDependency + 81.IBPluginDependency + 83.IBPluginDependency + 92.IBPluginDependency + + + YES + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + + + + YES + + + + + + YES + + + + + 529 + + + 0 + IBCocoaFramework + + com.apple.InterfaceBuilder.CocoaPlugin.macosx + + + + com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 + + + YES + 3 + + YES + + YES + NSMenuCheckmark + NSMenuMixedState + + + YES + {9, 8} + {7, 2} + + + + diff --git a/AVReaderWriter/Objective-C/AVReaderWriterOSX/main.m b/AVReaderWriter/Objective-C/AVReaderWriterOSX/main.m new file mode 100644 index 00000000..d11ad240 --- /dev/null +++ b/AVReaderWriter/Objective-C/AVReaderWriterOSX/main.m @@ -0,0 +1,12 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + */ + +@import AppKit; + +int main(int argc, char *argv[]) +{ + return NSApplicationMain(argc, (const char **)argv); +} diff --git a/AVReaderWriter/README.md b/AVReaderWriter/README.md new file mode 100644 index 00000000..2945c3e3 --- /dev/null +++ b/AVReaderWriter/README.md @@ -0,0 +1,43 @@ +# AVReaderWriter + +## Description + +AVReaderWriter demonstrates how to use AVAssetReader to read and decode sample data from a movie file, work directly with the sample data, then use AVAssetWriter to encode and write the sample data to a new movie file. + +There are two versions: One is written in Objective-C and runs on OS X, the other is written in Swift and runs on iOS. Despite differing in the area of user interface management, both versions demonstrate the same basic concepts involved in working with raw media data. The execution of these concepts is concentrated in CyanifyOperation.swift (Swift/iOS version) and AAPLDocument.m (Objective-C/OS X version). + +## Build Requirements + +Xcode 8.0, macOS 10.12 SDK, iOS 10.0 SDK + +## Runtime Requirements + +OS X 10.11, iOS 9.0 + +# Structure + +Objective-C Version: + Source files: AAPLDocument.h/m, AAPLProgressPanelController.h/m, main.m + Project bundle: AVReaderWriter.xcodeproj, AVReaderWriterOSX-Info.plist, InfoPlist.strings + User interface files: AAPLProgressPanel.xib, MainMenu.xib, AAPLDocument.xib + User interface resources: AudioOnly2x.png, ErrorLoading2x.png + +Swift Version: + Main source file: CyanifyOperation.swift + User interface source files: ProgressViewController.swift, ResultViewController.swift, StartViewController.swift, AppDelegate.swift + Project bundle: AVReaderWriter.xcodeproj, Info.plist + User interface files: LaunchScreen.storyboard, Main.storyboard, Assets.xcassets + Resources: ElephantSeals.mov + +## Changes + +Version 1.0 +- First version. + +Version 2.0 +- Add Swift version. + +Version 3.1 +- Update for Swift 2.3 + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/AVReaderWriter/Swift/AVReaderWriter.xcodeproj/project.pbxproj b/AVReaderWriter/Swift/AVReaderWriter.xcodeproj/project.pbxproj new file mode 100644 index 00000000..83f98069 --- /dev/null +++ b/AVReaderWriter/Swift/AVReaderWriter.xcodeproj/project.pbxproj @@ -0,0 +1,326 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 001465081B20B08200A03D94 /* ElephantSeals.mov in Resources */ = {isa = PBXBuildFile; fileRef = 001465071B20B08200A03D94 /* ElephantSeals.mov */; }; + 00A8BFF71B0FB4D600631442 /* CyanifyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A8BFF61B0FB4D600631442 /* CyanifyOperation.swift */; }; + 00F9B2C41B0EB234001220EE /* ProgressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F9B2C31B0EB234001220EE /* ProgressViewController.swift */; }; + 00F9B2C61B0EB247001220EE /* ResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F9B2C51B0EB247001220EE /* ResultViewController.swift */; }; + 00FA95311B050A620026871B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00FA95301B050A620026871B /* AppDelegate.swift */; }; + 00FA95331B050A620026871B /* StartViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00FA95321B050A620026871B /* StartViewController.swift */; }; + 00FA95361B050A620026871B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 00FA95341B050A620026871B /* Main.storyboard */; }; + 00FA95381B050A620026871B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 00FA95371B050A620026871B /* Assets.xcassets */; }; + 00FA953B1B050A620026871B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 00FA95391B050A620026871B /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 001465071B20B08200A03D94 /* ElephantSeals.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = ElephantSeals.mov; sourceTree = ""; }; + 00A8BFF61B0FB4D600631442 /* CyanifyOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CyanifyOperation.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 00F9B2C31B0EB234001220EE /* ProgressViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ProgressViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 00F9B2C51B0EB247001220EE /* ResultViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultViewController.swift; sourceTree = ""; }; + 00FA952D1B050A620026871B /* AVReaderWriter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AVReaderWriter.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 00FA95301B050A620026871B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 00FA95321B050A620026871B /* StartViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartViewController.swift; sourceTree = ""; }; + 00FA95351B050A620026871B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 00FA95371B050A620026871B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 00FA953A1B050A620026871B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 00FA953C1B050A620026871B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3E7793F81B1D347D00735293 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 00FA952A1B050A620026871B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00E816EA1B050D1100750F7D /* Resources */ = { + isa = PBXGroup; + children = ( + 001465071B20B08200A03D94 /* ElephantSeals.mov */, + ); + path = Resources; + sourceTree = ""; + }; + 00FA95241B050A620026871B = { + isa = PBXGroup; + children = ( + 3E7793F81B1D347D00735293 /* README.md */, + 00FA952F1B050A620026871B /* AVReaderWriter */, + 00E816EA1B050D1100750F7D /* Resources */, + 00FA952E1B050A620026871B /* Products */, + ); + sourceTree = ""; + }; + 00FA952E1B050A620026871B /* Products */ = { + isa = PBXGroup; + children = ( + 00FA952D1B050A620026871B /* AVReaderWriter.app */, + ); + name = Products; + sourceTree = ""; + }; + 00FA952F1B050A620026871B /* AVReaderWriter */ = { + isa = PBXGroup; + children = ( + 00FA95341B050A620026871B /* Main.storyboard */, + 00FA95301B050A620026871B /* AppDelegate.swift */, + 00FA95321B050A620026871B /* StartViewController.swift */, + 00F9B2C31B0EB234001220EE /* ProgressViewController.swift */, + 00F9B2C51B0EB247001220EE /* ResultViewController.swift */, + 00A8BFF61B0FB4D600631442 /* CyanifyOperation.swift */, + 00FA95391B050A620026871B /* LaunchScreen.storyboard */, + 00FA95371B050A620026871B /* Assets.xcassets */, + 00FA953C1B050A620026871B /* Info.plist */, + ); + path = AVReaderWriter; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 00FA952C1B050A620026871B /* AVReaderWriter */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00FA954A1B050A630026871B /* Build configuration list for PBXNativeTarget "AVReaderWriter" */; + buildPhases = ( + 00FA95291B050A620026871B /* Sources */, + 00FA952A1B050A620026871B /* Frameworks */, + 00FA952B1B050A620026871B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AVReaderWriter; + productName = AVReaderWriter; + productReference = 00FA952D1B050A620026871B /* AVReaderWriter.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 00FA95251B050A620026871B /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + TargetAttributes = { + 00FA952C1B050A620026871B = { + CreatedOnToolsVersion = 7.0; + DevelopmentTeam = U6UQQ7LHZN; + DevelopmentTeamName = "Scott Kuechle"; + LastSwiftMigration = 0800; + }; + }; + }; + buildConfigurationList = 00FA95281B050A620026871B /* Build configuration list for PBXProject "AVReaderWriter" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 00FA95241B050A620026871B; + productRefGroup = 00FA952E1B050A620026871B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 00FA952C1B050A620026871B /* AVReaderWriter */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 00FA952B1B050A620026871B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00FA953B1B050A620026871B /* LaunchScreen.storyboard in Resources */, + 00FA95381B050A620026871B /* Assets.xcassets in Resources */, + 001465081B20B08200A03D94 /* ElephantSeals.mov in Resources */, + 00FA95361B050A620026871B /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 00FA95291B050A620026871B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00F9B2C41B0EB234001220EE /* ProgressViewController.swift in Sources */, + 00A8BFF71B0FB4D600631442 /* CyanifyOperation.swift in Sources */, + 00FA95331B050A620026871B /* StartViewController.swift in Sources */, + 00FA95311B050A620026871B /* AppDelegate.swift in Sources */, + 00F9B2C61B0EB247001220EE /* ResultViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 00FA95341B050A620026871B /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 00FA95351B050A620026871B /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 00FA95391B050A620026871B /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 00FA953A1B050A620026871B /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 00FA95481B050A630026871B /* 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"; + SWIFT_VERSION = 2.3; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 00FA95491B050A630026871B /* 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; + SWIFT_VERSION = 2.3; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 00FA954B1B050A630026871B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = AVReaderWriter/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVReaderWriter"; + PRODUCT_NAME = $TARGET_NAME; + SWIFT_VERSION = 2.3; + }; + name = Debug; + }; + 00FA954C1B050A630026871B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = AVReaderWriter/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AVReaderWriter"; + PRODUCT_NAME = $TARGET_NAME; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 2.3; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 00FA95281B050A620026871B /* Build configuration list for PBXProject "AVReaderWriter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00FA95481B050A630026871B /* Debug */, + 00FA95491B050A630026871B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 00FA954A1B050A630026871B /* Build configuration list for PBXNativeTarget "AVReaderWriter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00FA954B1B050A630026871B /* Debug */, + 00FA954C1B050A630026871B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 00FA95251B050A620026871B /* Project object */; +} diff --git a/AVReaderWriter/Swift/AVReaderWriter/AppDelegate.swift b/AVReaderWriter/Swift/AVReaderWriter/AppDelegate.swift new file mode 100644 index 00000000..bf01b0f4 --- /dev/null +++ b/AVReaderWriter/Swift/AVReaderWriter/AppDelegate.swift @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Main application entry point. +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + // MARK: Properties + + var window: UIWindow? +} diff --git a/AVReaderWriter/Swift/AVReaderWriter/Assets.xcassets/AppIcon.appiconset/Contents.json b/AVReaderWriter/Swift/AVReaderWriter/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/AVReaderWriter/Swift/AVReaderWriter/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AVReaderWriter/Swift/AVReaderWriter/Base.lproj/LaunchScreen.storyboard b/AVReaderWriter/Swift/AVReaderWriter/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..2eed82f6 --- /dev/null +++ b/AVReaderWriter/Swift/AVReaderWriter/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVReaderWriter/Swift/AVReaderWriter/Base.lproj/Main.storyboard b/AVReaderWriter/Swift/AVReaderWriter/Base.lproj/Main.storyboard new file mode 100644 index 00000000..16406bf5 --- /dev/null +++ b/AVReaderWriter/Swift/AVReaderWriter/Base.lproj/Main.storyboard @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AVReaderWriter/Swift/AVReaderWriter/CyanifyOperation.swift b/AVReaderWriter/Swift/AVReaderWriter/CyanifyOperation.swift new file mode 100644 index 00000000..7637f774 --- /dev/null +++ b/AVReaderWriter/Swift/AVReaderWriter/CyanifyOperation.swift @@ -0,0 +1,491 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Defines a subclass of NSOperation that adjusts the color of a video file. +*/ + +import AVFoundation +import Dispatch + +enum CyanifyError: ErrorType { + case NoMediaData +} + +class CyanifyOperation: NSOperation { + // MARK: Types + + enum Result { + case Success + case Cancellation + case Failure(ErrorType) + } + + // MARK: Properties + + override var executing: Bool { + return result == nil + } + + override var finished: Bool { + return result != nil + } + + private let asset: AVAsset + + private let outputURL: NSURL + + private var sampleTransferError: ErrorType? + + var result: Result? { + willSet { + willChangeValueForKey("isExecuting") + willChangeValueForKey("isFinished") + } + didSet { + didChangeValueForKey("isExecuting") + didChangeValueForKey("isFinished") + } + } + + // MARK: Initialization + + init(sourceURL: NSURL, outputURL: NSURL) { + asset = AVAsset(URL: sourceURL) + self.outputURL = outputURL + } + + override var asynchronous: Bool { + return true + } + + // Every path through `start()` must call `finish()` exactly once. + override func start() { + guard !cancelled else { + finish(.Cancellation) + return + } + + // Load asset properties in the background, to avoid blocking the caller with synchronous I/O. + asset.loadValuesAsynchronouslyForKeys(["tracks"]) { + guard !self.cancelled else { + self.finish(.Cancellation) + return + } + + // These are all initialized in the below 'do' block, assuming no errors are thrown. + let assetReader: AVAssetReader + let assetWriter: AVAssetWriter + let videoReaderOutputsAndWriterInputs: [ReaderOutputAndWriterInput] + let passthroughReaderOutputsAndWriterInputs: [ReaderOutputAndWriterInput] + + do { + // Make sure that the asset tracks loaded successfully. + + var trackLoadingError: NSError? + guard self.asset.statusOfValueForKey("tracks", error: &trackLoadingError) == .Loaded else { + throw trackLoadingError! + } + let tracks = self.asset.tracks + + // Create reader/writer objects. + + assetReader = try AVAssetReader(asset: self.asset) + assetWriter = try AVAssetWriter(URL: self.outputURL, fileType: AVFileTypeQuickTimeMovie) + + let (videoReaderOutputs, passthroughReaderOutputs) = try self.makeReaderOutputsForTracks(tracks, availableMediaTypes: assetWriter.availableMediaTypes) + + videoReaderOutputsAndWriterInputs = try self.makeVideoWriterInputsForVideoReaderOutputs(videoReaderOutputs) + passthroughReaderOutputsAndWriterInputs = try self.makePassthroughWriterInputsForPassthroughReaderOutputs(passthroughReaderOutputs) + + // Hook everything up. + + for (readerOutput, writerInput) in videoReaderOutputsAndWriterInputs { + assetReader.addOutput(readerOutput) + assetWriter.addInput(writerInput) + } + + for (readerOutput, writerInput) in passthroughReaderOutputsAndWriterInputs { + assetReader.addOutput(readerOutput) + assetWriter.addInput(writerInput) + } + + /* + Remove file if necessary. AVAssetWriter will not overwrite + an existing file. + */ + + let fileManager = NSFileManager() + if let outputPath = self.outputURL.path where fileManager.fileExistsAtPath(outputPath) { + try fileManager.removeItemAtURL(self.outputURL) + } + + // Start reading/writing. + + guard assetReader.startReading() else { + // `error` is non-nil when startReading returns false. + throw assetReader.error! + } + + guard assetWriter.startWriting() else { + // `error` is non-nil when startWriting returns false. + throw assetWriter.error! + } + + assetWriter.startSessionAtSourceTime(kCMTimeZero) + } + catch { + self.finish(.Failure(error)) + return + } + + let writingGroup = dispatch_group_create() + + // Transfer data from input file to output file. + self.transferVideoTracks(videoReaderOutputsAndWriterInputs, group: writingGroup) + self.transferPassthroughTracks(passthroughReaderOutputsAndWriterInputs, group: writingGroup) + + // Handle completion. + let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + + dispatch_group_notify(writingGroup, queue) { + // `readingAndWritingDidFinish()` is guaranteed to call `finish()` exactly once. + self.readingAndWritingDidFinish(assetReader, assetWriter: assetWriter) + } + } + } + + /** + A type used for correlating an `AVAssetWriterInput` with the `AVAssetReaderOutput` + that is the source of appended samples. + */ + private typealias ReaderOutputAndWriterInput = (readerOutput: AVAssetReaderOutput, writerInput: AVAssetWriterInput) + + private func makeReaderOutputsForTracks(tracks: [AVAssetTrack], availableMediaTypes: [String]) throws -> (videoReaderOutputs: [AVAssetReaderTrackOutput], passthroughReaderOutputs: [AVAssetReaderTrackOutput]) { + // Decompress source video to 32ARGB. + let videoDecompressionSettings: [String: AnyObject] = [ + String(kCVPixelBufferPixelFormatTypeKey): NSNumber(unsignedInt: kCVPixelFormatType_32ARGB), + String(kCVPixelBufferIOSurfacePropertiesKey): [:] + ] + + // Partition tracks into "video" and "passthrough" buckets, create reader outputs. + + var videoReaderOutputs = [AVAssetReaderTrackOutput]() + var passthroughReaderOutputs = [AVAssetReaderTrackOutput]() + + for track in tracks { + guard availableMediaTypes.contains(track.mediaType) else { continue } + + switch track.mediaType { + case AVMediaTypeVideo: + let videoReaderOutput = AVAssetReaderTrackOutput(track: track, outputSettings: videoDecompressionSettings) + videoReaderOutputs += [videoReaderOutput] + + default: + // `nil` output settings means "passthrough." + let passthroughReaderOutput = AVAssetReaderTrackOutput(track: track, outputSettings: nil) + passthroughReaderOutputs += [passthroughReaderOutput] + } + } + + return (videoReaderOutputs, passthroughReaderOutputs) + } + + private func makeVideoWriterInputsForVideoReaderOutputs(videoReaderOutputs: [AVAssetReaderTrackOutput]) throws -> [ReaderOutputAndWriterInput] { + // Compress modified source frames to H.264. + let videoCompressionSettings: [String: AnyObject] = [ + AVVideoCodecKey: AVVideoCodecH264 + ] + + /* + In order to find the source format we need to create a temporary asset + reader, plus a temporary track output for each "real" track output. + We will only read as many samples (typically just one) as necessary + to discover the format of the buffers that will be read from each "real" + track output. + */ + + let tempAssetReader = try AVAssetReader(asset: asset) + + let videoReaderOutputsAndTempVideoReaderOutputs: [(videoReaderOutput: AVAssetReaderTrackOutput, tempVideoReaderOutput: AVAssetReaderTrackOutput)] = videoReaderOutputs.map { videoReaderOutput in + let tempVideoReaderOutput = AVAssetReaderTrackOutput(track: videoReaderOutput.track, outputSettings: videoReaderOutput.outputSettings) + + tempAssetReader.addOutput(tempVideoReaderOutput) + + return (videoReaderOutput, tempVideoReaderOutput) + } + + // Start reading. + + guard tempAssetReader.startReading() else { + // 'error' will be non-nil if startReading fails. + throw tempAssetReader.error! + } + + /* + Create video asset writer inputs, using the source format hints read + from the "temporary" reader outputs. + */ + + var videoReaderOutputsAndWriterInputs = [ReaderOutputAndWriterInput]() + + for (videoReaderOutput, tempVideoReaderOutput) in videoReaderOutputsAndTempVideoReaderOutputs { + // Fetch format of source sample buffers. + + var videoFormatHint: CMFormatDescriptionRef? + + while videoFormatHint == nil { + guard let sampleBuffer = tempVideoReaderOutput.copyNextSampleBuffer() else { + // We ran out of sample buffers before we found one with a format description + throw CyanifyError.NoMediaData + } + + videoFormatHint = CMSampleBufferGetFormatDescription(sampleBuffer) + } + + // Create asset writer input. + + let videoWriterInput = AVAssetWriterInput(mediaType: AVMediaTypeVideo, outputSettings: videoCompressionSettings, sourceFormatHint: videoFormatHint) + + videoReaderOutputsAndWriterInputs.append((readerOutput: videoReaderOutput, writerInput: videoWriterInput)) + } + + // Shut down processing pipelines, since only a subset of the samples were read. + tempAssetReader.cancelReading() + + return videoReaderOutputsAndWriterInputs + } + + private func makePassthroughWriterInputsForPassthroughReaderOutputs(passthroughReaderOutputs: [AVAssetReaderTrackOutput]) throws -> [ReaderOutputAndWriterInput] { + /* + Create passthrough writer inputs, using the source track's format + descriptions as the format hint for each writer input. + */ + + var passthroughReaderOutputsAndWriterInputs = [ReaderOutputAndWriterInput]() + + for passthroughReaderOutput in passthroughReaderOutputs { + /* + For passthrough, we can simply ask the track for its format + description and use that as the writer input's format hint. + */ + let trackFormatDescriptions = passthroughReaderOutput.track.formatDescriptions as! [CMFormatDescriptionRef] + + guard let passthroughFormatHint = trackFormatDescriptions.first else { + throw CyanifyError.NoMediaData + } + + // Create asset writer input with nil (passthrough) output settings + let passthroughWriterInput = AVAssetWriterInput(mediaType: passthroughReaderOutput.mediaType, outputSettings: nil, sourceFormatHint: passthroughFormatHint) + + passthroughReaderOutputsAndWriterInputs.append((readerOutput: passthroughReaderOutput, writerInput: passthroughWriterInput)) + } + + return passthroughReaderOutputsAndWriterInputs + } + + private func transferVideoTracks(videoReaderOutputsAndWriterInputs: [ReaderOutputAndWriterInput], group: dispatch_group_t) { + for (videoReaderOutput, videoWriterInput) in videoReaderOutputsAndWriterInputs { + let perTrackDispatchQueue = dispatch_queue_create("Track data transfer queue: \(videoReaderOutput) -> \(videoWriterInput).", nil) + + // A block for changing color values of each video frame. + let videoProcessor: CMSampleBufferRef throws -> Void = { sampleBuffer in + if let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), + pixelBuffer: CVPixelBufferRef = imageBuffer + where CFGetTypeID(imageBuffer) == CVPixelBufferGetTypeID() { + + let redComponentIndex = 1 + try pixelBuffer.removeARGBColorComponentAtIndex(redComponentIndex) + } + } + + dispatch_group_enter(group) + transferSamplesAsynchronouslyFromReaderOutput(videoReaderOutput, toWriterInput: videoWriterInput, onQueue: perTrackDispatchQueue, sampleBufferProcessor: videoProcessor) { + dispatch_group_leave(group) + } + } + } + + private func transferPassthroughTracks(passthroughReaderOutputsAndWriterInputs: [ReaderOutputAndWriterInput], group: dispatch_group_t) { + for (passthroughReaderOutput, passthroughWriterInput) in passthroughReaderOutputsAndWriterInputs { + let perTrackDispatchQueue = dispatch_queue_create("Track data transfer queue: \(passthroughReaderOutput) -> \(passthroughWriterInput).", nil) + + dispatch_group_enter(group) + transferSamplesAsynchronouslyFromReaderOutput(passthroughReaderOutput, toWriterInput: passthroughWriterInput, onQueue: perTrackDispatchQueue) { + dispatch_group_leave(group) + } + } + } + + private func transferSamplesAsynchronouslyFromReaderOutput(readerOutput: AVAssetReaderOutput, toWriterInput writerInput: AVAssetWriterInput, onQueue queue: dispatch_queue_t, sampleBufferProcessor: ((sampleBuffer: CMSampleBufferRef) throws -> Void)? = nil, completionHandler: Void -> Void) { + + // Provide the asset writer input with a block to invoke whenever it wants to request more samples + + writerInput.requestMediaDataWhenReadyOnQueue(queue) { + var isDone = false + + /* + Loop, transferring one sample per iteration, until the asset writer + input has enough samples. At that point, exit the callback block + and the asset writer input will invoke the block again when it + needs more samples. + */ + while writerInput.readyForMoreMediaData { + guard !self.cancelled else { + isDone = true + break + } + + // Grab next sample from the asset reader output. + guard let sampleBuffer = readerOutput.copyNextSampleBuffer() else { + /* + At this point, the asset reader output has no more samples + to vend. + */ + isDone = true + break + } + + // Process the sample, if requested. + do { + try sampleBufferProcessor?(sampleBuffer: sampleBuffer) + } + catch { + // This error will be picked back up in `readingAndWritingDidFinish()`. + self.sampleTransferError = error + isDone = true + } + + // Append the sample to the asset writer input. + guard writerInput.appendSampleBuffer(sampleBuffer) else { + /* + The sample buffer could not be appended. Error information + will be fetched from the asset writer in + `readingAndWritingDidFinish()`. + */ + isDone = true + break + } + } + + if isDone { + /* + Calling `markAsFinished()` on the asset writer input will both: + 1. Unblock any other inputs that need more samples. + 2. Cancel further invocations of this "request media data" + callback block. + */ + writerInput.markAsFinished() + + // Tell the caller that we are done transferring samples. + completionHandler() + } + } + } + + private func readingAndWritingDidFinish(assetReader: AVAssetReader, assetWriter: AVAssetWriter) { + if cancelled { + assetReader.cancelReading() + assetWriter.cancelWriting() + } + + // Deal with any error that occurred during processing of the video. + guard sampleTransferError == nil else { + assetReader.cancelReading() + assetWriter.cancelWriting() + finish(.Failure(sampleTransferError!)) + return + } + + // Evaluate result of reading samples. + + guard assetReader.status == .Completed else { + let result: Result + + switch assetReader.status { + case .Cancelled: + assetWriter.cancelWriting() + result = .Cancellation + + case .Failed: + // `error` property is non-nil in the `.Failed` status. + result = .Failure(assetReader.error!) + + default: + fatalError("Unexpected terminal asset reader status: \(assetReader.status).") + } + + finish(result) + + return + } + + // Finish writing, (asynchronously) evaluate result of writing samples. + + assetWriter.finishWritingWithCompletionHandler { + let result: Result + + switch assetWriter.status { + case .Completed: + result = .Success + + case .Cancelled: + result = .Cancellation + + case .Failed: + // `error` property is non-nil in the `.Failed` status. + result = .Failure(assetWriter.error!) + + default: + fatalError("Unexpected terminal asset writer status: \(assetWriter.status).") + } + + self.finish(result) + } + } + + func finish(result: Result) { + self.result = result + } +} + +extension CVPixelBufferRef { + /** + Iterates through each pixel in the receiver (assumed to be in ARGB format) + and overwrites the color component at the given index with a zero. This + has the effect of "cyanifying," "rosifying," etc (depending on the chosen + color component) the overall image represented by the pixel buffer. + */ + func removeARGBColorComponentAtIndex(componentIndex: size_t) throws { + let lockBaseAddressResult = CVPixelBufferLockBaseAddress(self, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) + + guard lockBaseAddressResult == kCVReturnSuccess else { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(lockBaseAddressResult), userInfo: nil) + } + + let bufferHeight = CVPixelBufferGetHeight(self) + + let bufferWidth = CVPixelBufferGetWidth(self) + + let bytesPerRow = CVPixelBufferGetBytesPerRow(self) + + let bytesPerPixel = bytesPerRow / bufferWidth + + let base = UnsafeMutablePointer(CVPixelBufferGetBaseAddress(self)) + + // For each pixel, zero out selected color component. + for row in 0.. = base + (row * bytesPerRow) + (column * bytesPerPixel) + pixel[componentIndex] = 0 + } + } + + let unlockBaseAddressResult = CVPixelBufferUnlockBaseAddress(self, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) + + guard unlockBaseAddressResult == kCVReturnSuccess else { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(unlockBaseAddressResult), userInfo: nil) + } + } +} diff --git a/AVReaderWriter/Swift/AVReaderWriter/Info.plist b/AVReaderWriter/Swift/AVReaderWriter/Info.plist new file mode 100644 index 00000000..40c6215d --- /dev/null +++ b/AVReaderWriter/Swift/AVReaderWriter/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/AVReaderWriter/Swift/AVReaderWriter/ProgressViewController.swift b/AVReaderWriter/Swift/AVReaderWriter/ProgressViewController.swift new file mode 100644 index 00000000..df289608 --- /dev/null +++ b/AVReaderWriter/Swift/AVReaderWriter/ProgressViewController.swift @@ -0,0 +1,105 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Defines the view controller for the progress scene. +*/ + +import UIKit + +class ProgressViewController: UIViewController { + // MARK: Properties + + var sourceURL: NSURL? + + var outputURL: NSURL? + + lazy var operationQueue: NSOperationQueue = { + let operationQueue = NSOperationQueue() + + operationQueue.name = "com.example.apple-samplecode.progressviewcontroller.operationQueue" + + return operationQueue + }() + + weak var cyanifier: CyanifyOperation? + + static let finishingSegueName = "finishing" + + static let cancelSegueName = "cancel" + + // MARK: IBActions + + @IBAction func cancel() { + cyanifier?.cancel() + } + + // MARK: View Controller + + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + + guard let outputURL = outputURL, sourceURL = sourceURL else { + fatalError("`outputURL` and `sourceURL` should not be nil when \(#function) is called.") + } + + // Create video processing operation and add it to our operation queue. + + let cyanifier = CyanifyOperation(sourceURL: sourceURL, outputURL: outputURL) + + cyanifier.completionBlock = { [weak cyanifier] in + /* + Operation must still be alive when it invokes its completion handler. + It also must have set a non-nil result by the time it finishes. + */ + let result = cyanifier!.result! + + dispatch_async(dispatch_get_main_queue()) { + self.cyanificationDidFinish(result) + } + } + + operationQueue.addOperation(cyanifier) + + self.cyanifier = cyanifier + } + + private func cyanificationDidFinish(result: CyanifyOperation.Result) { + switch result { + case .Success: + performSegueWithIdentifier(ProgressViewController.finishingSegueName, sender: self) + + case .Failure(let error): + presentError(error as NSError) + + case .Cancellation: + performSegueWithIdentifier(ProgressViewController.cancelSegueName, sender: self) + } + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if segue.identifier == ProgressViewController.finishingSegueName { + let nextViewController = segue.destinationViewController as! ResultViewController + + nextViewController.outputURL = outputURL + } + } + + /// Present an `NSError` to the user. + func presentError(error: NSError) { + let failureTitle = error.localizedDescription + + let failureMessage = error.localizedRecoverySuggestion ?? error.localizedFailureReason + + let alertController = UIAlertController(title: failureTitle, message: failureMessage, preferredStyle: .Alert) + + let alertAction = UIAlertAction(title: "OK", style: .Default) { _ in + self.performSegueWithIdentifier("error", sender: self) + } + + alertController.addAction(alertAction) + + presentViewController(alertController, animated: true, completion: nil) + } +} diff --git a/AVReaderWriter/Swift/AVReaderWriter/ResultViewController.swift b/AVReaderWriter/Swift/AVReaderWriter/ResultViewController.swift new file mode 100644 index 00000000..4d4913ad --- /dev/null +++ b/AVReaderWriter/Swift/AVReaderWriter/ResultViewController.swift @@ -0,0 +1,50 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Defines the view controller for the result scene. +*/ + +import UIKit +import AVKit +import AVFoundation + +class ResultViewController: UIViewController { + // MARK: Properties + + private static let embedSegueName = "playerViewController" + + let player = AVPlayer() + + var outputURL: NSURL? { + // Update `playerViewController` with new output movie. + didSet { + let playerItem: AVPlayerItem? + + if let outputURL = outputURL { + playerItem = AVPlayerItem(URL: outputURL) + } + else { + playerItem = nil + } + + player.replaceCurrentItemWithPlayerItem(playerItem) + } + } + + // MARK: Segue Handling + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if segue.identifier == ResultViewController.embedSegueName { + // This segue fires before `viewDidLoad()` is invoked. + let playerViewController = segue.destinationViewController as! AVPlayerViewController + + playerViewController.player = player + } + else { + // Stop playback when transitioning to next scene. + player.pause() + } + } +} diff --git a/AVReaderWriter/Swift/AVReaderWriter/StartViewController.swift b/AVReaderWriter/Swift/AVReaderWriter/StartViewController.swift new file mode 100644 index 00000000..9c44c588 --- /dev/null +++ b/AVReaderWriter/Swift/AVReaderWriter/StartViewController.swift @@ -0,0 +1,77 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Defines the view controller for the default scene. +*/ + +import AVFoundation +import AVKit + +class StartViewController: UIViewController { + // MARK: Properties + + @IBOutlet weak var startButton: UIButton! + let player = AVPlayer() + + var sourceURL: NSURL? { + // Update `playerViewController` with new source movie. + didSet { + let playerItem: AVPlayerItem? + + if let sourceURL = sourceURL { + playerItem = AVPlayerItem(URL: sourceURL) + + startButton.enabled = true + } + else { + playerItem = nil + + startButton.enabled = false + } + + player.replaceCurrentItemWithPlayerItem(playerItem) + } + } + + let defaultSourceURL = NSBundle.mainBundle().URLForResource("ElephantSeals", withExtension: "mov")! + + let outputURL = NSURL(fileURLWithPath: NSTemporaryDirectory() + "out.mov") + + static let embedSegueName = "playerViewController" + + // MARK: View Controller + + override func viewDidLoad() { + super.viewDidLoad() + + // Default to disabled, until we establish a source. + startButton.enabled = false + + // Establish a source. + sourceURL = defaultSourceURL + } + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if segue.identifier == StartViewController.embedSegueName { + // This segue fires before `viewDidLoad()` is invoked. + let playerViewController = segue.destinationViewController as! AVPlayerViewController + + playerViewController.player = player + } + else { + // Stop playback when transitioning to next scene. + player.pause() + + let nextViewController = segue.destinationViewController as! ProgressViewController + + /* + We cannot get here if there is no source, because `startButton` + will be disabled. + */ + nextViewController.sourceURL = sourceURL + nextViewController.outputURL = outputURL + } + } +} diff --git a/AVReaderWriter/Swift/Resources/ElephantSeals.mov b/AVReaderWriter/Swift/Resources/ElephantSeals.mov new file mode 100644 index 00000000..69641555 Binary files /dev/null and b/AVReaderWriter/Swift/Resources/ElephantSeals.mov differ diff --git a/Badger/Badger.xcodeproj/project.pbxproj b/Badger/Badger.xcodeproj/project.pbxproj new file mode 100644 index 00000000..f51d398b --- /dev/null +++ b/Badger/Badger.xcodeproj/project.pbxproj @@ -0,0 +1,644 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 94683CD71CF487E600DFC25C /* BobHUD.png in Resources */ = {isa = PBXBuildFile; fileRef = 94683CD61CF487E600DFC25C /* BobHUD.png */; }; + 94683CD81CF487E600DFC25C /* BobHUD.png in Resources */ = {isa = PBXBuildFile; fileRef = 94683CD61CF487E600DFC25C /* BobHUD.png */; }; + 94683CD91CF487E600DFC25C /* BobHUD.png in Resources */ = {isa = PBXBuildFile; fileRef = 94683CD61CF487E600DFC25C /* BobHUD.png */; }; + 9496D5151CFDC6D40077931D /* launchScreen.png in Resources */ = {isa = PBXBuildFile; fileRef = 9496D5141CFDC6D40077931D /* launchScreen.png */; }; + 9C07A2461CDA0419007AA11E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C07A2451CDA0419007AA11E /* AppDelegate.swift */; }; + 9C07A2481CDA0419007AA11E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C07A2471CDA0419007AA11E /* ViewController.swift */; }; + 9C07A24B1CDA0419007AA11E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9C07A2491CDA0419007AA11E /* Main.storyboard */; }; + 9C07A24D1CDA0419007AA11E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9C07A24C1CDA0419007AA11E /* Assets.xcassets */; }; + 9C07A2501CDA0419007AA11E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9C07A24E1CDA0419007AA11E /* LaunchScreen.storyboard */; }; + 9C07A2611CDA0606007AA11E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C07A2601CDA0606007AA11E /* AppDelegate.swift */; }; + 9C07A2651CDA0606007AA11E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9C07A2641CDA0606007AA11E /* Assets.xcassets */; }; + 9C07A2681CDA0606007AA11E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9C07A2661CDA0606007AA11E /* Main.storyboard */; }; + 9C07A26D1CDA0650007AA11E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C07A2471CDA0419007AA11E /* ViewController.swift */; }; + 9C07A2751CDA067B007AA11E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C07A2741CDA067B007AA11E /* AppDelegate.swift */; }; + 9C07A27A1CDA067B007AA11E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9C07A2781CDA067B007AA11E /* Main.storyboard */; }; + 9C07A27C1CDA067B007AA11E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9C07A27B1CDA067B007AA11E /* Assets.xcassets */; }; + 9C07A2811CDA068A007AA11E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C07A2471CDA0419007AA11E /* ViewController.swift */; }; + 9C50E6791CE4998A00CC3490 /* CAAnimation+SceneName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C50E6781CE4998A00CC3490 /* CAAnimation+SceneName.swift */; }; + 9C50E67A1CE4998A00CC3490 /* CAAnimation+SceneName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C50E6781CE4998A00CC3490 /* CAAnimation+SceneName.swift */; }; + 9C50E67B1CE4998A00CC3490 /* CAAnimation+SceneName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C50E6781CE4998A00CC3490 /* CAAnimation+SceneName.swift */; }; + 9C50E67D1CE49C6100CC3490 /* ViewController+Controls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C50E67C1CE49C6100CC3490 /* ViewController+Controls.swift */; }; + 9C50E67E1CE49C6100CC3490 /* ViewController+Controls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C50E67C1CE49C6100CC3490 /* ViewController+Controls.swift */; }; + 9C50E67F1CE49C6100CC3490 /* ViewController+Controls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C50E67C1CE49C6100CC3490 /* ViewController+Controls.swift */; }; + 9C50E6811CE4B36F00CC3490 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C50E6801CE4B36F00CC3490 /* View.swift */; }; + 9C50E6821CE4B36F00CC3490 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C50E6801CE4B36F00CC3490 /* View.swift */; }; + 9C50E6831CE4B36F00CC3490 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C50E6801CE4B36F00CC3490 /* View.swift */; }; + 9C9C489A1CDA6897003283A6 /* SceneKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C9C48991CDA6897003283A6 /* SceneKit.framework */; }; + 9CBEC97D1CDA33D400372C21 /* badger.scnassets in Resources */ = {isa = PBXBuildFile; fileRef = 9CBEC97C1CDA33D400372C21 /* badger.scnassets */; }; + 9CBEC97E1CDA33D400372C21 /* badger.scnassets in Resources */ = {isa = PBXBuildFile; fileRef = 9CBEC97C1CDA33D400372C21 /* badger.scnassets */; }; + 9CBEC97F1CDA33D400372C21 /* badger.scnassets in Resources */ = {isa = PBXBuildFile; fileRef = 9CBEC97C1CDA33D400372C21 /* badger.scnassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 94683CD61CF487E600DFC25C /* BobHUD.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = BobHUD.png; sourceTree = ""; }; + 9496D5141CFDC6D40077931D /* launchScreen.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = launchScreen.png; sourceTree = ""; }; + 9C07A2421CDA0419007AA11E /* Badger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Badger.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9C07A2451CDA0419007AA11E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 9C07A2471CDA0419007AA11E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 9C07A24A1CDA0419007AA11E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 9C07A24C1CDA0419007AA11E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 9C07A24F1CDA0419007AA11E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 9C07A2511CDA0419007AA11E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9C07A25E1CDA0606007AA11E /* Badger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Badger.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9C07A2601CDA0606007AA11E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 9C07A2641CDA0606007AA11E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 9C07A2671CDA0606007AA11E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 9C07A2691CDA0606007AA11E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9C07A2721CDA067B007AA11E /* Badger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Badger.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9C07A2741CDA067B007AA11E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 9C07A2791CDA067B007AA11E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 9C07A27B1CDA067B007AA11E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 9C07A27D1CDA067B007AA11E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9C50E6781CE4998A00CC3490 /* CAAnimation+SceneName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CAAnimation+SceneName.swift"; sourceTree = ""; }; + 9C50E67C1CE49C6100CC3490 /* ViewController+Controls.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ViewController+Controls.swift"; sourceTree = ""; }; + 9C50E6801CE4B36F00CC3490 /* View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; + 9C9C48991CDA6897003283A6 /* SceneKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SceneKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.0.sdk/System/Library/Frameworks/SceneKit.framework; sourceTree = DEVELOPER_DIR; }; + 9CBEC97C1CDA33D400372C21 /* badger.scnassets */ = {isa = PBXFileReference; lastKnownFileType = wrapper.scnassets; path = badger.scnassets; sourceTree = ""; }; + C8BA56AD1D0750610053F425 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 9C07A23F1CDA0419007AA11E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9C9C489A1CDA6897003283A6 /* SceneKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9C07A25B1CDA0606007AA11E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9C07A26F1CDA067B007AA11E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 945D98F91CF324F600F2ED9C /* overlays */ = { + isa = PBXGroup; + children = ( + 94683CD61CF487E600DFC25C /* BobHUD.png */, + ); + path = overlays; + sourceTree = ""; + }; + 9C07A2391CDA0418007AA11E = { + isa = PBXGroup; + children = ( + C8BA56AD1D0750610053F425 /* README.md */, + 9C07A2821CDA07C6007AA11E /* Game assets */, + 9C07A2591CDA05C6007AA11E /* Common */, + 9C07A2441CDA0419007AA11E /* iOS */, + 9C07A2731CDA067B007AA11E /* tvOS */, + 9C07A25F1CDA0606007AA11E /* macOS */, + 9C07A2431CDA0419007AA11E /* Products */, + ); + sourceTree = ""; + }; + 9C07A2431CDA0419007AA11E /* Products */ = { + isa = PBXGroup; + children = ( + 9C07A2421CDA0419007AA11E /* Badger.app */, + 9C07A25E1CDA0606007AA11E /* Badger.app */, + 9C07A2721CDA067B007AA11E /* Badger.app */, + ); + name = Products; + sourceTree = ""; + }; + 9C07A2441CDA0419007AA11E /* iOS */ = { + isa = PBXGroup; + children = ( + 9C07A2451CDA0419007AA11E /* AppDelegate.swift */, + 9C07A2491CDA0419007AA11E /* Main.storyboard */, + 9C07A24C1CDA0419007AA11E /* Assets.xcassets */, + 9C07A24E1CDA0419007AA11E /* LaunchScreen.storyboard */, + 9496D5141CFDC6D40077931D /* launchScreen.png */, + 9C07A2511CDA0419007AA11E /* Info.plist */, + 9C9C48981CDA6896003283A6 /* Frameworks */, + ); + path = iOS; + sourceTree = ""; + }; + 9C07A2591CDA05C6007AA11E /* Common */ = { + isa = PBXGroup; + children = ( + 9C50E6801CE4B36F00CC3490 /* View.swift */, + 9C07A2471CDA0419007AA11E /* ViewController.swift */, + 9C50E67C1CE49C6100CC3490 /* ViewController+Controls.swift */, + 9C50E6781CE4998A00CC3490 /* CAAnimation+SceneName.swift */, + ); + path = Common; + sourceTree = ""; + }; + 9C07A25F1CDA0606007AA11E /* macOS */ = { + isa = PBXGroup; + children = ( + 9C07A2601CDA0606007AA11E /* AppDelegate.swift */, + 9C07A2641CDA0606007AA11E /* Assets.xcassets */, + 9C07A2661CDA0606007AA11E /* Main.storyboard */, + 9C07A2691CDA0606007AA11E /* Info.plist */, + ); + path = macOS; + sourceTree = ""; + }; + 9C07A2731CDA067B007AA11E /* tvOS */ = { + isa = PBXGroup; + children = ( + 9C07A2741CDA067B007AA11E /* AppDelegate.swift */, + 9C07A2781CDA067B007AA11E /* Main.storyboard */, + 9C07A27B1CDA067B007AA11E /* Assets.xcassets */, + 9C07A27D1CDA067B007AA11E /* Info.plist */, + ); + path = tvOS; + sourceTree = ""; + }; + 9C07A2821CDA07C6007AA11E /* Game assets */ = { + isa = PBXGroup; + children = ( + 945D98F91CF324F600F2ED9C /* overlays */, + 9CBEC97C1CDA33D400372C21 /* badger.scnassets */, + ); + path = "Game assets"; + sourceTree = ""; + }; + 9C9C48981CDA6896003283A6 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9C9C48991CDA6897003283A6 /* SceneKit.framework */, + ); + name = Frameworks; + path = ..; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 9C07A2411CDA0419007AA11E /* Badger iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9C07A2541CDA0419007AA11E /* Build configuration list for PBXNativeTarget "Badger iOS" */; + buildPhases = ( + 9C07A23E1CDA0419007AA11E /* Sources */, + 9C07A23F1CDA0419007AA11E /* Frameworks */, + 9C07A2401CDA0419007AA11E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Badger iOS"; + productName = Badger; + productReference = 9C07A2421CDA0419007AA11E /* Badger.app */; + productType = "com.apple.product-type.application"; + }; + 9C07A25D1CDA0606007AA11E /* Badger macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9C07A26A1CDA0606007AA11E /* Build configuration list for PBXNativeTarget "Badger macOS" */; + buildPhases = ( + 9C07A25A1CDA0606007AA11E /* Sources */, + 9C07A25B1CDA0606007AA11E /* Frameworks */, + 9C07A25C1CDA0606007AA11E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Badger macOS"; + productName = "Badger OS X"; + productReference = 9C07A25E1CDA0606007AA11E /* Badger.app */; + productType = "com.apple.product-type.application"; + }; + 9C07A2711CDA067B007AA11E /* Badger tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9C07A27E1CDA067B007AA11E /* Build configuration list for PBXNativeTarget "Badger tvOS" */; + buildPhases = ( + 9C07A26E1CDA067B007AA11E /* Sources */, + 9C07A26F1CDA067B007AA11E /* Frameworks */, + 9C07A2701CDA067B007AA11E /* Resources */, + 9419ACA41CFC84CF00725765 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Badger tvOS"; + productName = "Badger tvOS"; + productReference = 9C07A2721CDA067B007AA11E /* Badger.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 9C07A23A1CDA0418007AA11E /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Apple Inc."; + TargetAttributes = { + 9C07A2411CDA0419007AA11E = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + 9C07A25D1CDA0606007AA11E = { + CreatedOnToolsVersion = 8.0; + LastSwiftMigration = 0800; + ProvisioningStyle = Automatic; + }; + 9C07A2711CDA067B007AA11E = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 9C07A23D1CDA0418007AA11E /* Build configuration list for PBXProject "Badger" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 9C07A2391CDA0418007AA11E; + productRefGroup = 9C07A2431CDA0419007AA11E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 9C07A2411CDA0419007AA11E /* Badger iOS */, + 9C07A2711CDA067B007AA11E /* Badger tvOS */, + 9C07A25D1CDA0606007AA11E /* Badger macOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 9C07A2401CDA0419007AA11E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9C07A2501CDA0419007AA11E /* LaunchScreen.storyboard in Resources */, + 9C07A24D1CDA0419007AA11E /* Assets.xcassets in Resources */, + 9496D5151CFDC6D40077931D /* launchScreen.png in Resources */, + 94683CD71CF487E600DFC25C /* BobHUD.png in Resources */, + 9CBEC97D1CDA33D400372C21 /* badger.scnassets in Resources */, + 9C07A24B1CDA0419007AA11E /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9C07A25C1CDA0606007AA11E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9C07A2651CDA0606007AA11E /* Assets.xcassets in Resources */, + 9C07A2681CDA0606007AA11E /* Main.storyboard in Resources */, + 9CBEC97F1CDA33D400372C21 /* badger.scnassets in Resources */, + 94683CD91CF487E600DFC25C /* BobHUD.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9C07A2701CDA067B007AA11E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9C07A27C1CDA067B007AA11E /* Assets.xcassets in Resources */, + 9C07A27A1CDA067B007AA11E /* Main.storyboard in Resources */, + 9CBEC97E1CDA33D400372C21 /* badger.scnassets in Resources */, + 94683CD81CF487E600DFC25C /* BobHUD.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 9419ACA41CFC84CF00725765 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"Cleaning resources before signing.\"\nfind \"$CODESIGNING_FOLDER_PATH\" -type f -print0 | xargs -0 -J % xattr -d com.apple.FinderInfo %\nfind \"$CODESIGNING_FOLDER_PATH\" -name '.DS_Store' -print0 | xargs -0 -J % rm -rf \"%\""; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 9C07A23E1CDA0419007AA11E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9C07A2481CDA0419007AA11E /* ViewController.swift in Sources */, + 9C07A2461CDA0419007AA11E /* AppDelegate.swift in Sources */, + 9C50E6811CE4B36F00CC3490 /* View.swift in Sources */, + 9C50E6791CE4998A00CC3490 /* CAAnimation+SceneName.swift in Sources */, + 9C50E67D1CE49C6100CC3490 /* ViewController+Controls.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9C07A25A1CDA0606007AA11E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9C07A26D1CDA0650007AA11E /* ViewController.swift in Sources */, + 9C07A2611CDA0606007AA11E /* AppDelegate.swift in Sources */, + 9C50E6831CE4B36F00CC3490 /* View.swift in Sources */, + 9C50E67B1CE4998A00CC3490 /* CAAnimation+SceneName.swift in Sources */, + 9C50E67F1CE49C6100CC3490 /* ViewController+Controls.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9C07A26E1CDA067B007AA11E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9C07A2811CDA068A007AA11E /* ViewController.swift in Sources */, + 9C07A2751CDA067B007AA11E /* AppDelegate.swift in Sources */, + 9C50E6821CE4B36F00CC3490 /* View.swift in Sources */, + 9C50E67A1CE4998A00CC3490 /* CAAnimation+SceneName.swift in Sources */, + 9C50E67E1CE49C6100CC3490 /* ViewController+Controls.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 9C07A2491CDA0419007AA11E /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 9C07A24A1CDA0419007AA11E /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 9C07A24E1CDA0419007AA11E /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 9C07A24F1CDA0419007AA11E /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 9C07A2661CDA0606007AA11E /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 9C07A2671CDA0606007AA11E /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 9C07A2781CDA067B007AA11E /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 9C07A2791CDA067B007AA11E /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 9C07A2521CDA0419007AA11E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = lossless; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_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 = ""; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 9C07A2531CDA0419007AA11E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = "respect-asset-catalog"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_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 = ""; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 9C07A2551CDA0419007AA11E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Badger"; + PRODUCT_NAME = Badger; + SDKROOT = iphoneos; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 9C07A2561CDA0419007AA11E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = iOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Badger"; + PRODUCT_NAME = Badger; + SDKROOT = iphoneos; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + 9C07A26B1CDA0606007AA11E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = macOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.12; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Badger"; + PRODUCT_NAME = Badger; + SDKROOT = macosx; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 9C07A26C1CDA0606007AA11E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = macOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.12; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Badger"; + PRODUCT_NAME = Badger; + SDKROOT = macosx; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + 9C07A27F1CDA067B007AA11E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = tvOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Badger"; + PRODUCT_NAME = Badger; + SDKROOT = appletvos; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Debug; + }; + 9C07A2801CDA067B007AA11E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = tvOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Badger"; + PRODUCT_NAME = Badger; + SDKROOT = appletvos; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 9C07A23D1CDA0418007AA11E /* Build configuration list for PBXProject "Badger" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9C07A2521CDA0419007AA11E /* Debug */, + 9C07A2531CDA0419007AA11E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9C07A2541CDA0419007AA11E /* Build configuration list for PBXNativeTarget "Badger iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9C07A2551CDA0419007AA11E /* Debug */, + 9C07A2561CDA0419007AA11E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9C07A26A1CDA0606007AA11E /* Build configuration list for PBXNativeTarget "Badger macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9C07A26B1CDA0606007AA11E /* Debug */, + 9C07A26C1CDA0606007AA11E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9C07A27E1CDA067B007AA11E /* Build configuration list for PBXNativeTarget "Badger tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9C07A27F1CDA067B007AA11E /* Debug */, + 9C07A2801CDA067B007AA11E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 9C07A23A1CDA0418007AA11E /* Project object */; +} diff --git a/Badger/Common/CAAnimation+SceneName.swift b/Badger/Common/CAAnimation+SceneName.swift new file mode 100644 index 00000000..2fb6243b --- /dev/null +++ b/Badger/Common/CAAnimation+SceneName.swift @@ -0,0 +1,36 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An extension on CAAnimation used to load animations from an SCNScene. + */ + +import SceneKit + +// MARK: Core Animation + +extension CAAnimation { + class func animation(withSceneName name: String) -> CAAnimation { + guard let scene = SCNScene(named: name) else { + fatalError("Failed to find scene with name \(name).") + } + + var animation: CAAnimation? + scene.rootNode.enumerateChildNodes { (child, stop) in + guard let firstKey = child.animationKeys.first else { return } + animation = child.animation(forKey: firstKey) + stop.initialize(to: true) + } + + guard let foundAnimation = animation else { + fatalError("Failed to find animation named \(name).") + } + + foundAnimation.fadeInDuration = 0.3 + foundAnimation.fadeOutDuration = 0.3 + foundAnimation.repeatCount = 1 + + return foundAnimation + } +} diff --git a/Badger/Common/View.swift b/Badger/Common/View.swift new file mode 100644 index 00000000..abb8cff2 --- /dev/null +++ b/Badger/Common/View.swift @@ -0,0 +1,106 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An SCNView used to relay keyboard controls on OSX, and present + setup the 2D overlay. + */ + +import GameKit + +class View: SCNView { + + // MARK: Mouse and Keyboard Events + + #if os(OSX) + var eventsDelegate: KeyboardEventsDelegate? + + override func keyDown(with event: NSEvent) { + guard let eventsDelegate = eventsDelegate, eventsDelegate.keyDown(in: self, with: event) else { + super.keyDown(with: event) + return + } + } + + override func keyUp(with event: NSEvent) { + guard let eventsDelegate = eventsDelegate, eventsDelegate.keyUp(in: self, with: event) else { + super.keyUp(with: event) + return + } + } + #endif + + // Resizing + + #if os(iOS) || os(tvOS) + override func layoutSubviews() { + super.layoutSubviews() + update2DOverlays() + } + #elseif os(OSX) + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + update2DOverlays() + } + #endif + + // MARK: Overlays + + private let _overlayNode = SKNode() + private let _scaleNode = SKNode() + private let _collectedItemsCountLabel = SKLabelNode(fontNamed: "Superclarendon") + + private func update2DOverlays() { + _overlayNode.position = CGPoint(x: 0.0, y: bounds.size.height) + } + + func setup2DOverlay() { + let w = bounds.size.width + let h = bounds.size.height + + // Setup the game overlays using SpriteKit. + let skScene = SKScene(size: CGSize(width: w, height: h)) + skScene.scaleMode = .resizeFill + + skScene.addChild(_scaleNode) + _scaleNode.addChild(_overlayNode) + _overlayNode.position = CGPoint(x: 0.0, y: h) + + #if os(OSX) + _scaleNode.xScale = layer!.contentsScale + _scaleNode.yScale = layer!.contentsScale + #endif + + // The Bob icon. + let bobSprite = SKSpriteNode(imageNamed: "BobHUD.png") + bobSprite.position = CGPoint(x: 70, y:-50) + bobSprite.xScale = 0.5 + bobSprite.yScale = 0.5 + _overlayNode.addChild(bobSprite) + + _collectedItemsCountLabel.text = "x0" + _collectedItemsCountLabel.horizontalAlignmentMode = .left + _collectedItemsCountLabel.position = CGPoint(x: 135, y:-63) + _overlayNode.addChild(_collectedItemsCountLabel) + + // Assign the SpriteKit overlay to the SceneKit view. + self.overlaySKScene = skScene + skScene.isUserInteractionEnabled = false + } + + var collectedItemsCount = 0 { + didSet { + _collectedItemsCountLabel.text = "x\(collectedItemsCount)" + } + } + + func didCollectItem() { + collectedItemsCount = collectedItemsCount + 1 + } + + func didCollectBigItem() { + collectedItemsCount = collectedItemsCount + 10 + } + +} diff --git a/Badger/Common/ViewController+Controls.swift b/Badger/Common/ViewController+Controls.swift new file mode 100644 index 00000000..57d14b10 --- /dev/null +++ b/Badger/Common/ViewController+Controls.swift @@ -0,0 +1,127 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Handles keyboard (macOS), touch (iOS) and controller (iOS, tvOS) input for controlling the game. +*/ + +import GameKit + +#if os(OSX) +protocol KeyboardEventsDelegate { + func keyDown(in view: NSView, with event: NSEvent) -> Bool + func keyUp(in view: NSView, with event: NSEvent) -> Bool +} + +private enum KeyboardDirection: UInt16 { + case left = 123 + case right = 124 + case down = 125 + case up = 126 +} + +extension ViewController: KeyboardEventsDelegate {} +#endif + +extension ViewController { + + // MARK: Game Controller Events + + func setupGameControllers() { + #if os(OSX) + sceneView.eventsDelegate = self + #endif + + #if os(iOS) || os(tvOS) + // Gesture recognizers + let directions: [UISwipeGestureRecognizerDirection] = [.right, .left, .up, .down] + for direction in directions { + let gesture = UISwipeGestureRecognizer(target: self, action: #selector(didSwipe)) + gesture.direction = direction + sceneView.addGestureRecognizer(gesture) + } + #endif + } + + @objc func handleControllerDidConnectNotification(_ notification: NSNotification) { + let gameController = notification.object as! GCController + registerCharacterMovementEvents(gameController) + } + + private func registerCharacterMovementEvents(_ gameController: GCController) { + // An analog movement handler for D-pads and thumbsticks. + let movementHandler: GCControllerDirectionPadValueChangedHandler = { [unowned self] dpad, _, _ in + self.controllerDPad = dpad + } + + #if os(tvOS) + + // Apple TV remote + if let microGamepad = gameController.microGamepad { + // Allow the gamepad to handle transposing D-pad values when rotating the controller. + microGamepad.allowsRotation = true + microGamepad.dpad.valueChangedHandler = movementHandler + } + + #endif + + // Gamepad D-pad + if let gamepad = gameController.gamepad { + gamepad.dpad.valueChangedHandler = movementHandler + } + + // Extended gamepad left thumbstick + if let extendedGamepad = gameController.extendedGamepad { + extendedGamepad.leftThumbstick.valueChangedHandler = movementHandler + } + } + + // MARK: Touch Events + + #if os(iOS) || os(tvOS) + func didSwipe(sender: UISwipeGestureRecognizer) { + if startGameIfNeeded() { + return + } + + switch sender.direction { + case UISwipeGestureRecognizerDirection.up: jump() + case UISwipeGestureRecognizerDirection.down: squat() + case UISwipeGestureRecognizerDirection.left: leanLeft() + case UISwipeGestureRecognizerDirection.right: leanRight() + default: break + } + } + #endif + + // MARK: Keyboard Events + + #if os(OSX) + func keyDown(in view: NSView, with event: NSEvent) -> Bool { + if event.isARepeat { + return true + } + + if startGameIfNeeded() { + return true + } + + if let direction = KeyboardDirection(rawValue: event.keyCode) { + switch direction { + case .up: jump() + case .down: squat() + case .left: leanLeft() + case .right: leanRight() + } + return true + } + return false + } + + func keyUp(in view: NSView, with event: NSEvent) -> Bool { + let direction = KeyboardDirection(rawValue: event.keyCode) + return direction != nil ? true : false + } + #endif +} diff --git a/Badger/Common/ViewController.swift b/Badger/Common/ViewController.swift new file mode 100644 index 00000000..9e9040c8 --- /dev/null +++ b/Badger/Common/ViewController.swift @@ -0,0 +1,747 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The main ViewController used to host the scene and + configure gameplay. + */ + +import GameKit +import AVFoundation + +#if os(iOS) || os(tvOS) +typealias BaseViewController = UIViewController +#elseif os(OSX) +typealias BaseViewController = NSViewController +#endif + +class ViewController: BaseViewController, SCNSceneRendererDelegate { + // MARK: Types + + struct Assets { + static let basePath = "badger.scnassets/" + private static let soundsPath = basePath + "sounds/" + + static func sound(named name: String) -> SCNAudioSource { + guard let source = SCNAudioSource(named: soundsPath + name) else { + fatalError("Failed to load audio source \(name).") + } + return source + } + + static func animation(named name: String) -> CAAnimation { + return CAAnimation.animation(withSceneName: basePath + name) + } + + static func scene(named name: String) -> SCNScene { + guard let scene = SCNScene(named: basePath + name) else { + fatalError("Failed to load scene \(name).") + } + return scene + } + } + + struct Trigger { + let position: float3 + let action: (ViewController) -> () + } + + private enum CollectableState: UInt { + case notCollected = 0 + case beingCollected = 2 + } + + private enum GameState: UInt { + case notStarted = 0 + case started = 1 + } + + // MARK: Configuration Properties + + /// Determines if the level uses local sun. + let isUsingLocalSun = true + + /// Determines if audio should be enabled. + let isSoundEnabled = true + + let speedFactor: Float = 1.5 + + // MARK: Scene Properties + + @IBOutlet var sceneView: View! + + let scene = Assets.scene(named: "scene.scn") + + // MARK: Animation Properties + + let character: SCNNode + let idleAnimationOwner: SCNNode + + let cartAnimationName: String + + /** + These animations will be played when the user performs an action + and will temporarily disable the "idle" animation. + */ + + let jumpAnimation = Assets.animation(named: "animation-jump.scn") + let squatAnimation = Assets.animation(named: "animation-squat.scn") + let leanLeftAnimation = Assets.animation(named: "animation-lean-left.scn") + let leanRightAnimation = Assets.animation(named: "animation-lean-right.scn") + let slapAnimation = Assets.animation(named: "animation-slap.scn") + + let leftHand: SCNNode + let rightHand: SCNNode + + var sunTargetRelativeToCamera: SCNVector3 + var sunDirection: SCNVector3 + var sun: SCNNode + + // Sparkles effect + var sparkles: SCNParticleSystem + var stars: SCNParticleSystem + var leftWheelEmitter: SCNNode + var rightWheelEmitter: SCNNode + var headEmitter: SCNNode + var wheels: SCNNode + + // Collect particles + var collectParticleSystem: SCNParticleSystem + var collectBigParticleSystem: SCNParticleSystem + + // State + var squatCounter = 0 + var isOverWood = false + + // MARK: Sound Properties + + var railSoundSpeed: UInt = 0 + + let hitSound = Assets.sound(named: "hit.mp3") + let railHighSpeedSound = Assets.sound(named: "rail_highspeed_loop.mp3") + let railMediumSpeedSound = Assets.sound(named: "rail_normalspeed_loop.mp3") + let railLowSpeedSound = Assets.sound(named: "rail_slowspeed_loop.mp3") + let railWoodSound = Assets.sound(named: "rail_wood_loop.mp3") + let railSqueakSound = Assets.sound(named: "cart_turn_squeak.mp3") + let cartHide = Assets.sound(named: "cart_hide.mp3") + let cartJump = Assets.sound(named: "cart_jump.mp3") + let cartTurnLeft = Assets.sound(named: "cart_turn_left.mp3") + let cartTurnRight = Assets.sound(named: "cart_turn_right.mp3") + let cartBoost = Assets.sound(named: "cart_boost.mp3") + + // MARK: Collectable Properties + + let collectables: SCNNode + let speedItems: SCNNode + let collectSound = Assets.sound(named: "collect1.mp3") + let collectSound2 = Assets.sound(named: "collect2.mp3") + + // MARK: Triggers + + /// Triggers are configured in `configureScene()`. + var triggers = [Trigger]() + var activeTriggerIndex = -1 + + // MARK: Game controls + + var controllerDPad: GCControllerDirectionPad? + + /// Game state + private var gameState: GameState = .notStarted + + // MARK: View Controller Initialization + + #if os(iOS) || os(tvOS) + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + fatalError("init(coder:) has not been implemented") + } + #elseif os(OSX) + override init?(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + fatalError("init(coder:) has not been implemented") + } + #endif + + required init?(coder: NSCoder) { + // Retrieve the character and its animations. + + // The character node "Bob_root" initially is a placeholder. + // We will load the models from one of the animation scenes and add them to the empty node. + character = scene.rootNode.childNode(withName: "Bob_root", recursively: true)! + + let idleScene = Assets.scene(named: "animation-idle.scn") + let characterHierarchy = idleScene.rootNode.childNode(withName: "Bob_root", recursively: true)! + + for node in characterHierarchy.childNodes { + character.addChildNode(node) + } + + idleAnimationOwner = character.childNode(withName: "Dummy_kart_root", recursively: true)! + + // The animation for the cart is always running. The name of the animation is retrieved + // so that we can change its speed as the cart accelerates or decelerates. + cartAnimationName = scene.rootNode.animationKeys.first! + + // Play character idle animation. + let idleAnimation = Assets.animation(named: "animation-start-idle.scn") + idleAnimation.repeatCount = Float.infinity + character.addAnimation(idleAnimation, forKey: "start") + + // Load sparkles. + let sparkleScene = Assets.scene(named: "sparkles.scn") + let sparkleNode = sparkleScene.rootNode.childNode(withName: "sparkles", recursively: true)! + sparkles = sparkleNode.particleSystems![0] + sparkles.loops = false + + let starsNode = sparkleScene.rootNode.childNode(withName: "slap", recursively: true)! + stars = starsNode.particleSystems![0] + stars.loops = false + + // Collect particles. + collectParticleSystem = SCNParticleSystem(named: "collect.scnp", inDirectory: "badger.scnassets")! + collectParticleSystem.loops = false + + collectBigParticleSystem = SCNParticleSystem(named: "collect-big.scnp", inDirectory: "badger.scnassets")! + collectBigParticleSystem.loops = false + + leftHand = character.childNode(withName: "Bip001_L_Finger0Nub", recursively: true)! + rightHand = character.childNode(withName: "Bip001_R_Finger0Nub", recursively: true)! + + leftWheelEmitter = character.childNode(withName: "Dummy_rightWheel_sparks", recursively: true)! + rightWheelEmitter = character.childNode(withName: "Dummy_leftWheel_sparks", recursively: true)! + wheels = character.childNode(withName: "wheels_front", recursively: true)! + + headEmitter = SCNNode() + headEmitter.position = SCNVector3Make(0, 1, 0) + character.addChildNode(headEmitter) + + let wheelAnimation = CABasicAnimation(keyPath: "eulerAngles.x") + wheelAnimation.byValue = 10.0 + wheelAnimation.duration = 1.0 + wheelAnimation.repeatCount = Float.infinity + wheelAnimation.isCumulative = true + wheels.addAnimation(wheelAnimation, forKey: "wheel"); + + // Make sure the slap animation plays right away (no fading) + slapAnimation.fadeInDuration = 0.0 + + /// Similarly collectables are grouped under a common parent node. + /// In addition, load a sound file that will be played when the user collects an item. + collectables = scene.rootNode.childNode(withName: "Collectables", recursively: false)! + speedItems = scene.rootNode.childNode(withName: "SpeedItems", recursively: false)! + + // Load sounds. + collectSound.volume = 5.0 + collectSound2.volume = 5.0 + + // Configure sounds. + let sounds = [ + railSqueakSound, collectSound, collectSound2, + hitSound, railHighSpeedSound, railMediumSpeedSound, + railLowSpeedSound, railWoodSound, railSqueakSound, + cartHide, cartJump, cartTurnLeft, + cartTurnRight + ] + + for sound in sounds { + sound.isPositional = false + sound.load() + } + + railSqueakSound.loops = true + + // Configure the scene to use a local sun. + if isUsingLocalSun { + sun = scene.rootNode.childNode(withName: "Direct001", recursively: false)! + sun.light?.shadowMapSize = CGSize(width: 2048, height: 2048) + sun.light?.orthographicScale = 10 + + sunTargetRelativeToCamera = SCNVector3(x:0, y:0, z:-10) + sun.position = SCNVector3Zero + sunDirection = sun.convertPosition(SCNVector3(x:0, y:0, z:-1), to: nil) + } + else { + sun = SCNNode() + sunTargetRelativeToCamera = SCNVector3Zero + sunDirection = SCNVector3Zero + } + + super.init(coder: coder) + } + + func configureScene() { + // Add sparkles. + let leftEvent1 = SCNAnimationEvent(keyTime: 0.15) { [unowned self] _ in + self.leftWheelEmitter.addParticleSystem(self.sparkles) + } + let leftEvent2 = SCNAnimationEvent(keyTime: 0.9) { [unowned self] _ in + self.rightWheelEmitter.addParticleSystem(self.sparkles) + } + let rightEvent1 = SCNAnimationEvent(keyTime: 0.9) { [unowned self] _ in + self.leftWheelEmitter.addParticleSystem(self.sparkles) + } + leanLeftAnimation.animationEvents = [leftEvent1, leftEvent2] + leanRightAnimation.animationEvents = [rightEvent1] + + sceneView.antialiasingMode = .none + + // Configure triggers and collectables + + /// Special nodes ("triggers") are placed in the scene under a common parent node. + /// Their names indicate what event should occur as they are hit by the cart. + let triggerGroup = scene.rootNode.childNode(withName: "triggers", recursively: false)! + + triggers = triggerGroup.childNodes.flatMap { node in + let triggerName = node.name! as NSString + let triggerPosition = float3(node.position) + + if triggerName.hasPrefix("Trigger_speed") { + let speedValueOffset = "Trigger_speedX_".characters.count + var speedValue = triggerName.substring(from: speedValueOffset) + speedValue = speedValue.replacingOccurrences(of: "_", with: ".") + + guard let speed = Float(speedValue) else { + print("Failed to parse speed value \(speedValue).") + return nil + } + + return Trigger(position: triggerPosition, action: { controller in + controller.trigger(characterSpeed: speed) + }) + } + + if triggerName.hasPrefix("Trigger_obstacle") { + return Trigger(position: triggerPosition, action: { controller in + controller.triggerCollision() + }) + } + + if triggerName.hasPrefix("Trigger_reverb") && triggerName.hasSuffix("start") { + return Trigger(position: triggerPosition, action: { controller in + controller.startReverb() + }) + } + + if triggerName.hasPrefix("Trigger_reverb") && triggerName.hasSuffix("stop") { + return Trigger(position: triggerPosition, action: { controller in + controller.stopReverb() + }) + } + + if triggerName.hasPrefix("Trigger_turn_start") { + return Trigger(position: triggerPosition, action: { controller in + controller.startTurn() + }) + } + + if triggerName.hasPrefix("Trigger_turn_stop") { + return Trigger(position: triggerPosition, action: { controller in + controller.stopTurn() + }) + } + + if triggerName.hasPrefix("Trigger_wood_start") { + return Trigger(position: triggerPosition, action: { controller in + controller.startWood() + }) + } + + if triggerName.hasPrefix("Trigger_wood_stop") { + return Trigger(position: triggerPosition, action: { controller in + controller.stopWood() + }) + } + + if triggerName.hasPrefix("Trigger_highSpeed") { + return Trigger(position: triggerPosition, action: { controller in + controller.changeSpeedSound(speed: 3) + }) + } + + if triggerName.hasPrefix("Trigger_normalSpeed") { + return Trigger(position: triggerPosition, action: { controller in + controller.changeSpeedSound(speed: 2) + }) + } + + if triggerName.hasPrefix("Trigger_slowSpeed") { + return Trigger(position: triggerPosition, action: { controller in + controller.changeSpeedSound(speed: 1) + }) + } + + return nil + } + } + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + // Configure scene post init. + configureScene() + + /// Set the scene and make sure all shaders and textures are pre-loaded. + sceneView.scene = scene + + // At every round regenerate collectables. + let cartAnimation = scene.rootNode.animation(forKey: cartAnimationName)! + cartAnimation.animationEvents = [SCNAnimationEvent(keyTime: 0.9, block: { [unowned self] _ in + self.respawnCollectables() + })] + scene.rootNode.addAnimation(cartAnimation, forKey: cartAnimationName) + + sceneView.prepare(scene, shouldAbortBlock: nil) + sceneView.delegate = self + sceneView.pointOfView = sceneView.scene?.rootNode.childNode(withName: "camera_depart", recursively: true) + + // Play wind sound at launch. + let sound = Assets.sound(named: "wind.m4a") + sound.loops = true + sound.isPositional = false + sound.shouldStream = true + sound.volume = 8.0 + sceneView.scene?.rootNode.addAudioPlayer(SCNAudioPlayer(source: sound)) + + #if os(iOS) + sceneView.contentScaleFactor = 1.3 + #elseif os(tvOS) + sceneView.contentScaleFactor = 1.0 + #else + sceneView.layer?.contentsScale = 1.0 + #endif + + // Start at speed 0. + characterSpeed = 0.0 + + setupGameControllers() + } + + // MARK: Render loop + + /// At each frame, verify if an event should occur + func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) { + activateTriggers() + collectItems() + + // Update sun position + if isUsingLocalSun { + let target = (renderer.pointOfView?.presentation.convertPosition(sunTargetRelativeToCamera, to: nil))! + sun.position = SCNVector3(float3(target) - float3(sunDirection) * 10.0) + } + } + + // MARK: Sound effects + + func startReverb() { + } + + func stopReverb() { + } + + func startTurn() { + guard isSoundEnabled else { return } + + let player = SCNAudioPlayer(source:railSqueakSound) + leftWheelEmitter.addAudioPlayer(player) + } + + func stopTurn() { + guard isSoundEnabled else { return } + + leftWheelEmitter.removeAllAudioPlayers() + } + + func startWood() { + isOverWood = true + updateCartSound() + } + + func stopWood() { + isOverWood = false + updateCartSound() + } + + func trigger(characterSpeed speed: Float) { + SCNTransaction.begin() + SCNTransaction.animationDuration = 2.0 + characterSpeed = speed + SCNTransaction.commit() + } + + func triggerCollision() { + guard squatCounter <= 0 else { return } + + // Play sound and animate. + character.runAction(.playAudio(hitSound, waitForCompletion: false)) + character.addAnimation(slapAnimation, forKey: nil) + + // Add stars. + let emitter = character.childNode(withName: "Bip001_Head", recursively: true) + emitter?.addParticleSystem(stars) + } + + private func activateTriggers() { + let characterPosition = float3(character.presentation.convertPosition(SCNVector3Zero, to: nil)) + + var index = 0 + var didTrigger = false + + for trigger in triggers { + if length_squared(characterPosition - trigger.position) < 0.05 { + if activeTriggerIndex != index { + activeTriggerIndex = index + trigger.action(self) + } + didTrigger = true + break + } + + index = index + 1 + } + + if didTrigger == false { + activeTriggerIndex = -1 + } + } + + // MARK: Collectables + + private func respawnCollectables() { + for collectable in collectables.childNodes { + collectable.categoryBitMask = 0 + collectable.scale = SCNVector3(x:1, y:1, z:1) + } + + for collectable in speedItems.childNodes { + collectable.categoryBitMask = 0 + collectable.scale = SCNVector3(x:1, y:1, z:1) + } + } + + private func collectItems() { + let leftHandPosition = float3(leftHand.presentation.convertPosition(SCNVector3Zero, to: nil)) + let rightHandPosition = float3(rightHand.presentation.convertPosition(SCNVector3Zero, to: nil)) + + for collectable in collectables.childNodes { + guard collectable.categoryBitMask != Int(CollectableState.beingCollected.rawValue) else { continue } + + let collectablePosition = float3(collectable.position) + if length_squared(leftHandPosition - collectablePosition) < 0.05 || length_squared(rightHandPosition - collectablePosition) < 0.05 { + collectable.categoryBitMask = Int(CollectableState.beingCollected.rawValue) + + SCNTransaction.begin() + SCNTransaction.animationDuration = 0.25 + + collectable.scale = SCNVector3Zero + + #if os(iOS) || os(tvOS) + scene.addParticleSystem(collectParticleSystem, transform: collectable.presentation.worldTransform) + #else + scene.addParticleSystem(collectParticleSystem, transform: collectable.presentation.worldTransform) + #endif + + if let name = collectable.name, name.hasPrefix("big") { + headEmitter.addParticleSystem(collectBigParticleSystem) + + sceneView.didCollectBigItem() + collectable.runAction(.playAudio(collectSound2, waitForCompletion: false)) + } + else { + sceneView.didCollectItem() + collectable.runAction(.playAudio(collectSound, waitForCompletion: false)) + } + + SCNTransaction.commit() + + break + } + } + + for collectable in speedItems.childNodes { + guard collectable.categoryBitMask != Int(CollectableState.beingCollected.rawValue) else { continue } + + let collectablePosition = float3(collectable.position) + if length_squared(rightHandPosition - collectablePosition) < 0.05 { + collectable.categoryBitMask = Int(CollectableState.beingCollected.rawValue) + + SCNTransaction.begin() + SCNTransaction.animationDuration = 0.25 + + collectable.scale = SCNVector3Zero + collectable.runAction(.playAudio(collectSound2, waitForCompletion: false)) + + #if os(iOS) || os(tvOS) + scene.addParticleSystem(collectParticleSystem, transform: collectable.presentation.worldTransform) + #else + scene.addParticleSystem(collectParticleSystem, transform: collectable.presentation.worldTransform) + #endif + + SCNTransaction.commit() + + // Speed boost! + SCNTransaction.begin() + SCNTransaction.animationDuration = 1.0 + + let pov = sceneView.pointOfView! + pov.camera?.xFov = 100.0 + + #if !os(tvOS) + pov.camera?.motionBlurIntensity = 1.0 + #endif + + let adjustCamera = SCNAction.run { _ in + SCNTransaction.begin() + SCNTransaction.animationDuration = 1.0 + + pov.camera?.xFov = 70 + pov.camera?.motionBlurIntensity = 0.0 + + SCNTransaction.commit() + } + + pov.runAction(.sequence([.wait(duration: 2.0), adjustCamera])) + character.runAction(.playAudio(cartBoost, waitForCompletion: false)) + + SCNTransaction.commit() + + break + } + } + } + + // MARK: Controlling the Character + + func changeSpeedSound(speed: UInt) { + railSoundSpeed = speed + updateCartSound() + } + + func updateCartSound() { + guard isSoundEnabled else { return } + wheels.removeAllAudioPlayers() + + switch railSoundSpeed { + case _ where isOverWood: + wheels.addAudioPlayer(SCNAudioPlayer(source:railWoodSound)) + + case 1: + wheels.addAudioPlayer(SCNAudioPlayer(source:railLowSpeedSound)) + + case 3: + wheels.addAudioPlayer(SCNAudioPlayer(source:railHighSpeedSound)) + + case let speed where speed > 0: + wheels.addAudioPlayer(SCNAudioPlayer(source:railMediumSpeedSound)) + + default: break + } + } + + func updateSpeed () { + let speed = boostSpeedFactor * characterSpeed + let effectiveSpeed = CGFloat(speedFactor * speed) + scene.rootNode.setAnimationSpeed(effectiveSpeed, forKey: cartAnimationName) + wheels.setAnimationSpeed(effectiveSpeed, forKey: "wheel") + idleAnimationOwner.setAnimationSpeed(effectiveSpeed, forKey: "bob_idle-1") + + // Update sound. + updateCartSound() + } + + private var boostSpeedFactor: Float = 1.0 { + didSet { + updateSpeed() + } + } + + var characterSpeed: Float = 1.0 { + didSet { + updateSpeed() + } + } + + func squat() { + SCNTransaction.begin() + SCNTransaction.completionBlock = { + self.squatCounter -= 1 + } + squatCounter += 1 + + character.addAnimation(squatAnimation, forKey: nil) + character.runAction(.playAudio(cartHide, waitForCompletion: false)) + + SCNTransaction.commit() + } + + func jump() { + character.addAnimation(jumpAnimation, forKey: nil) + character.runAction(.playAudio(cartJump, waitForCompletion: false)) + } + + func leanLeft() { + character.addAnimation(leanLeftAnimation, forKey: nil) + character.runAction(.playAudio(cartTurnLeft, waitForCompletion: false)) + } + + func leanRight() { + character.addAnimation(leanRightAnimation, forKey: nil) + character.runAction(.playAudio(cartTurnRight, waitForCompletion: false)) + } + + func startMusic() { + guard isSoundEnabled else { return } + + let musicIntroSource = Assets.sound(named: "music_intro.mp3") + let musicLoopSource = Assets.sound(named: "music_loop.mp3") + musicLoopSource.loops = true + musicIntroSource.isPositional = false + musicLoopSource.isPositional = false + + // `shouldStream` must be false to wait for completion. + musicIntroSource.shouldStream = false + musicLoopSource.shouldStream = true + + sceneView.scene?.rootNode.runAction(.playAudio(musicIntroSource, waitForCompletion: true)) { [unowned self] in + self.sceneView.scene?.rootNode.addAudioPlayer(SCNAudioPlayer(source:musicLoopSource)) + } + } + + func startGameIfNeeded() -> Bool { + guard gameState == .notStarted else { return false } + sceneView.setup2DOverlay() + + // Stop wind. + sceneView.scene?.rootNode.removeAllAudioPlayers() + + // Play some music. + startMusic() + + gameState = .started + + SCNTransaction.begin() + SCNTransaction.animationDuration = 2.0 + SCNTransaction.completionBlock = { + self.jump() + } + + let idleAnimation = Assets.animation(named: "animation-start.scn") + character.addAnimation(idleAnimation, forKey: nil) + character.removeAnimation(forKey: "start", fadeOutDuration: 0.3) + + sceneView.pointOfView = sceneView.scene?.rootNode.childNode(withName: "Camera", recursively: true) + + SCNTransaction.commit() + + SCNTransaction.begin() + SCNTransaction.animationDuration = 5.0 + + characterSpeed = 1.0 + railSoundSpeed = 1 + + SCNTransaction.commit() + + return true + } +} diff --git a/Badger/Game assets/badger.scnassets/animation-idle.scn b/Badger/Game assets/badger.scnassets/animation-idle.scn new file mode 100644 index 00000000..779beba8 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/animation-idle.scn differ diff --git a/Badger/Game assets/badger.scnassets/animation-jump.scn b/Badger/Game assets/badger.scnassets/animation-jump.scn new file mode 100644 index 00000000..af460ba0 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/animation-jump.scn differ diff --git a/Badger/Game assets/badger.scnassets/animation-lean-left.scn b/Badger/Game assets/badger.scnassets/animation-lean-left.scn new file mode 100644 index 00000000..bd68ed16 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/animation-lean-left.scn differ diff --git a/Badger/Game assets/badger.scnassets/animation-lean-right.scn b/Badger/Game assets/badger.scnassets/animation-lean-right.scn new file mode 100644 index 00000000..c9aee60b Binary files /dev/null and b/Badger/Game assets/badger.scnassets/animation-lean-right.scn differ diff --git a/Badger/Game assets/badger.scnassets/animation-slap.scn b/Badger/Game assets/badger.scnassets/animation-slap.scn new file mode 100644 index 00000000..cbba8f59 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/animation-slap.scn differ diff --git a/Badger/Game assets/badger.scnassets/animation-squat.scn b/Badger/Game assets/badger.scnassets/animation-squat.scn new file mode 100644 index 00000000..8edda109 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/animation-squat.scn differ diff --git a/Badger/Game assets/badger.scnassets/animation-start-idle.scn b/Badger/Game assets/badger.scnassets/animation-start-idle.scn new file mode 100644 index 00000000..81837768 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/animation-start-idle.scn differ diff --git a/Badger/Game assets/badger.scnassets/animation-start.scn b/Badger/Game assets/badger.scnassets/animation-start.scn new file mode 100644 index 00000000..bfbf6907 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/animation-start.scn differ diff --git a/Badger/Game assets/badger.scnassets/areaLight.scn b/Badger/Game assets/badger.scnassets/areaLight.scn new file mode 100644 index 00000000..9ee2fdbe Binary files /dev/null and b/Badger/Game assets/badger.scnassets/areaLight.scn differ diff --git a/Badger/Game assets/badger.scnassets/boost.scn b/Badger/Game assets/badger.scnassets/boost.scn new file mode 100644 index 00000000..7d1e158e Binary files /dev/null and b/Badger/Game assets/badger.scnassets/boost.scn differ diff --git a/Badger/Game assets/badger.scnassets/collect-big.scnp b/Badger/Game assets/badger.scnassets/collect-big.scnp new file mode 100644 index 00000000..cc426a59 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/collect-big.scnp differ diff --git a/Badger/Game assets/badger.scnassets/collect.scnp b/Badger/Game assets/badger.scnassets/collect.scnp new file mode 100644 index 00000000..3517badf Binary files /dev/null and b/Badger/Game assets/badger.scnassets/collect.scnp differ diff --git a/Badger/Game assets/badger.scnassets/collectable-big.scn b/Badger/Game assets/badger.scnassets/collectable-big.scn new file mode 100644 index 00000000..60999719 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/collectable-big.scn differ diff --git a/Badger/Game assets/badger.scnassets/collectable-small.scn b/Badger/Game assets/badger.scnassets/collectable-small.scn new file mode 100644 index 00000000..31689e31 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/collectable-small.scn differ diff --git a/Badger/Game assets/badger.scnassets/collectables.scn b/Badger/Game assets/badger.scnassets/collectables.scn new file mode 100644 index 00000000..89f67355 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/collectables.scn differ diff --git a/Badger/Game assets/badger.scnassets/convert.sh b/Badger/Game assets/badger.scnassets/convert.sh new file mode 100755 index 00000000..ce07604a --- /dev/null +++ b/Badger/Game assets/badger.scnassets/convert.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# usage: convert.sh + +STARTTIME=$(date +%s) + +myfiles=`find "$1" -iname '*_normal.png' -o -iname '*_albedo.png'` + +for file in $myfiles +do + echo + echo Processing $(basename "$file")... + + ASTC_STARTTIME=$(date +%s) + `xcode-select -p`/Platforms/iPhoneOS.platform/Developer/usr/bin/texturetool -e ASTC --compression-mode-fast --block-width-4 --block-height-4 -o "$(dirname "$file")/$(basename "$file" .png).ktx" -f KTX "$file" + ASTC_ENDTIME=$(date +%s) + + echo $(($ASTC_ENDTIME - $ASTC_STARTTIME)) seconds to process $(basename "$file"). +done + +ENDTIME=$(date +%s) + +echo $(($ENDTIME - $STARTTIME)) seconds to convert images of $(basename "$1"). + diff --git a/Badger/Game assets/badger.scnassets/particles.scn b/Badger/Game assets/badger.scnassets/particles.scn new file mode 100644 index 00000000..76f1104a Binary files /dev/null and b/Badger/Game assets/badger.scnassets/particles.scn differ diff --git a/Badger/Game assets/badger.scnassets/scene.scn b/Badger/Game assets/badger.scnassets/scene.scn new file mode 100644 index 00000000..e0f19d85 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/scene.scn differ diff --git a/Badger/Game assets/badger.scnassets/sounds/cart_boost.mp3 b/Badger/Game assets/badger.scnassets/sounds/cart_boost.mp3 new file mode 100755 index 00000000..b5cdccc6 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/cart_boost.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/cart_hide.mp3 b/Badger/Game assets/badger.scnassets/sounds/cart_hide.mp3 new file mode 100755 index 00000000..b2fff963 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/cart_hide.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/cart_jump copy.mp3 b/Badger/Game assets/badger.scnassets/sounds/cart_jump copy.mp3 new file mode 100755 index 00000000..08ebe48d Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/cart_jump copy.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/cart_jump.mp3 b/Badger/Game assets/badger.scnassets/sounds/cart_jump.mp3 new file mode 100755 index 00000000..ff994e7e Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/cart_jump.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/cart_turn_left.mp3 b/Badger/Game assets/badger.scnassets/sounds/cart_turn_left.mp3 new file mode 100755 index 00000000..7551f8a8 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/cart_turn_left.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/cart_turn_right.mp3 b/Badger/Game assets/badger.scnassets/sounds/cart_turn_right.mp3 new file mode 100755 index 00000000..ed53db0d Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/cart_turn_right.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/cart_turn_squeak.mp3 b/Badger/Game assets/badger.scnassets/sounds/cart_turn_squeak.mp3 new file mode 100755 index 00000000..42f14e4f Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/cart_turn_squeak.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/collect1.mp3 b/Badger/Game assets/badger.scnassets/sounds/collect1.mp3 new file mode 100644 index 00000000..68f668a9 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/collect1.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/collect2.mp3 b/Badger/Game assets/badger.scnassets/sounds/collect2.mp3 new file mode 100644 index 00000000..91e8f961 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/collect2.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/hit.mp3 b/Badger/Game assets/badger.scnassets/sounds/hit.mp3 new file mode 100755 index 00000000..a05e24b9 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/hit.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/music_intro.mp3 b/Badger/Game assets/badger.scnassets/sounds/music_intro.mp3 new file mode 100755 index 00000000..1aba0b13 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/music_intro.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/music_loop.mp3 b/Badger/Game assets/badger.scnassets/sounds/music_loop.mp3 new file mode 100755 index 00000000..f210beb2 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/music_loop.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/rail_highspeed_loop.mp3 b/Badger/Game assets/badger.scnassets/sounds/rail_highspeed_loop.mp3 new file mode 100755 index 00000000..cc6b25fb Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/rail_highspeed_loop.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/rail_normalspeed_loop.mp3 b/Badger/Game assets/badger.scnassets/sounds/rail_normalspeed_loop.mp3 new file mode 100755 index 00000000..55e0d002 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/rail_normalspeed_loop.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/rail_slowspeed_loop.mp3 b/Badger/Game assets/badger.scnassets/sounds/rail_slowspeed_loop.mp3 new file mode 100755 index 00000000..c44cd0bf Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/rail_slowspeed_loop.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/rail_wood_loop.mp3 b/Badger/Game assets/badger.scnassets/sounds/rail_wood_loop.mp3 new file mode 100755 index 00000000..1ae6b415 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/rail_wood_loop.mp3 differ diff --git a/Badger/Game assets/badger.scnassets/sounds/wind.m4a b/Badger/Game assets/badger.scnassets/sounds/wind.m4a new file mode 100644 index 00000000..14861785 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sounds/wind.m4a differ diff --git a/Badger/Game assets/badger.scnassets/sparkles.scn b/Badger/Game assets/badger.scnassets/sparkles.scn new file mode 100644 index 00000000..68380707 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/sparkles.scn differ diff --git a/Badger/Game assets/badger.scnassets/textures/01_terrasses_AO.png b/Badger/Game assets/badger.scnassets/textures/01_terrasses_AO.png new file mode 100755 index 00000000..5c05653f Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/01_terrasses_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/01_terrasses_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/01_terrasses_lightmap.exr new file mode 100755 index 00000000..955b7c40 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/01_terrasses_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/01_terrasses_lightmap.png b/Badger/Game assets/badger.scnassets/textures/01_terrasses_lightmap.png new file mode 100644 index 00000000..fce3459d Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/01_terrasses_lightmap.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/02_GP_AO.png b/Badger/Game assets/badger.scnassets/textures/02_GP_AO.png new file mode 100755 index 00000000..dc2adce2 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/02_GP_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/02_GP_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/02_GP_lightmap.exr new file mode 100755 index 00000000..2daec659 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/02_GP_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/02_GP_lightmap.png b/Badger/Game assets/badger.scnassets/textures/02_GP_lightmap.png new file mode 100644 index 00000000..e53df809 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/02_GP_lightmap.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/03_GP_AO.png b/Badger/Game assets/badger.scnassets/textures/03_GP_AO.png new file mode 100755 index 00000000..033162af Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/03_GP_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/03_GP_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/03_GP_lightmap.exr new file mode 100755 index 00000000..db667683 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/03_GP_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/03_GP_lightmap.png b/Badger/Game assets/badger.scnassets/textures/03_GP_lightmap.png new file mode 100644 index 00000000..f0a16288 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/03_GP_lightmap.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/05_cactus_AO.png b/Badger/Game assets/badger.scnassets/textures/05_cactus_AO.png new file mode 100755 index 00000000..3a6f1a77 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/05_cactus_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/05_cactus_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/05_cactus_lightmap.exr new file mode 100755 index 00000000..1d89405f Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/05_cactus_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/05_cactus_lightmap.png b/Badger/Game assets/badger.scnassets/textures/05_cactus_lightmap.png new file mode 100644 index 00000000..e5205eb5 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/05_cactus_lightmap.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/06_Gplay_AO.png b/Badger/Game assets/badger.scnassets/textures/06_Gplay_AO.png new file mode 100755 index 00000000..5da0dca4 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/06_Gplay_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/06_Gplay_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/06_Gplay_lightmap.exr new file mode 100755 index 00000000..f2eeef4c Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/06_Gplay_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/06_Gplay_lightmap.png b/Badger/Game assets/badger.scnassets/textures/06_Gplay_lightmap.png new file mode 100644 index 00000000..222bf852 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/06_Gplay_lightmap.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/07_GrotteChampi_AO.png b/Badger/Game assets/badger.scnassets/textures/07_GrotteChampi_AO.png new file mode 100755 index 00000000..e3ab60dc Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/07_GrotteChampi_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/07_GrotteChampi_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/07_GrotteChampi_lightmap.exr new file mode 100755 index 00000000..a1f93f3a Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/07_GrotteChampi_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/09_GP_AO.png b/Badger/Game assets/badger.scnassets/textures/09_GP_AO.png new file mode 100755 index 00000000..dbc12c6f Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/09_GP_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/09_GP_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/09_GP_lightmap.exr new file mode 100755 index 00000000..f942cf9c Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/09_GP_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/09_GP_lightmap.png b/Badger/Game assets/badger.scnassets/textures/09_GP_lightmap.png new file mode 100644 index 00000000..9d8b1815 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/09_GP_lightmap.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/10_GP_AO.png b/Badger/Game assets/badger.scnassets/textures/10_GP_AO.png new file mode 100755 index 00000000..a248ec6f Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/10_GP_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/10_GP_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/10_GP_lightmap.exr new file mode 100755 index 00000000..3940f48c Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/10_GP_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/10_GP_lightmap.png b/Badger/Game assets/badger.scnassets/textures/10_GP_lightmap.png new file mode 100644 index 00000000..6ee716b9 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/10_GP_lightmap.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/11_cristaux_AO.png b/Badger/Game assets/badger.scnassets/textures/11_cristaux_AO.png new file mode 100755 index 00000000..bbb64ba2 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/11_cristaux_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/11_cristaux_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/11_cristaux_lightmap.exr new file mode 100755 index 00000000..2d4cecb6 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/11_cristaux_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/11_cristaux_lightmap.png b/Badger/Game assets/badger.scnassets/textures/11_cristaux_lightmap.png new file mode 100644 index 00000000..1613c4d8 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/11_cristaux_lightmap.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/12_GP trou_AO.png b/Badger/Game assets/badger.scnassets/textures/12_GP trou_AO.png new file mode 100755 index 00000000..cfcb1948 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/12_GP trou_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/12_GP trou_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/12_GP trou_lightmap.exr new file mode 100755 index 00000000..e8862024 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/12_GP trou_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/12_GP trou_lightmap.png b/Badger/Game assets/badger.scnassets/textures/12_GP trou_lightmap.png new file mode 100644 index 00000000..64a60f61 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/12_GP trou_lightmap.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/12_avant mine_AO.png b/Badger/Game assets/badger.scnassets/textures/12_avant mine_AO.png new file mode 100755 index 00000000..5f245404 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/12_avant mine_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/12_avant mine_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/12_avant mine_lightmap.exr new file mode 100755 index 00000000..1623f90c Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/12_avant mine_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/12_avant mine_lightmap.png b/Badger/Game assets/badger.scnassets/textures/12_avant mine_lightmap.png new file mode 100644 index 00000000..f515d8cc Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/12_avant mine_lightmap.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/13_mine rouge_AO.png b/Badger/Game assets/badger.scnassets/textures/13_mine rouge_AO.png new file mode 100755 index 00000000..c6a992c7 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/13_mine rouge_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/13_mine rouge_lightmap.exr b/Badger/Game assets/badger.scnassets/textures/13_mine rouge_lightmap.exr new file mode 100755 index 00000000..0c8fb7a2 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/13_mine rouge_lightmap.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/Background_sky.png b/Badger/Game assets/badger.scnassets/textures/Background_sky.png new file mode 100755 index 00000000..bf5e029b Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/Background_sky.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/Coussin_albedo.png b/Badger/Game assets/badger.scnassets/textures/Coussin_albedo.png new file mode 100755 index 00000000..77d1cbca Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/Coussin_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/Coussin_normal.png b/Badger/Game assets/badger.scnassets/textures/Coussin_normal.png new file mode 100755 index 00000000..6be771e5 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/Coussin_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/Coussin_roughness.png b/Badger/Game assets/badger.scnassets/textures/Coussin_roughness.png new file mode 100755 index 00000000..a0deed59 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/Coussin_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/accessories_albedo.png b/Badger/Game assets/badger.scnassets/textures/accessories_albedo.png new file mode 100755 index 00000000..82ec0fbd Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/accessories_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/accessories_metal.png b/Badger/Game assets/badger.scnassets/textures/accessories_metal.png new file mode 100755 index 00000000..ab78a950 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/accessories_metal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/accessories_roughness.png b/Badger/Game assets/badger.scnassets/textures/accessories_roughness.png new file mode 100755 index 00000000..b9344c69 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/accessories_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/bob_AO.png b/Badger/Game assets/badger.scnassets/textures/bob_AO.png new file mode 100755 index 00000000..2416ca93 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/bob_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/bob_albedo.png b/Badger/Game assets/badger.scnassets/textures/bob_albedo.png new file mode 100755 index 00000000..96b2e9ce Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/bob_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/bob_roughness.png b/Badger/Game assets/badger.scnassets/textures/bob_roughness.png new file mode 100755 index 00000000..325a5ca8 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/bob_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/cactusG_albedo.png b/Badger/Game assets/badger.scnassets/textures/cactusG_albedo.png new file mode 100755 index 00000000..83dadc8b Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/cactusG_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/cactusG_normal.png b/Badger/Game assets/badger.scnassets/textures/cactusG_normal.png new file mode 100755 index 00000000..9779c247 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/cactusG_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/cactusG_roughness.png b/Badger/Game assets/badger.scnassets/textures/cactusG_roughness.png new file mode 100755 index 00000000..ed4bef7a Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/cactusG_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/cactusP_albedo.png b/Badger/Game assets/badger.scnassets/textures/cactusP_albedo.png new file mode 100755 index 00000000..b7c7b154 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/cactusP_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/cactusP_normal.png b/Badger/Game assets/badger.scnassets/textures/cactusP_normal.png new file mode 100755 index 00000000..103a4ab9 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/cactusP_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/cactusP_roughness.png b/Badger/Game assets/badger.scnassets/textures/cactusP_roughness.png new file mode 100755 index 00000000..6d95d84d Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/cactusP_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/canyon_albedo.png b/Badger/Game assets/badger.scnassets/textures/canyon_albedo.png new file mode 100755 index 00000000..2c9f8150 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/canyon_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/canyon_normal.png b/Badger/Game assets/badger.scnassets/textures/canyon_normal.png new file mode 100755 index 00000000..602c6d27 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/canyon_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/canyon_roughness.png b/Badger/Game assets/badger.scnassets/textures/canyon_roughness.png new file mode 100755 index 00000000..c927eec1 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/canyon_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/crate_albedo.png b/Badger/Game assets/badger.scnassets/textures/crate_albedo.png new file mode 100755 index 00000000..52e4bb34 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/crate_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/crate_danger_albedo.png b/Badger/Game assets/badger.scnassets/textures/crate_danger_albedo.png new file mode 100755 index 00000000..15d18552 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/crate_danger_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/crate_normal.png b/Badger/Game assets/badger.scnassets/textures/crate_normal.png new file mode 100755 index 00000000..22b3dc91 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/crate_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/crate_roughness.png b/Badger/Game assets/badger.scnassets/textures/crate_roughness.png new file mode 100755 index 00000000..b2634657 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/crate_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/crystal_BG_albedo.png b/Badger/Game assets/badger.scnassets/textures/crystal_BG_albedo.png new file mode 100755 index 00000000..84df5745 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/crystal_BG_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/crystal_BG_emissive.png b/Badger/Game assets/badger.scnassets/textures/crystal_BG_emissive.png new file mode 100755 index 00000000..61b9dc87 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/crystal_BG_emissive.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/crystal_BG_roughness.png b/Badger/Game assets/badger.scnassets/textures/crystal_BG_roughness.png new file mode 100755 index 00000000..4af4ad72 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/crystal_BG_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/crystal_albedo.png b/Badger/Game assets/badger.scnassets/textures/crystal_albedo.png new file mode 100755 index 00000000..86a9b7b5 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/crystal_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/crystal_emissive.png b/Badger/Game assets/badger.scnassets/textures/crystal_emissive.png new file mode 100755 index 00000000..982ede66 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/crystal_emissive.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/crystal_metal.png b/Badger/Game assets/badger.scnassets/textures/crystal_metal.png new file mode 100755 index 00000000..a39f7a15 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/crystal_metal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/crystal_roughness.png b/Badger/Game assets/badger.scnassets/textures/crystal_roughness.png new file mode 100755 index 00000000..b0ce08d0 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/crystal_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/dot.png b/Badger/Game assets/badger.scnassets/textures/dot.png new file mode 100644 index 00000000..9dbe7366 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/dot.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/figuier_albedo.png b/Badger/Game assets/badger.scnassets/textures/figuier_albedo.png new file mode 100755 index 00000000..66896931 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/figuier_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/figuier_normal.png b/Badger/Game assets/badger.scnassets/textures/figuier_normal.png new file mode 100755 index 00000000..c049c330 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/figuier_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/figuier_roughness.png b/Badger/Game assets/badger.scnassets/textures/figuier_roughness.png new file mode 100755 index 00000000..79ad0ca4 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/figuier_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/glow01.png b/Badger/Game assets/badger.scnassets/textures/glow01.png new file mode 100755 index 00000000..3d830f50 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/glow01.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/glow_ellipse.png b/Badger/Game assets/badger.scnassets/textures/glow_ellipse.png new file mode 100644 index 00000000..849ab51d Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/glow_ellipse.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/grass_albedo.png b/Badger/Game assets/badger.scnassets/textures/grass_albedo.png new file mode 100755 index 00000000..8ad53413 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/grass_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/grass_roughness.png b/Badger/Game assets/badger.scnassets/textures/grass_roughness.png new file mode 100755 index 00000000..67ac2fd6 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/grass_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/ground_crystals_albedo.png b/Badger/Game assets/badger.scnassets/textures/ground_crystals_albedo.png new file mode 100755 index 00000000..45819dcc Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/ground_crystals_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/ground_crystals_normal.png b/Badger/Game assets/badger.scnassets/textures/ground_crystals_normal.png new file mode 100755 index 00000000..28ea20c4 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/ground_crystals_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/ground_crystals_roughness.png b/Badger/Game assets/badger.scnassets/textures/ground_crystals_roughness.png new file mode 100755 index 00000000..54144d93 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/ground_crystals_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/ground_gameplay_albedo.png b/Badger/Game assets/badger.scnassets/textures/ground_gameplay_albedo.png new file mode 100755 index 00000000..32d05096 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/ground_gameplay_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/ground_gameplay_normal.png b/Badger/Game assets/badger.scnassets/textures/ground_gameplay_normal.png new file mode 100755 index 00000000..10e021c7 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/ground_gameplay_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/ground_gameplay_roughness.png b/Badger/Game assets/badger.scnassets/textures/ground_gameplay_roughness.png new file mode 100755 index 00000000..0d21a9c4 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/ground_gameplay_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/ground_geo_albedo.png b/Badger/Game assets/badger.scnassets/textures/ground_geo_albedo.png new file mode 100755 index 00000000..00966cb1 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/ground_geo_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/ground_sand_albedo.png b/Badger/Game assets/badger.scnassets/textures/ground_sand_albedo.png new file mode 100755 index 00000000..f0cdff63 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/ground_sand_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/ground_sand_normal.png b/Badger/Game assets/badger.scnassets/textures/ground_sand_normal.png new file mode 100755 index 00000000..90271459 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/ground_sand_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/ground_sand_roughness.png b/Badger/Game assets/badger.scnassets/textures/ground_sand_roughness.png new file mode 100755 index 00000000..bd4ad937 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/ground_sand_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/kartTotal_AO.png b/Badger/Game assets/badger.scnassets/textures/kartTotal_AO.png new file mode 100755 index 00000000..b6e2b13a Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/kartTotal_AO.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/kartTotal_Base_Color.png b/Badger/Game assets/badger.scnassets/textures/kartTotal_Base_Color.png new file mode 100755 index 00000000..3d5f67ad Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/kartTotal_Base_Color.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/kartTotal_Base_Metal.png b/Badger/Game assets/badger.scnassets/textures/kartTotal_Base_Metal.png new file mode 100755 index 00000000..5ddbc515 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/kartTotal_Base_Metal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/kartTotal_Base_Normal.png b/Badger/Game assets/badger.scnassets/textures/kartTotal_Base_Normal.png new file mode 100755 index 00000000..3c38774b Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/kartTotal_Base_Normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/kartTotal_Roughness.png b/Badger/Game assets/badger.scnassets/textures/kartTotal_Roughness.png new file mode 100755 index 00000000..1574cadd Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/kartTotal_Roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Color.png b/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Color.png new file mode 100755 index 00000000..c0ece575 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Color.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Metal.png b/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Metal.png new file mode 100755 index 00000000..a28fc513 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Metal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Normal.png b/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Normal.png new file mode 100755 index 00000000..d5788798 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Roughness.png b/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Roughness.png new file mode 100755 index 00000000..c05f2a8f Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/kartTotal_rusty_Roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/montagne_albedo.png b/Badger/Game assets/badger.scnassets/textures/montagne_albedo.png new file mode 100755 index 00000000..feb99683 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/montagne_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/montagne_crystals_albedo.png b/Badger/Game assets/badger.scnassets/textures/montagne_crystals_albedo.png new file mode 100755 index 00000000..334468e7 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/montagne_crystals_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/montagne_crystals_metal.png b/Badger/Game assets/badger.scnassets/textures/montagne_crystals_metal.png new file mode 100755 index 00000000..668640a0 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/montagne_crystals_metal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/montagne_crystals_normal.png b/Badger/Game assets/badger.scnassets/textures/montagne_crystals_normal.png new file mode 100755 index 00000000..5d8d72fc Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/montagne_crystals_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/montagne_crystals_roughness.png b/Badger/Game assets/badger.scnassets/textures/montagne_crystals_roughness.png new file mode 100755 index 00000000..3d266cd9 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/montagne_crystals_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/montagne_normal.png b/Badger/Game assets/badger.scnassets/textures/montagne_normal.png new file mode 100755 index 00000000..dbb12283 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/montagne_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/montagne_roughness.png b/Badger/Game assets/badger.scnassets/textures/montagne_roughness.png new file mode 100755 index 00000000..a2945794 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/montagne_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/montagne_top_albedo.png b/Badger/Game assets/badger.scnassets/textures/montagne_top_albedo.png new file mode 100755 index 00000000..af3affee Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/montagne_top_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/montagne_top_normal.png b/Badger/Game assets/badger.scnassets/textures/montagne_top_normal.png new file mode 100755 index 00000000..b32148b3 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/montagne_top_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/montagne_top_roughness.png b/Badger/Game assets/badger.scnassets/textures/montagne_top_roughness.png new file mode 100755 index 00000000..1293a708 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/montagne_top_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/mushroom_albedo.png b/Badger/Game assets/badger.scnassets/textures/mushroom_albedo.png new file mode 100755 index 00000000..60fda2b5 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/mushroom_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/mushroom_normal.png b/Badger/Game assets/badger.scnassets/textures/mushroom_normal.png new file mode 100755 index 00000000..c0e82003 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/mushroom_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/mushroom_roughness.png b/Badger/Game assets/badger.scnassets/textures/mushroom_roughness.png new file mode 100755 index 00000000..d3577a3b Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/mushroom_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/particle_smoke.png b/Badger/Game assets/badger.scnassets/textures/particle_smoke.png new file mode 100755 index 00000000..4bac5f75 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/particle_smoke.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/particle_spore.png b/Badger/Game assets/badger.scnassets/textures/particle_spore.png new file mode 100755 index 00000000..9f8b6442 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/particle_spore.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/planks_albedo.png b/Badger/Game assets/badger.scnassets/textures/planks_albedo.png new file mode 100755 index 00000000..67240afc Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/planks_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/planks_normal.png b/Badger/Game assets/badger.scnassets/textures/planks_normal.png new file mode 100755 index 00000000..0329f6fb Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/planks_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/planks_roughness.png b/Badger/Game assets/badger.scnassets/textures/planks_roughness.png new file mode 100755 index 00000000..53447301 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/planks_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/rail_albedo.png b/Badger/Game assets/badger.scnassets/textures/rail_albedo.png new file mode 100755 index 00000000..f0b29efc Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/rail_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/rail_metal.png b/Badger/Game assets/badger.scnassets/textures/rail_metal.png new file mode 100755 index 00000000..af4d1a39 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/rail_metal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/rail_roughness.png b/Badger/Game assets/badger.scnassets/textures/rail_roughness.png new file mode 100755 index 00000000..cd54cfee Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/rail_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/rocks_albedo.png b/Badger/Game assets/badger.scnassets/textures/rocks_albedo.png new file mode 100755 index 00000000..a3ec6159 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/rocks_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/rocks_lichens_albedo.png b/Badger/Game assets/badger.scnassets/textures/rocks_lichens_albedo.png new file mode 100755 index 00000000..e4fdf490 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/rocks_lichens_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/rocks_lichens_normal.png b/Badger/Game assets/badger.scnassets/textures/rocks_lichens_normal.png new file mode 100755 index 00000000..7e189472 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/rocks_lichens_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/rocks_lichens_roughness.png b/Badger/Game assets/badger.scnassets/textures/rocks_lichens_roughness.png new file mode 100755 index 00000000..63a067ae Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/rocks_lichens_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/rocks_normal.png b/Badger/Game assets/badger.scnassets/textures/rocks_normal.png new file mode 100755 index 00000000..7594d8f1 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/rocks_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/rocks_roughness.png b/Badger/Game assets/badger.scnassets/textures/rocks_roughness.png new file mode 100755 index 00000000..20f7e993 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/rocks_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/roundLamp_albedo.png b/Badger/Game assets/badger.scnassets/textures/roundLamp_albedo.png new file mode 100755 index 00000000..984391a1 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/roundLamp_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/roundLamp_metal.png b/Badger/Game assets/badger.scnassets/textures/roundLamp_metal.png new file mode 100755 index 00000000..f9c7a2fc Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/roundLamp_metal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/roundLamp_roughness.png b/Badger/Game assets/badger.scnassets/textures/roundLamp_roughness.png new file mode 100755 index 00000000..c50e2d22 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/roundLamp_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/signDanger_albedo.png b/Badger/Game assets/badger.scnassets/textures/signDanger_albedo.png new file mode 100755 index 00000000..bbeb9ff6 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/signDanger_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/signDanger_normal.png b/Badger/Game assets/badger.scnassets/textures/signDanger_normal.png new file mode 100755 index 00000000..6e70394e Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/signDanger_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/signDanger_roughness.png b/Badger/Game assets/badger.scnassets/textures/signDanger_roughness.png new file mode 100755 index 00000000..ad1fdcb8 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/signDanger_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/sign_start_albedo.png b/Badger/Game assets/badger.scnassets/textures/sign_start_albedo.png new file mode 100755 index 00000000..49b003ad Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/sign_start_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/sign_start_normal.png b/Badger/Game assets/badger.scnassets/textures/sign_start_normal.png new file mode 100755 index 00000000..fea27675 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/sign_start_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/sign_start_roughness.png b/Badger/Game assets/badger.scnassets/textures/sign_start_roughness.png new file mode 100755 index 00000000..a4b57035 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/sign_start_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/sky_cube-ldr.png b/Badger/Game assets/badger.scnassets/textures/sky_cube-ldr.png new file mode 100644 index 00000000..f04f16fc Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/sky_cube-ldr.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/sky_cube.exr b/Badger/Game assets/badger.scnassets/textures/sky_cube.exr new file mode 100755 index 00000000..de32e529 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/sky_cube.exr differ diff --git a/Badger/Game assets/badger.scnassets/textures/star.png b/Badger/Game assets/badger.scnassets/textures/star.png new file mode 100755 index 00000000..fcdce4b7 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/star.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/terrasses_rocks_light_albedo.png b/Badger/Game assets/badger.scnassets/textures/terrasses_rocks_light_albedo.png new file mode 100755 index 00000000..626eb756 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/terrasses_rocks_light_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/terrasses_rocks_normal.png b/Badger/Game assets/badger.scnassets/textures/terrasses_rocks_normal.png new file mode 100755 index 00000000..462330bc Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/terrasses_rocks_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/terrasses_rocks_roughness.png b/Badger/Game assets/badger.scnassets/textures/terrasses_rocks_roughness.png new file mode 100755 index 00000000..4caa61e2 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/terrasses_rocks_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/terrasses_water_albedo.png b/Badger/Game assets/badger.scnassets/textures/terrasses_water_albedo.png new file mode 100755 index 00000000..355dbd1f Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/terrasses_water_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/terrasses_water_normal.png b/Badger/Game assets/badger.scnassets/textures/terrasses_water_normal.png new file mode 100755 index 00000000..5df0d188 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/terrasses_water_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/terrasses_water_orange_albedo.png b/Badger/Game assets/badger.scnassets/textures/terrasses_water_orange_albedo.png new file mode 100755 index 00000000..4e839d71 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/terrasses_water_orange_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/terrasses_water_roughness.png b/Badger/Game assets/badger.scnassets/textures/terrasses_water_roughness.png new file mode 100755 index 00000000..a8aa166e Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/terrasses_water_roughness.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/watertank_albedo.png b/Badger/Game assets/badger.scnassets/textures/watertank_albedo.png new file mode 100755 index 00000000..6ee15ba4 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/watertank_albedo.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/watertank_metal.png b/Badger/Game assets/badger.scnassets/textures/watertank_metal.png new file mode 100755 index 00000000..2b04a5cf Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/watertank_metal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/watertank_normal.png b/Badger/Game assets/badger.scnassets/textures/watertank_normal.png new file mode 100755 index 00000000..633c03b2 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/watertank_normal.png differ diff --git a/Badger/Game assets/badger.scnassets/textures/watertank_roughness.png b/Badger/Game assets/badger.scnassets/textures/watertank_roughness.png new file mode 100755 index 00000000..26119105 Binary files /dev/null and b/Badger/Game assets/badger.scnassets/textures/watertank_roughness.png differ diff --git a/Badger/Game assets/overlays/BobHUD.png b/Badger/Game assets/overlays/BobHUD.png new file mode 100644 index 00000000..803415a2 Binary files /dev/null and b/Badger/Game assets/overlays/BobHUD.png differ diff --git a/Badger/LICENSE.txt b/Badger/LICENSE.txt new file mode 100644 index 00000000..3fc6bc3c --- /dev/null +++ b/Badger/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: Badger: Advanced Rendering in SceneKit +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) 2016 Apple Inc. All Rights Reserved. diff --git a/Badger/README.md b/Badger/README.md new file mode 100644 index 00000000..8db92d24 --- /dev/null +++ b/Badger/README.md @@ -0,0 +1,13 @@ +# Badger: Advanced Rendering in SceneKit + +Demonstrates how to build a SceneKit-based game with advanced graphics. + +### Build + +Xcode 8.0 or later; iOS 10.0 SDK or later; tvOS 10.0 SDK or later; macOS 10.12 SDK or later + +### Runtime + +iOS 10.0 or later; tvOS 10.0 or later; macOS 10.12 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/Badger/iOS/AppDelegate.swift b/Badger/iOS/AppDelegate.swift new file mode 100644 index 00000000..f6dcc718 --- /dev/null +++ b/Badger/iOS/AppDelegate.swift @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + iOS AppDelegate +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? +} + diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-29.png b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-29.png new file mode 100644 index 00000000..b5d1efc8 Binary files /dev/null and b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-29.png differ diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-29@2x.png b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-29@2x.png new file mode 100644 index 00000000..92f63dbb Binary files /dev/null and b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-29@2x.png differ diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-29@3x.png b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-29@3x.png new file mode 100644 index 00000000..01aa3640 Binary files /dev/null and b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-29@3x.png differ diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-40.png b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-40.png new file mode 100644 index 00000000..85d14826 Binary files /dev/null and b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-40.png differ diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-40@2x.png b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-40@2x.png new file mode 100644 index 00000000..41217d5c Binary files /dev/null and b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-40@2x.png differ diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-40@3x.png b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-40@3x.png new file mode 100644 index 00000000..6d3988ff Binary files /dev/null and b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-40@3x.png differ diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-60@2x.png b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-60@2x.png new file mode 100644 index 00000000..6d3988ff Binary files /dev/null and b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-60@2x.png differ diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-60@3x.png b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-60@3x.png new file mode 100644 index 00000000..5ca3fe2c Binary files /dev/null and b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-60@3x.png differ diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-76.png b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-76.png new file mode 100644 index 00000000..60de02a2 Binary files /dev/null and b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-76.png differ diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-76@2x.png b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-76@2x.png new file mode 100644 index 00000000..29e24920 Binary files /dev/null and b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-76@2x.png differ diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-83.5@2x.png b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-83.5@2x.png new file mode 100644 index 00000000..a9b39726 Binary files /dev/null and b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/BobIcon-83.5@2x.png differ diff --git a/Badger/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..f3427701 --- /dev/null +++ b/Badger/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,86 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "BobIcon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "BobIcon-29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "BobIcon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "BobIcon-40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "BobIcon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "BobIcon-60@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "BobIcon-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "BobIcon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "BobIcon-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "BobIcon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "BobIcon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "BobIcon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "BobIcon-83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/iOS/Base.lproj/LaunchScreen.storyboard b/Badger/iOS/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..41fa653e --- /dev/null +++ b/Badger/iOS/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Badger/iOS/Base.lproj/Main.storyboard b/Badger/iOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000..d82a3f72 --- /dev/null +++ b/Badger/iOS/Base.lproj/Main.storyboard @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Badger/iOS/BobIcon.png b/Badger/iOS/BobIcon.png new file mode 100644 index 00000000..3ca710c4 Binary files /dev/null and b/Badger/iOS/BobIcon.png differ diff --git a/Badger/iOS/Info.plist b/Badger/iOS/Info.plist new file mode 100644 index 00000000..175712f0 --- /dev/null +++ b/Badger/iOS/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + SCNDisableWideGamut + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarHidden + + UIViewControllerBasedStatusBarAppearance + + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Badger/iOS/launchScreen.png b/Badger/iOS/launchScreen.png new file mode 100644 index 00000000..128223d4 Binary files /dev/null and b/Badger/iOS/launchScreen.png differ diff --git a/Badger/macOS/AppDelegate.swift b/Badger/macOS/AppDelegate.swift new file mode 100644 index 00000000..f3aafefe --- /dev/null +++ b/Badger/macOS/AppDelegate.swift @@ -0,0 +1,14 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + macOS AppDelegate + */ + +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { +} + diff --git a/Badger/macOS/Assets.xcassets/AppIcon.appiconset/BobIcon.png b/Badger/macOS/Assets.xcassets/AppIcon.appiconset/BobIcon.png new file mode 100644 index 00000000..99a9d329 Binary files /dev/null and b/Badger/macOS/Assets.xcassets/AppIcon.appiconset/BobIcon.png differ diff --git a/Badger/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Badger/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..b220b815 --- /dev/null +++ b/Badger/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,59 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "BobIcon.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/macOS/Base.lproj/Main.storyboard b/Badger/macOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000..9ab2fa1f --- /dev/null +++ b/Badger/macOS/Base.lproj/Main.storyboard @@ -0,0 +1,697 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Badger/macOS/BobIcon.png b/Badger/macOS/BobIcon.png new file mode 100644 index 00000000..99a9d329 Binary files /dev/null and b/Badger/macOS/BobIcon.png differ diff --git a/Badger/macOS/Info.plist b/Badger/macOS/Info.plist new file mode 100644 index 00000000..fe057ecb --- /dev/null +++ b/Badger/macOS/Info.plist @@ -0,0 +1,36 @@ + + + + + SCNDisableWideGamut + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2016 Apple Inc. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + + diff --git a/Badger/tvOS/AppDelegate.swift b/Badger/tvOS/AppDelegate.swift new file mode 100644 index 00000000..3c46af74 --- /dev/null +++ b/Badger/tvOS/AppDelegate.swift @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + tvOS AppDelegate + */ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? +} + diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json new file mode 100644 index 00000000..8bf75d9f --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json new file mode 100644 index 00000000..8bf75d9f --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json new file mode 100644 index 00000000..6d596bc7 --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "assets" : [ + { + "size" : "1280x768", + "idiom" : "tv", + "filename" : "App Icon - Large.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "400x240", + "idiom" : "tv", + "filename" : "App Icon - Small.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "2320x720", + "idiom" : "tv", + "filename" : "Top Shelf Image Wide.imageset", + "role" : "top-shelf-image-wide" + }, + { + "size" : "1920x720", + "idiom" : "tv", + "filename" : "Top Shelf Image.imageset", + "role" : "top-shelf-image" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/Contents.json b/Badger/tvOS/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Assets.xcassets/LaunchImage.launchimage/Contents.json b/Badger/tvOS/Assets.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 00000000..29d94c78 --- /dev/null +++ b/Badger/tvOS/Assets.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "orientation" : "landscape", + "idiom" : "tv", + "extent" : "full-screen", + "minimum-system-version" : "9.0", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Badger/tvOS/Base.lproj/Main.storyboard b/Badger/tvOS/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f1414f11 --- /dev/null +++ b/Badger/tvOS/Base.lproj/Main.storyboard @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Badger/tvOS/Info.plist b/Badger/tvOS/Info.plist new file mode 100644 index 00000000..e04d8d98 --- /dev/null +++ b/Badger/tvOS/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + SCNDisableWideGamut + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UIUserInterfaceStyle + Automatic + + diff --git a/Footprint/LICENSE.txt b/Footprint/LICENSE.txt new file mode 100644 index 00000000..5df72349 --- /dev/null +++ b/Footprint/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: Footprint: Indoor Positioning with Core Location +Version: 2.1 + +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) 2016 Apple Inc. All Rights Reserved. diff --git a/Footprint/ObjC/Footprint.xcodeproj/project.pbxproj b/Footprint/ObjC/Footprint.xcodeproj/project.pbxproj new file mode 100644 index 00000000..1f8a68df --- /dev/null +++ b/Footprint/ObjC/Footprint.xcodeproj/project.pbxproj @@ -0,0 +1,325 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + A68417651B159E0400368E55 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = A68417641B159E0400368E55 /* main.m */; }; + A684176E1B159E0400368E55 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A684176C1B159E0400368E55 /* Main.storyboard */; }; + A68417701B159E0400368E55 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A684176F1B159E0400368E55 /* Images.xcassets */; }; + A68417961B159E3D00368E55 /* AAPLAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = A68417891B159E3D00368E55 /* AAPLAppDelegate.m */; }; + A68417971B159E3D00368E55 /* AAPLCoordinateConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = A684178B1B159E3D00368E55 /* AAPLCoordinateConverter.m */; }; + A68417981B159E3D00368E55 /* AAPLFloorplanOverlay.m in Sources */ = {isa = PBXBuildFile; fileRef = A684178D1B159E3D00368E55 /* AAPLFloorplanOverlay.m */; }; + A68417991B159E3D00368E55 /* AAPLFloorplanOverlayRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = A684178F1B159E3D00368E55 /* AAPLFloorplanOverlayRenderer.m */; }; + A684179A1B159E3D00368E55 /* AAPLHideBackgroundOverlay.m in Sources */ = {isa = PBXBuildFile; fileRef = A68417911B159E3D00368E55 /* AAPLHideBackgroundOverlay.m */; }; + A684179B1B159E3D00368E55 /* AAPLViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A68417931B159E3D00368E55 /* AAPLViewController.m */; }; + A684179E1B159E9600368E55 /* Floorplans in Resources */ = {isa = PBXBuildFile; fileRef = A684179D1B159E9600368E55 /* Floorplans */; }; + A6CAE3891B1681E700DC7979 /* AAPLMKMapRectRotated.m in Sources */ = {isa = PBXBuildFile; fileRef = A6CAE3881B1681E700DC7979 /* AAPLMKMapRectRotated.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A68417601B159E0400368E55 /* Footprint.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Footprint.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A68417641B159E0400368E55 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + A684176D1B159E0400368E55 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + A684176F1B159E0400368E55 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + A68417741B159E0400368E55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A68417881B159E3D00368E55 /* AAPLAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLAppDelegate.h; sourceTree = ""; }; + A68417891B159E3D00368E55 /* AAPLAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLAppDelegate.m; sourceTree = ""; }; + A684178A1B159E3D00368E55 /* AAPLCoordinateConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AAPLCoordinateConverter.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + A684178B1B159E3D00368E55 /* AAPLCoordinateConverter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AAPLCoordinateConverter.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + A684178C1B159E3D00368E55 /* AAPLFloorplanOverlay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = AAPLFloorplanOverlay.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + A684178D1B159E3D00368E55 /* AAPLFloorplanOverlay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AAPLFloorplanOverlay.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + A684178E1B159E3D00368E55 /* AAPLFloorplanOverlayRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLFloorplanOverlayRenderer.h; sourceTree = ""; }; + A684178F1B159E3D00368E55 /* AAPLFloorplanOverlayRenderer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = AAPLFloorplanOverlayRenderer.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + A68417901B159E3D00368E55 /* AAPLHideBackgroundOverlay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLHideBackgroundOverlay.h; sourceTree = ""; }; + A68417911B159E3D00368E55 /* AAPLHideBackgroundOverlay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLHideBackgroundOverlay.m; sourceTree = ""; }; + A68417921B159E3D00368E55 /* AAPLViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLViewController.h; sourceTree = ""; }; + A68417931B159E3D00368E55 /* AAPLViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLViewController.m; sourceTree = ""; }; + A684179D1B159E9600368E55 /* Floorplans */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Floorplans; sourceTree = ""; }; + A6CAE3871B1681E700DC7979 /* AAPLMKMapRectRotated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLMKMapRectRotated.h; sourceTree = ""; }; + A6CAE3881B1681E700DC7979 /* AAPLMKMapRectRotated.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLMKMapRectRotated.m; sourceTree = ""; }; + B55119521D9AE48300CDECCC /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A684175D1B159E0400368E55 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A68417571B159E0400368E55 = { + isa = PBXGroup; + children = ( + B55119521D9AE48300CDECCC /* README.md */, + A68417621B159E0400368E55 /* Footprint */, + A68417611B159E0400368E55 /* Products */, + ); + sourceTree = ""; + }; + A68417611B159E0400368E55 /* Products */ = { + isa = PBXGroup; + children = ( + A68417601B159E0400368E55 /* Footprint.app */, + ); + name = Products; + sourceTree = ""; + }; + A68417621B159E0400368E55 /* Footprint */ = { + isa = PBXGroup; + children = ( + A68417921B159E3D00368E55 /* AAPLViewController.h */, + A68417931B159E3D00368E55 /* AAPLViewController.m */, + A68417881B159E3D00368E55 /* AAPLAppDelegate.h */, + A68417891B159E3D00368E55 /* AAPLAppDelegate.m */, + A6CAE3871B1681E700DC7979 /* AAPLMKMapRectRotated.h */, + A6CAE3881B1681E700DC7979 /* AAPLMKMapRectRotated.m */, + A684178A1B159E3D00368E55 /* AAPLCoordinateConverter.h */, + A684178B1B159E3D00368E55 /* AAPLCoordinateConverter.m */, + A684178C1B159E3D00368E55 /* AAPLFloorplanOverlay.h */, + A684178D1B159E3D00368E55 /* AAPLFloorplanOverlay.m */, + A684178E1B159E3D00368E55 /* AAPLFloorplanOverlayRenderer.h */, + A684178F1B159E3D00368E55 /* AAPLFloorplanOverlayRenderer.m */, + A68417901B159E3D00368E55 /* AAPLHideBackgroundOverlay.h */, + A68417911B159E3D00368E55 /* AAPLHideBackgroundOverlay.m */, + A684179D1B159E9600368E55 /* Floorplans */, + A684176C1B159E0400368E55 /* Main.storyboard */, + A684176F1B159E0400368E55 /* Images.xcassets */, + A68417741B159E0400368E55 /* Info.plist */, + A68417631B159E0400368E55 /* Supporting Files */, + ); + path = Footprint; + sourceTree = ""; + }; + A68417631B159E0400368E55 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + A68417641B159E0400368E55 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A684175F1B159E0400368E55 /* Footprint */ = { + isa = PBXNativeTarget; + buildConfigurationList = A68417821B159E0400368E55 /* Build configuration list for PBXNativeTarget "Footprint" */; + buildPhases = ( + A684175C1B159E0400368E55 /* Sources */, + A684175D1B159E0400368E55 /* Frameworks */, + A684175E1B159E0400368E55 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Footprint; + productName = footprint; + productReference = A68417601B159E0400368E55 /* Footprint.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A68417581B159E0400368E55 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = "Apple, Inc"; + TargetAttributes = { + A684175F1B159E0400368E55 = { + CreatedOnToolsVersion = 7.0; + }; + }; + }; + buildConfigurationList = A684175B1B159E0400368E55 /* Build configuration list for PBXProject "Footprint" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A68417571B159E0400368E55; + productRefGroup = A68417611B159E0400368E55 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A684175F1B159E0400368E55 /* Footprint */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A684175E1B159E0400368E55 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A684176E1B159E0400368E55 /* Main.storyboard in Resources */, + A68417701B159E0400368E55 /* Images.xcassets in Resources */, + A684179E1B159E9600368E55 /* Floorplans in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A684175C1B159E0400368E55 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A684179A1B159E3D00368E55 /* AAPLHideBackgroundOverlay.m in Sources */, + A68417991B159E3D00368E55 /* AAPLFloorplanOverlayRenderer.m in Sources */, + A684179B1B159E3D00368E55 /* AAPLViewController.m in Sources */, + A6CAE3891B1681E700DC7979 /* AAPLMKMapRectRotated.m in Sources */, + A68417981B159E3D00368E55 /* AAPLFloorplanOverlay.m in Sources */, + A68417971B159E3D00368E55 /* AAPLCoordinateConverter.m in Sources */, + A68417961B159E3D00368E55 /* AAPLAppDelegate.m in Sources */, + A68417651B159E0400368E55 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + A684176C1B159E0400368E55 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + A684176D1B159E0400368E55 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + A68417801B159E0400368E55 /* 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 = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = 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; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A68417811B159E0400368E55 /* 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 = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + 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"; + }; + name = Release; + }; + A68417831B159E0400368E55 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = footprint/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = Footprint; + }; + name = Debug; + }; + A68417841B159E0400368E55 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = footprint/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = Footprint; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A684175B1B159E0400368E55 /* Build configuration list for PBXProject "Footprint" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A68417801B159E0400368E55 /* Debug */, + A68417811B159E0400368E55 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A68417821B159E0400368E55 /* Build configuration list for PBXNativeTarget "Footprint" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A68417831B159E0400368E55 /* Debug */, + A68417841B159E0400368E55 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A68417581B159E0400368E55 /* Project object */; +} diff --git a/Footprint/ObjC/Footprint.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Footprint/ObjC/Footprint.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..822c0c3d --- /dev/null +++ b/Footprint/ObjC/Footprint.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Footprint/ObjC/Footprint.xcodeproj/project.xcworkspace/xcshareddata/footprint.xcscmblueprint b/Footprint/ObjC/Footprint.xcodeproj/project.xcworkspace/xcshareddata/footprint.xcscmblueprint new file mode 100644 index 00000000..460e2003 --- /dev/null +++ b/Footprint/ObjC/Footprint.xcodeproj/project.xcworkspace/xcshareddata/footprint.xcscmblueprint @@ -0,0 +1,23 @@ +{ + "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "776691BC8E6962E6150B0296245405EC7EC558F3", + "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : { + + }, + "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : { + "776691BC8E6962E6150B0296245405EC7EC558F3" : 0 + }, + "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "EE58E738-2418-4F74-AA22-C35ED66B5984", + "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : { + "776691BC8E6962E6150B0296245405EC7EC558F3" : "footprint-to-swift\/" + }, + "DVTSourceControlWorkspaceBlueprintNameKey" : "footprint", + "DVTSourceControlWorkspaceBlueprintVersion" : 203, + "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "ObjC\/footprint.xcodeproj", + "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [ + { + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "gitlab.sd.apple.com:iveygman\/footprint-to-swift.git", + "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git", + "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "776691BC8E6962E6150B0296245405EC7EC558F3" + } + ] +} \ No newline at end of file diff --git a/Footprint/ObjC/Footprint/AAPLAppDelegate.h b/Footprint/ObjC/Footprint/AAPLAppDelegate.h new file mode 100644 index 00000000..9b51bf75 --- /dev/null +++ b/Footprint/ObjC/Footprint/AAPLAppDelegate.h @@ -0,0 +1,15 @@ +/** + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Delegate to track the state transitions for the application. +*/ + +@import UIKit; + +@interface AAPLAppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/Footprint/ObjC/Footprint/AAPLAppDelegate.m b/Footprint/ObjC/Footprint/AAPLAppDelegate.m new file mode 100644 index 00000000..3208e188 --- /dev/null +++ b/Footprint/ObjC/Footprint/AAPLAppDelegate.m @@ -0,0 +1,12 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Delegate to track the state transitions for the application. +*/ + +#import "AAPLAppDelegate.h" + +@implementation AAPLAppDelegate +@end diff --git a/Footprint/ObjC/Footprint/AAPLCoordinateConverter.h b/Footprint/ObjC/Footprint/AAPLCoordinateConverter.h new file mode 100644 index 00000000..460744dd --- /dev/null +++ b/Footprint/ObjC/Footprint/AAPLCoordinateConverter.h @@ -0,0 +1,149 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This class converts PDF coordinates of a floorplan to Geographic coordinates on Earth. NOTE: This class can also be used for any "right-handed" coordinate system (other than PDF) but should not be used as-is for "Raster image" coordinates (such as PNGs or JPEGs) because those require left-handed coordinate frames. + + There are other reasons we discourage the use of raster images as indoor floorplans. See the code comments inside AAPLFloorplanOverlay initWithFloorplanURL: for more info. +*/ + +@import CoreLocation; +@import MapKit; +@import UIKit; + +/** + @note In iOS, the term "pixel" usually refers to screen pixels whereas the + term "point" is used to describe coordinates inside a visual/image asset. + + For more information, see: "Points Versus Pixels" under 2DDrawing and + WindowsViews. + + This class matches a specific latitude & longitude (a coordinate on Earth) + to a specifc x,y coordinate (a position on your floorplan PDF). + + PDFs are defined in a coordinate system where +y is counter-clockwise of +x + (a.k.a. "a right handed coordinate system"). PDF coordinates. + + @param latitudeLongitude The latitude-longitude coordinate for this anchor. + @param PDFPoint corresponding PDF coordinate. + */ +typedef struct { + CLLocationCoordinate2D latitudeLongitude; + CGPoint PDFPoint; +} AAPLGeoAnchor; + +/** + Defines a pair of \c AAPLGeoAnchors. + + @param fromAnchor starting anchor. + @param toAnchor ending anchor. +*/ +typedef struct { + AAPLGeoAnchor fromAnchor; + AAPLGeoAnchor toAnchor; +} AAPLGeoAnchorPair; + +/** + This class converts PDF coordinates of a floorplan to Geographic coordinates + on Earth. + + @note This class can also be used for any "right-handed" coordinate system + (other than PDF) but should not be used as-is for "Raster image" + coordinates (such as PNGs or JPEGs) because those require left-handed + coordinate frames. There are other reasons we discourage the use of + raster images as indoor floorplans. See the code & comments inside + \c AAPLFloorplanOverlay initWithFloorplanURL for more info. +*/ +@interface AAPLCoordinateConverter : NSObject + +/** + Initializes this class from a given \c AAPLGeoAnchorPair. + + @param anchors the anchors that this class will use for converting. + + @note This is the designated initializer. If you add any other initializers, + make sure to annotate with \c NS_DESIGNATED_INITIALIZER. +*/ +- (instancetype)initWithAnchorPair:(AAPLGeoAnchorPair)anchors; + +/// The \c AAPLGeoAnchorPair used to define this converter. +@property (nonatomic, readonly) AAPLGeoAnchorPair anchors; + +/** + Calculate the \c MKMapPoint from a specific PDF coordinate. + + @param PDFPoint starting point in the PDF. + @return The corresponding \c MKMapPoint. +*/ +- (MKMapPoint)MKMapPointFromPDFPoint:(CGPoint)PDFPoint; + +/** + @return a single \c CGAffineTransform that can transform any \c CGPoint in a + PDF into its corresponding \c MKMapPoint. + + In theory, the following equalities should always hold: + + \code + CGPointApplyAffineTransform(PDFPoint, [self transformerFromPDFToMk]).x === [self MKMapPointFromPDFPoint:PDFPoint].x + + CGPointApplyAffineTransform(PDFPoint, [self transformerFromPDFToMk]).y === [self MKMapPointFromPDFPoint:PDFPoint].y + \endcode + + However, in practice we find that \c MKMapPointFromPDFPoint can be slightly + more accurate than \c transformerFromPDFToMk due to hardware acceleration + and/or numerical precision losses of \c CGAffineTransform operations. +*/ +@property (readonly) CGAffineTransform PDFToMapKitAffineTransform; + +/// @return the size in meters of 1.0 \c CGPoint distance +@property (readonly) CLLocationDistance unitSizeInMeters; + +/** + This coordinate, expressed in latitude & longitude (global coordinates), + corresponds to exactly the same location as \c tangentPDFPoint. +*/ +@property (nonatomic, readonly) CLLocationCoordinate2D tangentLatitudeLongitude; + +/** + This vector, expressed in points (PDF coordinates), has length one meter + and direction due East. +*/ +@property (nonatomic, readonly) CGVector oneMeterEastward; + +/** + This vector, expressed in points (PDF coordinates), has length one meter + and direction due South. +*/ +@property (nonatomic, readonly) CGVector oneMeterSouthward; + +/** + This coordinate, expressed in points (PDF coordinates), corresponds to + exactly the same location as \c tangentLatitudeLongitude. +*/ +@property (nonatomic, readonly) CGPoint tangentPDFPoint; + +/** + Converts each corner of a PDF rectangle into an \c MKMapPoint + (in MapKit space). The collection of \c MKMapPoints is returned as an + \c MKPolygon overlay. + + @param pdfRect A PDF rectangle. + @return the corners of the PDF in an \c MKPolygon (obviously there should be + four points since it's a rectangle). +*/ +- (MKPolygon *)polygonFromPDFRectCorners:(CGRect)PDFRect; + +/** + @return the smallest \c MKMapRect that can show all rotations of the given + PDF rectangle. +*/ +- (MKMapRect)boundingMapRectIncludingRotations:(CGRect)PDFRect; + +/** + @return the \c MKMapCamera heading required to display your PDF (user space) + coordinate system upright so that PDF +x is rightward and PDF +y is upward. +*/ +@property (readonly) CLLocationDirection uprightMKMapCameraHeading; + +@end diff --git a/Footprint/ObjC/Footprint/AAPLCoordinateConverter.m b/Footprint/ObjC/Footprint/AAPLCoordinateConverter.m new file mode 100644 index 00000000..fc51ab38 --- /dev/null +++ b/Footprint/ObjC/Footprint/AAPLCoordinateConverter.m @@ -0,0 +1,327 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This class converts PDF coordinates of a floorplan to Geographic coordinates on Earth. NOTE: This class can also be used for any "right-handed" coordinate system (other than PDF) but should not be used as-is for "Raster image" coordinates (such as PNGs or JPEGs) because those require left-handed coordinate frames. + + There are other reasons we discourage the use of raster images as indoor floorplans. See the code comments inside AAPLFloorplanOverlay initWithFloorplanURL: for more info. +*/ + +#import "AAPLCoordinateConverter.h" +@import MapKit; + +/** + @param a a vector. + @param b a vector. + @return the dot product of the two input vectors. +*/ +static CGFloat AAPLCGVectorDot(CGVector a, CGVector b) { + return a.dx * b.dx + a.dy * b.dy; +} + +/** + @param v the initial vector. + @param scale how much to scale (e.g. 1.0, 1.5, 0.2, etc). + @return a copy of the input vector, rescaled by the amount given. +*/ +static CGVector AAPLCGVectorScaled(CGVector v, CGFloat scale) { + return (CGVector) { + .dx = v.dx * scale, + .dy = v.dy * scale + }; +} + +/** + @param v the initial vector. + @param radians how many radians you want to rotate by. + @return a copy of the input vector, after being rotated in the "positive + radians" direction by the amount given. +*/ +static CGVector AAPLCGVectorRotatedRadians(CGVector v, CGFloat radians) { + CGFloat cosRadians = cos(radians); + CGFloat sinRadians = sin(radians); + + return (CGVector) { + .dx = +cosRadians * v.dx -sinRadians * v.dy, + .dy = +sinRadians * v.dx +cosRadians * v.dy, + }; +} + +/** + @param a Point A. + @param b Point B. + @return The midpoint between A and B. +*/ +static MKMapPoint AAPLMKMapPointMidpoint(MKMapPoint a, MKMapPoint b) { + return (MKMapPoint) { + .x = (a.x + b.x) * 0.5, + .y = (a.y + b.y) * 0.5 + }; +} + +/** + @param a point A. + @param b point B. + @return the mean of the two CGPoint objects. +*/ +static CGPoint AAPLCGPointAverage(CGPoint a, CGPoint b) { + return (CGPoint) { + .x = (a.x + b.x) * 0.5, + .y = (a.y + b.y) * 0.5 + }; +} + +/** + @param a coordinate A. + @param b coordinate B. + @return The distance between the two coordinates in meters. +*/ +static CLLocationDistance AAPLCLDistanceBetweenLocationCoordinates2D(CLLocationCoordinate2D a, CLLocationCoordinate2D b) { + + CLLocation *locA = [[CLLocation alloc]initWithLatitude:a.latitude longitude:a.longitude]; + CLLocation *locB = [[CLLocation alloc]initWithLatitude:b.latitude longitude:b.longitude]; + + return [locA distanceFromLocation:locB]; +} + +/** + Struct that contains a position in meters (east and south) with respect to + an origin position (in geographic space). We use East & South because + \c MKMapPoint's x value is positive in the Eastward direction and + \c MKMapPoint's y value is positive in the Southward direction. + + @param east The distance eastward. + @param south The distance southward. +*/ +typedef struct { + CLLocationDistance east; + CLLocationDistance south; +} AAPLEastSouthDistance; + + +@implementation AAPLCoordinateConverter + +- (instancetype)initWithAnchorPair:(AAPLGeoAnchorPair)anchors { + self = [super init]; + if (self) { + _anchors = anchors; + + /* + Next, to compute the direction between two geographical + co-ordinates, we first need to convert to MapKit coordinates... + */ + MKMapPoint fromAnchorMercatorCoordinate = MKMapPointForCoordinate(anchors.fromAnchor.latitudeLongitude); + MKMapPoint toAnchorMercatorCoordinate = MKMapPointForCoordinate(anchors.toAnchor.latitudeLongitude); + + CGVector pdfDisplacement = { + .dx = anchors.toAnchor.PDFPoint.x - anchors.fromAnchor.PDFPoint.x, + .dy = anchors.toAnchor.PDFPoint.y - anchors.fromAnchor.PDFPoint.y + }; + + // ... so that we can use MapKit's Mercator coordinate system where +x is always eastward and +y is always southward. + + // Imagine an arrow connecting fromAnchor to toAnchor... + double anchorDisplacementMapkitX = (toAnchorMercatorCoordinate.x - fromAnchorMercatorCoordinate.x); + double anchorDisplacementMapkitY = (toAnchorMercatorCoordinate.y - fromAnchorMercatorCoordinate.y); + + /* + What is the angle of this arrow (geographically)? + atan2 always returns: + exactly 0.0 radians if the arrow is exactly in the +x direction + ("MapKit's +x" is due East). + positive radians as the arrow is rotated toward and through the + +y direction ("MapKit's +y" is due South). + In the case of MapKit, this is radians clockwise from due East. + */ + float radiansClockwiseOfDueEast = atan2(anchorDisplacementMapkitY, anchorDisplacementMapkitX); + + /* + That means if we rotate cgDisplacement COUNTER-clockwise by this + value, it will be facing due east. In the CG coordinate frame, + positive radians is counter-clockwise because in a PDF +x is + rightward and +y is upward. + */ + CGVector cgDueEast = AAPLCGVectorRotatedRadians(pdfDisplacement, radiansClockwiseOfDueEast); + + // Now, get the distance (in meters) between the two anchors... + CLLocationDistance distanceBetweenAnchorsMeters = AAPLCLDistanceBetweenLocationCoordinates2D(anchors.fromAnchor.latitudeLongitude, anchors.toAnchor.latitudeLongitude); + + // ...and rescale so that it's exactly one meter in length. + _oneMeterEastward = AAPLCGVectorScaled(cgDueEast, 1.0 / distanceBetweenAnchorsMeters); + + /* + Lastly, due south is PI/2 clockwise of due east. + In the CG coordinate frame, clockwise rotation is NEGATIVE radians + because in a PDF +x is rightward and +y is upward. + */ + _oneMeterSouthward = AAPLCGVectorRotatedRadians(_oneMeterEastward, -M_PI_2); + + /* + We'll choose the midpoint between the two anchors to be our "tangent + point". This is the MKMapPoint that will correspond to both + _tangentLatitudeLongitude on Earth and _tangentPDFPoint in the PDF. + */ + MKMapPoint tangentMercatorCoordinate = AAPLMKMapPointMidpoint(fromAnchorMercatorCoordinate, toAnchorMercatorCoordinate); + + _tangentLatitudeLongitude = MKCoordinateForMapPoint(tangentMercatorCoordinate); + + _tangentPDFPoint = AAPLCGPointAverage(anchors.fromAnchor.PDFPoint, anchors.toAnchor.PDFPoint); + + } + + return self; +} + +- (MKMapPoint)MKMapPointFromPDFPoint:(CGPoint)PDFPoint { + /* + To perform this conversion, we start by seeing how far we are from the + tangentPoint. The tangentPoint is the one place on the PDF where we know + exactly the corresponding Earth latitude & lontigude. + */ + CGVector displacementFromTangentPoint = CGVectorMake(PDFPoint.x - self.tangentPDFPoint.x, PDFPoint.y - self.tangentPDFPoint.y); + + // Now, let's figure out how far East & South we are from this tangentPoint. + + CGFloat dotProductEast = AAPLCGVectorDot(displacementFromTangentPoint, _oneMeterEastward); + CGFloat dotProductSouth = AAPLCGVectorDot(displacementFromTangentPoint, _oneMeterSouthward); + + AAPLEastSouthDistance eastSouthDistanceMeters = { + // How many meters Eastward is cgPoint from tangentPoint? + .east = dotProductEast / AAPLCGVectorDot(_oneMeterEastward, _oneMeterEastward), + + // How many meters Southward is cgPoint from tangentPoint? + .south = dotProductSouth / AAPLCGVectorDot(_oneMeterSouthward, _oneMeterSouthward) + }; + + + CLLocationDistance metersPerMapPoint = MKMetersPerMapPointAtLatitude(self.tangentLatitudeLongitude.latitude); + MKMapPoint tangentMercatorCoordinate = MKMapPointForCoordinate(self.tangentLatitudeLongitude); + + /* + Each meter is about (1.0 / metersPerMapPoint) MKMapPoints, as long as we + are nearby _tangentLatitudeLongitude. So just move this many meters East + and South and we're done! + */ + MKMapPoint result = { + .x = tangentMercatorCoordinate.x + eastSouthDistanceMeters.east / metersPerMapPoint, + .y = tangentMercatorCoordinate.y + eastSouthDistanceMeters.south / metersPerMapPoint, + }; + + return result; + +} + +- (CGAffineTransform)PDFToMapKitAffineTransform { + CLLocationDistance metersPerMapPoint = MKMetersPerMapPointAtLatitude(self.tangentLatitudeLongitude.latitude); + MKMapPoint tangentMercatorCoordinate = MKMapPointForCoordinate(self.tangentLatitudeLongitude); + + /* + CGAffineTransform operations easier to construct in reverse-order. + Start with the last operation: + */ + CGAffineTransform resultOfTangentMercatorCoordinate = CGAffineTransformMakeTranslation(tangentMercatorCoordinate.x, tangentMercatorCoordinate.y); + + /* + Revise the CGAffineTransform to first scale by + (1.0 / metersPerMapPoint), and then perform the above translation. + */ + CGAffineTransform resultOfEastSouthDistanceMeters = CGAffineTransformScale(resultOfTangentMercatorCoordinate, 1.0 / metersPerMapPoint, 1.0 / metersPerMapPoint); + + /* + Revise the AffineTransform to first scale by + (1.0 / AAPLCGVectorDot(...)) before performing the transform so far. + */ + CGAffineTransform resultOfDotProduct = + CGAffineTransformScale( + resultOfEastSouthDistanceMeters, + 1.0 / AAPLCGVectorDot(_oneMeterEastward, _oneMeterEastward), + 1.0 / AAPLCGVectorDot(_oneMeterSouthward, _oneMeterSouthward) + ); + + /* + Revise the affine transform to first perform dot products against our + reference vectors before performing the transform so far. + */ + CGAffineTransform resultOfDisplacementFromTangentPoint = + CGAffineTransformConcat( + CGAffineTransformMake( + _oneMeterEastward.dx, _oneMeterEastward.dy, + _oneMeterSouthward.dx, _oneMeterSouthward.dy, + 0.0, 0.0 + ), + resultOfDotProduct + ); + + /* + Lastly, revise the CGAffineTransform to first perform the initial + subtraction before performing the remaining operations. + + Each meter is about (1.0 / metersPerMapPoint) MKMapPoints, as + long as we are nearby _tangentLatitudeLongitude. + */ + return CGAffineTransformTranslate( + resultOfDisplacementFromTangentPoint, + - self.tangentPDFPoint.x, + - self.tangentPDFPoint.y + ); +} + +- (CLLocationDistance)unitSizeInMeters { + return 1.0 / hypot(_oneMeterEastward.dx, _oneMeterEastward.dy); +} + +- (MKPolygon *)polygonFromPDFRectCorners:(CGRect)pdfRect { + MKMapPoint corners[4]; + corners[0] = [self MKMapPointFromPDFPoint:CGPointMake(CGRectGetMaxX(pdfRect), CGRectGetMaxY(pdfRect))]; + corners[1] = [self MKMapPointFromPDFPoint:CGPointMake(CGRectGetMinX(pdfRect), CGRectGetMaxY(pdfRect))]; + corners[2] = [self MKMapPointFromPDFPoint:CGPointMake(CGRectGetMinX(pdfRect), CGRectGetMinY(pdfRect))]; + corners[3] = [self MKMapPointFromPDFPoint:CGPointMake(CGRectGetMaxX(pdfRect), CGRectGetMinY(pdfRect))]; + return [MKPolygon polygonWithPoints:corners count:4]; +} + +- (MKMapRect)boundingMapRectIncludingRotations:(CGRect)rect { + + // Start with the nominal rendering box for this rect is. + MKMapRect nominalRenderingRect = [self polygonFromPDFRectCorners:rect].boundingMapRect; + + /* + In order to account for all rotations, any bounding map rect must have + diameter equal to the longest distance inside the rectangle. + */ + double boundsDiameter = hypot(nominalRenderingRect.size.width, nominalRenderingRect.size.height); + + CGPoint rectCenterPoints = { + .x = CGRectGetMidX(rect), + .y = CGRectGetMidY(rect) + }; + + MKMapPoint boundsCenter = [self MKMapPointFromPDFPoint:rectCenterPoints]; + + /* + Return a square MKMapRect centered at boundsCenterMercator with edge + length diameterMercator. + */ + return MKMapRectMake( + boundsCenter.x - boundsDiameter / 2.0, + boundsCenter.y - boundsDiameter / 2.0, + boundsDiameter, + boundsDiameter); +} + +- (CLLocationDirection)uprightMKMapCameraHeading { + /* + To make the floorplan upright, we want to rotate the floorplan +x vector + toward due east. + */ + CGFloat resultRadians = atan2(_oneMeterEastward.dy, _oneMeterEastward.dx); + CLLocationDirection result = resultRadians * 180.0 / M_PI; + + /* + According to the CLLocationDirection documentation we must store a + positive value if it is valid. + */ + return (result < 0.0) ? (result + 360.0) : result; +} + +@end diff --git a/Footprint/ObjC/Footprint/AAPLFloorplanOverlay.h b/Footprint/ObjC/Footprint/AAPLFloorplanOverlay.h new file mode 100644 index 00000000..b24113c0 --- /dev/null +++ b/Footprint/ObjC/Footprint/AAPLFloorplanOverlay.h @@ -0,0 +1,135 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This class describes a floorplan for an indoor venue. +*/ + +#import "AAPLMKMapRectRotated.h" +#import "AAPLCoordinateConverter.h" + +@import MapKit; + +/// This class describes a floorplan for an indoor venue. +@interface AAPLFloorplanOverlay : NSObject + +/** + Same as boundingMapRect but slightly larger to fit on-screen under any \c MKMapCamera + rotation. +*/ +@property (nonatomic, readonly) MKMapRect boundingMapRectIncludingRotations; + +/** + Cache the \c CGAffineTransform used to help draw the floorplan to the screen + inside an \c MKMapView. +*/ +@property (nonatomic, readonly) CGAffineTransform transformerFromPDFToMk; + +/// Current floor level. +@property (nonatomic, readonly) NSInteger floorLevel; + +/** + Reference to the internal page data of the selected page of the PDF you are + drawing. It is very likely that the PDF of your floorplan is a single page. + */ +@property (nonatomic, readonly) CGPDFPageRef PDFPage; + +/** + Same as \c boundingMapRect, but more precise. The \c AAPLMapRectRotated you'll + get here fits snugly accounting for the rotation of the floorplan (relative + to North) whereas the \c boundingMapRect must be "North-aligned" since it's + an \c MKMapRect. If you're still not 100% sure, toggle the "debug switch" in + the sample code and look at the overlays that are drawn. +*/ +@property (nonatomic, readonly) AAPLMKMapRectRotated floorplanPDFBox; + +/// For debugging, remember the PDF page box selected at initialization. +@property (nonatomic, readonly) CGRect PDFBoxRect; + +/// \c MKOverlay protocol return values. +@property (nonatomic, readonly) MKMapRect boundingMapRect; +@property (nonatomic, readonly) CLLocationCoordinate2D coordinate; + +/** + The coordinate converter for converting between PDF coordinates (point) + and MapKit coordinates (\c MKMapPoint). +*/ +@property (nonatomic, readonly) AAPLCoordinateConverter *coordinateConverter; + +/** + In this example, our floorplan is described by four things. + 1. The URL of a PDF. This is the visual data for the floorplan itself. + 2. The PDF page box to draw. This tells us which section of the PDF we + will actually draw. + 3. A pair of anchors. This tells us where the floorplan appears in + the real world. + 4. A floor level. This tells us which floor our floorplan represents. + + @param floorplanURL the path to a PDF containing the floorplan drawing. + @param PDFBox which section of the PDF do we draw? + @param anchors real-world anchors of this floorplan -- opposite corners. + @param forFloorAtLevel which floor is it on? + + @note This is the designated initializer. If you add any other initializers, + make sure to annotate with \c NS_DESIGNATED_INITIALIZER. +*/ +- (instancetype)initWithFloorplanURL:(NSURL *)floorplanURL PDFBox:(CGPDFBox)PDFBox anchors:(AAPLGeoAnchorPair)anchors forFloorAtLevel:(NSInteger)level; + +/** + This is different from \c AAPLCoordinateConverter + \c getUprightMKMapCameraHeading because here we also account for the PDF + Page Dictionary's Rotate entry. + + @return the \c MKMapCamera heading needed to display your floorplan upright. +*/ +@property (readonly) CLLocationDirection floorplanUprightMKMapCameraHeading; + +/** + Create an \c MKPolygon overlay given a custom \c CGPath (whose coordinates + are specified in the PDF points). + + @param pdfPath an array of \c CGPoint, each element is a PDF coordinate + along the path. + @return A closed MapKit polygon made up of the points in the PDF path. +*/ +- (MKPolygon *)polygonFromCustomPDFPath:(CGPoint *)pdfPath count:(size_t)count; + +/** + @return For debugging, you may want to draw the reference anchors that + define this floor's coordinate converter. +*/ +@property (readonly) AAPLGeoAnchorPair anchors; + +/** + @return For debugging, you may want to draw the the (0.0, 0.0) point of + the PDF. +*/ +@property (readonly) MKMapPoint PDFOrigin; + +/** + @return For debugging, you may want to know the real-world coordinates of + the PDF page box. +*/ +@property (readonly, strong) MKPolygon *polygonFromFloorplanPDFBoxCorners; + +/** + @return For debugging, you may want to have the \c boundingMapRect in the + form of an \c MKPolygon overlay. +*/ +@property (readonly, strong) MKPolygon *polygonFromBoundingMapRect; + +/** + @return For debugging, you may want to have the + \c boundingMapRectIncludingRotations in the form of + an \c MKPolygon overlay. +*/ +@property (readonly, strong) MKPolygon *polygonFromBoundingMapRectIncludingRotations; + +/** + @return For debugging, you may want to know the real-world meters size of + one PDF "point" distance. +*/ +@property (readonly) CLLocationDistance PDFPointSizeInMeters; + +@end diff --git a/Footprint/ObjC/Footprint/AAPLFloorplanOverlay.m b/Footprint/ObjC/Footprint/AAPLFloorplanOverlay.m new file mode 100644 index 00000000..f127dde7 --- /dev/null +++ b/Footprint/ObjC/Footprint/AAPLFloorplanOverlay.m @@ -0,0 +1,208 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This class describes a floorplan for an indoor venue. +*/ + +#import "AAPLFloorplanOverlay.h" + +/** + @return The point at the center of the rectangle. + @param rect A rectangle. +*/ +static MKMapPoint AAPLMKMapRectGetCenter(MKMapRect rect) { + return MKMapPointMake(MKMapRectGetMidX(rect), MKMapRectGetMidY(rect)); +} + +/** + @param rect a rectangle + @return an \c MKMapRect converted to an \c MKPolygon. +*/ +static MKPolygon *polygonFromMapRect(MKMapRect rect) { + MKMapPoint corners[4]; + corners[0] = MKMapPointMake(MKMapRectGetMaxX(rect), MKMapRectGetMaxY(rect)); + corners[1] = MKMapPointMake(MKMapRectGetMinX(rect), MKMapRectGetMaxY(rect)); + corners[2] = MKMapPointMake(MKMapRectGetMinX(rect), MKMapRectGetMinY(rect)); + corners[3] = MKMapPointMake(MKMapRectGetMaxX(rect), MKMapRectGetMinY(rect)); + + return [MKPolygon polygonWithPoints:corners count:4]; +} + +@implementation AAPLFloorplanOverlay { + /// The PDF document to be rendered. + CGPDFDocumentRef _PDFDoc; +} + +- (instancetype)initWithFloorplanURL:(NSURL *)floorplanURL PDFBox:(CGPDFBox)pdfBox anchors:(AAPLGeoAnchorPair) anchors forFloorAtLevel:(NSInteger)level { + + // We only support PDF floorplans at this time. + NSAssert([[floorplanURL absoluteString] hasSuffix:@"pdf"], @"Sanity check: The URL should point to a PDF file."); + + /* + Using raster images (such as PNG or JPEG) would create a number of + complications, such as: + + you need multiple sizes of each image, and each would need its own + AAPLGeoAnchorPair (see "Icon and Image Sizes" in MobileHIG + for more). + + raster/bitmap images use a different coordinate system than PDFs do, + so the code from AAPLCoordinateConverter could not be used + out-of-the-box. Instead, you would need a separate implementation of + AAPLCoordinateConverter that works for left-handed coordinate + frames. PDFs use a right-handed coordinate frame. + + text and fine details of raster images may not render as clearly as + vector images when zoomed in. PDF is primarily a vector image format. + + some raster image formats, such as JPEG, are designed for photographs + and may suffer from loss of detail due to compression artifacts when + being used for floorplans. + */ + + self = [super init]; + if (self) { + _coordinateConverter = [[AAPLCoordinateConverter alloc] initWithAnchorPair:anchors]; + _transformerFromPDFToMk = _coordinateConverter.PDFToMapKitAffineTransform; + _floorLevel = level; + + /* + Read the PDF file from disk into memory. Remember to CFRelease it + when we dealloc. + (see "The Create Rule" CFMemoryMgmt for more). + */ + _PDFDoc = CGPDFDocumentCreateWithURL((__bridge CFURLRef)floorplanURL); + + /* + In this example the floorplan PDF has only one page, so we pick + "page 1" of the PDF. + */ + _PDFPage = CGPDFDocumentGetPage(_PDFDoc, 1); + + // Figure out which region of the PDF is to be drawn. + _PDFBoxRect = CGPDFPageGetBoxRect(_PDFPage, pdfBox); + + MKPolygon * polygonFromPDFRectCorners = [_coordinateConverter polygonFromPDFRectCorners:_PDFBoxRect]; + + /* + There is no need to display this floorplan if your MapView camera is + beyond the four corners of the PDF page box. + Thus, our boundingMapRect is based on the PDF page box corners. + */ + _boundingMapRect = polygonFromPDFRectCorners.boundingMapRect; + + /* + We need a quick way to check whether your screen is currently + looking inside vs. outside the floorplan, in order to "clamp" your + MKMapView. + */ + assert(polygonFromPDFRectCorners.pointCount == 4); + _floorplanPDFBox = AAPLMKMapRectRotatedMake(polygonFromPDFRectCorners.points[0], + polygonFromPDFRectCorners.points[1], + polygonFromPDFRectCorners.points[2], + polygonFromPDFRectCorners.points[3] ); + + /* + For the purposes of clamping MKMapCamera zoom, we need a slightly + padded MKMapRect that allows the entire floorplan can be visible + regardless of camera rotation. + + Otherwise, depending on the MKMapCamera rotation, auto-zoom might + prevent the user from zooming out far enough to see the entire + floorplan and/or auto-scroll might prevent the user from seeing the + edge of the floorplan. + */ + _boundingMapRectIncludingRotations = [_coordinateConverter boundingMapRectIncludingRotations:_PDFBoxRect]; + + // For self.coordinate just return the centroid of self.boundingMapRect. + _coordinate = MKCoordinateForMapPoint(AAPLMKMapRectGetCenter(_boundingMapRect)); + + } + return self; +} + +-(void)dealloc { + /* + We are about to CFRelease _PDFDoc further below. + Once that happens _PDFPage will no longer be valid, so let's clear it. + */ + _PDFPage = nil; + + /* + The only non Objective-C "Create" call in our designated initializer is + _pdfDoc = CGPDFDocumentCreateWithURL(...) + so remember to release it here. + */ + if (_PDFDoc) { + CFRelease(_PDFDoc); + } +} + +- (CLLocationDirection)floorplanUprightMKMapCameraHeading { + /* + Applying this heading to the MKMapCamera will cause PDF +x to face + MapKit +x + */ + CLLocationDirection rotatePdfXToMapKitX = self.coordinateConverter.uprightMKMapCameraHeading; + + /* + If a PDF Page Dictionary contains the "Rotate" entry, it is a request to + the reader to rotate the _printed_ page *clockwise* by the given number + of degrees before reading it. + */ + int PDFPageDictionaryRotationEntryDegrees = CGPDFPageGetRotationAngle(_PDFPage); + + /* + In the MapView world that is equivalent to subtracting that amount from + the MKMapCamera heading. + */ + CLLocationDirection result = rotatePdfXToMapKitX - PDFPageDictionaryRotationEntryDegrees; + + /* + According to the CLLocationDirection documentation we must store a + positive value if it is valid. + */ + return ((result < 0.0) ? (result + 360.0) : result); +} + +- (MKPolygon *)polygonFromCustomPDFPath:(CGPoint *)pdfPath count:(size_t)count { + // Create a temporary buffer. + MKMapPoint *coords = calloc(count, sizeof(MKMapPoint)); + + // Calculate the corresponding MKMapPoint for each PDF point. + for (size_t i=0; i max) ? max : val); +} + +/** + @param a Point A. + @param b Point B. + + @return An \c MKMapPoint object representing the midpoints of \c a and \c b +*/ +static MKMapPoint AAPLMKMapPointMidpoint(MKMapPoint a, MKMapPoint b) { + return (MKMapPoint) { + .x = (a.x + b.x) * 0.5, + .y = (a.y + b.y) * 0.5 + }; +} + +/** + @param to ending point. + @param from starting point. + + @return The displacement between two MKMapPoint objects. +*/ +static AAPLMKMapPointDisplacement AAPLMKMapPointSubtract(MKMapPoint to, MKMapPoint from) { + double dx = to.x - from.x; + double dy = to.y - from.y; + + double distance = hypot(dx, dy); + + return (AAPLMKMapPointDisplacement) { + .direction = (AAPLMKMapDirection) { + .eX = dx / distance, + .eY = dy / distance + }, + .distance = distance + }; +} + +AAPLMKMapRectRotated AAPLMKMapRectRotatedMake(MKMapPoint corner1, MKMapPoint corner2, MKMapPoint corner3, MKMapPoint corner4) { + + // Avg the points to get the center of the rectangle in MKMapPoint space. + MKMapPoint center = { + .x = (corner1.x + corner2.x + corner3.x + corner4.x) / 4.0, + .y = (corner1.y + corner2.y + corner3.y + corner4.y) / 4.0 + }; + + // Figure out the "width direction" and "height direction"... + MKMapPoint heightMax = AAPLMKMapPointMidpoint(corner1, corner2); + MKMapPoint heightMin = AAPLMKMapPointMidpoint(corner4, corner3); + MKMapPoint widthMax = AAPLMKMapPointMidpoint(corner1, corner4); + MKMapPoint widthMin = AAPLMKMapPointMidpoint(corner2, corner3); + + // ...as well as the actual width and height. + AAPLMKMapPointDisplacement width = AAPLMKMapPointSubtract(widthMax, widthMin); + AAPLMKMapPointDisplacement height = AAPLMKMapPointSubtract(heightMax, heightMin); + + return (AAPLMKMapRectRotated) { + .rectCenter = center, + .rectSize = (MKMapSize) { + .width = width.distance, + .height = height.distance + }, + .widthDirection = width.direction, + .heightDirection = height.direction + }; +} + +MKMapPoint AAPLMKMapRectRotatedNearestPoint(AAPLMKMapRectRotated mapRectRotated, MKMapPoint point) { + double dxCenter = (point.x - mapRectRotated.rectCenter.x); + double dyCenter = (point.y - mapRectRotated.rectCenter.y); + + /* + We use a dot product against a unit vector (a.k.a. projection) to find + distance "along a particular direction." + */ + double widthDistance = dxCenter * mapRectRotated.widthDirection.eX + + dyCenter * mapRectRotated.widthDirection.eY; + + /* + We use a dot product against a unit vector (a.k.a. projection) to find + distance "along a particular direction." + */ + double heightDistance = dxCenter * mapRectRotated.heightDirection.eX + + dyCenter * mapRectRotated.heightDirection.eY; + + // "If this rectangle _were_ upright, this would be the result." + double widthNearestPoint = clamp(widthDistance, -0.5 * mapRectRotated.rectSize.width, 0.5 * mapRectRotated.rectSize.width); + double heightNearestPoint = clamp(heightDistance, -0.5 * mapRectRotated.rectSize.height, 0.5 * mapRectRotated.rectSize.height); + + /* + Since it's not upright, just combine the width and height in their corresponding + directions! + */ + return (MKMapPoint) { + .x = mapRectRotated.rectCenter.x + widthNearestPoint * mapRectRotated.widthDirection.eX + heightNearestPoint * mapRectRotated.heightDirection.eX, + .y = mapRectRotated.rectCenter.y + widthNearestPoint * mapRectRotated.widthDirection.eY + heightNearestPoint * mapRectRotated.heightDirection.eY + }; +} diff --git a/Footprint/ObjC/Footprint/AAPLViewController.h b/Footprint/ObjC/Footprint/AAPLViewController.h new file mode 100644 index 00000000..53d0127e --- /dev/null +++ b/Footprint/ObjC/Footprint/AAPLViewController.h @@ -0,0 +1,24 @@ +/** + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Primary view controller for what is displayed by the application. In this class we configure an MKMapView to display a floorplan, recieve location updates to determine floor number, as well as provide a few helpful debugging annotations. + + We will also show how to highlight a region that you have defined in PDF coordinates but not Latitude Longitude. +*/ + +@import UIKit; + +/** + Primary view controller for what is displayed by the application. + + In this class we configure an \c MKMapView to display a floorplan, recieve + location updates to determine floor number, as well as provide a few helpful + debugging annotations. + + We will also show how to highlight a region that you have defined in PDF + coordinates but not Latitude & Longitude. +*/ +@interface AAPLViewController : UIViewController +@end diff --git a/Footprint/ObjC/Footprint/AAPLViewController.m b/Footprint/ObjC/Footprint/AAPLViewController.m new file mode 100644 index 00000000..86b0e7e7 --- /dev/null +++ b/Footprint/ObjC/Footprint/AAPLViewController.m @@ -0,0 +1,835 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Primary view controller for what is displayed by the application. In this class we configure an MKMapView to display a floorplan, recieve location updates to determine floor number, as well as provide a few helpful debugging annotations. + + We will also show how to highlight a region that you have defined in PDF coordinates but not Latitude Longitude. +*/ + +#import "AAPLViewController.h" +#import "AAPLCoordinateConverter.h" +#import "AAPLFloorplanOverlay.h" +#import "AAPLFloorplanOverlayRenderer.h" +#import "AAPLHideBackgroundOverlay.h" + +#define USE_DEBUG_ANNOTATIONS + +#define AAPL_HIGHLIGHT_REGION_COUNT 3 + +@import MapKit; + +/** + This class manages an \c MKMapView camera scroll & zoom by implementing the + typical \c MKMapViewDelegate \c regionDidChangeAnimated and + \c regionWillChangeAnimated to add bounce-back when the user scrolls/zooms + away from the floorplan. + */ +@interface AAPLVisibleMapRegionDelegate : NSObject + +/* + Keep track of changes to [mapView camera].altitude so that we know + whether to auto-zoom or auto-scroll. + */ +@property (nonatomic) CLLocationDistance lastAltitude; + +// Properties of the floorplan. See AAPLFloorplanOverlay for more. +@property (nonatomic) MKMapRect boundingMapRectIncludingRotations; +@property (nonatomic) AAPLMKMapRectRotated boundingPDFBox; +@property (nonatomic) CLLocationCoordinate2D floorplanCenter; +@property (nonatomic) CLLocationDirection floorplanUprightMKMapCameraHeading; + +- (instancetype)initWithFloorplanBounds:(MKMapRect)boundingMapRectWithRotations pdfBoundingBox:(AAPLMKMapRectRotated)pdfBoundingBox centerOfFloorplan:(CLLocationCoordinate2D)centerOfFloorplan floorplanUprightMKMapCameraHeading:(CLLocationDirection)heading; + +- (void)mapViewResetCameraToFloorplan:(MKMapView *)mapView; + +- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated; + +@end + + +@interface AAPLViewController () + +/// Outlet for the map view in the storyboard. +@property (nonatomic, weak) IBOutlet MKMapView *mapView; + +/// Outlet for the debug visuals switch at the lower-right of the storyboard. +@property (weak, nonatomic) IBOutlet UISwitch *debugVisualsSwitch; + +/** + To enable user location to be shown in the map, go to Main.storyboard, + select the Map View, open its Attribute Inspector and click the checkbox + next to User Location + + The user will need to authorize this app to use their location either by + enabling it in Settings or by selecting the appropriate option when + prompted. + */ +@property (nonatomic, strong) CLLocationManager *locationManager; + +/** + This is the alpha value we'll use for the White overlay that hides the + underlying Apple base map tiles. + */ +@property (nonatomic) CGFloat hideBackgroundOverlayAlpha; + +/// Helper class for managing the scroll & zoom of the MapView camera. +@property (nonatomic, strong) AAPLVisibleMapRegionDelegate *visibleMapRegionDelegate; + +/// Store the data about our floorplan here. +@property (nonatomic, strong) AAPLFloorplanOverlay *floorplan0; + +/// This property remembers which floor we're on. See documentation for CLFloor. +@property (strong) CLFloor *lastFloor; + +#ifdef USE_DEBUG_ANNOTATIONS +@property (nonatomic, strong, nonnull) NSArray> *debuggingOverlays; +@property (nonatomic, strong, nonnull) NSArray> *debuggingAnnotations; + +/** + Set to NO if you want to turn off \c AAPLFloorplanOverlayRenderer's + diagnostic visuals. +*/ +@property (nonatomic) BOOL showsPDFDiagnosticVisuals; +#endif + +/** + Set to NO if you want to turn off auto-scroll & auto-zoom that snaps to the + floorplan in case you scroll or zoom too far away. +*/ +@property (nonatomic) BOOL snapsMapViewToFloorplan; + +/** + Set to YES when we reveal the MapKit tileset (by pressing the + X button). +*/ +@property (nonatomic) BOOL mapKitTilesetRevealed; + +/// Call this to reset the camera. +- (IBAction)resetCamera:(id)sender; + +/** + When the X icon hasn't yet been pressed, this toggles the debug visuals. + Otherwise, this toggles the floorplan. +*/ +- (IBAction)toggleDebugVisuals:(id)sender; + +/** + Remove all the overlays except for the debug visuals. Forces the debug + visuals switch off. +*/ +- (IBAction)revealMapKitTileset:(id)sender; + +/** + If you have set up your anchors correctly, this function will create: + 1. a red pin at the location of your \c fromAnchor. + 2. a green pin at the location of your \c toAnchor. + 3. a purple pin at the location of the PDF's internal origin. + + Use these pins to: + * Compare the location of pins #1 and #2 with the underlying Apple Maps + tiles. + + The pins should appear, on the real world, in the physical + locations corresponding to the landmarks that you chose for each + anchor. + + If either pin does not seem to be at the correct position on Apple + Maps, double-check for typos in the \c CLLocationCoordinate2D + values of your \c AAPLGeoAnchor struct. + * Compare the location of pins #1 and #2 with the matching colored + squares drawn by AAPLFloorplanOverlayRenderer.m:drawDiagnosticVisuals + on your floorplan overlay. + + The red pin should appear at the same location as the red square; + the green pin should appear at the same location as the green + square. + + If either pin does not match the location of its corresponding + square, you may be having problems with coordinate conversion + accuracy. Try picking anchor points that are further apart. + + @param mapView MapView to draw on. + @param aboutFloorplan floorplan from which we get anchors and coordinates. +*/ ++ (nonnull NSArray> *)createDebuggingAnnotationsForMapView:(MKMapView *)mapView aboutFloorplan:(AAPLFloorplanOverlay *)floorplan; + + +/** + Return an NSArray of three debugging overlays. These overlays will show: + 1. the PDF Page Box that was selected for this floor. + 2. the \c boundingMapRect used to define the rendering of this floorplan + by \c MKMapView. + 3. the \c boundingMapRectIncludingRotations used to define the rendering + of this floorplan. + + Use these outlines to: + * Ensure that #1 shows a polygon that is just small enough to enclose + all of the important visual content in your floorplan. + + If this polygon is much larger than your floorplan, you may + experience runtime performance issues. In this case it's better + to choose or define a smaller PDF Page Box. + + * Ensure that #2 shows a polygon that encloses your floorplan exactly. + + If any important visual floorplan information is outside this + polygon, those parts of the floorplan might not be displayed to + the user, depending on their zoom & scrolling. In this case it's + better to choose or define a larger PDF Page Box. + + * Ensure that #3 shows a polygon that is large enough to contain your + floorplan comfortably, but still small enough to cause bounce-back + when the user scrolls/zooms out too far. + + The \c boundingMapRect is based on the PDF Page Box, so the best + way to adjust the \c boundingMapRect is to get a more accurate + PDF Page Box. + + Note: In this sample code app we use the \c boundingMapRect also + to determine the limits where zoom/scroll bounce-back takes + place. + + For more information, see enum \c CGPDFBox and + \c AAPLFloorplanOverlay \c initWithFloorplanURL:... \c PDFBox: +*/ ++ (nonnull NSArray> *)createDebuggingOverlaysForMapView:(MKMapView *)mapView aboutFloorplan:(AAPLFloorplanOverlay *)floorplan; + +@end + +#pragma mark AAPLViewController + +@implementation AAPLViewController + +- (IBAction)resetCamera:(id)sender { + [self.visibleMapRegionDelegate mapViewResetCameraToFloorplan:self.mapView]; +} + +- (IBAction)toggleDebugVisuals:(id)sender { + if (![sender isKindOfClass:[UISwitch class]]) { + return; + } + + UISwitch *senderSwitch = (UISwitch *)sender; + + if (self.mapKitTilesetRevealed) { + if (senderSwitch.isOn) { + [self showFloorplan]; + } + else { + [self hideFloorplan]; + } + } + else { + if (senderSwitch.isOn) { + [self showDebugVisuals]; + } + else { + [self hideDebugVisuals]; + } + } +} + +- (IBAction)revealMapKitTileset:(id)sender { + [self.mapView removeOverlays:self.mapView.overlays]; + [self.mapView removeAnnotations:self.debuggingAnnotations]; + + // Show labels for restaurants, schools, etc. + self.mapView.showsPointsOfInterest = YES; + + // Show building outlines. + self.mapView.showsBuildings = YES; + self.mapKitTilesetRevealed = YES; + + // Set switch to off. + [self.debugVisualsSwitch setOn:NO animated:YES]; + [self showDebugVisuals]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.mapKitTilesetRevealed = NO; + + self.locationManager = [[CLLocationManager alloc] init]; + + // === Configure our floorplan + + /* + We setup a pair of anchors that will define how the "floorplan image" + maps to geographic co-ordinates. + */ + AAPLGeoAnchor anchor1 = { + .latitudeLongitude = CLLocationCoordinate2DMake(37.770419, -122.465726), + .PDFPoint = CGPointMake(26.2, 86.4) + }; + + AAPLGeoAnchor anchor2 = { + .latitudeLongitude = CLLocationCoordinate2DMake(37.769288, -122.466376), + .PDFPoint = CGPointMake(570.1, 317.7) + }; + + AAPLGeoAnchorPair anchorPair = { + .fromAnchor = anchor1, + .toAnchor = anchor2 + }; + + /* + Pick a triangle on your PDF that you would like to highlight in yellow. + Feel free to try regions with more than three edges, up to you. + */ + CGPoint pdfTriangleRegionToHighlight[AAPL_HIGHLIGHT_REGION_COUNT] = { + /* + Note that these coordinates are given in PDF coordinates, but they + will show up on just fine on MapKit in MapKit coordinates. + */ + CGPointMake(205.0, 335.3), + CGPointMake(205.0, 367.3), + CGPointMake(138.5, 367.3) + }; + + // === Initialize our assets + + /* + We have to specify subdirectory here since we copy our folder reference + during "Copy Bundle Resources" section under target settings build + phases. + */ + NSURL *pdfUrl = [[NSBundle mainBundle] URLForResource:@"floorplan_overlay_floor0" withExtension:@"pdf" subdirectory:@"Floorplans"]; + + self.floorplan0 = [[AAPLFloorplanOverlay alloc] initWithFloorplanURL:pdfUrl PDFBox:kCGPDFTrimBox anchors:anchorPair forFloorAtLevel:0]; + + self.visibleMapRegionDelegate = + [[AAPLVisibleMapRegionDelegate alloc] + initWithFloorplanBounds:self.floorplan0.boundingMapRectIncludingRotations + pdfBoundingBox:self.floorplan0.floorplanPDFBox + centerOfFloorplan:self.floorplan0.coordinate + floorplanUprightMKMapCameraHeading:self.floorplan0.floorplanUprightMKMapCameraHeading + ]; + +#ifdef USE_DEBUG_ANNOTATIONS + // The following are provided for debugging. + self.debuggingOverlays = [AAPLViewController createDebuggingOverlaysForMapView:self.mapView aboutFloorplan:self.floorplan0]; + self.debuggingAnnotations = [AAPLViewController createDebuggingAnnotationsForMapView:self.mapView aboutFloorplan:self.floorplan0]; +#endif + + // Turn on AAPLFloorplanOverlayRenderer's diagnostic visuals. + self.showsPDFDiagnosticVisuals = YES; + + // === Initialize our view + + self.hideBackgroundOverlayAlpha = 1.0; + // disable tileset. + [self.mapView addOverlay:[AAPLHideBackgroundOverlay hideBackgroundOverlay] level:MKOverlayLevelAboveRoads]; + + // Draw the floorplan! + [self.mapView addOverlay:self.floorplan0]; + + // Highlight our region (originally specified in PDF coordinates) in yellow! + MKPolygon *customHighlightRegion = [self.floorplan0 polygonFromCustomPDFPath:pdfTriangleRegionToHighlight count:AAPL_HIGHLIGHT_REGION_COUNT]; + customHighlightRegion.title = @"Hello World"; + customHighlightRegion.subtitle = @"This custom region will be highlighted in Yellow!"; + [self.mapView addOverlay:customHighlightRegion]; + + /* + By default, we listen to the scroll & zoom events to make sure that if + the user scrolls/zooms too far away from the floorplan, we automatically + bounce back. + + To disable this behavior, comment out the following line. + */ + self.snapsMapViewToFloorplan = YES; +} + +- (void)viewDidAppear:(BOOL)animated { + /* + For additional debugging, you may prefer to use non-satellite (standard) + view instead of satellite view. If so, uncomment the line below. + + However, satellite view allows you to zoom in more closely than + non-satellite view so you probably do not want to leave it this way + in production. + */ + //self.mapView.mapType = MKMapTypeStandard; +} + +- (BOOL)shouldAutorotate { + return NO; +} + +/// Respond to CoreLocation updates. +- (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation NS_AVAILABLE(10_9, 4_0) { + CLLocation *location = userLocation.location; + + // CLLocation updates will not always have floor information... + if (location.floor != nil) { + NSLog(@"Location (Floor %@): %@", location.floor, location.description); + // ...but when they do, take note! + self.lastFloor = location.floor; + NSLog(@"We are on floor %ld", (long)self.lastFloor.level); + } +} + + +/// Request authorization if needed. +- (void)mapViewWillStartLocatingUser:(MKMapView *)mapView { + switch ([CLLocationManager authorizationStatus]) { + case kCLAuthorizationStatusNotDetermined: + // Ask the user for permission to use location. + [self.locationManager requestWhenInUseAuthorization]; + break; + + case kCLAuthorizationStatusDenied: + NSLog(@"Please authorize location services for this app under Settings > Privacy."); + break; + + case kCLAuthorizationStatusAuthorizedAlways: + case kCLAuthorizationStatusAuthorizedWhenInUse: + case kCLAuthorizationStatusRestricted: + // Nothing to do. + break; + } +} + +/// Helper method that shows the floorplan. +- (void)showFloorplan { + [self.mapView addOverlay:self.floorplan0]; +} + +/// Helper function that hides the floorplan. +- (void)hideFloorplan { + [self.mapView removeOverlay:self.floorplan0]; +} + +/// Helper function that shows the debug visuals. +- (void)showDebugVisuals { + // Make the background transparent to reveal, slightly the underlying grid. + self.hideBackgroundOverlayAlpha = 0.5; + // Show debugging bounding boxes. + [self.mapView addOverlays:self.debuggingOverlays level: MKOverlayLevelAboveRoads]; + // Show debugging pins. + [self.mapView addAnnotations:self.debuggingAnnotations]; +} + +/// Helper function that hides the debug visuals. +- (void)hideDebugVisuals { + [self.mapView removeAnnotations:self.debuggingAnnotations]; + [self.mapView removeOverlays:self.debuggingOverlays]; + self.hideBackgroundOverlayAlpha = 1.0; +} + +/** + Check for when the \c MKMapView is zoomed or scrolled in case we need to + bounce back to the floorplan. + If, instead, you're using e.g. \c MKUserTrackingModeFollow then you'll want + to disable \c snapsMapViewToFloorplan since it will conflict with the + user-follow scroll/zoom. +*/ +- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated { + if (self.snapsMapViewToFloorplan) { + [self.visibleMapRegionDelegate mapView:mapView regionDidChangeAnimated:animated]; + } +} + +/// Produce each type of renderer that might exist in our mapView. +- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id)overlay { + + if ([overlay isKindOfClass:[AAPLFloorplanOverlay class]]) { + AAPLFloorplanOverlayRenderer *renderer = [[AAPLFloorplanOverlayRenderer alloc] initWithOverlay:overlay]; + return renderer; + } + + if ([overlay isKindOfClass:[AAPLHideBackgroundOverlay class]]) { + MKPolygonRenderer *renderer = [[MKPolygonRenderer alloc] initWithPolygon:overlay]; + + /* + AAPLHideBackgroundOverlay covers the entire world, so this means + all of MapKit tiles will be replaced with a solid white background. + */ + renderer.fillColor = [[UIColor whiteColor] colorWithAlphaComponent:self.hideBackgroundOverlayAlpha]; + + // no border. + renderer.lineWidth = 0.0; + renderer.strokeColor = [[UIColor whiteColor] colorWithAlphaComponent:0.0]; + + return renderer; + } + + if ([overlay isKindOfClass:[MKPolygon class]]) { + MKPolygon *polygon = (MKPolygon *)overlay; + + /* + A quick and dirty MKPolygon renderer for addDebuggingOverlays and + our custom highlight region. + + In production, you'll want to implement this more cleanly: + "However, if each overlay uses different colors or drawing + attributes, you should find a way to initialize that information + using the annotation object, rather than having a large decision + tree in mapView:rendererForOverlay:" + + See "Creating Overlay Renderers from Your Delegate Object" for more. + */ + + if ([polygon.title isEqualToString:@"Hello World"]) { + MKPolygonRenderer *renderer = [[MKPolygonRenderer alloc] initWithPolygon:polygon]; + renderer.fillColor = [[UIColor yellowColor] colorWithAlphaComponent:0.5]; + renderer.strokeColor = [[UIColor yellowColor] colorWithAlphaComponent:0.0]; + renderer.lineWidth = 0.0; + return renderer; + } + + if ([polygon.title isEqualToString:@"debug"]) { + MKPolygonRenderer *renderer = [[MKPolygonRenderer alloc] initWithPolygon:polygon]; + renderer.fillColor = [[UIColor grayColor] colorWithAlphaComponent:0.1]; + renderer.strokeColor = [[UIColor cyanColor] colorWithAlphaComponent:0.5]; + renderer.lineWidth = 2.0; + return renderer; + } + } + + return nil; +} + +/// Produce each type of annotation view that might exist in our MapView. +- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation { + /* + For now, all we have are some quick and dirty pins for viewing debug + annotations. + To learn more about showing annotations, see "Annotating Maps" doc + */ + if ([annotation.title isEqualToString:@"red"]) { + MKPinAnnotationView *pinView = [[MKPinAnnotationView alloc] init]; + pinView.pinTintColor = [UIColor redColor]; + pinView.canShowCallout = YES; + return pinView; + } + + if ([annotation.title isEqualToString:@"green"]) { + MKPinAnnotationView *pinView = [[MKPinAnnotationView alloc] init]; + pinView.pinTintColor = [UIColor greenColor]; + pinView.canShowCallout = YES; + return pinView; + } + + if ([annotation.title isEqualToString:@"purple"]) { + MKPinAnnotationView *pinView = [[MKPinAnnotationView alloc] init]; + pinView.pinTintColor = [UIColor purpleColor]; + pinView.canShowCallout = YES; + return pinView; + } + + return nil; +} + ++ (nonnull NSArray> *)createDebuggingAnnotationsForMapView:(MKMapView *)mapView aboutFloorplan:(AAPLFloorplanOverlay *)floorplan { + // Drop a red pin on the fromAnchor latitudeLongitude location. + MKPointAnnotation *fromAnchor = [[MKPointAnnotation alloc] init]; + fromAnchor.title = @"red"; + fromAnchor.subtitle = @"fromAnchor should be here"; + fromAnchor.coordinate = floorplan.anchors.fromAnchor.latitudeLongitude; + + // Drop a green pin on the toAnchor latitudeLongitude location. + MKPointAnnotation *toAnchor = [[MKPointAnnotation alloc] init]; + toAnchor.title = @"green"; + toAnchor.subtitle = @"toAnchor should be here"; + toAnchor.coordinate = floorplan.anchors.toAnchor.latitudeLongitude; + + // Drop a purple pin showing the (0.0 pt, 0.0 pt) location of the PDF. + MKPointAnnotation *pdfOrigin = [[MKPointAnnotation alloc] init]; + pdfOrigin.title = @"purple"; + pdfOrigin.subtitle = @"This is the 0.0, 0.0 coordinate of your PDF"; + pdfOrigin.coordinate = MKCoordinateForMapPoint(floorplan.PDFOrigin); + + return @[ + fromAnchor, + toAnchor, + pdfOrigin + ]; +} + ++ (nonnull NSArray> *)createDebuggingOverlaysForMapView:(MKMapView *)mapView aboutFloorplan:(AAPLFloorplanOverlay *)floorplan { + MKPolygon *floorplanPDFBox = floorplan.polygonFromFloorplanPDFBoxCorners; + floorplanPDFBox.title = @"debug"; + floorplanPDFBox.subtitle = @"PDF Page Box"; + + MKPolygon *floorplanBoundingMapRect = floorplan.polygonFromBoundingMapRect; + floorplanBoundingMapRect.title = @"debug"; + floorplanBoundingMapRect.subtitle = @"boundingMapRect"; + + MKPolygon *floorplanBoundingMapRectIncludingRotations = floorplan.polygonFromBoundingMapRectIncludingRotations; + floorplanBoundingMapRect.title = @"debug"; + floorplanBoundingMapRect.subtitle = @"boundingMapRectIncludingRotations"; + + return @[ + floorplanPDFBox, + floorplanBoundingMapRect, + floorplanBoundingMapRectIncludingRotations + ]; +} + +@end + +#pragma mark - Functions related to AAPLVisibleMapRegionDelegate + +/** + @param a an \c MKMapSize object. + @return The area of an \c MKMapSize. + */ +static double AAPLMKMapSizeArea(MKMapSize a) { + return a.height * a.width; +} + +/** + @param a point A. + @param b point B. + @return The hypotenuse defined by two. + */ +static double AAPLCGPointHypot(CGPoint a, CGPoint b) { + return hypot(b.x - a.x, b.y - a.y); +} + +/** + Resets the camera orientation to the given centerpoint with the given + heading/orientation. + + @param mapView MapView which needs to be re-centered. + @param center new centerpoint. + @param heading orientation to use. + */ +static void resetCameraOrientation(MKMapView *mapView, CLLocationCoordinate2D center, CLLocationDirection heading) { + MKMapCamera *newCamera = [mapView.camera copy]; + + // Center the floorplan... + newCamera.centerCoordinate = center; + + // ...and rotate so the floorplan is upright. + newCamera.heading = heading; + [mapView setCamera:newCamera animated:YES]; +} + +/** + @return YES if the floorplan doesn't fill the screen + @param mapView MapView to check + @param floorplanBoundingMapRect \c MKMapRect that defines the floorplan's boundaries + */ +static BOOL floorplanDoesNotFillScreen(MKMapView *mapView, MKMapRect floorplanBoundingMapRect) { + if (MKMapRectContainsRect(floorplanBoundingMapRect, mapView.visibleMapRect)) { + // Your view is already entirely inside the floorplan. Nothing to do. + return NO; + } + + + // The specific part of the floorplan that is currently visible. + MKMapRect visiblePartOfFloorplan = MKMapRectIntersection(floorplanBoundingMapRect, mapView.visibleMapRect); + + /* + The floorplan does not fill your screen in either direction. You must have + scrolled or zoomed out too far. + */ + return visiblePartOfFloorplan.size.width < mapView.visibleMapRect.size.width && + visiblePartOfFloorplan.size.height < mapView.visibleMapRect.size.height; +} + +/** + Helper function for \c clampZoomToFloorplan(). + @return the MapCamera altitude required to bounce back the MapCamera zoom + back onto the floorplan. if no zoom adjustment is needed, returns NAN. + @param mapView The \c MKMapView we're looking at. + @param floorplanBoundingMapRect bounding rectangle of the floorplan. + */ +static double getZoomAdjustment(MKMapView *mapView, MKMapRect floorplanBoundingMapRect) { + double mapViewVisibleMapRectArea = AAPLMKMapSizeArea(mapView.visibleMapRect.size); + + MKMapRect maxZoomedOut = [mapView mapRectThatFits:floorplanBoundingMapRect]; + double maxZoomedOutArea = AAPLMKMapSizeArea(maxZoomedOut.size); + + + if (maxZoomedOutArea < mapViewVisibleMapRectArea) { + // You have zoomed out too far? + + double zoomFactor = sqrt(maxZoomedOutArea / mapViewVisibleMapRectArea); + CLLocationDistance currentAltitude = mapView.camera.altitude; + CLLocationDistance newAltitude = currentAltitude * zoomFactor; + + // getUsableAltitude(newAltitude, detectZoomLevel); + CLLocationDistance newAltitudeUsable = newAltitude; + + /* + NOTE: MapKit's internal zoom level counter is by powers of two, so a + 0.5x buffer here is safe and should prevent pulsing when we're near + the maximum zoom level. + + Assumption: We will never see a lowestGoodAltitude smaller than 0.5x + a stable MapKit altitude. + */ + if (newAltitudeUsable < currentAltitude) { + // Zoom back in. + return newAltitudeUsable; + } + } + + // No change. Return NAN. + return NAN; +} + +/** + Detect whether the user has zoomed away from the floorplan and, if so, + bounce back. + + @return `YES` if we needed to bounce back. + @param mapView mapview we're working on. + @param floorplanBoundingMapRect bounds of the floorplan. + @param floorplanCenter center of the floorplan. + */ +static BOOL clampZoomToFloorplan(MKMapView *mapView, MKMapRect floorplanBoundingMapRect, CLLocationCoordinate2D floorplanCenter) { + + if (floorplanDoesNotFillScreen(mapView, floorplanBoundingMapRect)) { + // Clamp! + + CLLocationDistance newAltitude = getZoomAdjustment(mapView, floorplanBoundingMapRect); + + if (!isnan(newAltitude)) { + // We have a zoom change to make! + + MKMapCamera *newCamera = [mapView.camera copy]; + newCamera.altitude = newAltitude; + + // Since we've zoomed out enough to see the entire floorplan anyway, let's re-center to make sure the entire floorplan is actually on-screen. + newCamera.centerCoordinate = floorplanCenter; + + [mapView setCamera:newCamera animated:YES]; + + // DONE + return YES; + } + } + + // No zoom correction took place. + return NO; +} + +/** + Detect whether the user has scrolled away from the floorplan, and if so, + bounce back. + + @param mapView The MapView to scroll + @param floorplanBoundingMapRect A map rect that must be "in view" when the + scrolling is complete. We will only scroll until this map rect + enters the view. + @param optionalCameraHeading If you give valid \c CLLocationDirection, we + will also adjust the camera heading. If you give an invalid + \c CLLocationDirection (e.g. -1.0), we'll keep whatever heading the + camera already has. + */ +static void clampScrollToFloorplan(MKMapView *mapView, AAPLMKMapRectRotated floorplanBoundingPDFBoxRect, CLLocationDirection optionalCameraHeading) { + + BOOL rotationNeeded = 0.0 <= optionalCameraHeading && optionalCameraHeading < 360.0; + + /* + Assuming we are zoomed at the correct level, we still can't see the + floorplan. You have scrolled too far? + */ + + MKMapPoint visibleMapRectMid = { + .x = MKMapRectGetMidX(mapView.visibleMapRect), + .y = MKMapRectGetMidY(mapView.visibleMapRect) + }; + + MKMapPoint visibleMapRectOriginProposed = AAPLMKMapRectRotatedNearestPoint(floorplanBoundingPDFBoxRect, visibleMapRectMid); + + double dxOffset = visibleMapRectOriginProposed.x - visibleMapRectMid.x; + double dyOffset = visibleMapRectOriginProposed.y - visibleMapRectMid.y; + + // Okay, now we know the "proposed" scroll adjustment... + + CGPoint visibleMapRectMidPixels = [mapView convertCoordinate:MKCoordinateForMapPoint(visibleMapRectMid) toPointToView:mapView]; + CGPoint visibleMapRectProposedPixels = [mapView convertCoordinate:MKCoordinateForMapPoint(visibleMapRectOriginProposed) toPointToView:mapView]; + + double scrollDistancePixels = AAPLCGPointHypot(visibleMapRectProposedPixels, visibleMapRectMidPixels); + + /* + ... but is it more than 1.0 screen pixel worth? (Otherwise the user + probably wouldn't even notice). + + NOTE: Due to rounding errors it's hard to get exactly + scrollDistancePixels == 0.0 anyway, so doing a check like this improves + general responsiveness overall. + */ + BOOL scrollNeeded = scrollDistancePixels > 1.0; + + if (rotationNeeded || scrollNeeded) { + MKMapCamera *newCamera = [mapView.camera copy]; + if (rotationNeeded) { + // Rotation the camera (e.g. to make the floorplan upright). + newCamera.heading = optionalCameraHeading; + } + + if (scrollNeeded) { + // Scroll back toward the floorplan. + MKMapPoint cameraCenter = MKMapPointForCoordinate(mapView.camera.centerCoordinate); + cameraCenter.x += dxOffset; + cameraCenter.y += dyOffset; + newCamera.centerCoordinate = MKCoordinateForMapPoint(cameraCenter); + } + + [mapView setCamera:newCamera animated:YES]; + } + +} + +#pragma mark - AAPLVisibleMapRegionDelegate + +@interface AAPLVisibleMapRegionDelegate () + +/// Set to YES if you would want reset the MapCamera to center on the floorplan. +@property BOOL needsCameraOrientationReset; + +@end + +@implementation AAPLVisibleMapRegionDelegate + +@synthesize boundingMapRectIncludingRotations; +@synthesize boundingPDFBox; +@synthesize floorplanCenter; +@synthesize floorplanUprightMKMapCameraHeading; + +- (instancetype)initWithFloorplanBounds:(MKMapRect)boundingMapRectWithRotations pdfBoundingBox:(AAPLMKMapRectRotated)pdfBoundingBox centerOfFloorplan:(CLLocationCoordinate2D)centerOfFloorplan floorplanUprightMKMapCameraHeading:(CLLocationDirection)heading { + + self = [super init]; + + if (self) { + boundingMapRectIncludingRotations = boundingMapRectWithRotations; + boundingPDFBox = pdfBoundingBox; + floorplanCenter = centerOfFloorplan; + floorplanUprightMKMapCameraHeading = heading; + + _lastAltitude = NAN; + + _needsCameraOrientationReset = YES; + } + + return self; +} + +- (void)mapViewResetCameraToFloorplan:(MKMapView *)mapView { + resetCameraOrientation(mapView, floorplanCenter, floorplanUprightMKMapCameraHeading); +} + +// Catch regionDidChange events to ensure that we can always see the floorplan. +- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated { + MKMapCamera * camera = mapView.camera; + + BOOL didClampZoom = NO; + + // Has the zoom level stabilized? + if (_lastAltitude != camera.altitude) { + // Not yet! Someone is changing the zoom! + + _lastAltitude = camera.altitude; + + // Auto-zoom the camera to fit the floorplan. + didClampZoom = clampZoomToFloorplan(mapView, boundingMapRectIncludingRotations, floorplanCenter); + } + + if (!didClampZoom) { + // Once the zoom level has stabilized, auto-scroll if needed. + clampScrollToFloorplan(mapView, boundingPDFBox, (self.needsCameraOrientationReset) ? floorplanUprightMKMapCameraHeading : NAN); + self.needsCameraOrientationReset = NO; + } +} + +@end diff --git a/Footprint/ObjC/Footprint/Base.lproj/Main.storyboard b/Footprint/ObjC/Footprint/Base.lproj/Main.storyboard new file mode 100644 index 00000000..8099312e --- /dev/null +++ b/Footprint/ObjC/Footprint/Base.lproj/Main.storyboard @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Footprint/ObjC/Footprint/Floorplans/floorplan_overlay_floor0.pdf b/Footprint/ObjC/Footprint/Floorplans/floorplan_overlay_floor0.pdf new file mode 100644 index 00000000..0a1dbfe2 Binary files /dev/null and b/Footprint/ObjC/Footprint/Floorplans/floorplan_overlay_floor0.pdf differ diff --git a/Footprint/ObjC/Footprint/Images.xcassets/AppIcon.appiconset/Contents.json b/Footprint/ObjC/Footprint/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/Footprint/ObjC/Footprint/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Footprint/ObjC/Footprint/Info.plist b/Footprint/ObjC/Footprint/Info.plist new file mode 100644 index 00000000..97fdee9f --- /dev/null +++ b/Footprint/ObjC/Footprint/Info.plist @@ -0,0 +1,49 @@ + + + + + NSLocationWhenInUseUsageDescription + This is an example of the app using user location only upon request + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.example.apple-samplecode.Footprint + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Footprint/ObjC/Footprint/main.m b/Footprint/ObjC/Footprint/main.m new file mode 100644 index 00000000..0153b2d1 --- /dev/null +++ b/Footprint/ObjC/Footprint/main.m @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + main + */ + +#import +#import "AAPLAppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AAPLAppDelegate class])); + } +} diff --git a/Footprint/ObjC/README.md b/Footprint/ObjC/README.md new file mode 100644 index 00000000..5402f150 --- /dev/null +++ b/Footprint/ObjC/README.md @@ -0,0 +1,64 @@ +# Footprint: Indoor Location with Core Location + +Display device location against a custom floorplan PDF. + * First, we will draw your floorplan inside MapKit so that it matches its real-world Latitude/Longitude values. + * Then, using Core Location, we will take the position in Latitude/Longitude and display it in MapKit. + +We will demonstrate how to do the conversions between Geographic coordinate systems (Latitude/Longitude), floorplan PDF coordinate systems (x,y), and MapKit coordinates. + +Note: For this sample to show a real location & floor number, you must have a floorplan for a venue that is Indoor Positioning enabled and the device will need to be in that venue. If you are not in a venue, try emulating a position in the venue using "Custom Location" in the simulator. Otherwise, the user's location will be shown far from the default floorplan and venue displayed in this app. Additionally, to ensure that user location is actually visible, you will need to enable it in the Attribute Inspector on the Map View in Main.storyboard. From there, the user would need to either allow the app to use their location from Settings or select "Allow" when prompted by the app the first time it is run. + +## AAPLViewController + +This is your main view controller class which handles most of the display functionality. Three IBActions are available from the storyboard: + +To help with debugging we’ve added a toolbar at the bottom to provide some common operations. +* Close (remove the AAPLHideBackgroundOverlay to reveal the underlying base maps, as you would see in a typical “outdoor” app that uses MKMapView — most importantly, this allows you to verify the real-world coordinates of the AAPLGeoAnchorPoints that you set in AAPLViewController) +* Rotate (restore the origin camera view, centered on the floorplan and, most importantly, with the "floorplan facing up” — instead of "North facing up” which is the default when you use MKMapView out-of-the-box) +* Debug (toggle debugging visuals on/off — shows additional overlays and annotations that correspond to the various internal variables inside the code. Use this as a reference when debugging anchor points, overlays, or any other rendering/interaction provided in this sample code. If Close has been pressed, this also toggles the floorplan on and off so it can be seen over the underlying maps) + +## AAPLVisibleMapRegionDelegate + +This handles the case where the user zooms or scrolls too far away from the floorplan by automatically "bouncing back" when this happens. + +## Converters and Renderers + +### AAPLCoordinateConvereter + +This is used as a converter from the coordinates in the PDF image of the floorplan to geographic coordinates. This may be modified to work with raster images such as JPEGs and PNGs but this is not recommended as they won't render as clearly. See comments for more information + +### AAPLFloorplanOverlay + +This is used to describe the floorplan of an indoor venue and is based upon MKOverlay + +### AAPLFloorplanOverlayRenderer + +This is used to render an AAPLFloorplanOverlay onto a map view and also handles some debugging tasks + +### AAPLHideBackgroundOverlay + +This is used to hide MapKit's underlying map tiles so that only our floorplan is visible + +## Using Your Own Floorplan +If you have a venue floorplan you would like to use, make the following changes: + +Step \#1: Replace (or add) `floorplan_overlay_floor0.pdf` in `Floorplans`. If necessary, update the filename in `AAPLViewController.m` `viewDidLoad` + +Step \#2: Look for the `AAPLGeoAnchorPair` struct in `AAPLViewController.m` `viewDidLoad` and set them to your own values. Pick any two points on the floorplan (in PDF x,y) as well as their corresponding real-world locations (in Latitude/Longitude) + +Step \#3: Follow the code comments of `drawDiagnosticVisuals` in `AAPLFloorplanOverlayRenderer.m` to verify that your (x,y) values from Step \#2 are correct. + +Step \#4: Follow the code comments of `setDebuggingAnnotationsOfMapView`: in `AAPLViewController.m` to verify that your (Latitude/Longitude) values from Step \#2 are correct. + +## Requirements + +### Build + +Xcode 7.0, iOS 9 or later + +### Runtime + +Xcode 7.0 Simulator (OS X 10.10.3) +iOS 9 or later + +Copyright (C) 2014-2015 Apple Inc. All rights reserved. diff --git a/Footprint/Swift/AppIcon.appiconset/Contents.json b/Footprint/Swift/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..b7f3352e --- /dev/null +++ b/Footprint/Swift/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Footprint/Swift/Footprint.xcodeproj/project.pbxproj b/Footprint/Swift/Footprint.xcodeproj/project.pbxproj new file mode 100644 index 00000000..8e37885a --- /dev/null +++ b/Footprint/Swift/Footprint.xcodeproj/project.pbxproj @@ -0,0 +1,315 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + A641A13F1B586C6A00B1035B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A641A13E1B586C6A00B1035B /* AppDelegate.swift */; }; + A641A1411B586C6A00B1035B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A641A1401B586C6A00B1035B /* ViewController.swift */; }; + A641A1441B586C6A00B1035B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A641A1421B586C6A00B1035B /* Main.storyboard */; }; + A641A16D1B586EA000B1035B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = A641A16C1B586EA000B1035B /* Utilities.swift */; }; + A641A16F1B586EB900B1035B /* CoordinateConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A641A16E1B586EB900B1035B /* CoordinateConverter.swift */; }; + A641A1711B586EDF00B1035B /* MKMapRectRotated.swift in Sources */ = {isa = PBXBuildFile; fileRef = A641A1701B586EDF00B1035B /* MKMapRectRotated.swift */; }; + A641A1731B586EF200B1035B /* FloorplanOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A641A1721B586EF200B1035B /* FloorplanOverlay.swift */; }; + A641A1751B586F1600B1035B /* FloorplanOverlayRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A641A1741B586F1600B1035B /* FloorplanOverlayRenderer.swift */; }; + A641A1771B586F4400B1035B /* VisibleMapRegionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A641A1761B586F4400B1035B /* VisibleMapRegionDelegate.swift */; }; + A641A1791B586F5900B1035B /* HideBackgroundOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = A641A1781B586F5900B1035B /* HideBackgroundOverlay.swift */; }; + A641A17B1B58705C00B1035B /* Floorplans in Resources */ = {isa = PBXBuildFile; fileRef = A641A17A1B58705C00B1035B /* Floorplans */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A641A13E1B586C6A00B1035B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + A641A1401B586C6A00B1035B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + A641A1431B586C6A00B1035B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + A641A14A1B586C6A00B1035B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A641A16C1B586EA000B1035B /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; + A641A16E1B586EB900B1035B /* CoordinateConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoordinateConverter.swift; sourceTree = ""; }; + A641A1701B586EDF00B1035B /* MKMapRectRotated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MKMapRectRotated.swift; sourceTree = ""; }; + A641A1721B586EF200B1035B /* FloorplanOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloorplanOverlay.swift; sourceTree = ""; }; + A641A1741B586F1600B1035B /* FloorplanOverlayRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloorplanOverlayRenderer.swift; sourceTree = ""; }; + A641A1761B586F4400B1035B /* VisibleMapRegionDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisibleMapRegionDelegate.swift; sourceTree = ""; }; + A641A1781B586F5900B1035B /* HideBackgroundOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HideBackgroundOverlay.swift; sourceTree = ""; }; + A641A17A1B58705C00B1035B /* Floorplans */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Floorplans; sourceTree = ""; }; + B55119511D9AE46800CDECCC /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + FC996BC61D6626D40083EB8C /* Footprint.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Footprint.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A641A1381B586C6A00B1035B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A641A1321B586C6A00B1035B = { + isa = PBXGroup; + children = ( + B55119511D9AE46800CDECCC /* README.md */, + A641A13D1B586C6A00B1035B /* Footprint */, + FC996BC61D6626D40083EB8C /* Footprint.app */, + ); + sourceTree = ""; + }; + A641A13D1B586C6A00B1035B /* Footprint */ = { + isa = PBXGroup; + children = ( + A641A13E1B586C6A00B1035B /* AppDelegate.swift */, + A641A16E1B586EB900B1035B /* CoordinateConverter.swift */, + A641A1721B586EF200B1035B /* FloorplanOverlay.swift */, + A641A1741B586F1600B1035B /* FloorplanOverlayRenderer.swift */, + A641A1781B586F5900B1035B /* HideBackgroundOverlay.swift */, + A641A1701B586EDF00B1035B /* MKMapRectRotated.swift */, + A641A1401B586C6A00B1035B /* ViewController.swift */, + A641A1761B586F4400B1035B /* VisibleMapRegionDelegate.swift */, + A641A16C1B586EA000B1035B /* Utilities.swift */, + A641A1421B586C6A00B1035B /* Main.storyboard */, + A641A17A1B58705C00B1035B /* Floorplans */, + A641A14A1B586C6A00B1035B /* Info.plist */, + ); + path = Footprint; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A641A13A1B586C6A00B1035B /* Footprint */ = { + isa = PBXNativeTarget; + buildConfigurationList = A641A1631B586C6B00B1035B /* Build configuration list for PBXNativeTarget "Footprint" */; + buildPhases = ( + A641A1371B586C6A00B1035B /* Sources */, + A641A1381B586C6A00B1035B /* Frameworks */, + A641A1391B586C6A00B1035B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Footprint; + productName = Footprint; + productReference = FC996BC61D6626D40083EB8C /* Footprint.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A641A1331B586C6A00B1035B /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Apple Inc."; + TargetAttributes = { + A641A13A1B586C6A00B1035B = { + CreatedOnToolsVersion = 7.0; + LastSwiftMigration = 0800; + }; + }; + }; + buildConfigurationList = A641A1361B586C6A00B1035B /* Build configuration list for PBXProject "Footprint" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A641A1321B586C6A00B1035B; + productRefGroup = A641A1321B586C6A00B1035B; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A641A13A1B586C6A00B1035B /* Footprint */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A641A1391B586C6A00B1035B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A641A17B1B58705C00B1035B /* Floorplans in Resources */, + A641A1441B586C6A00B1035B /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A641A1371B586C6A00B1035B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A641A1751B586F1600B1035B /* FloorplanOverlayRenderer.swift in Sources */, + A641A16F1B586EB900B1035B /* CoordinateConverter.swift in Sources */, + A641A16D1B586EA000B1035B /* Utilities.swift in Sources */, + A641A1711B586EDF00B1035B /* MKMapRectRotated.swift in Sources */, + A641A1771B586F4400B1035B /* VisibleMapRegionDelegate.swift in Sources */, + A641A1411B586C6A00B1035B /* ViewController.swift in Sources */, + A641A1731B586EF200B1035B /* FloorplanOverlay.swift in Sources */, + A641A1791B586F5900B1035B /* HideBackgroundOverlay.swift in Sources */, + A641A13F1B586C6A00B1035B /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + A641A1421B586C6A00B1035B /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + A641A1431B586C6A00B1035B /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + A641A1611B586C6B00B1035B /* 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_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos.internal; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A641A1621B586C6B00B1035B /* 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_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + 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.internal; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A641A1641B586C6B00B1035B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = Footprint/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.footprint.Footprint"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + VALIDATE_PRODUCT = NO; + }; + name = Debug; + }; + A641A1651B586C6B00B1035B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = Footprint/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.footprint.Footprint"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + VALIDATE_PRODUCT = NO; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A641A1361B586C6A00B1035B /* Build configuration list for PBXProject "Footprint" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A641A1611B586C6B00B1035B /* Debug */, + A641A1621B586C6B00B1035B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A641A1631B586C6B00B1035B /* Build configuration list for PBXNativeTarget "Footprint" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A641A1641B586C6B00B1035B /* Debug */, + A641A1651B586C6B00B1035B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A641A1331B586C6A00B1035B /* Project object */; +} diff --git a/Footprint/Swift/Footprint.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Footprint/Swift/Footprint.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..a88f737a --- /dev/null +++ b/Footprint/Swift/Footprint.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Footprint/Swift/Footprint/AppDelegate.swift b/Footprint/Swift/Footprint/AppDelegate.swift new file mode 100644 index 00000000..807fc877 --- /dev/null +++ b/Footprint/Swift/Footprint/AppDelegate.swift @@ -0,0 +1,70 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Main application delegate. +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + /* + Sent when the application is about to move from active to inactive + state. This can occur for certain types of temporary interruptions + (such as an incoming phone call or SMS message) or when the user + quits the application and it begins the transition to the background + state. Use this method to pause ongoing tasks, disable timers, and + throttle down OpenGL ES frame rates. Games should use this method to + pause the game. + */ + } + + func applicationDidEnterBackground(_ application: UIApplication) { + /* + Use this method to release shared resources, save user data, + invalidate timers, and store enough application state information to + restore your application to its current state in case it is + terminated later. If your application supports background execution, + this method is called instead of applicationWillTerminate: when the + user quits. + */ + } + + func applicationWillEnterForeground(_ application: UIApplication) { + /* + Called as part of the transition from the background to the inactive + state; here you can undo many of the changes made on entering the + background. + */ + } + + func applicationDidBecomeActive(_ application: UIApplication) { + /* + Restart any tasks that were paused (or not yet started) while the + application was inactive. If the application was previously in the + background, optionally refresh the user interface. + */ + } + + func applicationWillTerminate(_ application: UIApplication) { + /* + Called when the application is about to terminate. Save data if + appropriate. See also applicationDidEnterBackground:. + */ + } + + +} + diff --git a/Footprint/Swift/Footprint/Base.lproj/Main.storyboard b/Footprint/Swift/Footprint/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f35739d4 --- /dev/null +++ b/Footprint/Swift/Footprint/Base.lproj/Main.storyboard @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Footprint/Swift/Footprint/CoordinateConverter.swift b/Footprint/Swift/Footprint/CoordinateConverter.swift new file mode 100644 index 00000000..f34ba179 --- /dev/null +++ b/Footprint/Swift/Footprint/CoordinateConverter.swift @@ -0,0 +1,331 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This class converts PDF coordinates of a floorplan to Geographic + coordinates on Earth. + NOTE: This class can also be used for any "right-handed" + coordinate system (other than PDF) but should not be used as-is + for "Raster image" coordinates (such as PNGs or JPEGs) because + those require left-handed coordinate frames. + There are other reasons we discourage the use of raster images + as indoor floorplans. See the code comments inside + FloorplanOverlay init for more info. +*/ + +import CoreLocation +import Foundation +import MapKit + +/** + - note: In iOS, the term "pixel" usually refers to screen pixels whereas the + term "point" is used to describe coordinates inside a visual/image asset. + + For more information, see: "Points Versus Pixels" on developer.apple.com + + This class matches a specific latitude & longitude (a coordinate on Earth) + to a specifc x,y coordinate (a position on your floorplan PDF). + + PDFs are defined in a coordinate system where +y is counter-clockwise of +x + (a.k.a. "a right handed coordinate system"). PDF coordinates + + - parameter latitudeLongitude: The lat-lon coordinate for this anchor + - parameter pdfPoint: corresponding PDF coordinate +*/ +struct GeoAnchor { + var latitudeLongitudeCoordinate = CLLocationCoordinate2D() + var pdfPoint = CGPoint.zero +} + +/** + Defines a pair of GeoAnchors + + - parameter fromAnchor: starting anchor + - parameter toAnchor: ending anchor +*/ +struct GeoAnchorPair { + var fromAnchor = GeoAnchor() + var toAnchor = GeoAnchor() +} + +/** + This class converts PDF coordinates of a floorplan to Geographic coordinates + on Earth. + + **This class can also be used for any "right-handed" coordinate system + (other than PDF) but should not be used as-is for "Raster image" coordinates + (such as PNGs or JPEGs) because those require left-handed coordinate frames. + There are other reasons we discourage the use of raster images as indoor + floorplans. See the code & comments inside FloorplanOverlay init for more + info.** +*/ +class CoordinateConverter: NSObject { + + /// The GeoAnchorPair used to define this converter + var anchors: GeoAnchorPair = GeoAnchorPair() + + /** + This vector, expressed in points (PDF coordinates), has length one meter + and direction due East. + */ + fileprivate var oneMeterEastwardVector: CGVector + + /** + This vector, expressed in points (PDF coordinates), has length one meter + and direction due South. + */ + fileprivate var oneMeterSouthwardVector: CGVector + + /** + This coordinate, expressed in points (PDF coordinates), corresponds to + exactly the same location as tangentLatLng + */ + fileprivate var tangentPDFPoint: CGPoint + + /** + This coordinate, expressed in latitude & longitude (global coordinates), + corresponds to exactly the same location as tangentPoint + */ + fileprivate var tangentLatitudeLongitudeCoordinate: CLLocationCoordinate2D + + /** + Initializes this class from a given GeoAnchorPair + + - parameter Anchors: the anchors that this class will use for converting + */ + init(anchors: GeoAnchorPair) { + self.anchors = anchors + + /* + Next, to compute the direction between two geographical coordinates, + we first need to convert to MapKit coordinates... + */ + let fromAnchorMercatorCoordinate = MKMapPointForCoordinate(anchors.fromAnchor.latitudeLongitudeCoordinate) + let toAnchorMercatorCoordinate = MKMapPointForCoordinate(anchors.toAnchor.latitudeLongitudeCoordinate) + + let pdfDisplacement = CGPoint(x: anchors.toAnchor.pdfPoint.x - anchors.fromAnchor.pdfPoint.x, y: anchors.toAnchor.pdfPoint.y - anchors.fromAnchor.pdfPoint.y) + + /* + ...so that we can use MapKit's Mercator coordinate system where +x + is always eastward and +y is always southward. Imagine an arrow + connecting fromAnchor to toAnchor... + */ + let anchorDisplacementMapKitX = (toAnchorMercatorCoordinate.x - fromAnchorMercatorCoordinate.x) + let anchorDisplacementMapKitY = (toAnchorMercatorCoordinate.y - fromAnchorMercatorCoordinate.y) + + /* + What is the angle of this arrow (geographically)? + atan2 always returns: + exactly 0.0 radians if the arrow is exactly in the +x direction + ("MapKit's +x" is due East). + positive radians as the arrow is rotated toward and through the +y + direction ("MapKit's +y" is due South). + In the case of MapKit, this is radians clockwise from due East. + */ + let radiansClockwiseOfDueEast = atan2(anchorDisplacementMapKitY, anchorDisplacementMapKitX) + + /* + That means if we rotate pdfDisplacement COUNTER-clockwise by this + value, it will be facing due east. In the CG coordinate frame, + positive radians is counter-clockwise because in a PDF +x is + rightward and +y is upward. + */ + let cgDueEast = CGVector(dx: pdfDisplacement.x, dy: pdfDisplacement.y).rotatedByRadians(CGFloat(radiansClockwiseOfDueEast)) + + // Now, get the distance (in meters) between the two anchors... + let distanceBetweenAnchorsMeters = CLLocationDistance.distanceBetweenLocationCoordinates2D(anchors.fromAnchor.latitudeLongitudeCoordinate, b: anchors.toAnchor.latitudeLongitudeCoordinate) + + // ...and rescale so that it's exactly one meter in length. + oneMeterEastwardVector = cgDueEast.scaledByFloat(CGFloat(1.0 / distanceBetweenAnchorsMeters)) + + /* + Lastly, due south is PI/2 clockwise of due east. + In the CG coordinate frame, clockwise rotation is NEGATIVE radians + because in a PDF +x is rightward and +y is upward. + */ + oneMeterSouthwardVector = oneMeterEastwardVector.rotatedByRadians(CGFloat(-M_PI_2)) + + /* + We'll choose the midpoint between the two anchors to be our "tangent + point". This is the MKMapPoint that will correspond to both + tangentLatitudeLongitudeCoordinate on Earth and _tangentPDFPoint + in the PDF. + */ + let tangentMercatorCoordinate = MKMapPoint.midpoint(fromAnchorMercatorCoordinate, b: toAnchorMercatorCoordinate) + + tangentLatitudeLongitudeCoordinate = MKCoordinateForMapPoint(tangentMercatorCoordinate) + + tangentPDFPoint = CGPoint.pointAverage(anchors.fromAnchor.pdfPoint, b: anchors.toAnchor.pdfPoint) + + } + + /** + Calculate the MKMapPoint from a specific PDF coordinate + + - parameter pdfPoint: starting point in the PDF + - returns: The corresponding MKMapPoint + */ + func MKMapPointFromPDFPoint(_ pdfPoint: CGPoint) -> MKMapPoint { + /* + To perform this conversion, we start by seeing how far we are from + the tangentPoint. The tangentPoint is the one place on the PDF where + we know exactly the corresponding Earth latitude & lontigude. + */ + let displacementFromTangentPoint = CGVector(dx: pdfPoint.x - tangentPDFPoint.x, dy: pdfPoint.y - tangentPDFPoint.y) + + // Now, let's figure out how far East & South we are from this point. + let dotProductEast = displacementFromTangentPoint.dotProductWithVector(oneMeterEastwardVector) + let dotProductSouth = displacementFromTangentPoint.dotProductWithVector(oneMeterSouthwardVector) + + let eastSouthDistanceMeters = ( + east: CLLocationDistance(dotProductEast / oneMeterEastwardVector.dotProductWithVector(oneMeterEastwardVector)), + south: CLLocationDistance(dotProductSouth / oneMeterSouthwardVector.dotProductWithVector(oneMeterSouthwardVector)) + ) + + let metersPerMapPoint = MKMetersPerMapPointAtLatitude(tangentLatitudeLongitudeCoordinate.latitude) + let tangentMercatorCoordinate = MKMapPointForCoordinate(tangentLatitudeLongitudeCoordinate) + + /* + Each meter is about (1.0 / metersPerMapPoint) 'MKMapPoint's, as long + as we are nearby _tangentLatLng. So just move this many meters East + and South and we're done! + */ + return MKMapPoint(x: tangentMercatorCoordinate.x + eastSouthDistanceMeters.east / metersPerMapPoint, + y: tangentMercatorCoordinate.y + eastSouthDistanceMeters.south / metersPerMapPoint) + } + + /** + - returns: a single CGAffineTransform that can transform any CGPoint in + a PDF into its corresponding MKMapPoint. + + In theory, the following equalities should always hold: + + CGPointApplyAffineTransform(pdfPoint, transformerFromPDFToMk).x + == MKMapPointFromPDFPoint(pdfPoint).x + CGPointApplyAffineTransform(pdfPoint, transformerFromPDFToMk).y + == MKMapPointFromPDFPoint(pdfPoint).y + + However, in practice we find that MKMapPointFromPDFPoint can be slightly + more accurate than transformerFromPDFToMk due to hardware acceleration + and/or numerical precision losses of CGAffineTransform operations. + */ + func transformerFromPDFToMk() -> CGAffineTransform { + let metersPerMapPoint = MKMetersPerMapPointAtLatitude(tangentLatitudeLongitudeCoordinate.latitude) + let tangentMercatorCoordinate = MKMapPointForCoordinate(tangentLatitudeLongitudeCoordinate) + + /* + CGAffineTransform operations are easier to construct in reverse-order. + Start with the last operation: + */ + let resultOfTangentMercatorCoordinate = CGAffineTransform(translationX: CGFloat(tangentMercatorCoordinate.x), y: CGFloat(tangentMercatorCoordinate.y)) + + /* + Revise the AffineTransform to first scale by + (1.0 / metersPerMapPoint), and then perform the above translation. + */ + let resultOfEastSouthDistanceMeters = resultOfTangentMercatorCoordinate.scaledBy(x: CGFloat(1.0 / metersPerMapPoint), y: CGFloat(1.0 / metersPerMapPoint)) + + /* + Revise the AffineTransform to first scale by + (1.0 / dotProduct(...)) before performing the transform so far. + */ + let resultOfDotProduct = resultOfEastSouthDistanceMeters.scaledBy(x: 1.0 / oneMeterEastwardVector.dotProductWithVector(oneMeterEastwardVector), + y: 1.0 / oneMeterSouthwardVector.dotProductWithVector(oneMeterSouthwardVector)) + + /* + Revise the AffineTransform to first perform dot products aginst our + reference vectors before performing the transform so far. + */ + let resultOfDisplacementFromTangentPoint = CGAffineTransform( + a: oneMeterEastwardVector.dx, b: oneMeterEastwardVector.dy, + c: oneMeterSouthwardVector.dx, d: oneMeterSouthwardVector.dy, + tx: 0.0, ty: 0.0 + ).concatenating(resultOfDotProduct + ) + + /* + Lastly, revise the AffineTransform to first perform the initial + subtraction before performing the remaining operations. + Each meter is about (1.0 / metersPerMapPoint) 'MKMapPoint's, as long + as we are nearby tangentLatitudeLongitudeCoordinate. + */ + return resultOfDisplacementFromTangentPoint.translatedBy(x: -tangentPDFPoint.x, y: -tangentPDFPoint.y) + } + + /// - returns: the size in meters of 1.0 CGPoint distance + var unitSizeInMeters: CLLocationDistance { + return CLLocationDistance(1.0 / hypot(oneMeterEastwardVector.dx, oneMeterEastwardVector.dy)) + } + + /** + Converts each corner of a PDF rectangle into an MKMapPoint (in MapKit + space). The collection of MKMapPoints is returned as an MKPolygon + overlay. + + - parameter pdfRect: A PDF rectangle + - returns: the corners of the PDF in an MKPolygon (obviously there + should be four points since it's a rectangle) + */ + func polygonFromPDFRectCorners(_ pdfRect: CGRect) -> MKPolygon { + var corners = [ MKMapPointFromPDFPoint(CGPoint(x: pdfRect.maxX, y: pdfRect.maxY)), + MKMapPointFromPDFPoint(CGPoint(x: pdfRect.minX, y: pdfRect.maxY)), + MKMapPointFromPDFPoint(CGPoint(x: pdfRect.minX, y: pdfRect.minY)), + MKMapPointFromPDFPoint(CGPoint(x: pdfRect.maxX, y: pdfRect.minY))] + + return MKPolygon(points: &corners, count: corners.count) + } + + /** + - returns: the smallest MKMapRect that can show all rotations of the + given PDF rectangle. + */ + func boundingMapRectIncludingRotations(_ rect: CGRect) -> MKMapRect { + // Start with the nominal rendering box for this rect is. + let nominalRenderingRect = polygonFromPDFRectCorners(rect).boundingMapRect + + /* + In order to account for all rotations, any bounding map rect must + have diameter equal to the longest distance inside the rectangle. + */ + let boundsDiameter = hypot(nominalRenderingRect.size.width, nominalRenderingRect.size.height) + + let rectCenterPoints = CGPoint(x: rect.midX, y: rect.midY) + + let boundsCenter = MKMapPointFromPDFPoint(rectCenterPoints) + + /* + Return a square MKMapRect centered at boundsCenterMercator with edge + length diameterMercator + */ + return MKMapRectMake( + boundsCenter.x - boundsDiameter / 2.0, + boundsCenter.y - boundsDiameter / 2.0, + boundsDiameter, boundsDiameter) + } + + /** + - returns: the MKMapCamera heading required to display your PDF (user + space) coordinate system upright so that PDF +x is rightward and + PDF +y is upward. + */ + func getUprightMKMapCameraHeading() -> CLLocationDirection { + /* + To make the floorplan upright, we want to rotate the floorplan +x + vector toward due east. + */ + let resultRadians: CGFloat = atan2(oneMeterEastwardVector.dy, oneMeterEastwardVector.dx) + let result = resultRadians * 180.0 / CGFloat(M_PI) + + /* + According to the CLLocationDirection documentation we must store a + positive value if it is valid. + */ + if result < 0.0 { + return CLLocationDirection(result + 360.0) + } else { + return CLLocationDirection(result) + } + } + +} diff --git a/Footprint/Swift/Footprint/FloorplanOverlay.swift b/Footprint/Swift/Footprint/FloorplanOverlay.swift new file mode 100644 index 00000000..3229e713 --- /dev/null +++ b/Footprint/Swift/Footprint/FloorplanOverlay.swift @@ -0,0 +1,248 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This class describes a floorplan for an indoor venue. +*/ + + +import Foundation +import MapKit + +/// This class describes a floorplan for an indoor venue. +@objc class FloorplanOverlay: NSObject, MKOverlay { + + /** + Same as boundingMapRect but slightly larger to fit on-screen under + any MKMapCamera rotation. + */ + var boundingMapRectIncludingRotations = MKMapRect() + + /** + Cache the CGAffineTransform used to help draw the floorplan to the + screen inside an MKMapView. + */ + var transformerFromPDFToMk = CGAffineTransform() + + /// Current floor level + var floorLevel = 0 + + /** + Reference to the internal page data of the selected page of the PDF you + are drawing. It is very likely that the PDF of your floorplan is a + single page. + */ + var pdfPage: CGPDFPage + + /** + Same as boundingMapRect, but more precise. + The AAPLMapRectRotated you'll get here fits snugly accounting for the + rotation of the floorplan (relative to North) whereas the + boundingMapRect must be "North-aligned" since it's an MKMapRect. + If you're still not 100% sure, toggle the "debug switch" in the sample + code and look at the overlays that are drawn. + */ + var floorplanPDFBox: MKMapRectRotated + + /// The PDF document to be rendered. + fileprivate var pdfDoc: CGPDFDocument + + /** + The coordinate converter for converting between PDF coordinates (point) + and MapKit coordinates (MKMapPoint). + */ + fileprivate var coordinateConverter: CoordinateConverter + + /// For debugging, remember the PDF page box selected at initialization. + fileprivate var pdfBoxRectangle = CGRect.null + + /// MKOverlay protocol return values. + var boundingMapRect = MKMapRect() + var coordinate = CLLocationCoordinate2D() + + /** + In this example, our floorplan is described by four things. + 1. The URL of a PDF. This is the visual data for the floorplan. + 2. The PDF page box to draw. This tells us which section of the PDF + we will actually draw. + 3. A pair of anchors. This tells us where the floorplan appears in + the real world. + 4. A floor level. This tells us which floor our floorplan represents + + - parameter floorplanUrl: the path to a PDF containing the drawing of + the floorplan. + - parameter pdfBox: which section of the PDF do we draw? + - parameter andAnchors: real-world anchors of this floorplan + -- opposite corners. + - parameter forFloorLevel: which floor is it on? + */ + init(floorplanUrl: URL, withPDFBox pdfBox: CGPDFBox, andAnchors anchors: GeoAnchorPair, forFloorLevel level: NSInteger) { + assert(floorplanUrl.absoluteString.hasSuffix("pdf"), "Sanity check: The URL should point to a PDF file") + + /* + Using raster images (such as PNG or JPEG) would create a number of + complications, such as: + + you need multiple sizes of each image, and each would need its + own GeoAnchorPair (see "Icon and Image Sizes" for iOS on + developer.apple.com for more). + + raster/bitmap images use a different coordinate system than PDFs + do, so the code from CoordinateConverter could not be used + out-of-the-box. Instead, you would need a separate + implementation of CoordinateConverter that works for left-handed + coordinate frames. PDFs use a right-handed coordinate frame. + + text and fine details of raster images may not render as clearly + as vector images when zoomed in. PDF is primarily a vector image + format. + + some raster image formats, such as JPEG, are designed for + photographs and may suffer from loss of detail due to + compression artifacts when being used for floorplans. + */ + coordinateConverter = CoordinateConverter(anchors: anchors) + transformerFromPDFToMk = coordinateConverter.transformerFromPDFToMk() + floorLevel = level + + /* + Read the PDF file from disk into memory. Remember to CFRelease it + when we dealloc. + (see "The Create Rule" on developer.apple.com for more) + */ + pdfDoc = CGPDFDocument(floorplanUrl as CFURL)! + + /* + In this example the floorplan PDF has only one page, so we pick + "page 1" of the PDF. + */ + pdfPage = pdfDoc.page(at: 1)! + + // Figure out which region of the PDF is to be drawn. + pdfBoxRectangle = pdfPage.getBoxRect(pdfBox) + + /* + There is no need to display this floorplan if your MapView camera is + beyond the four corners of the PDF page box. Thus, our + boundingMapRect is based on the PDF page box corners in the + MKMapPoint coordinate frame. + */ + let polygonFromPDFRectCorners = coordinateConverter.polygonFromPDFRectCorners(pdfBoxRectangle) + boundingMapRect = polygonFromPDFRectCorners.boundingMapRect + + /* + We need a quick way to check whether your screen is currently + looking inside vs. outside the floorplan, in order to "clamp" your + MKMapView. + */ + assert(polygonFromPDFRectCorners.pointCount == 4) + let points = polygonFromPDFRectCorners.points() + floorplanPDFBox = MKMapRectRotatedMake(points[0], corner2: points[1], corner3: points[2], corner4: points[3]) + + /* + For the purposes of clamping MKMapCamera zoom, we need a slightly + padded MKMapRect that allows the entire floorplan can be visible + regardless of camera rotation. Otherwise, depending on the + MKMapCamera rotation, auto-zoom might prevent the user from zooming + out far enough to see the entire floorplan and/or auto-scroll might + prevent the user from seeing the edge of the floorplan. + */ + boundingMapRectIncludingRotations = coordinateConverter.boundingMapRectIncludingRotations(pdfBoxRectangle) + + // For coordinate just return the centroid of boundingMapRect + coordinate = MKCoordinateForMapPoint(boundingMapRect.getCenter()) + } + + /** + This is different from CoordinateConverter getUprightMKMapCameraHeading + because here we also account for the PDF Page Dictionary's Rotate entry. + + - returns: the MKMapCamera heading required to display your *floorplan* + upright. + */ + func getFloorplanUprightMKMapCameraHeading() -> CLLocationDirection { + /* + Applying this heading to the MKMapCamera will cause PDF +x to face + MapKit +x. + */ + let rotatePDFXToMapKitX = coordinateConverter.getUprightMKMapCameraHeading() + + /* + If a PDF Page Dictionary contains the "Rotate" entry, it is a + request to the reader to rotate the _printed_ page *clockwise* by + the given number of degrees before reading it. + */ + let pdfPageDictionaryRotationEntryDegrees = pdfPage.rotationAngle + + /* + In the MapView world that is equivalent to subtracting that amount + from the MKMapCamera heading. + */ + let result = CLLocationDirection(rotatePDFXToMapKitX) - CLLocationDirection(pdfPageDictionaryRotationEntryDegrees) + + /* + According to the CLLocationDirection documentation we must store a + positive value if it is valid. + */ + return ((result < CLLocationDirection(0.0)) ? (result + CLLocationDirection(360.0)) : result) + } + + /** + Create an MKPolygon overlay given a custom CGPath (whose coordinates + are specified in the PDF points) + - parameter pdfPath: an array of CGPoint, each element is a PDF + coordinate along the path. + - returns: A closed MapKit polygon made up of the points in PDF path. + */ + func polygonFromCustomPDFPath(_ pdfPath: [CGPoint]) -> MKPolygon { + // Calculate the corresponding MKMapPoint for each PDF point. + var coordinates = pdfPath.map { pathPoint in + return coordinateConverter.MKMapPointFromPDFPoint(pathPoint) + } + + return MKPolygon(points: &coordinates, count: coordinates.count) + } + + /** + For debugging, you may want to draw the reference anchors that define + this floor's coordinate converter. + */ + var geoAnchorPair: GeoAnchorPair { + return coordinateConverter.anchors + } + + /// For debugging, you may want to draw the the (0.0, 0.0) point of the PDF. + var pdfOrigin: MKMapPoint { + return coordinateConverter.MKMapPointFromPDFPoint(CGPoint.zero) + } + + /** + For debugging, you may want to know the real-world coordinates of the + PDF page box. + */ + var polygonFromFloorplanPDFBoxCorners: MKPolygon { + return coordinateConverter.polygonFromPDFRectCorners(pdfBoxRectangle) + } + + /** + For debugging, you may want to have the boundingMapRect in the form of + an MKPolygon overlay + */ + var polygonFromBoundingMapRect: MKPolygon { + return boundingMapRect.polygonFromMapRect() + } + + /** + For debugging, you may want to have the + boundingMapRectIncludingRotations in the form of an MKPolygon overlay + */ + var polygonFromBoundingMapRectIncludingRotations: MKPolygon { + return boundingMapRectIncludingRotations.polygonFromMapRect() + } + + /** + For debugging, you may want to know the real-world meters size of one + PDF "point" distance. + */ + var pdfPointSizeInMeters: CLLocationDistance { + return coordinateConverter.unitSizeInMeters + } + +} diff --git a/Footprint/Swift/Footprint/FloorplanOverlayRenderer.swift b/Footprint/Swift/Footprint/FloorplanOverlayRenderer.swift new file mode 100644 index 00000000..9f7a3be7 --- /dev/null +++ b/Footprint/Swift/Footprint/FloorplanOverlayRenderer.swift @@ -0,0 +1,174 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This class draws your FloorplanOverlay into an MKMapView. + It is also capable of drawing diagnostic visuals to help with + debugging, if needed. +*/ + +import Foundation +import MapKit + +/** + Should we show diagnostic visuals? Set this to false prior to compile to + disable some of the diagnostic visuals +*/ +let SHOW_DIAGNOSTIC_VISUALS = false + +/** + This class draws your FloorplanOverlay into an MKMapView. + It is also capable of drawing diagnostic visuals to help with debugging, + if needed. +*/ +class FloorplanOverlayRenderer: MKOverlayRenderer { + + override init(overlay: MKOverlay) { + super.init(overlay: overlay) + } + + /** + - note: Overrides the drawMapRect method for MKOverlayRenderer. + */ + override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { + assert(overlay.isKind(of: FloorplanOverlay.self), "Wrong overlay type") + + let floorplanOverlay = overlay as! FloorplanOverlay + + let boundingMapRect = overlay.boundingMapRect + + /* + Mapkit converts to its own dynamic CGPoint frame, which we can read + through rectForMapRect. + */ + let mapkitToGraphicsConversion = rect(for: boundingMapRect) + + let graphicsFloorplanCenter = CGPoint(x: mapkitToGraphicsConversion.midX, y: mapkitToGraphicsConversion.midY) + let graphicsFloorplanWidth = mapkitToGraphicsConversion.width + let graphicsFloorplanHeight = mapkitToGraphicsConversion.height + + // Now, how does this compare to MapKit coordinates? + let mapkitFloorplanCenter = MKMapPoint(x: MKMapRectGetMidX(overlay.boundingMapRect), y: MKMapRectGetMidY(overlay.boundingMapRect)) + + let mapkitFloorplanWidth = MKMapRectGetWidth(overlay.boundingMapRect) + let mapkitFloorplanHeight = MKMapRectGetHeight(overlay.boundingMapRect) + + /* + Create the transformation that converts to Graphics coordinates from + MapKit coordinates. + + graphics.x = (mapkit.x - mapkitFloorplanCenter.x) * + graphicsFloorplanWidth / mapkitFloorplanWidth + + graphicsFloorplanCenter.x + */ + var fromMapKitToGraphics = CGAffineTransform.identity as CGAffineTransform + + fromMapKitToGraphics = fromMapKitToGraphics.translatedBy(x: CGFloat(-mapkitFloorplanCenter.x), y: CGFloat(-mapkitFloorplanCenter.y)) + fromMapKitToGraphics = fromMapKitToGraphics.scaledBy(x: graphicsFloorplanWidth / CGFloat(mapkitFloorplanWidth), + y: graphicsFloorplanHeight / CGFloat(mapkitFloorplanHeight) + ) + fromMapKitToGraphics = fromMapKitToGraphics.translatedBy(x: graphicsFloorplanCenter.x, y: graphicsFloorplanCenter.y) + + /* + Using this, we can send draw commands in MapKit coordinates and + cause the equivalent drawing in (the correct) graphics coordinates + For additional debugging, uncomment the following two lines to + highlight the floorplan's boundingMapRect in cyan. + */ + if (SHOW_DIAGNOSTIC_VISUALS == true) { + context.setFillColor(red: 0.0, green: 1.0, blue: 1.0, alpha: 0.5) + context.fill(mapkitToGraphicsConversion) + } + /* + However, we want to be able to send draw commands in the original + PDF coordinates though, so we'll also need the transformations that + convert to MapKit coordinates from PDF coordinates. + */ + let fromPDFToMapKit = floorplanOverlay.transformerFromPDFToMk + + context.concatenate(fromPDFToMapKit.concatenating(fromMapKitToGraphics)) + + context.drawPDFPage(floorplanOverlay.pdfPage) + + /* + The following diagnostic visuals are provided for debugging only. + In production, you'll want to remove them. + */ + if (SHOW_DIAGNOSTIC_VISUALS == true) { + drawDiagnosticVisuals(context, floorplanOverlay: floorplanOverlay) + } + } + + /** + This draws directly in the PDF coordinate system. + If drawing onto MapKit, the context object provided must already have + the appropriate transforms applied. + + If you have the transform correct, you should see the following: + [A] 1.0 m radius red square (50% alpha) centered on the 1st anchor pt. + [B] 1.0 m radius green square (50% alpha) centered on the 2nd anchor pt. + [C] a 1x1 point magenta square centered at the (0.0, 0.0) point of your + PDF. This square is created by the precise overlap of the + following two rectangles. + [C.1] a 10x1 point red rectangle (50% alpha) that covers the 1x1 point + square centered at PDF coordinate (0.0, 0.0) through the 1x1 + point square centered at PDF coordinate (10.0, 0.0). + [C.2] a 1x10 point blue rectangle (50% alpha) that covers the 1x1 point + square centered at PDF coordinate (0.0, 0.0) and the 1x1 point + square centered at PDF coordinate (10.0, 1.0). + + Use [A] & [B] to verify that your anchor points have been set to the + correct points on your PDF. If this does not match: + + check your PDF reader and make sure it is giving you values in + "points" and not "pixels" or some other unit of measure. + + look for typos in the CGPoint values of your GeoAnchor structs. + + Use [C] to verify the location of (0.0, 0.0) on your PDF. If this does + not match: + + check your PDF reader and make sure it is showing you values of the + underlying PDF coordinate system, and not its own internal display + coordinate system. A proper PDF coordinate system should have +x be + rightward and +y be upward. + + Use [C.1] & [C.2] to verify the sizes of "1.0 point" and "10.0 points" + on your PDF. If this does not match: + + check your PDF reader and make sure it is giving you values in + "points" and not "pixels" or some other unit of measure. + */ + func drawDiagnosticVisuals(_ context: CGContext, floorplanOverlay: FloorplanOverlay) { + // Draw a 1.0 meter radius square around each anchor point. + let radiusPDFPoints = CGFloat(1.0) / CGFloat(floorplanOverlay.pdfPointSizeInMeters) + let anchorMarkerSize = CGSize(width: radiusPDFPoints * 2.0, height: radiusPDFPoints * 2.0) + + let originPt = CGPoint(x: floorplanOverlay.geoAnchorPair.fromAnchor.pdfPoint.x - radiusPDFPoints, + y: floorplanOverlay.geoAnchorPair.fromAnchor.pdfPoint.y - radiusPDFPoints) + let destPt = CGPoint(x: floorplanOverlay.geoAnchorPair.toAnchor.pdfPoint.x - radiusPDFPoints, + y: floorplanOverlay.geoAnchorPair.toAnchor.pdfPoint.y - radiusPDFPoints) + let fromAnchorMarker = CGRect(origin: originPt, size: anchorMarkerSize) + let toAnchorMarker = CGRect(origin: destPt, size: anchorMarkerSize) + + // Anchor 1: Red. + context.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.75) + context.fill(fromAnchorMarker) + + // Anchor 2: Green. + context.setFillColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 0.75) + context.fill(toAnchorMarker) + + /** + Draw a 10pt x 1pt red rectangle that covers the square centered at + (0.0, 0.0) through the square centered at (10.0, 0.0). + */ + context.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.5) + context.fill(CGRect(x: -0.5, y: -0.5, width: 10.0, height: 1.0)) + + /** + Draw a 1pt x 10pt blue rectangle that covers the square centered at + (0.0, 0.0) through the square centered at (0.0, 10.0). + */ + context.setFillColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 0.5) + context.fill(CGRect(x: -0.5, y: -0.5, width: 1.0, height: 10.0)) + } + +} diff --git a/Footprint/Swift/Footprint/Floorplans/floorplan_overlay_floor0.pdf b/Footprint/Swift/Footprint/Floorplans/floorplan_overlay_floor0.pdf new file mode 100644 index 00000000..0a1dbfe2 Binary files /dev/null and b/Footprint/Swift/Footprint/Floorplans/floorplan_overlay_floor0.pdf differ diff --git a/Footprint/Swift/Footprint/HideBackgroundOverlay.swift b/Footprint/Swift/Footprint/HideBackgroundOverlay.swift new file mode 100644 index 00000000..b1bf9429 --- /dev/null +++ b/Footprint/Swift/Footprint/HideBackgroundOverlay.swift @@ -0,0 +1,40 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This class provides an MKOverlay that can be used to hide MapKit's + underlaying map tiles. +*/ + +import Foundation +import MapKit + +/** + This class provides an MKOverlay that can be used to hide MapKit's + underlaying map tiles. +*/ +class HideBackgroundOverlay: MKPolygon { + + /// - returns: a HideBackgroundOverlay object that covers the world. + class func hideBackgroundOverlay() -> HideBackgroundOverlay { + var corners = [MKMapPointMake(MKMapRectGetMaxX(MKMapRectWorld), MKMapRectGetMaxY(MKMapRectWorld)), + MKMapPointMake(MKMapRectGetMinX(MKMapRectWorld), MKMapRectGetMaxY(MKMapRectWorld)), + MKMapPointMake(MKMapRectGetMinX(MKMapRectWorld), MKMapRectGetMinY(MKMapRectWorld)), + MKMapPointMake(MKMapRectGetMaxX(MKMapRectWorld), MKMapRectGetMinY(MKMapRectWorld))] + return HideBackgroundOverlay(points: &corners, count: corners.count) + } + + /** + - returns: true to tell MapKit to hide its underlying map tiles, as long + as this overlay is visible (which, as you can see above, is + everywhere in the world), effectively hiding all map tiles and + replacing them with a solid colored MKPolygon. + */ + override func canReplaceMapContent() -> Bool { + return true + } + + +} + diff --git a/Footprint/Swift/Footprint/Images.xcassets/AppIcon.appiconset/Contents.json b/Footprint/Swift/Footprint/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/Footprint/Swift/Footprint/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Footprint/Swift/Footprint/Info.plist b/Footprint/Swift/Footprint/Info.plist new file mode 100644 index 00000000..d93a6fa1 --- /dev/null +++ b/Footprint/Swift/Footprint/Info.plist @@ -0,0 +1,49 @@ + + + + + NSLocationWhenInUseUsageDescription + This is an example of the app using user location only upon request + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.example.apple-samplecode.footprint + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Footprint/Swift/Footprint/MKMapRectRotated.swift b/Footprint/Swift/Footprint/MKMapRectRotated.swift new file mode 100644 index 00000000..d3af0326 --- /dev/null +++ b/Footprint/Swift/Footprint/MKMapRectRotated.swift @@ -0,0 +1,154 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + In order to properly clamp the MKMapView (see + VisibleMapRegionDelegate) to inside a floorplan (that may not be + "North up", and therefore may not be aligned with the standard + MKMapRect coordinate frames), + we'll need a way to store and quickly compute whether a specific + MKMapPoint is inside your floorplan or not, and the displacement to + the nearest edge of the floorplan. + + Since all PDF bounding boxes are still PDFs, after all, in the case + of this sample code we need only represent a "rotated" MKMapRect. + If you have transparency in your PDF or need something fancier, + consider an MKPolygon and some combination of CGPathContainsPoint() +*/ + +import Foundation +import MapKit + +/** + Represents a "direction vector" or a "unit vector" between MKMapPoints. + + It is intended to always have length 1.0, that is `hypot(eX, eY) === 1.0` + + - parameter eX: direction along x + - parameter eY: direction along y +*/ +struct MKMapDirection { + var eX = 0.0 + var eY = 0.0 +} + +/** + In order to properly clamp the MKMapView (see VisibleMapRegionDelegate) to + inside a floorplan (that may not be "North up", and therefore may not be + aligned with the standard MKMapRect coordinate frames), we'll need a way to + store and quickly compute whether a specific MKMapPoint is inside your + floorplan or not, and the displacement to the nearest edge of the floorplan. + + Since all PDF bounding boxes are still PDFs, after all, in the case of this + sample code we need only represent a "rotated" MKMapRect. + If you have transparency in your PDF or need something fancier, consider an + MKPolygon and some combination of CGPathContainsPoint(), etc. + + - parameter rectCenter: The center of the rectangle in MK coordinates. + - parameter rectSize: The size of the original rectangle in MK coordinates. + - parameter widthDirection: The "direction vector" of the "width" dimension. + This vector has length 1.0 and points in the direction of "width". + - parameter heightDirection: The "direction vector" of the "height" + dimension. This vector has length 1.0 and points in the direction of + "width". +*/ +struct MKMapRectRotated { + var rectCenter = MKMapPoint() + var rectSize = MKMapSize() + var widthDirection = MKMapDirection() + var heightDirection = MKMapDirection() +} + +/** + Displacement from two MKMapPoints -- a direction and distance. + + - parameter direction: The direction of displacement, a unit vector. + - parameter distance: The magnitude of the displacement. +*/ +struct MKMapPointDisplacement { + var direction = MKMapDirection() + var distance = 0.0 +} + +/** + - parameter corner1: First corner. + - parameter corner2: Next corner. + - parameter corner3: Corner after corner2. + - parameter corner4: Last corner. + + - note: The four corners MUST be in clockwise or counter-clockwise order + (i.e. going around the rectangle, and not criss-crossing through it)! + + - returns: MKMapRect constructed from the four corners of a (probably + rotated) rectangle. +*/ +func MKMapRectRotatedMake(_ corner1: MKMapPoint, corner2: MKMapPoint, corner3: MKMapPoint, corner4: MKMapPoint) -> MKMapRectRotated{ + + // Average the points to get the center of the rect in MKMapPoint space. + let averageX = (corner1.x + corner2.x + corner3.x + corner4.x) / 4.0 + let averageY = (corner1.y + corner2.y + corner3.y + corner4.y) / 4.0 + let center = MKMapPoint(x: averageX, y: averageY) + + // Figure out the "width direction" and "height direction"... + let heightMax = MKMapPoint.midpoint(corner1, b: corner2) + let heightMin = MKMapPoint.midpoint(corner4, b: corner3) + let widthMax = MKMapPoint.midpoint(corner1, b: corner4) + let widthMin = MKMapPoint.midpoint(corner2, b: corner3) + + // ...as well as the actual width and height. + let width = widthMax.displacementToPoint(widthMin) + let height = heightMax.displacementToPoint(heightMin) + let rotatedRectSize = MKMapSize(width: width.distance, height: height.distance) + + return MKMapRectRotated(rectCenter: center, rectSize: rotatedRectSize, + widthDirection: width.direction, heightDirection: height.direction) +} + +/** + Return the *nearest* MKMapPoint that is inside the MKMapRectRotated + For an "upright" rectangle, getting the nearest point is simple. Just clamp + the value to width and height! + + We'd love to have that simplicity too, so our underlying main strategy is to + simplify the problem. + + If we can answer the following two questions: + 1. how far away are you, from the rectangle, in the height direction? + 2. how far away are you, from the rectangle, in the width direction? + + Then we can use these values to take the exact same (simple) approach! + + - parameter mapRectRotated: Your (likely rotated) MKMapRectRotated. + - parameter point: An MKMapPoint. + + - returns: The MKMapPoint inside mapRectRotated that is closest to point +*/ +func MKMapRectRotatedNearestPoint(_ mapRectRotated: MKMapRectRotated, point: MKMapPoint) -> MKMapPoint { + let dxCenter = (point.x - mapRectRotated.rectCenter.x) + let dyCenter = (point.y - mapRectRotated.rectCenter.y) + + /* + We use a dot product against a unit vector (a.k.a. projection) to find + distance "along a particular direction." + */ + let widthDistance = dxCenter * mapRectRotated.widthDirection.eX + dyCenter * mapRectRotated.widthDirection.eY + + /* + We use a dot product against a unit vector (a.k.a. projection) to find + distance "along a particular direction." + */ + let heightDistance = dxCenter * mapRectRotated.heightDirection.eX + dyCenter * mapRectRotated.heightDirection.eY + + // "If this rectangle _were_ upright, this would be the result." + let widthNearestPoint = clamp(widthDistance, min: -0.5 * mapRectRotated.rectSize.width, max: 0.5 * mapRectRotated.rectSize.width) + let heightNearestPoint = clamp(heightDistance, min: -0.5 * mapRectRotated.rectSize.height, max: 0.5 * mapRectRotated.rectSize.height) + + /* + Since it's not upright, just combine the width and height in their + corresponding directions! + */ + let mapPointX = mapRectRotated.rectCenter.x + widthNearestPoint * mapRectRotated.widthDirection.eX + heightNearestPoint * mapRectRotated.heightDirection.eX + let mapPointY = mapRectRotated.rectCenter.y + widthNearestPoint * mapRectRotated.widthDirection.eY + heightNearestPoint * mapRectRotated.heightDirection.eY + return MKMapPoint(x: mapPointX, y: mapPointY) +} diff --git a/Footprint/Swift/Footprint/Utilities.swift b/Footprint/Swift/Footprint/Utilities.swift new file mode 100644 index 00000000..aefefaf4 --- /dev/null +++ b/Footprint/Swift/Footprint/Utilities.swift @@ -0,0 +1,154 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This contains several utility methods and extensions +*/ +import Foundation +import MapKit + +/** + - parameter a: + - parameter b: + - note: If numbers are the same, always chooses first + - returns: the SMALLER of the two numbers (not the minimum) e.g. + smallest(-5.0, 0.01) returns 0.01. +*/ +func smallest(_ a: Double, b: Double) -> Double { + return (fabs(a) <= fabs(b)) ? a: b +} + +/** + - parameter val: value to clamp. + - parameter min: least possible value. + - parameter max: greatest possible value. + + - returns: clamped version of val such that it falls between min and max. +*/ +func clamp(_ val: Double, min: Double, max: Double) -> Double { + return (val < min) ? min : ((val > max) ? max : val) +} + +extension MKMapPoint { + /** + - parameter a: Point A. + - parameter b: Point B. + - returns: An MKMapPoint object representing the midpoints of a and b. + */ + static func midpoint(_ a: MKMapPoint, b: MKMapPoint) -> MKMapPoint { + return MKMapPoint(x: (a.x + b.x) * 0.5, y: (a.y + b.y) * 0.5) + } + + /** + - parameter other: ending point. + - returns: The MKMapPointDisplacement between two MKMapPoint objects. + */ + func displacementToPoint(_ other: MKMapPoint) -> MKMapPointDisplacement { + let dx = (other.x - x) + let dy = (other.y - y) + let distance = hypot(dx, dy) + + return MKMapPointDisplacement(direction: MKMapDirection(eX: dx/distance, eY: dy/distance), distance: distance) + } +} + +extension CLLocationDistance { + /** + - parameter a: coordinate A. + - parameter b: coordinate B. + - returns: The distance between the two coordinates. + */ + static func distanceBetweenLocationCoordinates2D(_ a: CLLocationCoordinate2D, b: CLLocationCoordinate2D) -> CLLocationDistance { + + let locA: CLLocation = CLLocation(latitude: a.latitude, longitude: a.longitude) + let locB: CLLocation = CLLocation(latitude: b.latitude, longitude: b.longitude) + + return locA.distance(from: locB) + } +} + +extension CGPoint { + /** + - parameter a: point A. + - parameter b: point B. + - returns: the mean point of the two CGPoint objects. + */ + static func pointAverage(_ a: CGPoint, b: CGPoint) -> CGPoint { + return CGPoint(x:(a.x + b.x) * 0.5, y:(a.y + b.y) * 0.5) + } +} + +extension CGVector { + /** + - parameter other: a vector. + - returns: the dot product of the other vector with this vector. + */ + func dotProductWithVector(_ other: CGVector) -> CGFloat { + return dx * other.dx + dy * other.dy + } + + + /** + - parameter scale: how much to scale (e.g. 1.0, 1.5, 0.2, etc). + - returns: a copy of this vector, rescaled by the amount given. + */ + func scaledByFloat(_ scale: CGFloat) -> CGVector { + return CGVector(dx: dx * scale, dy: dy * scale) + } + + /** + - parameter radians: how many radians you want to rotate by. + - returns: a copy of this vector, after being rotated in the + "positive radians" direction by the amount given. + - note: If your coordinate frame is right-handed, positive radians + is counter-clockwise. + */ + func rotatedByRadians(_ radians: CGFloat) -> CGVector { + let cosRadians = cos(radians) + let sinRadians = sin(radians) + + return CGVector(dx: cosRadians * dx - sinRadians * dy, dy: sinRadians * dx + cosRadians * dy) + } +} + +extension CGPoint { + /** + - parameter a: point A. + - parameter b: point B. + - returns: The hypotenuse defined by the two. + */ + static func hypotenuse(_ a: CGPoint, b: CGPoint) -> Double { + return Double(hypot(b.x - a.x, b.y - a.y)) + } +} + +extension MKMapRect { + /** + - returns: The point at the center of the rectangle. + - parameter rect: A rectangle. + */ + func getCenter() -> MKMapPoint { + return MKMapPointMake(MKMapRectGetMidX(self), MKMapRectGetMidY(self)) + } + + /** + - parameter rect: a rectangle. + - returns: an MKMapRect converted to an MKPolygon. + */ + func polygonFromMapRect() -> MKPolygon { + var corners = [MKMapPointMake(MKMapRectGetMaxX(self), MKMapRectGetMaxY(self)), + MKMapPointMake(MKMapRectGetMinX(self), MKMapRectGetMaxY(self)), + MKMapPointMake(MKMapRectGetMinX(self), MKMapRectGetMinY(self)), + MKMapPointMake(MKMapRectGetMaxX(self), MKMapRectGetMinY(self))] + + return MKPolygon(points: &corners, count: corners.count) + } +} + +extension MKMapSize { + /// - returns: The area of this MKMapSize object + func area() -> Double { + return height * width + } +} diff --git a/Footprint/Swift/Footprint/ViewController.swift b/Footprint/Swift/Footprint/ViewController.swift new file mode 100644 index 00000000..1fbfb05d --- /dev/null +++ b/Footprint/Swift/Footprint/ViewController.swift @@ -0,0 +1,460 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Primary view controller for what is displayed by the application. + In this class we configure an MKMapView to display a floorplan, + recieve location updates to determine floor number, as well as + provide a few helpful debugging annotations. + We will also show how to highlight a region that you have defined in + PDF coordinates but not Latitude Longitude. +*/ + +import CoreLocation +import Foundation +import MapKit + +/** + Primary view controller for what is displayed by the application. + + In this class we configure an MKMapView to display a floorplan, recieve + location updates to determine floor number, as well as provide a few helpful + debugging annotations. + + We will also show how to highlight a region that you have defined in PDF + coordinates but not Latitude & Longitude. +*/ +class ViewController: UIViewController, MKMapViewDelegate { + + /// Outlet for the map view in the storyboard. + @IBOutlet weak var mapView: MKMapView! + + /// Outlet for the visuals switch at the lower-right of the storyboard. + @IBOutlet weak var debugVisualsSwitch: UISwitch! + + /** + To enable user location to be shown in the map, go to Main.storyboard, + select the Map View, open its Attribute Inspector and click the checkbox + next to User Location + + The user will need to authorize this app to use their location either by + enabling it in Settings or by selecting the appropriate option when + prompted. + */ + var locationManager: CLLocationManager! + + var hideBackgroundOverlayAlpha: CGFloat! + + /// Helper class for managing the scroll & zoom of the MapView camera. + var visibleMapRegionDelegate: VisibleMapRegionDelegate! + + /// Store the data about our floorplan here. + var floorplan0: FloorplanOverlay! + + var debuggingOverlays: [MKOverlay]! + var debuggingAnnotations: [MKAnnotation]! + + /// This property remembers which floor we're on. + var lastFloor: CLFloor! + + /** + Set to false if you want to turn off auto-scroll & auto-zoom that snaps + to the floorplan in case you scroll or zoom too far away. + */ + var snapMapViewToFloorplan: Bool! + + /** + Set to true when we reveal the MapKit tileset (by pressing the trashcan + button). + */ + var mapKitTilesetRevealed = false + + /// Call this to reset the camera. + @IBAction func resetCamera(_ sender: AnyObject) { + visibleMapRegionDelegate.mapViewResetCameraToFloorplan(mapView) + } + + /** + When the trashcan hasn't yet been pressed, this toggles the debug + visuals. Otherwise, this toggles the floorplan. + */ + @IBAction func toggleDebugVisuals(_ sender: AnyObject) { + if (sender.isKind(of: UISwitch.classForCoder())) { + let senderSwitch: UISwitch = sender as! UISwitch + /* + If we have revealed the mapkit tileset (i.e. the trash icon was + pressed), toggle the floorplan display off. + */ + if (mapKitTilesetRevealed == true) { + if (senderSwitch.isOn == true) { + showFloorplan() + } else { + hideFloorplan() + } + } else { + if (senderSwitch.isOn == true) { + showDebugVisuals() + } else { + hideDebugVisuals() + } + } + } + } + + /** + Remove all the overlays except for the debug visuals. Forces the debug + visuals switch off. + */ + @IBAction func revealMapKitTileset(_ sender: AnyObject) { + mapView.removeOverlays(mapView.overlays) + mapView.removeAnnotations(mapView.annotations) + // Show labels for restaurants, schools, etc. + mapView.showsPointsOfInterest = true + // Show building outlines. + mapView.showsBuildings = true + mapKitTilesetRevealed = true + // Set switch to off. + debugVisualsSwitch.setOn(false, animated: true) + showDebugVisuals() + } + + override func viewDidLoad() { + super.viewDidLoad() + + locationManager = CLLocationManager() + + // === Configure our floorplan. + + /* + We setup a pair of anchors that will define how the floorplan image + maps to geographic co-ordinates. + */ + let anchor1 = GeoAnchor(latitudeLongitudeCoordinate: CLLocationCoordinate2DMake(37.770419,-122.465726), pdfPoint: CGPoint(x: 26.2, y: 86.4)) + + let anchor2 = GeoAnchor(latitudeLongitudeCoordinate: CLLocationCoordinate2DMake(37.769288,-122.466376), pdfPoint: CGPoint(x: 570.1, y: 317.7)) + + let anchorPair = GeoAnchorPair(fromAnchor: anchor1, toAnchor: anchor2) + + /* + Pick a triangle on your PDF that you would like to highlight in + yellow. Feel free to try regions with more than three edges. + Note that these coordinates are given in PDF coordinates, but they + will show up on just fine on MapKit in MapKit coordinates. + */ + let pdfTriangleRegionToHighlight = [CGPoint(x: 205.0, y: 335.3), CGPoint(x: 205.0, y: 367.3), CGPoint(x: 138.5, y: 367.3)] + + // === Initialize our assets + + /* + We have to specify subdirectory here since we copy our folder + reference during "Copy Bundle Resources" section under target + settings build phases. + */ + let pdfUrl = Bundle.main.url(forResource: "floorplan_overlay_floor0", withExtension: "pdf", subdirectory:"Floorplans")! + + floorplan0 = FloorplanOverlay(floorplanUrl: pdfUrl, withPDFBox: CGPDFBox.trimBox, andAnchors: anchorPair, forFloorLevel: 0) + + visibleMapRegionDelegate = VisibleMapRegionDelegate(floorplanBounds: floorplan0.boundingMapRectIncludingRotations, boundingPDFBox: floorplan0.floorplanPDFBox, + floorplanCenter: floorplan0.coordinate, + floorplanUprightMKMapCameraHeading: floorplan0.getFloorplanUprightMKMapCameraHeading()) + + // === Initialize our view + hideBackgroundOverlayAlpha = 1.0 + + // Disable tileset. + mapView.add(HideBackgroundOverlay.hideBackgroundOverlay(), level: .aboveRoads) + + /* + The following are provided for debugging. + In production, you'll want to comment this out. + */ + debuggingOverlays = ViewController.createDebuggingOverlaysForMapView(mapView!, aboutFloorplan: floorplan0) + debuggingAnnotations = ViewController.createDebuggingAnnotationsForMapView(mapView!, aboutFloorplan: floorplan0) + + // Draw the floorplan! + mapView.add(floorplan0) + + /* + Highlight our region (originally specified in PDF coordinates) in + yellow! + */ + let customHighlightRegion = floorplan0.polygonFromCustomPDFPath(pdfTriangleRegionToHighlight) + customHighlightRegion.title = "Hello World" + customHighlightRegion.subtitle = "This custom region will be highlighted in Yellow!" + mapView!.add(customHighlightRegion) + + /* + By default, we listen to the scroll & zoom events to make sure that + if the user scrolls/zooms too far away from the floorplan, we + automatically bounce back. If you would like to disable this + behavior, comment out the following line. + */ + snapMapViewToFloorplan = true + } + + override func viewDidAppear(_ animated: Bool) { + /* + For additional debugging, you may prefer to use non-satellite + (standard) view instead of satellite view. If so, uncomment the line + below. However, satellite view allows you to zoom in more closely + than non-satellite view so you probably do not want to leave it this + way in production. + */ + //mapView.mapType = MKMapTypeStandard + } + + /// Respond to CoreLocation updates + func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { + let location: CLLocation = userLocation.location! + + // CLLocation updates will not always have floor information... + if (location.floor != nil) { + // ...but when they do, take note! + NSLog("Location (Floor %ld): %s", location.floor!, location.description) + lastFloor = location.floor + NSLog("We are on floor %ld", lastFloor.level) + } + } + + /// Request authorization if needed. + func mapViewWillStartLocatingUser(_ mapView: MKMapView) { + switch (CLLocationManager.authorizationStatus()) { + case CLAuthorizationStatus.notDetermined: + // Ask the user for permission to use location. + locationManager.requestWhenInUseAuthorization() + case CLAuthorizationStatus.denied: + NSLog("Please authorize location services for this app under Settings > Privacy") + case CLAuthorizationStatus.authorizedAlways, CLAuthorizationStatus.authorizedWhenInUse, CLAuthorizationStatus.restricted: + break + } + } + + /// Helper method that shows the floorplan. + func showFloorplan() { + mapView.add(floorplan0) + } + + /// Helper method that hides the floorplan. + func hideFloorplan() { + mapView.remove(floorplan0) + } + + /// Helper function that shows the debug visuals. + func showDebugVisuals() { + // Make the background transparent to reveal the underlying grid. + hideBackgroundOverlayAlpha = 0.5 + // Show debugging bounding boxes. + mapView.addOverlays(debuggingOverlays, level: .aboveRoads) + // Show debugging pins. + mapView.addAnnotations(debuggingAnnotations) + } + + /// Helper function that hides the debug visuals. + func hideDebugVisuals() { + mapView.removeAnnotations(debuggingAnnotations) + mapView.removeOverlays(debuggingOverlays) + hideBackgroundOverlayAlpha = 1.0 + } + + /** + Check for when the MKMapView is zoomed or scrolled in case we need to + bounce back to the floorplan. If, instead, you're using e.g. + MKUserTrackingModeFollow then you'll want to disable + snapMapViewToFloorplan since it will conflict with the user-follow + scroll/zoom. + */ + func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + if (snapMapViewToFloorplan == true) { + visibleMapRegionDelegate.mapView(mapView, regionDidChangeAnimated:animated) + } + } + + /// Produce each type of renderer that might exist in our mapView. + func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { + + if (overlay.isKind(of: FloorplanOverlay.self)) { + let renderer: FloorplanOverlayRenderer = FloorplanOverlayRenderer(overlay: overlay as MKOverlay) + return renderer + } + + if (overlay.isKind(of: HideBackgroundOverlay.self) == true) { + let renderer = MKPolygonRenderer(overlay: overlay as MKOverlay) + + /* + HideBackgroundOverlay covers the entire world, so this means all + of MapKit's tiles will be replaced with a solid white background + */ + renderer.fillColor = UIColor.white.withAlphaComponent(hideBackgroundOverlayAlpha) + + // No border. + renderer.lineWidth = 0.0 + renderer.strokeColor = UIColor.white.withAlphaComponent(0.0) + + return renderer + } + + if (overlay.isKind(of: MKPolygon.self) == true) { + let polygon: MKPolygon = overlay as! MKPolygon + + /* + A quick and dirty MKPolygon renderer for addDebuggingOverlays + and our custom highlight region. + In production, you'll want to implement this more cleanly. + "However, if each overlay uses different colors or drawing + attributes, you should find a way to initialize that information + using the annotation object, rather than having a large decision + tree in mapView:rendererForOverlay:" + + See "Creating Overlay Renderers from Your Delegate Object" + */ + if (polygon.title == "Hello World") { + let renderer = MKPolygonRenderer(polygon: polygon) + renderer.fillColor = UIColor.yellow.withAlphaComponent(0.5) + renderer.strokeColor = UIColor.yellow.withAlphaComponent(0.0) + renderer.lineWidth = 0.0 + return renderer + } + + if (polygon.title == "debug") { + let renderer = MKPolygonRenderer(polygon: polygon) + renderer.fillColor = UIColor.gray.withAlphaComponent(0.1) + renderer.strokeColor = UIColor.cyan.withAlphaComponent(0.5) + renderer.lineWidth = 2.0 + return renderer + } + } + + NSException(name:NSExceptionName(rawValue: "InvalidMKOverlay"), reason:"Did you add an overlay but forget to provide a matching renderer here? The class was type \(type(of: overlay))", userInfo:["wasClass": type(of: overlay)]).raise() + return MKOverlayRenderer() + } + + /// Produce each type of annotation view that might exist in our MapView. + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + /* + For now, all we have are some quick and dirty pins for viewing debug + annotations. To learn more about showing annotations, + see "Annotating Maps". + */ + + if (annotation.title! == "red") { + let pinView = MKPinAnnotationView() + pinView.pinTintColor = UIColor.red + pinView.canShowCallout = true + return pinView + } + + if (annotation.title! == "green") { + let pinView = MKPinAnnotationView() + pinView.pinTintColor = UIColor.green + pinView.canShowCallout = true + return pinView + } + + if (annotation.title! == "purple") { + let pinView = MKPinAnnotationView() + pinView.pinTintColor = UIColor.purple + pinView.canShowCallout = true + return pinView + } + + return nil + } + + /** + If you have set up your anchors correctly, this function will create: + 1. a red pin at the location of your fromAnchor. + 2. a green pin at the location of your toAnchor. + 3. a purple pin at the location of the PDF's internal origin. + + Use these pins to: + * Compare the location of pins #1 and #2 with the underlying Apple Maps + tiles. + + The pins should appear, on the real world, in the physical + locations corresponding to the landmarks that you chose for each + anchor. + + If either pin does not seem to be at the correct position on Apple + Maps, double-check for typos in the CLLocationCoordinate2D values + of your GeoAnchor struct. + * Compare the location of pins #1 and #2 with the matching colored + squares drawn by FloorplanOverlayRenderer.m:drawDiagnosticVisuals on + your floorplan overlay. + + The red pin should appear at the same location as the red square + the green pin should appear at the same location as the green + square. + + If either pin does not match the location of its corresponding + square, you may be having problems with coordinate conversion + accuracy. Try picking anchor points that are further apart. + + - parameter mapView: MapView to draw on. + - parameter aboutFloorplan: floorplan from which we get anchors and + coordinates. + */ + class func createDebuggingAnnotationsForMapView(_ mapView: MKMapView, aboutFloorplan floorplan: FloorplanOverlay) -> [MKPointAnnotation] { + // Drop a red pin on the fromAnchor latitudeLongitude location. + let fromAnchor = MKPointAnnotation() + fromAnchor.title = "red" + fromAnchor.subtitle = "fromAnchor should be here" + fromAnchor.coordinate = floorplan.geoAnchorPair.fromAnchor.latitudeLongitudeCoordinate + + // Drop a green pin on the toAnchor latitudeLongitude location. + let toAnchor = MKPointAnnotation() + toAnchor.title = "green" + toAnchor.subtitle = "toAnchor should be here" + toAnchor.coordinate = floorplan.geoAnchorPair.toAnchor.latitudeLongitudeCoordinate + + // Drop a purple pin showing the (0.0 pt, 0.0 pt) location of the PDF. + let pdfOrigin = MKPointAnnotation() + pdfOrigin.title = "purple" + pdfOrigin.subtitle = "This is the 0.0, 0.0 coordinate of your PDF" + pdfOrigin.coordinate = MKCoordinateForMapPoint(floorplan.pdfOrigin) + + return [fromAnchor, toAnchor, pdfOrigin] + } + + /** + Return an array of three debugging overlays. These overlays will show: + 1. the PDF Page Box that was selected for this floor. + 2. the boundingMapRect used to define the rendering of this floorplan by + MKMapView. + 3. the boundingMapRectIncludingRotations used to define the rendering of + this floorplan. + + Use these outlines to: + * Ensure that #1 shows a polygon that is just small enough to enclose + all of the important visual content in your floorplan. + + If this polygon is much larger than your floorplan, you may + experience runtime performance issues. In this case it's better + to choose or define a smaller PDF Page Box. + + * Ensure that #2 shows a polygon that encloses your floorplan exactly. + + If any important visual floorplan information is outside this + polygon, those parts of the floorplan might not be displayed to + the user, depending on their zoom & scrolling. In this case it's + better to choose or define a larger PDF Page Box. + + * Ensure that #3 shows a polygon that is large enough to contain your + floorplan comfortably, but still small enough to cause bounce-back + when the user scrolls/zooms out too far. + + The boundingMapRect is based on the PDF Page Box, so the best way + to adjust the boundingMapRect is to get a more accurate PDF Page + Box. + + Note: In this sample code app we use the boundingMapRect also to + determine the limits where zoom/scroll bounce-back takes place. + */ + class func createDebuggingOverlaysForMapView(_ mapView: MKMapView, aboutFloorplan floorplan: FloorplanOverlay) -> [MKPolygon] { + let floorplanPDFBox = floorplan.polygonFromFloorplanPDFBoxCorners + floorplanPDFBox.title = "debug" + floorplanPDFBox.subtitle = "PDF Page Box" + + let floorplanBoundingMapRect = floorplan.polygonFromBoundingMapRect + floorplanBoundingMapRect.title = "debug" + floorplanBoundingMapRect.subtitle = "boundingMapRect" + + let floorplanBoundingMapRectWithRotations = floorplan.polygonFromBoundingMapRectIncludingRotations + floorplanBoundingMapRectWithRotations.title = "debug" + floorplanBoundingMapRectWithRotations.subtitle = "boundingMapRectIncludingRotations" + + return [floorplanPDFBox, floorplanBoundingMapRect, floorplanBoundingMapRectWithRotations] + } +} diff --git a/Footprint/Swift/Footprint/VisibleMapRegionDelegate.swift b/Footprint/Swift/Footprint/VisibleMapRegionDelegate.swift new file mode 100644 index 00000000..123535f1 --- /dev/null +++ b/Footprint/Swift/Footprint/VisibleMapRegionDelegate.swift @@ -0,0 +1,268 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This class manages an MKMapView camera scroll zoom by implementing + the typical MKMapViewDelegate regionDidChangeAnimated and + regionWillChangeAnimated to add bounce-back when the user + scrolls/zooms away from the floorplan. +*/ + +import CoreLocation +import Foundation +import MapKit + +/** + This class manages an MKMapView camera scroll & zoom by implementing the + typical MKMapViewDelegate regionDidChangeAnimated and + regionWillChangeAnimated to add bounce-back when the user scrolls/zooms away + from the floorplan. +*/ +class VisibleMapRegionDelegate: NSObject { + + /** + Set to true if you would want reset the MapCamera to center on the + floorplan. + */ + var needResetCameraOrientation = true + + /** + Keep track of changes to [mapView camera].altitude so that we know + whether to auto-zoom or auto-scroll. + */ + fileprivate var lastAltitude: CLLocationDistance + + /** + Properties of the floorplan. See FloorplanOverlay for more. + */ + fileprivate var boundingMapRectIncludingRotations: MKMapRect + fileprivate var boundingPDFBox: MKMapRectRotated + fileprivate var floorplanCenter: CLLocationCoordinate2D! + fileprivate var floorplanUprightMKMapCameraHeading: CLLocationDirection! + + /// Initializes on floorplan bounds. + init(floorplanBounds: MKMapRect, boundingPDFBox: MKMapRectRotated, floorplanCenter: CLLocationCoordinate2D, floorplanUprightMKMapCameraHeading heading: CLLocationDirection) { + boundingMapRectIncludingRotations = floorplanBounds + self.boundingPDFBox = boundingPDFBox + self.floorplanCenter = floorplanCenter + floorplanUprightMKMapCameraHeading = heading + + lastAltitude = Double.nan + + needResetCameraOrientation = true + } + + /** + Resets the camera orientation to the floorplan on our mapview. + - parameter mapView: MKMapView upon which we reset. + */ + func mapViewResetCameraToFloorplan(_ mapView: MKMapView) { + resetCameraOrientation(mapView, center: floorplanCenter, heading: floorplanUprightMKMapCameraHeading) + } + + /// Handles zoom and floorplan autofit. + func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + let camera = mapView.camera + + var didClampZoom = false + + // Has the zoom level stabilized? + if (lastAltitude != camera.altitude) { + // Not yet! Someone is changing the zoom! + lastAltitude = camera.altitude + + // Auto-zoom the camera to fit the floorplan. + didClampZoom = clampZoomToFloorplan(mapView, floorplanBoundingMapRect: boundingMapRectIncludingRotations, floorplanCenter: floorplanCenter) + } + + if (!didClampZoom) { + // Once the zoom level has stabilized, auto-scroll if needed. + clampScrollToFloorplan(mapView, floorplanBoundingPDFBoxRect: boundingPDFBox, optionalCameraHeading: needResetCameraOrientation ? floorplanUprightMKMapCameraHeading : Double.nan) + needResetCameraOrientation = false + } + } + + /** + Resets the camera orientation to the given centerpoint with the given + heading/orientation. + - parameter mapView: MapView which needs to be re-centered. + - parameter center: new centerpoint. + - parameter heading: orientation to use. + */ + func resetCameraOrientation(_ mapView: MKMapView, center: CLLocationCoordinate2D, heading: CLLocationDirection) { + let newCamera = mapView.camera.copy() as! MKMapCamera + // Center the floorplan... + newCamera.centerCoordinate = center + // ...and rotate so the floorplan is upright. + newCamera.heading = heading + + mapView.setCamera(newCamera, animated: true) + } + + /** + - returns: `true` if the floorplan doesn't fill the screen. + - parameter mapView: MapView to check. + - parameter floorplanBoundingMapRect: MKMapRect that defines the + floorplan's boundaries. + */ + func floorplanDoesNotFillScreen(_ mapView: MKMapView, floorplanBoundingMapRect: MKMapRect) -> Bool { + + if (MKMapRectContainsRect(floorplanBoundingMapRect, mapView.visibleMapRect)) { + // Your view is already entirely inside the floorplan. + return false + } + + // The specific part of the floorplan that is currently visible. + let visiblePartOfFloorplan = MKMapRectIntersection(floorplanBoundingMapRect, mapView.visibleMapRect) + + // The floorplan does not fill your screen in either direction. + return ( + (visiblePartOfFloorplan.size.width < mapView.visibleMapRect.size.width) + && + (visiblePartOfFloorplan.size.height < mapView.visibleMapRect.size.height) + ) + } + + /** + Helper function for clampZoomToFloorplan() + - returns: the MapCamera altitude required to bounce back the MapCamera + zoom back onto the floorplan. if no zoom adjustment is needed, + returns NAN. + - parameter mapView: The MKMapView we're looking at + - parameter floorplanBoundingMapRect: floorplan's bounding rectangle. + */ + func getZoomAdjustment(_ mapView: MKMapView, floorplanBoundingMapRect: MKMapRect) -> Double { + let mapViewVisibleMapRectArea: Double = mapView.visibleMapRect.size.area() + + let maxZoomedOut: MKMapRect = mapView.mapRectThatFits(floorplanBoundingMapRect) + let maxZoomedOutArea: Double = maxZoomedOut.size.area() + + if (maxZoomedOutArea < mapViewVisibleMapRectArea) { + // You have zoomed out too far? + + let zoomFactor: Double = sqrt(maxZoomedOutArea / mapViewVisibleMapRectArea) + let currentAltitude: CLLocationDistance = mapView.camera.altitude + let newAltitude: CLLocationDistance = currentAltitude * zoomFactor + + let newAltitudeUsable: CLLocationDistance = newAltitude + + /** + NOTE: Supposedly MapKit's internal zoom level counter is by + powers of two, so a 0.5x buffer here is safe and should + prevent pulsing when we're near the maximum zoom level. + + Assumption: We will never see a lowestGoodAltitude smaller than + 0.5x a stable MapKit altitude. + */ + if (newAltitudeUsable < currentAltitude) { + // Zoom back in. + return newAltitudeUsable + } + } + + // No change. Return NAN. + return Double.nan + } + + /** + Detect whether the user has zoomed away from the floorplan and, if so, bounce back. + - returns: `true` if we needed to bounce back + - parameter mapView: mapview we're working on + - parameter floorplanBoundingMapRect: bounds of the floorplan + - parameter floorplanCenter: center of the floorplan + */ + func clampZoomToFloorplan(_ mapView: MKMapView, floorplanBoundingMapRect: MKMapRect, floorplanCenter: CLLocationCoordinate2D) -> Bool { + + if (floorplanDoesNotFillScreen(mapView, floorplanBoundingMapRect: floorplanBoundingMapRect)) { + // Clamp! + + let newAltitude: CLLocationDistance = getZoomAdjustment(mapView, floorplanBoundingMapRect: floorplanBoundingMapRect) + + if (!newAltitude.isNaN) { + // We have a zoom change to make! + + let newCamera: MKMapCamera = mapView.camera.copy() as! MKMapCamera + + newCamera.altitude = newAltitude + + /** + Since we've zoomed out enough to see the entire floorplan + anyway, let's re-center to make sure the entire floorplan is + actually on-screen. + */ + newCamera.centerCoordinate = floorplanCenter + + mapView.setCamera(newCamera, animated: true) + + return true + } + } + + // No zoom correction took place. + return false + } + + /** + Detect whether the user has scrolled away from the floorplan, and if so, + bounce back. + - parameter mapView: The MapView to scroll. + - parameter floorplanBoundingMapRect: A map rect that must be "in view" + when the scrolling is complete. We will only scroll until this map + rect enters the view. + - parameter optionalCameraHeading: If you give valid CLLocationDirection + we will also adjust the camera heading. If you give an invalid + CLLocationDirection (e.g. -1.0), we'll keep whatever heading the + camera already has. + */ + func clampScrollToFloorplan(_ mapView: MKMapView, floorplanBoundingPDFBoxRect: MKMapRectRotated, optionalCameraHeading: CLLocationDirection) { + + let rotationNeeded: Bool = 0.0 <= optionalCameraHeading && optionalCameraHeading < 360.0 + + /** + Assuming we are zoomed at the correct level, we still can't see the + floorplan. Maybe you have scrolled too far? + */ + + let visibleMapRectMid = MKMapPoint(x: MKMapRectGetMidX(mapView.visibleMapRect), y: MKMapRectGetMidY(mapView.visibleMapRect)) + + let visibleMapRectOriginProposed = MKMapRectRotatedNearestPoint(floorplanBoundingPDFBoxRect, point: visibleMapRectMid) + + let dxOffset = visibleMapRectOriginProposed.x - visibleMapRectMid.x + let dyOffset = visibleMapRectOriginProposed.y - visibleMapRectMid.y + + // Okay, now we know the "proposed" scroll adjustment... + + let visibleMapRectMidPixels = mapView.convert(MKCoordinateForMapPoint(visibleMapRectMid), toPointTo: mapView) + let visibleMapRectProposedPixels = mapView.convert(MKCoordinateForMapPoint(visibleMapRectOriginProposed), toPointTo: mapView) + + let scrollDistancePixels = CGPoint.hypotenuse(visibleMapRectProposedPixels, b: visibleMapRectMidPixels) + + /** + ...but is it more than 1.0 screen pixel worth? (Otherwise the user + probably wouldn't even notice) + + NOTE: Due to rounding errors it's hard to get exactly + scrollDistancePixels == 0.0 anyway, so doing a check like this + improves general responsiveness overall. + */ + let scrollNeeded = scrollDistancePixels > 1.0 + + if (rotationNeeded || scrollNeeded) { + let newCamera = mapView.camera.copy() as! MKMapCamera + if (rotationNeeded) { + // Rotation the camera (e.g. to make the floorplan upright). + newCamera.heading = optionalCameraHeading + } + if (scrollNeeded) { + // Scroll back toward the floorplan. + var cameraCenter = MKMapPointForCoordinate(mapView.camera.centerCoordinate) + cameraCenter.x += dxOffset + cameraCenter.y += dyOffset + newCamera.centerCoordinate = MKCoordinateForMapPoint(cameraCenter) + } + mapView.setCamera(newCamera, animated: true) + } + + } +} diff --git a/Footprint/Swift/LaunchImage.launchimage/Contents.json b/Footprint/Swift/LaunchImage.launchimage/Contents.json new file mode 100644 index 00000000..6f870a46 --- /dev/null +++ b/Footprint/Swift/LaunchImage.launchimage/Contents.json @@ -0,0 +1,51 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "subtype" : "retina4", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Footprint/Swift/README.md b/Footprint/Swift/README.md new file mode 100644 index 00000000..1cd742d6 --- /dev/null +++ b/Footprint/Swift/README.md @@ -0,0 +1,68 @@ +# Footprint: Indoor Location with Core Location + +Display device location against a custom floorplan PDF. + * First, we will draw your floorplan inside MapKit so that it matches its real-world Latitude/Longitude values. + * Then, using CoreLocation, we will take the position in Latitude/Longitude and display it in MapKit. + +We will demonstrate how to do the conversions between Geographic coordinate systems (Latitude/Longitude), floorplan PDF coordinate systems (x,y), and MapKit coordinates. + +Note: For this sample to show a real location & floor number, you must have a floorplan for a venue that is Indoor Positioning-enabled and the device will need to be in that venue. If you are not in a venue, try emulating a position in the venue using "Custom Location" in the simulator. Otherwise, the user's location will be shown far from the default floorplan and venue displayed in this app. Additionally, to ensure that user location is actually visible, you will need to enable it in the Attribute Inspector on the Map View in Main.storyboard. From there, the user would need to either allow the app to use their location from Settings or select "Allow" when prompted by the app the first time it is run. + +## ViewController + +This is your main view controller class which handles most of the display functionality. Three IBActions are available from the storyboard: + +To help with debugging we’ve added a toolbar at the bottom to provide some common operations. +* Close: removes HideBackgroundOverlay to reveal the underlying base maps, as you would see in a typical "outdoor" app that uses MKMapView. Most importantly, this allows you to verify the real-wrodl coordinates of the GeoAnchorPoints that you set in ViewController. +* Rotate: restores the origin camera view, centered on the floorplan and, most importantly, with the "floorplan facing up” — instead of "North facing up” which is the default when you use MKMapView out-of-the-box +* Debug: toggle debugging visuals on/off — shows additional overlays and annotations that correspond to the various internal variables inside the code. Use this as a reference when debugging anchor points, overlays, or any other rendering/interaction provided in this sample code. If Close has been pressed, this also toggles the floorplan on and off so that it can be visible on the underlying maps + +## VisibleMapRegionDelegate + +This handles the case where the user zooms or scrolls too far away from the floorplan by automatically "bouncing back" when this happens. + +## Converters and Renderers + +### CoordinateConvereter + +This is used as a converter from the coordinates in the PDF image of the floorplan to geographic coordinates. This may be modified to work with raster images such as JPEGs and PNGs but this is not recommended as they won't render as clearly. See comments for more information + +### FloorplanOverlay + +This is used to describe the floorplan of an indoor venue and is based upon MKOverlay + +### FloorplanOverlayRenderer + +This is used to render a FloorplanOverlay onto a map view and also handles some debugging tasks + +### HideBackgroundOverlay + +This is used to hide MapKit's underlying map tiles so that only our floorplan is visible + +### Utilities + +Some additional Swift extensions + +## Using Your Own Floorplan +If you have a venue floorplan you would like to use, make the following changes: + +Step \#1: Replace (or add) `floorplan_overlay_floor0.pdf` in `Floorplans`. If necessary, update the filename in `ViewController.m` `viewDidLoad` + +Step \#2: Look for the `GeoAnchorPair` struct in `ViewController.m` `viewDidLoad` and set them to your own values. Pick any two points on the floorplan (in PDF x,y) as well as their corresponding real-world locations (in Latitude/Longitude) + +Step \#3: Follow the code comments of `drawDiagnosticVisuals` in `FloorplanOverlayRenderer.m` to verify that your (x,y) values from Step \#2 are correct. + +Step \#4: Follow the code comments of `setDebuggingAnnotationsOfMapView`: in `ViewController.m` to verify that your (Latitude/Longitude) values from Step \#2 are correct. + +## Requirements + +### Build + +Xcode 8.0, iOS 9 or later + +### Runtime + +Xcode 8.0 Simulator (OS X 10.10.3) +iOS 9 or later + +Copyright (C) 2014-2015 Apple Inc. All rights reserved. diff --git a/ForceTouchCatalog/ForceTouchCatalog.xcodeproj/project.pbxproj b/ForceTouchCatalog/ForceTouchCatalog.xcodeproj/project.pbxproj new file mode 100644 index 00000000..b6b430f7 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog.xcodeproj/project.pbxproj @@ -0,0 +1,326 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 872E0FC01B11104B00F6E445 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 872E0FBF1B11104B00F6E445 /* AppDelegate.swift */; }; + 872E0FC21B11104B00F6E445 /* SquireViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 872E0FC11B11104B00F6E445 /* SquireViewController.swift */; }; + 872E0FC41B11104B00F6E445 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 872E0FC31B11104B00F6E445 /* Assets.xcassets */; }; + 872E0FC71B11104B00F6E445 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 872E0FC51B11104B00F6E445 /* Main.storyboard */; }; + 872E0FD31B11175900F6E445 /* Lola1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 872E0FCF1B11175900F6E445 /* Lola1.jpg */; }; + 872E0FD41B11175900F6E445 /* Lola2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 872E0FD01B11175900F6E445 /* Lola2.jpg */; }; + 872E0FD51B11175900F6E445 /* Lola3.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 872E0FD11B11175900F6E445 /* Lola3.jpg */; }; + 872E0FD61B11175900F6E445 /* Lola4.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 872E0FD21B11175900F6E445 /* Lola4.jpg */; }; + 872E0FD81B11263D00F6E445 /* KnightViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 872E0FD71B11263D00F6E445 /* KnightViewController.swift */; }; + 872E0FDA1B11266C00F6E445 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 872E0FD91B11266C00F6E445 /* MasterViewController.swift */; }; + 87BACCC61B15689B00733551 /* DrawingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BACCC51B15689B00733551 /* DrawingView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 3EB05B7C1B1B851A00AA6D78 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 872E0FBC1B11104B00F6E445 /* ForceTouchCatalog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ForceTouchCatalog.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 872E0FBF1B11104B00F6E445 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 872E0FC11B11104B00F6E445 /* SquireViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquireViewController.swift; sourceTree = ""; }; + 872E0FC31B11104B00F6E445 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 872E0FC61B11104B00F6E445 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 872E0FC81B11104B00F6E445 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 872E0FCF1B11175900F6E445 /* Lola1.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Lola1.jpg; sourceTree = ""; }; + 872E0FD01B11175900F6E445 /* Lola2.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Lola2.jpg; sourceTree = ""; }; + 872E0FD11B11175900F6E445 /* Lola3.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Lola3.jpg; sourceTree = ""; }; + 872E0FD21B11175900F6E445 /* Lola4.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Lola4.jpg; sourceTree = ""; }; + 872E0FD71B11263D00F6E445 /* KnightViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KnightViewController.swift; sourceTree = ""; }; + 872E0FD91B11266C00F6E445 /* MasterViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = ""; }; + 87BACCC51B15689B00733551 /* DrawingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DrawingView.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 872E0FB91B11104B00F6E445 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 872E0FB31B11104B00F6E445 = { + isa = PBXGroup; + children = ( + 3EB05B7C1B1B851A00AA6D78 /* README.md */, + 872E0FBE1B11104B00F6E445 /* ForceTouchCatalog */, + 872E0FBD1B11104B00F6E445 /* Products */, + ); + sourceTree = ""; + }; + 872E0FBD1B11104B00F6E445 /* Products */ = { + isa = PBXGroup; + children = ( + 872E0FBC1B11104B00F6E445 /* ForceTouchCatalog.app */, + ); + name = Products; + sourceTree = ""; + }; + 872E0FBE1B11104B00F6E445 /* ForceTouchCatalog */ = { + isa = PBXGroup; + children = ( + 872E0FBF1B11104B00F6E445 /* AppDelegate.swift */, + 872E0FC11B11104B00F6E445 /* SquireViewController.swift */, + 872E0FD71B11263D00F6E445 /* KnightViewController.swift */, + 872E0FD91B11266C00F6E445 /* MasterViewController.swift */, + 87BACCC51B15689B00733551 /* DrawingView.swift */, + 872E0FC31B11104B00F6E445 /* Assets.xcassets */, + 872E0FC51B11104B00F6E445 /* Main.storyboard */, + 872E0FC81B11104B00F6E445 /* Info.plist */, + 872E0FCE1B11175900F6E445 /* Lola */, + ); + path = ForceTouchCatalog; + sourceTree = ""; + }; + 872E0FCE1B11175900F6E445 /* Lola */ = { + isa = PBXGroup; + children = ( + 872E0FCF1B11175900F6E445 /* Lola1.jpg */, + 872E0FD01B11175900F6E445 /* Lola2.jpg */, + 872E0FD11B11175900F6E445 /* Lola3.jpg */, + 872E0FD21B11175900F6E445 /* Lola4.jpg */, + ); + path = Lola; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 872E0FBB1B11104B00F6E445 /* ForceTouchCatalog */ = { + isa = PBXNativeTarget; + buildConfigurationList = 872E0FCB1B11104B00F6E445 /* Build configuration list for PBXNativeTarget "ForceTouchCatalog" */; + buildPhases = ( + 872E0FB81B11104B00F6E445 /* Sources */, + 872E0FB91B11104B00F6E445 /* Frameworks */, + 872E0FBA1B11104B00F6E445 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ForceTouchCatalog; + productName = ForceTouchCatalog; + productReference = 872E0FBC1B11104B00F6E445 /* ForceTouchCatalog.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 872E0FB41B11104B00F6E445 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Apple, Inc."; + TargetAttributes = { + 872E0FBB1B11104B00F6E445 = { + CreatedOnToolsVersion = 7.0; + LastSwiftMigration = 0800; + }; + }; + }; + buildConfigurationList = 872E0FB71B11104B00F6E445 /* Build configuration list for PBXProject "ForceTouchCatalog" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 872E0FB31B11104B00F6E445; + productRefGroup = 872E0FBD1B11104B00F6E445 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 872E0FBB1B11104B00F6E445 /* ForceTouchCatalog */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 872E0FBA1B11104B00F6E445 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 872E0FC41B11104B00F6E445 /* Assets.xcassets in Resources */, + 872E0FD51B11175900F6E445 /* Lola3.jpg in Resources */, + 872E0FD61B11175900F6E445 /* Lola4.jpg in Resources */, + 872E0FC71B11104B00F6E445 /* Main.storyboard in Resources */, + 872E0FD41B11175900F6E445 /* Lola2.jpg in Resources */, + 872E0FD31B11175900F6E445 /* Lola1.jpg in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 872E0FB81B11104B00F6E445 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 872E0FDA1B11266C00F6E445 /* MasterViewController.swift in Sources */, + 872E0FD81B11263D00F6E445 /* KnightViewController.swift in Sources */, + 87BACCC61B15689B00733551 /* DrawingView.swift in Sources */, + 872E0FC21B11104B00F6E445 /* SquireViewController.swift in Sources */, + 872E0FC01B11104B00F6E445 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 872E0FC51B11104B00F6E445 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 872E0FC61B11104B00F6E445 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 872E0FC91B11104B00F6E445 /* 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_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 872E0FCA1B11104B00F6E445 /* 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_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + }; + name = Release; + }; + 872E0FCC1B11104B00F6E445 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = ForceTouchCatalog/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.ForceTouchCatalog"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 872E0FCD1B11104B00F6E445 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = ForceTouchCatalog/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.ForceTouchCatalog"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 872E0FB71B11104B00F6E445 /* Build configuration list for PBXProject "ForceTouchCatalog" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 872E0FC91B11104B00F6E445 /* Debug */, + 872E0FCA1B11104B00F6E445 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 872E0FCB1B11104B00F6E445 /* Build configuration list for PBXNativeTarget "ForceTouchCatalog" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 872E0FCC1B11104B00F6E445 /* Debug */, + 872E0FCD1B11104B00F6E445 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 872E0FB41B11104B00F6E445 /* Project object */; +} diff --git a/ForceTouchCatalog/ForceTouchCatalog/AppDelegate.swift b/ForceTouchCatalog/ForceTouchCatalog/AppDelegate.swift new file mode 100644 index 00000000..f9a09ab5 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/AppDelegate.swift @@ -0,0 +1,12 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Main application entry point. +*/ + +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { } diff --git a/ForceTouchCatalog/ForceTouchCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json b/ForceTouchCatalog/ForceTouchCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..2db2b1c7 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ForceTouchCatalog/ForceTouchCatalog/Base.lproj/Main.storyboard b/ForceTouchCatalog/ForceTouchCatalog/Base.lproj/Main.storyboard new file mode 100644 index 00000000..d25cf8c2 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/Base.lproj/Main.storyboard @@ -0,0 +1,954 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ForceTouchCatalog/ForceTouchCatalog/DrawingView.swift b/ForceTouchCatalog/ForceTouchCatalog/DrawingView.swift new file mode 100644 index 00000000..eb33b503 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/DrawingView.swift @@ -0,0 +1,242 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A custom view that changes the brush size based on the pressure the user applies to the Force Touch Trackpad. Also contains a subclass of DrawingView (MasterDrawingView) that provides an example of how to configure the trackpad so that the user does not get force clicks while drawing. +*/ + +import Cocoa + +class MasterDrawingView: DrawingView { + override func awakeFromNib() { + super.awakeFromNib() + + // Configures the trackpad so that the user does not get force clicks when drawing. + pressureConfiguration = NSPressureConfiguration(pressureBehavior: .primaryGeneric) + } +} + +class DrawingView: NSView { + // MARK: Properties + + static let minStrokeWidth: CGFloat = 1.0 + static let maxStrokeWidth: CGFloat = 15.0 + + var drawingBitmap: NSBitmapImageRep? + var eraseTimer: Timer? + var penColor = NSColor.darkGray + + // MARK: Force Touch Trackpad Event Handling + + func dataFromMouseEvent(_ event: NSEvent, pressure: CGFloat) -> (loc: NSPoint, pressure: CGFloat, isUp: Bool) { + let loc = convert(event.locationInWindow, from: nil) + var isUp = false + var outPressure = pressure + + switch event.type { + case .leftMouseUp: + isUp = true + + case .leftMouseDragged: + if event.subtype == .tabletPoint { + // Pressure is always in the range [0,1]. + outPressure = CGFloat(event.pressure) + } + + case .tabletPoint: + /* + Tablets issue pure tablet point events between the mouse down and + the first mouse drag. After that it should be all mouse drag events. + Pressure is always in the range [0,1]. + */ + outPressure = CGFloat(event.pressure) + + case .pressure: + if event.stage > 1 { + /* + Cap pressure at 1. If we moved to stage 2, then consider this max pressure. + Note: Generally, do not add the stage value to the pressure value to get + a larger dynamic range. The force click feedback will be distracting + to the user and the additional pressure curves are not tuned for this. + You should set the pressureConfiguration to NSPressureBehaviorGeneric + to get a single stage pressure gesture with a large, properly tuned + input range. See MasterDrawingView below for an example. + */ + outPressure = 1.0 + } + else { + // Pressure is always in the range [0,1]. + outPressure = CGFloat(event.pressure) + } + + default: + break + } + + return (loc, outPressure, isUp) + } + + override func mouseDown(with mouseDownEvent: NSEvent) { + cancelEraseTimer() + + let drawingBitmap = drawingBitmapCreateIfNeeded() + + NSEvent.setMouseCoalescingEnabled(false) + var lastLocation = convert(mouseDownEvent.locationInWindow, from: nil) + + // This may not be a force capable or tablet device. Let's start off with 1/4 pressure. + var lastPressure: CGFloat = 0.25 + + /* + Add the pressure event mask to the drag events mask. + + Note: This value is used in the event coalescing loop, thus the `mouseUpMask` + is not included here. It's added in the eventTrackingMask below + */ + let dragEventsMask: NSEventMask = [.leftMouseDragged, .tabletPoint, .pressure] + + /* + The eventTracking mask is the same as dragEventMasks but it also includes + the mouse up event because tracking ends on mouse up. + */ + let eventTrackingMask = dragEventsMask.union(.leftMouseUp) + + window!.trackEvents(matching: eventTrackingMask, timeout: NSEventDurationForever, mode: RunLoopMode.eventTrackingRunLoopMode) { event, stop in + var newLocation = lastLocation + var newPressure = lastPressure + var isUp: Bool + + // Update new mouse event properties based on tuple return from `dataFromMouseEvent()`. + (newLocation, newPressure, isUp) = self.dataFromMouseEvent(event, pressure: lastPressure) + + self.needsDisplay = true + + if isUp { + /* + Avoid drawing a point for the mouse up. The pressure on the mouse up + will will be close to 0, and it's generally at the last mouse drag + location anyway. + */ + stop.pointee = true + return + } + + self.drawInBitmap(drawingBitmap) { + self.penColor.set() + + self.strokeLineFromPoint(lastLocation, toPoint: newLocation, pressure: newPressure, minWidth: DrawingView.minStrokeWidth, maxWidth: DrawingView.maxStrokeWidth) + + lastLocation = newLocation + lastPressure = newPressure + + /* + Mouse event coalescing is turned off so that we get all of the input. + To keep up, we need to absorb all events still in the queue. + Note: A custom run loop mode is specified to prevent timers and other run + loop sources from firing while we absorb these events. + */ + while let absorbedEvent = self.window!.nextEvent(matching: NSEventMask(rawValue: UInt64(Int(dragEventsMask.rawValue))), until: Date.distantPast, inMode: RunLoopMode(rawValue: "DrawingView_Event_Coalescing_Mode"), dequeue: true) { + + (newLocation, newPressure, isUp) = self.dataFromMouseEvent(absorbedEvent, pressure: lastPressure) + + self.strokeLineFromPoint(lastLocation, toPoint: newLocation, pressure: newPressure, minWidth: DrawingView.minStrokeWidth, maxWidth: DrawingView.maxStrokeWidth) + + lastLocation = newLocation + + lastPressure = newPressure + } + } + } + + NSEvent.setMouseCoalescingEnabled(true) + + installEraseTimer() + } + + + // MARK: Drawing + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + if let drawingBitmap = drawingBitmap { + drawingBitmap.draw(in: bounds, from: NSZeroRect, operation: .sourceOver, fraction: 1.0, respectFlipped: false, hints: nil) + } + else { + NSColor.white.set() + NSRectFill(dirtyRect) + + let drawHereString = NSAttributedString(string: "Draw Here", attributes: [ + NSForegroundColorAttributeName: NSColor.gray, + NSFontAttributeName: NSFont.userFont(ofSize: 24.0)! + ]) + + let stringSize = drawHereString.size() + + let drawPointX = bounds.midX - (stringSize.width / 2.0) + let drawPointY = bounds.midY - (stringSize.height / 2.0) + let drawPoint = NSPoint(x: drawPointX, y: drawPointY) + + drawHereString.draw(at: drawPoint) + } + + NSColor.black.set() + NSFrameRectWithWidth(bounds, 2.0) + } + + // MARK: Convenience + + func eraseTimerFired(_ timer: Timer) { + eraseTimer = nil + drawingBitmap = nil + needsDisplay = true + } + + func installEraseTimer() { + eraseTimer = Timer.scheduledTimer(timeInterval: 4.0, target: self, selector: #selector(DrawingView.eraseTimerFired(_:)), userInfo: nil, repeats: false) + } + + func cancelEraseTimer() { + eraseTimer?.invalidate() + eraseTimer = nil + } + + func drawingBitmapCreateIfNeeded() -> NSBitmapImageRep { + if drawingBitmap == nil { + drawingBitmap = bitmapImageRepForCachingDisplay(in: bounds) + + let bitmapGraphicsContext = NSGraphicsContext(bitmapImageRep: drawingBitmap!) + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.setCurrent(bitmapGraphicsContext) + NSColor.white.set() + + let fillRect = NSRect(x: 0, y: 0, width: bounds.width, height: bounds.height) + NSRectFillUsingOperation(fillRect, .sourceOver) + + NSGraphicsContext.restoreGraphicsState() + } + + return drawingBitmap! + } + + func strokeLineFromPoint(_ fromPoint: NSPoint, toPoint: NSPoint, pressure: CGFloat, minWidth: CGFloat, maxWidth:CGFloat) { + let width = minWidth + (pressure * (maxWidth - minWidth)) + let bezierPath = NSBezierPath() + bezierPath.move(to: fromPoint) + bezierPath.line(to: toPoint) + bezierPath.lineWidth = width + bezierPath.lineCapStyle = .roundLineCapStyle + bezierPath.stroke() + } + + func drawInBitmap(_ bitmap: NSBitmapImageRep, handler: (Void) -> Void) { + let bitmapGraphicsContext = NSGraphicsContext(bitmapImageRep: bitmap) + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.setCurrent(bitmapGraphicsContext) + + handler() + + NSGraphicsContext.restoreGraphicsState() + } +} diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/AppDelegate.swift b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/AppDelegate.swift new file mode 100644 index 00000000..f1905b6f --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/AppDelegate.swift @@ -0,0 +1,13 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An empty application delegate. +*/ + +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate {} + diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..2db2b1c7 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Base.lproj/Main.storyboard b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Base.lproj/Main.storyboard new file mode 100644 index 00000000..20029181 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Base.lproj/Main.storyboard @@ -0,0 +1,954 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/DrawingView.swift b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/DrawingView.swift new file mode 100644 index 00000000..100fa099 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/DrawingView.swift @@ -0,0 +1,210 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A custom view that changes the brush size based on the pressure the user applies to the Force Touch Trackpad. Also contains a subclass of DrawingView (MasterDrawingView) that provides an example of how to configure the trackpad so that the user does not get force clicks while drawing. +*/ + +import Cocoa + +class DrawingView: NSView { + static let minStrokeWidth = CGFloat(1.0) + static let maxStrokeWidth = CGFloat(15.0) + + var drawingBitmap: NSBitmapImageRep? + var eraseTimer: NSTimer? + var penColor = NSColor.darkGrayColor() + + override func drawRect(dirtyRect: NSRect) { + super.drawRect(dirtyRect) + + if let drawingBitmap = drawingBitmap { + drawingBitmap.drawInRect(bounds, fromRect: NSZeroRect, operation: .CompositeSourceOver, fraction: 1.0, respectFlipped: false, hints: nil) + } else { + NSColor.whiteColor().set() + NSRectFill(dirtyRect) + + let drawHereString = NSAttributedString(string: "Draw Here", attributes: [NSForegroundColorAttributeName : NSColor.grayColor(), NSFontAttributeName : NSFont.userFontOfSize(24.0)!]) + let stringSize = drawHereString.size() + let drawPoint = NSMakePoint(bounds.midX - (stringSize.width / 2.0), bounds.midY - (stringSize.height / 2.0)) + drawHereString.drawAtPoint(drawPoint) + } + + NSColor.blackColor().set() + NSFrameRectWithWidth(bounds, 2.0) + } + + func eraseTimerFired(timer: NSTimer) { + eraseTimer = nil + drawingBitmap = nil + needsDisplay = true + } + + func installEraseTimer() { + eraseTimer = NSTimer.scheduledTimerWithTimeInterval(4.0, target: self, selector: "eraseTimerFired:", userInfo: nil, repeats: false) + } + + func cancelEraseTimer() { + eraseTimer?.invalidate() + eraseTimer = nil + } + + func drawingBitmapCreateIfNeeded() -> NSBitmapImageRep! { + if drawingBitmap == nil { + drawingBitmap = bitmapImageRepForCachingDisplayInRect(bounds) + + let bitmapGraphicsContext = NSGraphicsContext(bitmapImageRep: drawingBitmap!) + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.setCurrentContext(bitmapGraphicsContext) + NSColor.whiteColor().set() + NSRectFillUsingOperation(NSMakeRect(0, 0, bounds.width, bounds.height), NSCompositingOperation.CompositeSourceOver) + NSGraphicsContext.restoreGraphicsState() + + } + + return drawingBitmap + } + + func strokeLineFromPoint(fromPoint:NSPoint, toPoint:NSPoint, pressure:CGFloat, minWidth:CGFloat, maxWidth:CGFloat) { + let width = minWidth + (pressure * (maxWidth - minWidth)) + let bezierPath = NSBezierPath() + bezierPath.moveToPoint(fromPoint) + bezierPath.lineToPoint(toPoint) + bezierPath.lineWidth = width + bezierPath.lineCapStyle = .RoundLineCapStyle + bezierPath.stroke() + } + + func drawInBitmap(bitmap: NSBitmapImageRep, handler: () -> Void) { + let bitmapGraphicsContext = NSGraphicsContext(bitmapImageRep: bitmap) + NSGraphicsContext.saveGraphicsState() + NSGraphicsContext.setCurrentContext(bitmapGraphicsContext) + + handler() + + NSGraphicsContext.restoreGraphicsState() + } + + func dataFromMouseEvent(event: NSEvent, pressure:CGFloat) -> (loc: NSPoint, pressure: CGFloat, isUp: Bool) { + let loc = convertPoint(event.locationInWindow, fromView:nil) + var isUp = false + var outPressure = pressure + + switch event.type { + case .LeftMouseUp: + isUp = true + break + + case .LeftMouseDragged: + if event.subtype == .NSTabletPointEventSubtype { + // Pressure is always in the range [0,1]. + outPressure = CGFloat(event.pressure) + } + break + + case .TabletPoint: + /* + Tablets issue pure tablet point events between the mouse down and + the first mouse drag. After that it should be all mouse drag events. + Pressure is always in the range [0,1]. + */ + outPressure = CGFloat(event.pressure) + break + + case .EventTypePressure: + if event.stage > 1 { + /* + Cap pressure at 1. If we moved to stage 2, then consider this max pressure. + Note: Generally, do not add the stage value to the pressure value to get + a larger dynamic range. The force click feedback will be distracting + to the user and the additional pressure curves are not tuned for this. + You should set the pressureConfiguration to NSPressureBehaviorGeneric + to get a single stage pressure gesture with a large, properly tuned + input range. See MasterDrawingView below for an example. + */ + outPressure = 1.0 + } else { + // Pressure is always in the range [0,1]. + outPressure = CGFloat(event.pressure) + } + break + + default: + break + } + + return (loc, outPressure, isUp) + } + + override func mouseDown(mouseDownEvent: NSEvent) { + + cancelEraseTimer() + + let drawingBitmap = drawingBitmapCreateIfNeeded() + + /* + Add the pressure event mask to the drag events mask. + Note: This value is also used in the event coalescing loop, thus the mouseUpMask + is not included here. It's added directly in the outer tracking loop. + */ + let dragEventsMask: NSEventMask = [.LeftMouseDraggedMask, .TabletPointMask, .EventMaskPressure] + + NSEvent.setMouseCoalescingEnabled(false) + var lastLocation = convertPoint(mouseDownEvent.locationInWindow, fromView:nil) + + // This may not be a force capable or tablet device. Let's start off with 1/4 pressure. + var lastPressure: CGFloat = 0.25 + + window!.trackEventsMatchingMask(dragEventsMask.union(.LeftMouseUpMask), timeout: NSEventDurationForever, mode:NSEventTrackingRunLoopMode) { (event, stop) in + var newLocation = lastLocation + var newPressure = lastPressure + var isUp: Bool + (lastLocation, newPressure, isUp) = self.dataFromMouseEvent(event, pressure: lastPressure) + + self.needsDisplay = true + if isUp { + /* + Avoid drawing a point for the mouse up. The pressure on the mouse up + will will be close to 0, and it's generally at the last mouse drag + location anyway. + */ + stop.memory = true + return + } + + self.drawInBitmap(drawingBitmap) { + self.penColor.set() + self.strokeLineFromPoint(lastLocation, toPoint: newLocation, pressure: newPressure, minWidth: DrawingView.minStrokeWidth, maxWidth: DrawingView.maxStrokeWidth) + lastLocation = newLocation + lastPressure = newPressure + + /* + Mouse event coalescing is turned off so that we get all of the input. + To keep up, we need to absorb all events still in the queue. + Note: A custom run loop mode is specified to prevent timers and other run + loop sources from firing while we absorb these events. + */ + while let absorbedEvent = self.window!.nextEventMatchingMask(Int(dragEventsMask.rawValue), untilDate: NSDate.distantPast(), inMode: "DrawingView_Event_Coalescing_Mode", dequeue:true) { + (newLocation, newPressure, isUp) = self.dataFromMouseEvent(absorbedEvent, pressure: lastPressure) + self.strokeLineFromPoint(lastLocation, toPoint: newLocation, pressure: newPressure, minWidth: DrawingView.minStrokeWidth, maxWidth: DrawingView.maxStrokeWidth) + lastLocation = newLocation + lastPressure = newPressure + } + } + } + + NSEvent.setMouseCoalescingEnabled(true) + + installEraseTimer() + } +} + +class MasterDrawingView: DrawingView { + override func awakeFromNib() { + super.awakeFromNib() + pressureConfiguration = NSPressureConfiguration(pressureBehavior: .PrimaryGeneric) + } +} + + diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Info.plist b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Info.plist new file mode 100644 index 00000000..9a56c251 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2015 Apple, Inc. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + + diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/KnightViewController.swift b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/KnightViewController.swift new file mode 100644 index 00000000..29a20173 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/KnightViewController.swift @@ -0,0 +1,13 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller for the Knight Tab. +*/ + +import Cocoa + +class KnightViewController: NSViewController { + +} diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola1.jpg b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola1.jpg new file mode 100755 index 00000000..f7b4f204 Binary files /dev/null and b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola1.jpg differ diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola2.jpg b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola2.jpg new file mode 100755 index 00000000..0d5bfecd Binary files /dev/null and b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola2.jpg differ diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola3.jpg b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola3.jpg new file mode 100755 index 00000000..b5ae3082 Binary files /dev/null and b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola3.jpg differ diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola4.jpg b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola4.jpg new file mode 100755 index 00000000..95e2adb6 Binary files /dev/null and b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola4.jpg differ diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/MasterViewController.swift b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/MasterViewController.swift new file mode 100644 index 00000000..eed62f89 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/MasterViewController.swift @@ -0,0 +1,22 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller for the Master tab. Example of how to manually perform haptic feedback. +*/ + +import Cocoa + +class MasterViewController: NSViewController { + @IBOutlet weak var rotateableImage: NSImageView! + + @IBAction func sliderValueChanged(sender: NSSlider) { + let rotationValue = CGFloat(sender.integerValue) + rotateableImage.frameCenterRotation = rotationValue + + if rotationValue == 0 { + NSHapticFeedbackManager.defaultPerformer().performFeedbackPattern(.Alignment, performanceTime: .Default) + } + } +} \ No newline at end of file diff --git a/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/SquireViewController.swift b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/SquireViewController.swift new file mode 100644 index 00000000..7cb81e82 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/ForceTouchCatalog/SquireViewController.swift @@ -0,0 +1,77 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller for the Squire tab. Examples of high-level Force Touch API. Force Touch and spring loaded Buttons +*/ + +import Cocoa + +class SquireViewController: NSViewController { + @IBOutlet weak var nextPhotoButton: NSButton! + @IBOutlet weak var imageView: NSImageView! + @IBOutlet weak var pressureIndicator: NSLevelIndicator! + @IBOutlet weak var levelIndicator: NSLevelIndicator! + + static let imageBaseName = "Lola" + static let maxPhotoIndex = 4 + + var photoIndex = 1 + + override func viewDidLoad() { + super.viewDidLoad() + + /* + This is how you manually set a continuous accelerator button. + The other buttons are set in the IB Attributes Inspector for that button. + */ + nextPhotoButton.setButtonType(.AcceleratorButton) + nextPhotoButton.continuous = true + } + + func nextPhotoName() -> String { + photoIndex++ + if photoIndex > SquireViewController.maxPhotoIndex { + photoIndex = 1 + } + + return "\(SquireViewController.imageBaseName)\(photoIndex)" + } + + @IBAction func nextPhotoAction(sender: NSButton) { + var startFrame = imageView.frame + let nextPhoto = NSImage(named: nextPhotoName()) + startFrame.origin.x = -startFrame.size.width + let newImageView = NSImageView(frame: startFrame) + newImageView.image = nextPhoto + + view.addSubview(newImageView) + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.1 + newImageView.animator().frame = self.imageView.frame + }, completionHandler: { + self.imageView.image = nextPhoto + newImageView.removeFromSuperviewWithoutNeedingDisplay() + }) + } + + @IBAction func acceleratorChanged(sender: NSButton) { + if sender.doubleValue >= 1 { + pressureIndicator.integerValue = Int((sender.doubleValue - 1.0) * 1000.0) + } else { + pressureIndicator.integerValue = 0 + } + } + + @IBAction func multiLevelAcceleratorChanged(sender: NSButton) { + levelIndicator.integerValue = sender.integerValue + } + + @IBAction func beepAction(sender: NSButton) { + NSBeep() + } + +} + + diff --git a/ForceTouchCatalog/ForceTouchCatalog/Info.plist b/ForceTouchCatalog/ForceTouchCatalog/Info.plist new file mode 100644 index 00000000..9a56c251 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2015 Apple, Inc. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + + diff --git a/ForceTouchCatalog/ForceTouchCatalog/KnightViewController.swift b/ForceTouchCatalog/ForceTouchCatalog/KnightViewController.swift new file mode 100644 index 00000000..a473028d --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/KnightViewController.swift @@ -0,0 +1,11 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller for the Knight tab. +*/ + +import Cocoa + +class KnightViewController: NSViewController { } diff --git a/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola1.jpg b/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola1.jpg new file mode 100755 index 00000000..f7b4f204 Binary files /dev/null and b/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola1.jpg differ diff --git a/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola2.jpg b/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola2.jpg new file mode 100755 index 00000000..0d5bfecd Binary files /dev/null and b/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola2.jpg differ diff --git a/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola3.jpg b/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola3.jpg new file mode 100755 index 00000000..b5ae3082 Binary files /dev/null and b/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola3.jpg differ diff --git a/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola4.jpg b/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola4.jpg new file mode 100755 index 00000000..95e2adb6 Binary files /dev/null and b/ForceTouchCatalog/ForceTouchCatalog/Lola/Lola4.jpg differ diff --git a/ForceTouchCatalog/ForceTouchCatalog/MasterViewController.swift b/ForceTouchCatalog/ForceTouchCatalog/MasterViewController.swift new file mode 100644 index 00000000..806712fd --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/MasterViewController.swift @@ -0,0 +1,37 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller for the Master tab. Example of how to manually perform haptic feedback. +*/ + +import Cocoa + +class MasterViewController: NSViewController { + @IBOutlet weak var rotatableImage: NSImageView! + + @IBAction func sliderValueChanged(_ sender: NSSlider) { + let rotationValue = CGFloat(sender.integerValue) + rotatableImage.frameCenterRotation = rotationValue + + guard rotationValue == 0 else { return } + + /* + Use the `NSHapticFeedbackManager` class to perform alignment haptic + feedback on the Force Touch trackpad. + + Note: You can call this even if this Macintosh doesn't have a Force + Touch Trackpad Haptic feedback should be used sparingly. Here we are + performing it when the user aligns the photo to 0 degrees. A more + real world example would be when the user aligns the photo to when + the horizon is level. + + Ideally, the velocity of slider value changes would be considered such + that haptic feedback is only performed when the user is trying to find + the alignment point (aka moving slowly). This is left as an exercise + for the reader. + */ + NSHapticFeedbackManager.defaultPerformer().perform(.alignment, performanceTime: .default) + } +} diff --git a/ForceTouchCatalog/ForceTouchCatalog/SquireViewController.swift b/ForceTouchCatalog/ForceTouchCatalog/SquireViewController.swift new file mode 100644 index 00000000..7c6df5b6 --- /dev/null +++ b/ForceTouchCatalog/ForceTouchCatalog/SquireViewController.swift @@ -0,0 +1,85 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller for the Squire tab. Examples of high-level Force Touch API. Uses Force Touch and spring loaded buttons. +*/ + +import Cocoa + +class SquireViewController: NSViewController { + // MARK: Properties + + @IBOutlet weak var nextPhotoButton: NSButton! + @IBOutlet weak var imageView: NSImageView! + @IBOutlet weak var pressureIndicator: NSLevelIndicator! + @IBOutlet weak var levelIndicator: NSLevelIndicator! + + static let imageBaseName = "Lola" + static let maxPhotoIndex = 4 + + var photoIndex = 1 + + // MARK: View Controller + + override func viewDidLoad() { + super.viewDidLoad() + + /* + This is how you manually set a continuous accelerator button. + The other buttons are set in the IB Attributes Inspector for that button. + */ + nextPhotoButton.setButtonType(.accelerator) + nextPhotoButton.isContinuous = true + } + + func nextPhotoName() -> String { + photoIndex += 1 + + if photoIndex > SquireViewController.maxPhotoIndex { + photoIndex = 1 + } + + return "\(SquireViewController.imageBaseName)\(photoIndex)" + } + + // MARK: IBActions + + @IBAction func nextPhotoAction(_ sender: NSButton) { + var startFrame = imageView.frame + let nextPhoto = NSImage(named: nextPhotoName()) + startFrame.origin.x = -startFrame.size.width + + let newImageView = NSImageView(frame: startFrame) + newImageView.image = nextPhoto + + view.addSubview(newImageView) + + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.1 + newImageView.animator().frame = self.imageView.frame + }, completionHandler: { + self.imageView.image = nextPhoto + newImageView.removeFromSuperviewWithoutNeedingDisplay() + }) + } + + @IBAction func acceleratorChanged(_ sender: NSButton) { + if sender.doubleValue >= 1 { + pressureIndicator.integerValue = Int((sender.doubleValue - 1.0) * 1000.0) + } + else { + pressureIndicator.integerValue = 0 + } + } + + @IBAction func multiLevelAcceleratorChanged(_ sender: NSButton) { + levelIndicator.integerValue = sender.integerValue + } + + @IBAction func beepAction(_ sender: NSButton) { + NSBeep() + } +} + diff --git a/ForceTouchCatalog/LICENSE.txt b/ForceTouchCatalog/LICENSE.txt new file mode 100644 index 00000000..8ca4b117 --- /dev/null +++ b/ForceTouchCatalog/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: ForceTouchCatalog: Using the Force Touch Trackpad API +Version: 1.2 + +IMPORTANT: This Apple software is supplied to you by Apple +Inc. ("Apple") in consideration of your agreement to the following +terms, and your use, installation, modification or redistribution of +this Apple software constitutes acceptance of these terms. If you do +not agree with these terms, please do not use, install, modify or +redistribute this Apple software. + +In consideration of your agreement to abide by the following terms, and +subject to these terms, Apple grants you a personal, non-exclusive +license, under Apple's copyrights in this original Apple software (the +"Apple Software"), to use, reproduce, modify and redistribute the Apple +Software, with or without modifications, in source and/or binary forms; +provided that if you redistribute the Apple Software in its entirety and +without modifications, you must retain this notice and the following +text and disclaimers in all such redistributions of the Apple Software. +Neither the name, trademarks, service marks or logos of Apple Inc. may +be used to endorse or promote products derived from the Apple Software +without specific prior written permission from Apple. Except as +expressly stated in this notice, no other rights or licenses, express or +implied, are granted by Apple herein, including but not limited to any +patent rights that may be infringed by your derivative works or by other +works in which the Apple Software may be incorporated. + +The Apple Software is provided by Apple on an "AS IS" basis. APPLE +MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION +THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND +OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + +IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, +MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED +AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), +STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +Copyright (C) 2016 Apple Inc. All Rights Reserved. diff --git a/ForceTouchCatalog/README.md b/ForceTouchCatalog/README.md new file mode 100644 index 00000000..07d5d843 --- /dev/null +++ b/ForceTouchCatalog/README.md @@ -0,0 +1,23 @@ +# ForceTouchCatalog: Using the Force Touch Trackpad API + +Demonstrates how to use the Force Touch Trackpad APIs. This sample is divided into three levels of mastery through which you’ll learn how to process pressure events, perform spring loading, configure the trackpad, and perform feedback. + +## Structure + +Squire: Accelerator and Multi-Level Accelerator buttons. Drag and Drop spring loaded buttons. + +Knight: A drawing view that uses the new pressure event in its tracking loop to change the size of the brush. + +Master: Modified drawing view that configures the trackpad to not force click and provide a better pressure mapping for drawing purposes. Also, provides an example of how to manually perform haptic feedback on the Force Touch trackpad when an image is rotated to 0 degrees. + +## Requirements + +### Build + +Xcode 8.0, OS X 10.11 + +### Runtime + +OS X 10.11 + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/HomeKitCatalog/HMCatalog.entitlements b/HomeKitCatalog/HMCatalog.entitlements new file mode 100644 index 00000000..fba8c1f3 --- /dev/null +++ b/HomeKitCatalog/HMCatalog.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.homekit + + com.apple.external-accessory.wireless-configuration + + + diff --git a/HomeKitCatalog/HMCatalog.xcodeproj/project.pbxproj b/HomeKitCatalog/HMCatalog.xcodeproj/project.pbxproj new file mode 100644 index 00000000..06feb854 --- /dev/null +++ b/HomeKitCatalog/HMCatalog.xcodeproj/project.pbxproj @@ -0,0 +1,714 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 6A3B541F1AF92D37007CA237 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6A3B541E1AF92D37007CA237 /* Launch Screen.storyboard */; }; + 6A4D140F1ADDF2DC00364DE0 /* HMCatalogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4D140E1ADDF2DC00364DE0 /* HMCatalogViewController.swift */; }; + 6A589F641B0FAF3200CDD54B /* SegmentedTimeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A589F631B0FAF3200CDD54B /* SegmentedTimeCell.swift */; }; + 6A58EE961AF147BE00ECAD21 /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A58EE951AF147BE00ECAD21 /* FavoritesViewController.swift */; }; + 6A5D85281B0CF3C5008DF524 /* TriggerCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D85271B0CF3C5008DF524 /* TriggerCreator.swift */; }; + 6A5D852A1B0CFE6C008DF524 /* TimerTriggerCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D85291B0CFE6C008DF524 /* TimerTriggerCreator.swift */; }; + 6A5D852C1B0D05DD008DF524 /* LocationTriggerCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D852B1B0D05DD008DF524 /* LocationTriggerCreator.swift */; }; + 6A5D852F1B0D2155008DF524 /* EventTriggerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D852E1B0D2155008DF524 /* EventTriggerViewController.swift */; }; + 6A5D85321B0D3032008DF524 /* TimeConditionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D85311B0D3032008DF524 /* TimeConditionViewController.swift */; }; + 6A5D85341B0D40B0008DF524 /* TimePickerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D85331B0D40B0008DF524 /* TimePickerCell.swift */; }; + 6A5D85391B0D5100008DF524 /* EventTriggerCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D85381B0D5100008DF524 /* EventTriggerCreator.swift */; }; + 6A5D853B1B0D63EB008DF524 /* NSPredicate+Condition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5D853A1B0D63EB008DF524 /* NSPredicate+Condition.swift */; }; + 6A6DA8881B1FADE800D1FA8A /* HMActionSet+BuiltIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6DA8871B1FADE800D1FA8A /* HMActionSet+BuiltIn.swift */; }; + 6A8347531B0574040055198E /* CharacteristicSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8347521B0574040055198E /* CharacteristicSelectionViewController.swift */; }; + 6A8347551B06668B0055198E /* CharacteristicTriggerCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8347541B06668B0055198E /* CharacteristicTriggerCreator.swift */; }; + 6A96D1211B051326004DD072 /* UIColor+Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A96D1201B051326004DD072 /* UIColor+Custom.swift */; }; + 6AA42B471AEEF28500A92A79 /* FavoritesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AA42B461AEEF28500A92A79 /* FavoritesManager.swift */; }; + 6AA850DA1B1E645300A77A3E /* UITableViewController+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AA850D91B1E645300A77A3E /* UITableViewController+Convenience.swift */; }; + 6AAE046E1B0A93660084A575 /* LocationTriggerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AAE046D1B0A93660084A575 /* LocationTriggerViewController.swift */; }; + 6ABBF3021ADF1A1C00C1CF69 /* Array+Sorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ABBF3011ADF1A1C00C1CF69 /* Array+Sorting.swift */; }; + 6ACBCF5D1B02DAA000851BD3 /* CharacteristicTriggerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF571B02DAA000851BD3 /* CharacteristicTriggerViewController.swift */; }; + 6ACBCF5E1B02DAA000851BD3 /* MapOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF581B02DAA000851BD3 /* MapOverlayView.swift */; }; + 6ACBCF5F1B02DAA000851BD3 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF591B02DAA000851BD3 /* MapViewController.swift */; }; + 6ACBCF601B02DAA000851BD3 /* TimerTriggerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF5C1B02DAA000851BD3 /* TimerTriggerViewController.swift */; }; + 6ACBCF621B02DB1700851BD3 /* TriggerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF611B02DB1700851BD3 /* TriggerViewController.swift */; }; + 6ACBCFA01B040AA600851BD3 /* HMEventTrigger+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACBCF9F1B040AA600851BD3 /* HMEventTrigger+Convenience.swift */; }; + 6ACCD8C21B1266A70002FA61 /* ConditionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACCD8C11B1266A70002FA61 /* ConditionCell.swift */; }; + 6AD5641C1AFA792A00321F78 /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD5641B1AFA792A00321F78 /* TabBarController.swift */; }; + 6ADF251E1AE1940B00CD05D0 /* HomeKitObjectCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ADF251D1AE1940B00CD05D0 /* HomeKitObjectCollection.swift */; }; + 6ADF25221AE5AA8200CD05D0 /* TextCharacteristicCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6ADF25211AE5AA8200CD05D0 /* TextCharacteristicCell.xib */; }; + 6ADF25241AE5AAA100CD05D0 /* TextCharacteristicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ADF25231AE5AAA100CD05D0 /* TextCharacteristicCell.swift */; }; + 6AE2A5641B17B84B002586A9 /* CNMutablePostalAddress+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE2A5631B17B84B002586A9 /* CNMutablePostalAddress+Convenience.swift */; }; + DC0BBB991A1FB60E002AB35C /* ServiceGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0BBB981A1FB60E002AB35C /* ServiceGroupViewController.swift */; }; + DC0BBB9D1A1FBC46002AB35C /* AddServicesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0BBB9C1A1FBC46002AB35C /* AddServicesViewController.swift */; }; + DC1310FC1A1438CB004E5DB5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1310EE1A1438CB004E5DB5 /* AppDelegate.swift */; }; + DC1310FF1A1438CB004E5DB5 /* HMCharacteristic+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1310F31A1438CB004E5DB5 /* HMCharacteristic+Properties.swift */; }; + DC1311001A1438CB004E5DB5 /* HMService+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1310F41A1438CB004E5DB5 /* HMService+Properties.swift */; }; + DC1311041A1438CB004E5DB5 /* UIAlertController+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1310F91A1438CB004E5DB5 /* UIAlertController+Convenience.swift */; }; + DC1311051A1438CB004E5DB5 /* UIViewController+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC1310FA1A1438CB004E5DB5 /* UIViewController+Convenience.swift */; }; + DC5042D41A1D5EFD000E3973 /* ActionSetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5042D31A1D5EFD000E3973 /* ActionSetViewController.swift */; }; + DC5042D61A1D5FFE000E3973 /* ActionSetCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5042D51A1D5FFE000E3973 /* ActionSetCreator.swift */; }; + DC53509F1A1D71F2000A8F0E /* ActionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC53509E1A1D71F2000A8F0E /* ActionCell.swift */; }; + DC58FA801A1BD10400550AD3 /* ServicesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA7F1A1BD10400550AD3 /* ServicesViewController.swift */; }; + DC58FA831A1BE32000550AD3 /* CharacteristicsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA821A1BE32000550AD3 /* CharacteristicsViewController.swift */; }; + DC58FA881A1BE59500550AD3 /* CharacteristicCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DC58FA841A1BE59500550AD3 /* CharacteristicCell.xib */; }; + DC58FA891A1BE59500550AD3 /* SegmentedControlCharacteristicCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DC58FA851A1BE59500550AD3 /* SegmentedControlCharacteristicCell.xib */; }; + DC58FA8A1A1BE59500550AD3 /* SliderCharacteristicCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DC58FA861A1BE59500550AD3 /* SliderCharacteristicCell.xib */; }; + DC58FA8B1A1BE59500550AD3 /* SwitchCharacteristicCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DC58FA871A1BE59500550AD3 /* SwitchCharacteristicCell.xib */; }; + DC58FA8D1A1BE5A300550AD3 /* CharacteristicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA8C1A1BE5A300550AD3 /* CharacteristicCell.swift */; }; + DC58FA8F1A1BE5B700550AD3 /* SegmentedControlCharacteristicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA8E1A1BE5B700550AD3 /* SegmentedControlCharacteristicCell.swift */; }; + DC58FA911A1BE5C400550AD3 /* SliderCharacteristicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA901A1BE5C400550AD3 /* SliderCharacteristicCell.swift */; }; + DC58FA931A1BE5D000550AD3 /* SwitchCharacteristicCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA921A1BE5D000550AD3 /* SwitchCharacteristicCell.swift */; }; + DC58FA951A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA941A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift */; }; + DC58FA971A1BF4DD00550AD3 /* AccessoryUpdateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC58FA961A1BF4DD00550AD3 /* AccessoryUpdateController.swift */; }; + DCD480611A16B31C001BFEE3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DCD480601A16B31C001BFEE3 /* Main.storyboard */; }; + DCD480631A16B371001BFEE3 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DCD480621A16B371001BFEE3 /* Images.xcassets */; }; + DCD480651A16B3AC001BFEE3 /* HomeListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD480641A16B3AC001BFEE3 /* HomeListViewController.swift */; }; + DCD480671A16B4C3001BFEE3 /* HomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD480661A16B4C3001BFEE3 /* HomeStore.swift */; }; + DCD4806E1A16BD1D001BFEE3 /* HMHome+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4806D1A16BD1D001BFEE3 /* HMHome+Properties.swift */; }; + DCD480701A16C682001BFEE3 /* ControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD4806F1A16C682001BFEE3 /* ControlsViewController.swift */; }; + DCD480721A16C69C001BFEE3 /* ControlsTableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD480711A16C69C001BFEE3 /* ControlsTableViewDataSource.swift */; }; + DCD480741A16C948001BFEE3 /* ServiceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD480731A16C948001BFEE3 /* ServiceCell.swift */; }; + DCD480761A16CDDB001BFEE3 /* HomeListConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD480751A16CDDB001BFEE3 /* HomeListConfigurationViewController.swift */; }; + DCF046BE1A1A5D92002DBFBF /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046BD1A1A5D92002DBFBF /* HomeViewController.swift */; }; + DCF046C21A1A5E50002DBFBF /* UIStoryboardSegue+IntendedDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046C11A1A5E50002DBFBF /* UIStoryboardSegue+IntendedDestination.swift */; }; + DCF046C41A1A939F002DBFBF /* RoomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046C31A1A939F002DBFBF /* RoomViewController.swift */; }; + DCF046C61A1A9A73002DBFBF /* ZoneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046C51A1A9A73002DBFBF /* ZoneViewController.swift */; }; + DCF046C81A1A9D25002DBFBF /* AddRoomViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046C71A1A9D25002DBFBF /* AddRoomViewController.swift */; }; + DCF046D41A1AB19F002DBFBF /* AccessoryBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046D31A1AB19F002DBFBF /* AccessoryBrowserViewController.swift */; }; + DCF046D61A1AB627002DBFBF /* ModifyAccessoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF046D51A1AB627002DBFBF /* ModifyAccessoryViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + CAA68A951AF0614300905CFE /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 3E7CACDF1B1E4AAB00891CE0 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 6A10AEE81AF84BEA000A2CD6 /* HMCatalog.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = HMCatalog.entitlements; sourceTree = ""; }; + 6A3B541E1AF92D37007CA237 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; + 6A4D140E1ADDF2DC00364DE0 /* HMCatalogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HMCatalogViewController.swift; sourceTree = ""; }; + 6A589F631B0FAF3200CDD54B /* SegmentedTimeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = SegmentedTimeCell.swift; sourceTree = ""; tabWidth = 4; }; + 6A58EE951AF147BE00ECAD21 /* FavoritesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FavoritesViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6A5D85271B0CF3C5008DF524 /* TriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TriggerCreator.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6A5D85291B0CFE6C008DF524 /* TimerTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TimerTriggerCreator.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6A5D852B1B0D05DD008DF524 /* LocationTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = LocationTriggerCreator.swift; sourceTree = ""; tabWidth = 4; }; + 6A5D852E1B0D2155008DF524 /* EventTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = EventTriggerViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6A5D85311B0D3032008DF524 /* TimeConditionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TimeConditionViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6A5D85331B0D40B0008DF524 /* TimePickerCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = TimePickerCell.swift; sourceTree = ""; tabWidth = 4; }; + 6A5D85381B0D5100008DF524 /* EventTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = EventTriggerCreator.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6A5D853A1B0D63EB008DF524 /* NSPredicate+Condition.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "NSPredicate+Condition.swift"; sourceTree = ""; tabWidth = 4; }; + 6A6DA8871B1FADE800D1FA8A /* HMActionSet+BuiltIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HMActionSet+BuiltIn.swift"; sourceTree = ""; }; + 6A8347521B0574040055198E /* CharacteristicSelectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicSelectionViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6A8347541B06668B0055198E /* CharacteristicTriggerCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicTriggerCreator.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6A96D1201B051326004DD072 /* UIColor+Custom.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Custom.swift"; sourceTree = ""; tabWidth = 4; }; + 6AA42B461AEEF28500A92A79 /* FavoritesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = FavoritesManager.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6AA850D91B1E645300A77A3E /* UITableViewController+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableViewController+Convenience.swift"; sourceTree = ""; }; + 6AAE046D1B0A93660084A575 /* LocationTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = LocationTriggerViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6ABBF3011ADF1A1C00C1CF69 /* Array+Sorting.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "Array+Sorting.swift"; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6ACBCF571B02DAA000851BD3 /* CharacteristicTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicTriggerViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6ACBCF581B02DAA000851BD3 /* MapOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = MapOverlayView.swift; sourceTree = ""; tabWidth = 4; }; + 6ACBCF591B02DAA000851BD3 /* MapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = ""; tabWidth = 4; }; + 6ACBCF5C1B02DAA000851BD3 /* TimerTriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TimerTriggerViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6ACBCF611B02DB1700851BD3 /* TriggerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = TriggerViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6ACBCF9F1B040AA600851BD3 /* HMEventTrigger+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "HMEventTrigger+Convenience.swift"; sourceTree = ""; tabWidth = 4; }; + 6ACCD8C11B1266A70002FA61 /* ConditionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ConditionCell.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6AD5641B1AFA792A00321F78 /* TabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; + 6ADF251D1AE1940B00CD05D0 /* HomeKitObjectCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeKitObjectCollection.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 6ADF25211AE5AA8200CD05D0 /* TextCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TextCharacteristicCell.xib; sourceTree = ""; }; + 6ADF25231AE5AAA100CD05D0 /* TextCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextCharacteristicCell.swift; sourceTree = ""; }; + 6AE2A5631B17B84B002586A9 /* CNMutablePostalAddress+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CNMutablePostalAddress+Convenience.swift"; sourceTree = ""; }; + DC0BBB981A1FB60E002AB35C /* ServiceGroupViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = ServiceGroupViewController.swift; sourceTree = ""; tabWidth = 4; }; + DC0BBB9C1A1FBC46002AB35C /* AddServicesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AddServicesViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC1310BE1A14201E004E5DB5 /* HMCatalog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HMCatalog.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DC1310EE1A1438CB004E5DB5 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC1310F31A1438CB004E5DB5 /* HMCharacteristic+Properties.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "HMCharacteristic+Properties.swift"; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC1310F41A1438CB004E5DB5 /* HMService+Properties.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "HMService+Properties.swift"; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC1310F81A1438CB004E5DB5 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DC1310F91A1438CB004E5DB5 /* UIAlertController+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Convenience.swift"; sourceTree = ""; tabWidth = 4; }; + DC1310FA1A1438CB004E5DB5 /* UIViewController+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "UIViewController+Convenience.swift"; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC5042D31A1D5EFD000E3973 /* ActionSetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ActionSetViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC5042D51A1D5FFE000E3973 /* ActionSetCreator.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ActionSetCreator.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC53509E1A1D71F2000A8F0E /* ActionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ActionCell.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC58FA7F1A1BD10400550AD3 /* ServicesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ServicesViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC58FA821A1BE32000550AD3 /* CharacteristicsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicsViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC58FA841A1BE59500550AD3 /* CharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CharacteristicCell.xib; sourceTree = ""; }; + DC58FA851A1BE59500550AD3 /* SegmentedControlCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SegmentedControlCharacteristicCell.xib; sourceTree = ""; }; + DC58FA861A1BE59500550AD3 /* SliderCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SliderCharacteristicCell.xib; sourceTree = ""; }; + DC58FA871A1BE59500550AD3 /* SwitchCharacteristicCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SwitchCharacteristicCell.xib; sourceTree = ""; }; + DC58FA8C1A1BE5A300550AD3 /* CharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicCell.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC58FA8E1A1BE5B700550AD3 /* SegmentedControlCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = SegmentedControlCharacteristicCell.swift; sourceTree = ""; tabWidth = 4; }; + DC58FA901A1BE5C400550AD3 /* SliderCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 3; lastKnownFileType = sourcecode.swift; path = SliderCharacteristicCell.swift; sourceTree = ""; tabWidth = 3; }; + DC58FA921A1BE5D000550AD3 /* SwitchCharacteristicCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = SwitchCharacteristicCell.swift; sourceTree = ""; tabWidth = 4; }; + DC58FA941A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = CharacteristicsTableViewDataSource.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DC58FA961A1BF4DD00550AD3 /* AccessoryUpdateController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AccessoryUpdateController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DCD480601A16B31C001BFEE3 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; + DCD480621A16B371001BFEE3 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + DCD480641A16B3AC001BFEE3 /* HomeListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeListViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DCD480661A16B4C3001BFEE3 /* HomeStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = HomeStore.swift; sourceTree = ""; tabWidth = 4; }; + DCD4806D1A16BD1D001BFEE3 /* HMHome+Properties.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "HMHome+Properties.swift"; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DCD4806F1A16C682001BFEE3 /* ControlsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ControlsViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DCD480711A16C69C001BFEE3 /* ControlsTableViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ControlsTableViewDataSource.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DCD480731A16C948001BFEE3 /* ServiceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = ServiceCell.swift; sourceTree = ""; tabWidth = 4; }; + DCD480751A16CDDB001BFEE3 /* HomeListConfigurationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeListConfigurationViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DCF046BD1A1A5D92002DBFBF /* HomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = HomeViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DCF046C11A1A5E50002DBFBF /* UIStoryboardSegue+IntendedDestination.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboardSegue+IntendedDestination.swift"; sourceTree = ""; tabWidth = 4; }; + DCF046C31A1A939F002DBFBF /* RoomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = RoomViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DCF046C51A1A9A73002DBFBF /* ZoneViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ZoneViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DCF046C71A1A9D25002DBFBF /* AddRoomViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AddRoomViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DCF046D31A1AB19F002DBFBF /* AccessoryBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AccessoryBrowserViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + DCF046D51A1AB627002DBFBF /* ModifyAccessoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ModifyAccessoryViewController.swift; sourceTree = ""; tabWidth = 4; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + DC1310BB1A14201E004E5DB5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6A58EE941AF1477700ECAD21 /* Favorites */ = { + isa = PBXGroup; + children = ( + 6AA42B461AEEF28500A92A79 /* FavoritesManager.swift */, + 6A58EE951AF147BE00ECAD21 /* FavoritesViewController.swift */, + ); + path = Favorites; + sourceTree = ""; + }; + 6A5D852D1B0D2134008DF524 /* Event */ = { + isa = PBXGroup; + children = ( + 6A5D852E1B0D2155008DF524 /* EventTriggerViewController.swift */, + 6A5D85381B0D5100008DF524 /* EventTriggerCreator.swift */, + 6A5D85301B0D3017008DF524 /* Conditions */, + 6A8347511B0573C60055198E /* Characteristic */, + 6A8347501B0573A00055198E /* Location */, + ); + path = Event; + sourceTree = ""; + }; + 6A5D85301B0D3017008DF524 /* Conditions */ = { + isa = PBXGroup; + children = ( + 6A5D85311B0D3032008DF524 /* TimeConditionViewController.swift */, + 6A5D85331B0D40B0008DF524 /* TimePickerCell.swift */, + 6A589F631B0FAF3200CDD54B /* SegmentedTimeCell.swift */, + 6ACCD8C11B1266A70002FA61 /* ConditionCell.swift */, + ); + path = Conditions; + sourceTree = ""; + }; + 6A8347501B0573A00055198E /* Location */ = { + isa = PBXGroup; + children = ( + 6ACBCF631B02DB2400851BD3 /* Mapping */, + 6AAE046D1B0A93660084A575 /* LocationTriggerViewController.swift */, + 6A5D852B1B0D05DD008DF524 /* LocationTriggerCreator.swift */, + ); + path = Location; + sourceTree = ""; + }; + 6A8347511B0573C60055198E /* Characteristic */ = { + isa = PBXGroup; + children = ( + 6ACBCF571B02DAA000851BD3 /* CharacteristicTriggerViewController.swift */, + 6A8347541B06668B0055198E /* CharacteristicTriggerCreator.swift */, + 6A8347521B0574040055198E /* CharacteristicSelectionViewController.swift */, + ); + path = Characteristic; + sourceTree = ""; + }; + 6ACBCF5B1B02DAA000851BD3 /* Timer */ = { + isa = PBXGroup; + children = ( + 6ACBCF5C1B02DAA000851BD3 /* TimerTriggerViewController.swift */, + 6A5D85291B0CFE6C008DF524 /* TimerTriggerCreator.swift */, + ); + path = Timer; + sourceTree = ""; + }; + 6ACBCF631B02DB2400851BD3 /* Mapping */ = { + isa = PBXGroup; + children = ( + 6ACBCF581B02DAA000851BD3 /* MapOverlayView.swift */, + 6ACBCF591B02DAA000851BD3 /* MapViewController.swift */, + ); + path = Mapping; + sourceTree = ""; + }; + DC1310B51A14201E004E5DB5 = { + isa = PBXGroup; + children = ( + 3E7CACDF1B1E4AAB00891CE0 /* README.md */, + 6A10AEE81AF84BEA000A2CD6 /* HMCatalog.entitlements */, + DC1310ED1A1438CB004E5DB5 /* HMCatalog */, + DC1310BF1A14201E004E5DB5 /* Products */, + ); + sourceTree = ""; + }; + DC1310BF1A14201E004E5DB5 /* Products */ = { + isa = PBXGroup; + children = ( + DC1310BE1A14201E004E5DB5 /* HMCatalog.app */, + ); + name = Products; + sourceTree = ""; + }; + DC1310ED1A1438CB004E5DB5 /* HMCatalog */ = { + isa = PBXGroup; + children = ( + DC1310EE1A1438CB004E5DB5 /* AppDelegate.swift */, + 6A4D140E1ADDF2DC00364DE0 /* HMCatalogViewController.swift */, + 6AD5641B1AFA792A00321F78 /* TabBarController.swift */, + DCD480601A16B31C001BFEE3 /* Main.storyboard */, + 6A3B541E1AF92D37007CA237 /* Launch Screen.storyboard */, + 6A58EE941AF1477700ECAD21 /* Favorites */, + DCF046D21A1AB107002DBFBF /* Homes */, + DCD480621A16B371001BFEE3 /* Images.xcassets */, + DC1310F71A1438CB004E5DB5 /* Supporting Files */, + ); + path = HMCatalog; + sourceTree = ""; + }; + DC1310F71A1438CB004E5DB5 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 6ABBF3011ADF1A1C00C1CF69 /* Array+Sorting.swift */, + 6AE2A5631B17B84B002586A9 /* CNMutablePostalAddress+Convenience.swift */, + 6A6DA8871B1FADE800D1FA8A /* HMActionSet+BuiltIn.swift */, + DC1310F31A1438CB004E5DB5 /* HMCharacteristic+Properties.swift */, + 6ACBCF9F1B040AA600851BD3 /* HMEventTrigger+Convenience.swift */, + DCD4806D1A16BD1D001BFEE3 /* HMHome+Properties.swift */, + DC1310F41A1438CB004E5DB5 /* HMService+Properties.swift */, + 6A5D853A1B0D63EB008DF524 /* NSPredicate+Condition.swift */, + DC1310F91A1438CB004E5DB5 /* UIAlertController+Convenience.swift */, + 6A96D1201B051326004DD072 /* UIColor+Custom.swift */, + DCF046C11A1A5E50002DBFBF /* UIStoryboardSegue+IntendedDestination.swift */, + 6AA850D91B1E645300A77A3E /* UITableViewController+Convenience.swift */, + DC1310FA1A1438CB004E5DB5 /* UIViewController+Convenience.swift */, + DC1310F81A1438CB004E5DB5 /* Info.plist */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + DCF046CA1A1AB0B9002DBFBF /* Rooms */ = { + isa = PBXGroup; + children = ( + DCF046C31A1A939F002DBFBF /* RoomViewController.swift */, + ); + path = Rooms; + sourceTree = ""; + }; + DCF046CB1A1AB0C1002DBFBF /* Zones */ = { + isa = PBXGroup; + children = ( + DCF046C71A1A9D25002DBFBF /* AddRoomViewController.swift */, + DCF046C51A1A9A73002DBFBF /* ZoneViewController.swift */, + ); + path = Zones; + sourceTree = ""; + }; + DCF046CC1A1AB0C5002DBFBF /* Accessories */ = { + isa = PBXGroup; + children = ( + DC58FA961A1BF4DD00550AD3 /* AccessoryUpdateController.swift */, + DCF046D31A1AB19F002DBFBF /* AccessoryBrowserViewController.swift */, + DCD4806F1A16C682001BFEE3 /* ControlsViewController.swift */, + DCD480711A16C69C001BFEE3 /* ControlsTableViewDataSource.swift */, + DCF046CD1A1AB0CB002DBFBF /* Services */, + DCF046D51A1AB627002DBFBF /* ModifyAccessoryViewController.swift */, + ); + path = Accessories; + sourceTree = ""; + }; + DCF046CD1A1AB0CB002DBFBF /* Services */ = { + isa = PBXGroup; + children = ( + DCD480731A16C948001BFEE3 /* ServiceCell.swift */, + DC58FA7F1A1BD10400550AD3 /* ServicesViewController.swift */, + DCF046CE1A1AB0D2002DBFBF /* Characteristic Cells */, + DC58FA821A1BE32000550AD3 /* CharacteristicsViewController.swift */, + DC58FA941A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift */, + ); + path = Services; + sourceTree = ""; + }; + DCF046CE1A1AB0D2002DBFBF /* Characteristic Cells */ = { + isa = PBXGroup; + children = ( + DC58FA8C1A1BE5A300550AD3 /* CharacteristicCell.swift */, + DC58FA841A1BE59500550AD3 /* CharacteristicCell.xib */, + DC58FA8E1A1BE5B700550AD3 /* SegmentedControlCharacteristicCell.swift */, + DC58FA851A1BE59500550AD3 /* SegmentedControlCharacteristicCell.xib */, + DC58FA901A1BE5C400550AD3 /* SliderCharacteristicCell.swift */, + DC58FA861A1BE59500550AD3 /* SliderCharacteristicCell.xib */, + DC58FA921A1BE5D000550AD3 /* SwitchCharacteristicCell.swift */, + DC58FA871A1BE59500550AD3 /* SwitchCharacteristicCell.xib */, + 6ADF25231AE5AAA100CD05D0 /* TextCharacteristicCell.swift */, + 6ADF25211AE5AA8200CD05D0 /* TextCharacteristicCell.xib */, + ); + path = "Characteristic Cells"; + sourceTree = ""; + }; + DCF046CF1A1AB0D9002DBFBF /* Service Groups */ = { + isa = PBXGroup; + children = ( + DC0BBB981A1FB60E002AB35C /* ServiceGroupViewController.swift */, + DC0BBB9C1A1FBC46002AB35C /* AddServicesViewController.swift */, + ); + path = "Service Groups"; + sourceTree = ""; + }; + DCF046D01A1AB0E5002DBFBF /* Action Sets */ = { + isa = PBXGroup; + children = ( + DC5042D31A1D5EFD000E3973 /* ActionSetViewController.swift */, + DC53509E1A1D71F2000A8F0E /* ActionCell.swift */, + DC5042D51A1D5FFE000E3973 /* ActionSetCreator.swift */, + ); + path = "Action Sets"; + sourceTree = ""; + }; + DCF046D11A1AB0EC002DBFBF /* Triggers */ = { + isa = PBXGroup; + children = ( + 6ACBCF611B02DB1700851BD3 /* TriggerViewController.swift */, + 6A5D85271B0CF3C5008DF524 /* TriggerCreator.swift */, + 6A5D852D1B0D2134008DF524 /* Event */, + 6ACBCF5B1B02DAA000851BD3 /* Timer */, + ); + path = Triggers; + sourceTree = ""; + }; + DCF046D21A1AB107002DBFBF /* Homes */ = { + isa = PBXGroup; + children = ( + DCD480641A16B3AC001BFEE3 /* HomeListViewController.swift */, + DCD480751A16CDDB001BFEE3 /* HomeListConfigurationViewController.swift */, + DCF046BD1A1A5D92002DBFBF /* HomeViewController.swift */, + 6ADF251D1AE1940B00CD05D0 /* HomeKitObjectCollection.swift */, + DCD480661A16B4C3001BFEE3 /* HomeStore.swift */, + DCF046CC1A1AB0C5002DBFBF /* Accessories */, + DCF046CA1A1AB0B9002DBFBF /* Rooms */, + DCF046CB1A1AB0C1002DBFBF /* Zones */, + DCF046D01A1AB0E5002DBFBF /* Action Sets */, + DCF046D11A1AB0EC002DBFBF /* Triggers */, + DCF046CF1A1AB0D9002DBFBF /* Service Groups */, + ); + path = Homes; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + DC1310BD1A14201E004E5DB5 /* HMCatalog */ = { + isa = PBXNativeTarget; + buildConfigurationList = DC1310DD1A14201E004E5DB5 /* Build configuration list for PBXNativeTarget "HMCatalog" */; + buildPhases = ( + DC1310BA1A14201E004E5DB5 /* Sources */, + DC1310BB1A14201E004E5DB5 /* Frameworks */, + DC1310BC1A14201E004E5DB5 /* Resources */, + CAA68A951AF0614300905CFE /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = HMCatalog; + productName = sldkfjghslkjdgh; + productReference = DC1310BE1A14201E004E5DB5 /* HMCatalog.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DC1310B61A14201E004E5DB5 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Apple, Inc"; + TargetAttributes = { + DC1310BD1A14201E004E5DB5 = { + CreatedOnToolsVersion = 6.1; + LastSwiftMigration = 0800; + SystemCapabilities = { + com.apple.HomeKit = { + enabled = 1; + }; + com.apple.WAC = { + enabled = 1; + }; + }; + }; + }; + }; + buildConfigurationList = DC1310B91A14201E004E5DB5 /* Build configuration list for PBXProject "HMCatalog" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = DC1310B51A14201E004E5DB5; + productRefGroup = DC1310BF1A14201E004E5DB5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DC1310BD1A14201E004E5DB5 /* HMCatalog */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + DC1310BC1A14201E004E5DB5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC58FA8B1A1BE59500550AD3 /* SwitchCharacteristicCell.xib in Resources */, + DCD480611A16B31C001BFEE3 /* Main.storyboard in Resources */, + 6A3B541F1AF92D37007CA237 /* Launch Screen.storyboard in Resources */, + 6ADF25221AE5AA8200CD05D0 /* TextCharacteristicCell.xib in Resources */, + DC58FA891A1BE59500550AD3 /* SegmentedControlCharacteristicCell.xib in Resources */, + DC58FA8A1A1BE59500550AD3 /* SliderCharacteristicCell.xib in Resources */, + DCD480631A16B371001BFEE3 /* Images.xcassets in Resources */, + DC58FA881A1BE59500550AD3 /* CharacteristicCell.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DC1310BA1A14201E004E5DB5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DCD480671A16B4C3001BFEE3 /* HomeStore.swift in Sources */, + 6ACCD8C21B1266A70002FA61 /* ConditionCell.swift in Sources */, + 6AA850DA1B1E645300A77A3E /* UITableViewController+Convenience.swift in Sources */, + 6ACBCF621B02DB1700851BD3 /* TriggerViewController.swift in Sources */, + DC58FA951A1BEF7400550AD3 /* CharacteristicsTableViewDataSource.swift in Sources */, + 6A58EE961AF147BE00ECAD21 /* FavoritesViewController.swift in Sources */, + DC1311041A1438CB004E5DB5 /* UIAlertController+Convenience.swift in Sources */, + 6ACBCF601B02DAA000851BD3 /* TimerTriggerViewController.swift in Sources */, + DC1310FF1A1438CB004E5DB5 /* HMCharacteristic+Properties.swift in Sources */, + DC1310FC1A1438CB004E5DB5 /* AppDelegate.swift in Sources */, + 6ADF25241AE5AAA100CD05D0 /* TextCharacteristicCell.swift in Sources */, + DCF046C61A1A9A73002DBFBF /* ZoneViewController.swift in Sources */, + DC1311051A1438CB004E5DB5 /* UIViewController+Convenience.swift in Sources */, + DCF046BE1A1A5D92002DBFBF /* HomeViewController.swift in Sources */, + 6A5D852C1B0D05DD008DF524 /* LocationTriggerCreator.swift in Sources */, + 6AA42B471AEEF28500A92A79 /* FavoritesManager.swift in Sources */, + DCD4806E1A16BD1D001BFEE3 /* HMHome+Properties.swift in Sources */, + 6A6DA8881B1FADE800D1FA8A /* HMActionSet+BuiltIn.swift in Sources */, + DC53509F1A1D71F2000A8F0E /* ActionCell.swift in Sources */, + DCF046D61A1AB627002DBFBF /* ModifyAccessoryViewController.swift in Sources */, + DCD480721A16C69C001BFEE3 /* ControlsTableViewDataSource.swift in Sources */, + 6A5D85281B0CF3C5008DF524 /* TriggerCreator.swift in Sources */, + DC0BBB991A1FB60E002AB35C /* ServiceGroupViewController.swift in Sources */, + 6A96D1211B051326004DD072 /* UIColor+Custom.swift in Sources */, + DC58FA831A1BE32000550AD3 /* CharacteristicsViewController.swift in Sources */, + DC1311001A1438CB004E5DB5 /* HMService+Properties.swift in Sources */, + DC58FA8D1A1BE5A300550AD3 /* CharacteristicCell.swift in Sources */, + 6A5D85341B0D40B0008DF524 /* TimePickerCell.swift in Sources */, + DC58FA911A1BE5C400550AD3 /* SliderCharacteristicCell.swift in Sources */, + 6A5D85321B0D3032008DF524 /* TimeConditionViewController.swift in Sources */, + 6A5D853B1B0D63EB008DF524 /* NSPredicate+Condition.swift in Sources */, + 6AD5641C1AFA792A00321F78 /* TabBarController.swift in Sources */, + 6ADF251E1AE1940B00CD05D0 /* HomeKitObjectCollection.swift in Sources */, + DCF046C81A1A9D25002DBFBF /* AddRoomViewController.swift in Sources */, + DCD480741A16C948001BFEE3 /* ServiceCell.swift in Sources */, + DC58FA931A1BE5D000550AD3 /* SwitchCharacteristicCell.swift in Sources */, + DC58FA971A1BF4DD00550AD3 /* AccessoryUpdateController.swift in Sources */, + DCD480701A16C682001BFEE3 /* ControlsViewController.swift in Sources */, + 6AAE046E1B0A93660084A575 /* LocationTriggerViewController.swift in Sources */, + 6A5D85391B0D5100008DF524 /* EventTriggerCreator.swift in Sources */, + 6A589F641B0FAF3200CDD54B /* SegmentedTimeCell.swift in Sources */, + 6A5D852F1B0D2155008DF524 /* EventTriggerViewController.swift in Sources */, + DCF046C41A1A939F002DBFBF /* RoomViewController.swift in Sources */, + 6ACBCF5F1B02DAA000851BD3 /* MapViewController.swift in Sources */, + 6A5D852A1B0CFE6C008DF524 /* TimerTriggerCreator.swift in Sources */, + 6ACBCF5D1B02DAA000851BD3 /* CharacteristicTriggerViewController.swift in Sources */, + 6A8347531B0574040055198E /* CharacteristicSelectionViewController.swift in Sources */, + 6ACBCFA01B040AA600851BD3 /* HMEventTrigger+Convenience.swift in Sources */, + 6ABBF3021ADF1A1C00C1CF69 /* Array+Sorting.swift in Sources */, + 6A4D140F1ADDF2DC00364DE0 /* HMCatalogViewController.swift in Sources */, + DCD480761A16CDDB001BFEE3 /* HomeListConfigurationViewController.swift in Sources */, + DCF046C21A1A5E50002DBFBF /* UIStoryboardSegue+IntendedDestination.swift in Sources */, + DC5042D41A1D5EFD000E3973 /* ActionSetViewController.swift in Sources */, + 6AE2A5641B17B84B002586A9 /* CNMutablePostalAddress+Convenience.swift in Sources */, + DC5042D61A1D5FFE000E3973 /* ActionSetCreator.swift in Sources */, + 6A8347551B06668B0055198E /* CharacteristicTriggerCreator.swift in Sources */, + DCF046D41A1AB19F002DBFBF /* AccessoryBrowserViewController.swift in Sources */, + 6ACBCF5E1B02DAA000851BD3 /* MapOverlayView.swift in Sources */, + DC58FA801A1BD10400550AD3 /* ServicesViewController.swift in Sources */, + DC58FA8F1A1BE5B700550AD3 /* SegmentedControlCharacteristicCell.swift in Sources */, + DC0BBB9D1A1FBC46002AB35C /* AddServicesViewController.swift in Sources */, + DCD480651A16B3AC001BFEE3 /* HomeListViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + DC1310DB1A14201E004E5DB5 /* 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 = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + 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_SYMBOLS_PRIVATE_EXTERN = NO; + 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 = 8.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DC1310DC1A14201E004E5DB5 /* 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 = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + 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 = 8.2; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + DC1310DE1A14201E004E5DB5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = HMCatalog.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + ENABLE_ON_DEMAND_RESOURCES = NO; + INFOPLIST_FILE = "$(SRCROOT)/HMCatalog/Supporting Files/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = HMCatalog; + PROVISIONING_PROFILE = ""; + SWIFT_VERSION = 2.3; + }; + name = Debug; + }; + DC1310DF1A14201E004E5DB5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = HMCatalog.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + ENABLE_ON_DEMAND_RESOURCES = NO; + INFOPLIST_FILE = "$(SRCROOT)/HMCatalog/Supporting Files/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = HMCatalog; + PROVISIONING_PROFILE = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 2.3; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DC1310B91A14201E004E5DB5 /* Build configuration list for PBXProject "HMCatalog" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC1310DB1A14201E004E5DB5 /* Debug */, + DC1310DC1A14201E004E5DB5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DC1310DD1A14201E004E5DB5 /* Build configuration list for PBXNativeTarget "HMCatalog" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC1310DE1A14201E004E5DB5 /* Debug */, + DC1310DF1A14201E004E5DB5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = DC1310B61A14201E004E5DB5 /* Project object */; +} diff --git a/HomeKitCatalog/HMCatalog/AppDelegate.swift b/HomeKitCatalog/HMCatalog/AppDelegate.swift new file mode 100644 index 00000000..d1551047 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/AppDelegate.swift @@ -0,0 +1,17 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `AppDelegate` handles higher-level app events. +*/ + +import UIKit + +/// A standard app delegate. +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + // MARK: Properties + + var window: UIWindow? +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Favorites/FavoritesManager.swift b/HomeKitCatalog/HMCatalog/Favorites/FavoritesManager.swift new file mode 100644 index 00000000..6ec21981 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Favorites/FavoritesManager.swift @@ -0,0 +1,300 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `FavoritesManager` stores and saves characteristics that have been pinned by the user. +*/ + +import HomeKit + +/// Handles interactions with `NSUserDefault`s to save the user's favorite accessories. +class FavoritesManager { + // MARK: Types + + static let accessoryToCharacteristicIdentifierMappingKey = "FavoritesManager.accessoryToCharacteristicIdentifierMappingKey" + + static let accessoryIdentifiersKey = "FavoritesManager.accessoryIdentifiersKey" + + // MARK: Properties + + /// A shared, singleton manager. + static let sharedManager = FavoritesManager() + + var home: HMHome? { + return HomeStore.sharedStore.home + } + + /** + An internal mapping of accessory unique identifiers to an array of their + favorite characteristic's unique identifiers. + */ + private var accessoryToCharacteristicIdentifiers = [NSUUID: [NSUUID]]() + + /// An internal array of all favorite accessory unique identifiers. + private var accessoryIdentifiers = [NSUUID]() + + /** + Loads the unique identifier map and array data from `NSUserDefaults` + into internal variables. + */ + init() { + let userDefaults = NSUserDefaults.standardUserDefaults() + + if let mapData = userDefaults.objectForKey(FavoritesManager.accessoryToCharacteristicIdentifierMappingKey) as? NSData, + arrayData = userDefaults.objectForKey(FavoritesManager.accessoryIdentifiersKey) as? NSData { + + accessoryToCharacteristicIdentifiers = NSKeyedUnarchiver.unarchiveObjectWithData(mapData) as? [NSUUID: [NSUUID]] ?? [:] + + accessoryIdentifiers = NSKeyedUnarchiver.unarchiveObjectWithData(arrayData) as? [NSUUID] ?? [] + } + } + + /** + - returns: An array of all favorite characteristics. + The array is sorted by localized type. + */ + var favoriteCharacteristics: [HMCharacteristic] { + // Find all of the favorite characteristics. + let favoriteCharacteristics = HomeStore.sharedStore.homeManager.homes.map { home in + return home.allCharacteristics.filter { return $0.isFavorite } + } + + // Need to flatten an [[HMCharacteristic]] to an [HMCharacteristic]. + return favoriteCharacteristics.reduce([], combine: +) + .sort(characteristicOrderedBefore) + } + + /** + - returns: An array of all favorite accessories. + The array is sorted by localized name. + */ + var favoriteAccessories: [HMAccessory] { + // Find all of the favorite accessories. + let newAccessories = accessoryIdentifiers.map { accessoryIdentifier in + return HomeStore.sharedStore.homeManager.homes.map { home in + return home.accessories.filter { accessory in + return accessory.uniqueIdentifier == accessoryIdentifier + } + } + } + + // Need to flatten [[[HMAccessory]]] to [HMAccessory]. + return newAccessories.reduce([], combine: +) + .reduce([], combine: +) + .sortByLocalizedName() + } + + + /** + - returns: An array of tuples representing accessories and + all favorite characteristics they contain. + The array is sorted by localized type. + */ + var favoriteGroups:[(accessory: HMAccessory, characteristics: [HMCharacteristic])] { + return favoriteAccessories.map { accessory in + let favoriteCharacteristics = favoriteCharacteristicsForAccessory(accessory) + + return (accessory: accessory, characteristics: favoriteCharacteristics) + } + } + + + /** + Evaluates whether or not an `HMCharacteristic` is a favorite. + + - parameter characteristic: The `HMCharacteristic` to evaluate. + + - returns: A `Bool`, whether or not the characteristic is a favorite. + */ + func characteristicIsFavorite(characteristic: HMCharacteristic) -> Bool { + guard let accessoryIdentifier = characteristic.service?.accessory?.uniqueIdentifier else { + return false + } + + guard let characteristicIdentifiers = accessoryToCharacteristicIdentifiers[accessoryIdentifier] else { + return false + } + + return characteristicIdentifiers.contains(characteristic.uniqueIdentifier) + } + + /** + Favorites a characteristic. + + - parameter characteristic: The `HMCharacteristic` to favorite. + */ + func favoriteCharacteristic(characteristic: HMCharacteristic) { + if characteristicIsFavorite(characteristic) { + return + } + + if let accessoryIdentifier = characteristic.service?.accessory?.uniqueIdentifier where accessoryToCharacteristicIdentifiers[accessoryIdentifier] != nil { + // Accessory is already favorite, add the characteristic. + accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.append(characteristic.uniqueIdentifier) + save() + } + else if let accessory = characteristic.service?.accessory { + // New accessory, make a new entry. + accessoryIdentifiers.append(accessory.uniqueIdentifier) + accessoryToCharacteristicIdentifiers[accessory.uniqueIdentifier] = [characteristic.uniqueIdentifier] + save() + } + } + + /** + Provides an array of favorite `HMCharacteristic`s within a given accessory. + + - parameter accessory: The `HMAccessory` to query. + + - returns: An array of `HMCharacteristic`s which are favorites for the provided accessory. + */ + func favoriteCharacteristicsForAccessory(accessory: HMAccessory) -> [HMCharacteristic] { + let characteristics = accessory.services.map { service in + return service.characteristics.filter { characteristic in + return characteristic.isFavorite + } + } + return characteristics.reduce([], combine: +) + .sort(characteristicOrderedBefore) + } + + + /** + Unfavorites a characteristic. + + - parameter characteristic: The `HMCharacteristic` to unfavorite. + */ + func unfavoriteCharacteristic(characteristic: HMCharacteristic) { + guard let accessoryIdentifier = characteristic.service?.accessory?.uniqueIdentifier else { return } + + guard let characteristicIdentifiers = accessoryToCharacteristicIdentifiers[accessoryIdentifier] else { return } + + guard let indexOfCharacteristic = characteristicIdentifiers.indexOf(characteristic.uniqueIdentifier) else { return } + + // Remove the characteristic from the mapped collection. + accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.removeAtIndex(indexOfCharacteristic) + if let indexOfAccessory = accessoryIdentifiers.indexOf(accessoryIdentifier), + isEmpty = accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.isEmpty + where isEmpty { + /* + If that was the last characteristic for that accessory, remove + the accessory from the internal array. + */ + accessoryIdentifiers.removeAtIndex(indexOfAccessory) + accessoryToCharacteristicIdentifiers.removeValueForKey(accessoryIdentifier) + } + + save() + } + + // MARK: Helper Methods + + /** + First, cleans out the internal identifier structures, then saves + the `accessoryToCharacteristicIdentifiers` map and `accessoryIdentifiers` + array into `NSUserDefaults`. + + This method should be called whenever a change is made to the internal structures. + */ + private func save() { + removeUnusedIdentifiers() + + let userDefaults = NSUserDefaults.standardUserDefaults() + + let mapData = NSKeyedArchiver.archivedDataWithRootObject(accessoryToCharacteristicIdentifiers) + let arrayData = NSKeyedArchiver.archivedDataWithRootObject(accessoryIdentifiers) + + userDefaults.setObject(mapData, forKey: FavoritesManager.accessoryToCharacteristicIdentifierMappingKey) + userDefaults.setObject(arrayData, forKey: FavoritesManager.accessoryIdentifiersKey) + } + + /** + Filters out any accessories or characteristic which are not longer + valid in HomeKit. + */ + private func removeUnusedIdentifiers() { + accessoryIdentifiers = accessoryIdentifiers.filter { identifier in + return accessoryIdentifierExists(identifier) + } + + let filteredPairs = accessoryToCharacteristicIdentifiers.filter { accessoryId, _ in + return accessoryIdentifierExists(accessoryId) + } + + accessoryToCharacteristicIdentifiers.removeAll() + + for (accessoryId, characteristicIds) in filteredPairs { + accessoryToCharacteristicIdentifiers[accessoryId] = characteristicIds + } + + for accessoryIdentifier in accessoryToCharacteristicIdentifiers.keys { + let filteredCharacteristics = accessoryToCharacteristicIdentifiers[accessoryIdentifier]?.filter { characteristicId in + return characteristicIdentifierExists(characteristicId) + } + + accessoryToCharacteristicIdentifiers[accessoryIdentifier] = filteredCharacteristics + } + } + + /** + - returns: `true` if there exists an accessory in HomeKit with the given + identifier; `false` otherwise. + */ + private func accessoryIdentifierExists(identifier: NSUUID) -> Bool { + return HomeStore.sharedStore.homeManager.homes.contains { home in + return home.accessories.contains { accessory in + return accessory.uniqueIdentifier == identifier + } + } + } + + /** + - returns: `true` if there exists a characteristic in HomeKit with the given + identifier; `false` otherwise. + */ + private func characteristicIdentifierExists(identifier: NSUUID) -> Bool { + return HomeStore.sharedStore.homeManager.homes.contains { home in + return home.accessories.contains { accessory in + return accessory.services.contains { service in + return service.characteristics.contains { characteristic in + return characteristic.uniqueIdentifier == identifier + } + } + } + } + } + + /** + Evaluates two `HMCharacteristic` objects to determine if the first is ordered before the second. + + - parameter characteristic1: The first `HMCharacteristic` to evaluate. + - parameter characteristic2: The second `HMCharacteristic` to evaluate. + + - returns: `true` if the characteristics are localized ordered ascending, `false` otherwise. + */ + private func characteristicOrderedBefore(characteristic1: HMCharacteristic, characteristic2: HMCharacteristic) -> Bool { + let type1 = characteristic1.localizedCharacteristicType + let type2 = characteristic2.localizedCharacteristicType + + return type1.localizedCompare(type2) == .OrderedAscending + } +} + +extension HMCharacteristic { + /// A convenience property to favorite, unfavorite, and query the status of a characteristic. + var isFavorite: Bool { + get { + return FavoritesManager.sharedManager.characteristicIsFavorite(self) + } + + set { + if newValue { + FavoritesManager.sharedManager.favoriteCharacteristic(self) + } + else { + FavoritesManager.sharedManager.unfavoriteCharacteristic(self) + } + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Favorites/FavoritesViewController.swift b/HomeKitCatalog/HMCatalog/Favorites/FavoritesViewController.swift new file mode 100644 index 00000000..6a81a2ad --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Favorites/FavoritesViewController.swift @@ -0,0 +1,256 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `FavoritesViewController` allows users to control pinned accessories. +*/ + +import UIKit +import HomeKit + +/** + Lists favorite characteristics (grouped by accessory) and allows users to + manipulate their values. +*/ +class FavoritesViewController: UITableViewController, UITabBarControllerDelegate, HMAccessoryDelegate, HMHomeManagerDelegate { + + // MARK: Types + + struct Identifiers { + static let characteristicCell = "CharacteristicCell" + static let segmentedControlCharacteristicCell = "SegmentedControlCharacteristicCell" + static let switchCharacteristicCell = "SwitchCharacteristicCell" + static let sliderCharacteristicCell = "SliderCharacteristicCell" + static let textCharacteristicCell = "TextCharacteristicCell" + static let serviceTypeCell = "ServiceTypeCell" + } + + // MARK: Properties + + var favoriteAccessories = FavoritesManager.sharedManager.favoriteAccessories + + var cellDelegate = AccessoryUpdateController() + + @IBOutlet weak var editButton: UIBarButtonItem! + + /// If `true`, the characteristic cells should show stars. + var showsFavorites = false { + didSet { + editButton.title = showsFavorites ? NSLocalizedString("Done", comment: "Done") : NSLocalizedString("Edit", comment: "Edit") + + reloadData() + } + } + + // MARK: View Methods + + /// Configures the table view and tab bar. + override func awakeFromNib() { + tableView.estimatedRowHeight = 44.0 + tableView.rowHeight = UITableViewAutomaticDimension + tableView.allowsSelectionDuringEditing = true + + registerReuseIdentifiers() + + tabBarController?.delegate = self + } + + /// Prepares HomeKit objects and reloads view. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + + registerAsDelegate() + + setNotificationsEnabled(true) + + reloadData() + } + + /// Disables notifications and "unregisters" as the delegate for the home manager. + override func viewWillDisappear(animated: Bool) { + super.viewWillDisappear(animated) + setNotificationsEnabled(false) + + // We don't want any more callbacks once the view has disappeared. + HomeStore.sharedStore.homeManager.delegate = nil + } + + /// Registers for all types of characteristic cells. + private func registerReuseIdentifiers() { + let characteristicNib = UINib(nibName: Identifiers.characteristicCell, bundle: nil) + tableView.registerNib(characteristicNib, forCellReuseIdentifier: Identifiers.characteristicCell) + + let sliderNib = UINib(nibName: Identifiers.sliderCharacteristicCell, bundle: nil) + tableView.registerNib(sliderNib, forCellReuseIdentifier: Identifiers.sliderCharacteristicCell) + + let switchNib = UINib(nibName: Identifiers.switchCharacteristicCell, bundle: nil) + tableView.registerNib(switchNib, forCellReuseIdentifier: Identifiers.switchCharacteristicCell) + + let segmentedNib = UINib(nibName: Identifiers.segmentedControlCharacteristicCell, bundle: nil) + tableView.registerNib(segmentedNib, forCellReuseIdentifier: Identifiers.segmentedControlCharacteristicCell) + + let textNib = UINib(nibName: Identifiers.textCharacteristicCell, bundle: nil) + tableView.registerNib(textNib, forCellReuseIdentifier: Identifiers.textCharacteristicCell) + + tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.serviceTypeCell) + } + + // MARK: Table View Methods + + /** + Provides the number of sections based on the favorite accessories count. + Also, add/removes the background message, if required. + + - returns: The favorite accessories count. + */ + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + let sectionCount = favoriteAccessories.count + + if sectionCount == 0 { + let message = NSLocalizedString("No Favorite Characteristics", comment: "No Favorite Characteristics") + + setBackgroundMessage(message) + } + else { + setBackgroundMessage(nil) + } + + return sectionCount + } + + /// - returns: The number of characteristics for accessory represented by the section index. + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let accessory = favoriteAccessories[section] + + let characteristics = FavoritesManager.sharedManager.favoriteCharacteristicsForAccessory(accessory) + + return characteristics.count + } + + /** + Dequeues the appropriate characteristic cell for the characteristic at the + given index path and configures the cell based on view configurations. + */ + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let characteristics = FavoritesManager.sharedManager.favoriteCharacteristicsForAccessory(favoriteAccessories[indexPath.section]) + + let characteristic = characteristics[indexPath.row] + + var reuseIdentifier = Identifiers.characteristicCell + + if characteristic.isReadOnly || characteristic.isWriteOnly { + reuseIdentifier = Identifiers.characteristicCell + } + else if characteristic.isBoolean { + reuseIdentifier = Identifiers.switchCharacteristicCell + } + else if characteristic.hasPredeterminedValueDescriptions { + reuseIdentifier = Identifiers.segmentedControlCharacteristicCell + } + else if characteristic.isNumeric { + reuseIdentifier = Identifiers.sliderCharacteristicCell + } + else if characteristic.isTextWritable { + reuseIdentifier = Identifiers.textCharacteristicCell + } + + let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CharacteristicCell + + cell.showsFavorites = showsFavorites + cell.delegate = cellDelegate + cell.characteristic = characteristic + + return cell + } + + /// - returns: The name of the accessory at the specified index path. + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return favoriteAccessories[section].name + } + + // MARK: IBAction Methods + + /// Toggles `showsFavorites`, which will also reload the view. + @IBAction func didTapEdit(sender: UIBarButtonItem) { + showsFavorites = !showsFavorites + } + + + // MARK: Helper Methods + + /** + Resets the `favoriteAccessories` array from the `FavoritesManager`, + resets the state of the edit button, and reloads the data. + */ + private func reloadData() { + favoriteAccessories = FavoritesManager.sharedManager.favoriteAccessories + + editButton.enabled = !favoriteAccessories.isEmpty + + tableView.reloadData() + } + + /** + Enables or disables notifications for all favorite characteristics which + support event notifications. + + - parameter notificationsEnabled: A `Bool` representing enabled or disabled. + */ + private func setNotificationsEnabled(notificationsEnabled: Bool) { + for characteristic in FavoritesManager.sharedManager.favoriteCharacteristics { + if characteristic.supportsEventNotification { + characteristic.enableNotification(notificationsEnabled) { error in + if let error = error { + print("HomeKit: Error enabling notification on characteristic \(characteristic): \(error.localizedDescription).") + } + } + } + } + } + + /** + Registers as the delegate for the home manager and all + favorite accessories. + */ + private func registerAsDelegate() { + HomeStore.sharedStore.homeManager.delegate = self + + for accessory in favoriteAccessories { + accessory.delegate = self + } + } + + // MARK: HMAccessoryDelegate Methods + + /// Update the view to disable cells with unavailable accessories. + func accessoryDidUpdateReachability(accessory: HMAccessory) { + reloadData() + } + + /// Search for the cell corresponding to that characteristic and update its value. + func accessory(accessory: HMAccessory, service: HMService, didUpdateValueForCharacteristic characteristic: HMCharacteristic) { + guard let accessory = characteristic.service?.accessory else { return } + + guard let indexOfAccessory = favoriteAccessories.indexOf(accessory) else { return } + + let favoriteCharacteristics = FavoritesManager.sharedManager.favoriteCharacteristicsForAccessory(accessory) + + guard let indexOfCharacteristic = favoriteCharacteristics.indexOf(characteristic) else { return } + + let indexPath = NSIndexPath(forRow: indexOfCharacteristic, inSection: indexOfAccessory) + + let cell = tableView.cellForRowAtIndexPath(indexPath) as! CharacteristicCell + + cell.setValue(characteristic.value, notify: false) + } + + // MARK: HMHomeManagerDelegate Methods + + /// Reloads views and re-configures characteristics. + func homeManagerDidUpdateHomes(manager: HMHomeManager) { + registerAsDelegate() + setNotificationsEnabled(true) + reloadData() + } +} diff --git a/HomeKitCatalog/HMCatalog/HMCatalogViewController.swift b/HomeKitCatalog/HMCatalog/HMCatalogViewController.swift new file mode 100644 index 00000000..14d060fd --- /dev/null +++ b/HomeKitCatalog/HMCatalog/HMCatalogViewController.swift @@ -0,0 +1,73 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `HMCatalogViewController` is a super class which mainly provides easy-access methods for shared HomeKit objects. +*/ + +import UIKit +import HomeKit + +/** + The super class for most table view controllers in this app. It manages home + delegate registration and facilitates 'popping back' when it's discovered that + a home has been deleted. +*/ +class HMCatalogViewController: UITableViewController, HMHomeDelegate { + // MARK: Properties + + var homeStore: HomeStore { + return HomeStore.sharedStore + } + + var home: HMHome! { + return homeStore.home + } + + // MARK: View Methods + + /** + Evaluates whether or not the view controller should pop to + the list of homes. + + - returns: `true` if this instance is not the root view controller + and the `home` is nil; `false` otherwise. + */ + private func shouldPopViewController() -> Bool { + if let rootViewController = navigationController?.viewControllers.first + where rootViewController == self { + return false + } + + return home == nil + } + + /// Pops the view controller, if required. Invokes the delegate registration method. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + + if shouldPopViewController() { + // Pop to root view controller if our home was destroyed while we were away. + navigationController?.popToRootViewControllerAnimated(true) + return + } + + registerAsDelegate() + } + + // MARK: Delegate Registration + + /** + A hierarchical method, to be overriden by superclasses. + The base implementation registers as the delegate for the `HomeStore`'s home. + Thus, any subclasses may override this, register as the delegate for any + objects they please, and then call `super.registerAsDelegate()` to register + as the home delegate as well. + + This method will be called when the view appears. + */ + func registerAsDelegate() { + homeStore.home?.delegate = self + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryBrowserViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryBrowserViewController.swift new file mode 100644 index 00000000..ecb11621 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryBrowserViewController.swift @@ -0,0 +1,307 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `AccessoryBrowserViewController` displays new accessories and allows the user to pair with them. +*/ + +import UIKit +import HomeKit +import ExternalAccessory + +/// Represents an accessory type and encapsulated accessory. +enum AccessoryType: Equatable, Nameable { + /// A HomeKit object + case HomeKit(accessory: HMAccessory) + + /// An external, `EAWiFiUnconfiguredAccessory` object + case External(accessory: EAWiFiUnconfiguredAccessory) + + /// The name of the accessory. + var name: String { + return accessory.name + } + + /// The accessory within the `AccessoryType`. + var accessory: AnyObject { + switch self { + case .HomeKit(let accessory): + return accessory + + case .External(let accessory): + return accessory + } + } +} + +/// Comparison of `AccessoryType`s based on name. +func ==(lhs: AccessoryType, rhs: AccessoryType) -> Bool { + return lhs.name == rhs.name +} + +/** + A view controller that displays a list of nearby accessories and allows the + user to add them to the provided HMHome. +*/ +class AccessoryBrowserViewController: HMCatalogViewController, ModifyAccessoryDelegate, EAWiFiUnconfiguredAccessoryBrowserDelegate, HMAccessoryBrowserDelegate { + // MARK: Types + + struct Identifiers { + static let accessoryCell = "AccessoryCell" + static let addedAccessoryCell = "AddedAccessoryCell" + static let addAccessorySegue = "Add Accessory" + } + + // MARK: Properties + + var addedAccessories = [HMAccessory]() + var displayedAccessories = [AccessoryType]() + let accessoryBrowser = HMAccessoryBrowser() + var externalAccessoryBrowser: EAWiFiUnconfiguredAccessoryBrowser? + + // MARK: View Methods + + /// Configures the table view and initializes the accessory browsers. + override func viewDidLoad() { + tableView.estimatedRowHeight = 44.0 + tableView.rowHeight = UITableViewAutomaticDimension + accessoryBrowser.delegate = self + + #if arch(arm) + // We can't use the ExternalAccessory framework on the iPhone simulator. + externalAccessoryBrowser = EAWiFiUnconfiguredAccessoryBrowser(delegate: self, queue: dispatch_get_main_queue()) + #endif + + startBrowsing() + } + + /// Reloads the view. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + reloadTable() + } + + // MARK: IBAction Methods + + /// Stops browsing and dismisses the view controller. + @IBAction func dismiss(sender: AnyObject) { + stopBrowsing() + dismissViewControllerAnimated(true, completion: nil) + } + + /// Sets the accessory, home, and delegate of a ModifyAccessoryViewController. + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + super.prepareForSegue(segue, sender: sender) + + if let sender = sender as? HMAccessory where segue.identifier == Identifiers.addAccessorySegue { + let modifyViewController = segue.intendedDestinationViewController as! ModifyAccessoryViewController + modifyViewController.accessory = sender + modifyViewController.delegate = self + } + } + + // MARK: Table View Methods + + /** + Generates the number of rows based on the number of displayed accessories. + + This method will also display a table view background message, if required. + + - returns: The number of rows based on the number of displayed accessories. + */ + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let rows = displayedAccessories.count + + if rows == 0 { + let message = NSLocalizedString("No Discovered Accessories", comment: "No Discovered Accessories") + setBackgroundMessage(message) + } + else { + setBackgroundMessage(nil) + } + + return rows + } + + /** + - returns: Creates a cell that lists an accessory, and if it hasn't been added to the home, + shows a disclosure indicator instead of a checkmark. + */ + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let accessoryType = displayedAccessories[indexPath.row] + + var reuseIdentifier = Identifiers.accessoryCell + + if case let .HomeKit(hmAccessory) = accessoryType where addedAccessories.contains(hmAccessory) { + reuseIdentifier = Identifiers.addedAccessoryCell + } + + let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) + cell.textLabel?.text = accessoryType.name + + if let accessory = accessoryType.accessory as? HMAccessory { + cell.detailTextLabel?.text = accessory.category.localizedDescription + } + else { + cell.detailTextLabel?.text = NSLocalizedString("External Accessory", comment: "External Accessory") + } + + return cell + } + + /// Configures the accessory based on its type. + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + switch displayedAccessories[indexPath.row] { + case .HomeKit(let accessory): + configureAccessory(accessory) + + case .External(let accessory): + externalAccessoryBrowser?.configureAccessory(accessory, withConfigurationUIOnViewController: self) + } + } + + // MARK: Helper Methods + + /// Starts browsing on both HomeKit and External accessory browsers. + private func startBrowsing(){ + accessoryBrowser.startSearchingForNewAccessories() + externalAccessoryBrowser?.startSearchingForUnconfiguredAccessoriesMatchingPredicate(nil) + } + + /// Stops browsing on both HomeKit and External accessory browsers. + private func stopBrowsing(){ + accessoryBrowser.stopSearchingForNewAccessories() + externalAccessoryBrowser?.stopSearchingForUnconfiguredAccessories() + } + + /** + Concatenates and sorts the discovered and added accessories. + + - returns: A sorted list of all accessories involved with this + browser session. + */ + func allAccessories() -> [AccessoryType] { + var accessories = [AccessoryType]() + accessories += accessoryBrowser.discoveredAccessories.map { .HomeKit(accessory: $0) } + + accessories += addedAccessories.flatMap { addedAccessory in + let accessoryType = AccessoryType.HomeKit(accessory: addedAccessory) + + return accessories.contains(accessoryType) ? nil : accessoryType + } + + if let external = externalAccessoryBrowser?.unconfiguredAccessories { + let unconfiguredAccessoriesArray = Array(external) + + accessories += unconfiguredAccessoriesArray.flatMap { addedAccessory in + let accessoryType = AccessoryType.External(accessory: addedAccessory) + + return accessories.contains(accessoryType) ? nil : accessoryType + } + } + + return accessories.sortByLocalizedName() + } + + /// Updates the displayed accesories array and reloads the table view. + private func reloadTable() { + displayedAccessories = allAccessories() + tableView.reloadData() + } + + /// Sends the accessory to the next view. + func configureAccessory(accessory: HMAccessory) { + if displayedAccessories.contains(.HomeKit(accessory: accessory)) { + performSegueWithIdentifier(Identifiers.addAccessorySegue, sender: accessory) + } + } + + /** + Finds an unconfigured accessory with a specified name. + + - parameter name: The name string of the accessory. + + - returns: An `HMAccessory?` from the search; `nil` if + the accessory could not be found. + */ + func unconfiguredHomeKitAccessoryWithName(name: String) -> HMAccessory? { + for type in displayedAccessories { + if case let .HomeKit(accessory) = type where accessory.name == name { + return accessory + } + } + return nil + } + + // MARK: ModifyAccessoryDelegate Methods + + /// Adds the accessory to the internal array and reloads the views. + func accessoryViewController(accessoryViewController: ModifyAccessoryViewController, didSaveAccessory accessory: HMAccessory) { + addedAccessories.append(accessory) + reloadTable() + } + + // MARK: EAWiFiUnconfiguredAccessoryBrowserDelegate Methods + + // Any updates to the external accessory browser causes a reload in the table view. + + func accessoryBrowser(browser: EAWiFiUnconfiguredAccessoryBrowser, didFindUnconfiguredAccessories accessories: Set) { + reloadTable() + } + + func accessoryBrowser(browser: EAWiFiUnconfiguredAccessoryBrowser, didRemoveUnconfiguredAccessories accessories: Set) { + reloadTable() + } + + func accessoryBrowser(browser: EAWiFiUnconfiguredAccessoryBrowser, didUpdateState state: EAWiFiUnconfiguredAccessoryBrowserState) { + reloadTable() + } + + /// If the configuration was successful, presents the 'Add Accessory' view. + func accessoryBrowser(browser: EAWiFiUnconfiguredAccessoryBrowser, didFinishConfiguringAccessory accessory: EAWiFiUnconfiguredAccessory, withStatus status: EAWiFiUnconfiguredAccessoryConfigurationStatus) { + if status != .Success { + return + } + + if let foundAccessory = unconfiguredHomeKitAccessoryWithName(accessory.name) { + configureAccessory(foundAccessory) + } + } + + // MARK: HMAccessoryBrowserDelegate Methods + + /** + Inserts the accessory into the internal array and inserts the + row into the table view. + */ + func accessoryBrowser(browser: HMAccessoryBrowser, didFindNewAccessory accessory: HMAccessory) { + let newAccessory = AccessoryType.HomeKit(accessory: accessory) + if displayedAccessories.contains(newAccessory) { + return + } + displayedAccessories.append(newAccessory) + displayedAccessories = displayedAccessories.sortByLocalizedName() + + if let newIndex = displayedAccessories.indexOf(newAccessory) { + let newIndexPath = NSIndexPath(forRow: newIndex, inSection: 0) + tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Automatic) + } + } + + /** + Removes the accessory from the internal array and deletes the + row from the table view. + */ + func accessoryBrowser(browser: HMAccessoryBrowser, didRemoveNewAccessory accessory: HMAccessory) { + let removedAccessory = AccessoryType.HomeKit(accessory: accessory) + if !displayedAccessories.contains(removedAccessory) { + return + } + if let removedIndex = displayedAccessories.indexOf(removedAccessory) { + let removedIndexPath = NSIndexPath(forRow: removedIndex, inSection: 0) + displayedAccessories.removeAtIndex(removedIndex) + tableView.deleteRowsAtIndexPaths([removedIndexPath], withRowAnimation: .Automatic) + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryUpdateController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryUpdateController.swift new file mode 100644 index 00000000..6009586b --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/AccessoryUpdateController.swift @@ -0,0 +1,110 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `AccessoryUpdateController` manages `CharacteristicCell` updates and buffers them up before sending them to HomeKit. +*/ + +import HomeKit + +/// An object that responds to `CharacteristicCell` updates and notifies HomeKit of changes. +class AccessoryUpdateController: NSObject, CharacteristicCellDelegate { + // MARK: Properties + + let updateQueue = dispatch_queue_create("com.sample.HMCatalog.CharacteristicUpdateQueue", DISPATCH_QUEUE_SERIAL) + + lazy var pendingWrites = [HMCharacteristic:AnyObject]() + lazy var sentWrites = [HMCharacteristic:AnyObject]() + + // Implicitly unwrapped optional because we need `self` to initialize. + var updateValueTimer: NSTimer! + + /// Starts the update timer on creation. + override init() { + super.init() + startListeningForCellUpdates() + } + + /// Responds to a cell change, and if the update was marked immediate, updates the characteristics. + func characteristicCell(cell: CharacteristicCell, didUpdateValue value: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool) { + pendingWrites[characteristic] = value + if immediate { + updateCharacteristics() + } + } + + /** + Reads the characteristic's value and calls the completion with the characteristic's value. + + If there is a pending write request on the same characteristic, the read is ignored to prevent + "UI glitching". + */ + func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void) { + characteristic.readValueWithCompletionHandler { error in + dispatch_sync(self.updateQueue) { + if let sentValue = self.sentWrites[characteristic] { + completion(sentValue, nil) + return + } + + dispatch_async(dispatch_get_main_queue()) { + completion(characteristic.value, error) + } + } + } + } + + /// Creates and starts the update value timer. + func startListeningForCellUpdates() { + updateValueTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(AccessoryUpdateController.updateCharacteristics), userInfo: nil, repeats: true) + } + + /// Invalidates the update timer. + func stopListeningForCellUpdates() { + updateValueTimer.invalidate() + } + + /// Sends all pending requests in the array. + func updateCharacteristics() { + dispatch_sync(updateQueue) { + for (characteristic, value) in self.pendingWrites { + self.sentWrites[characteristic] = value + + characteristic.writeValue(value) { error in + if let error = error { + print("HomeKit: Could not change value: \(error.localizedDescription).") + } + + self.didCompleteWrite(characteristic, value: value) + } + } + + self.pendingWrites.removeAll() + } + } + + /** + Synchronously adds the characteristic-value pair into the `sentWrites` map. + + - parameter characteristic: The `HMCharacteristic` to add. + - parameter value: The value of the `characteristic`. + */ + func didSendWrite(characteristic: HMCharacteristic, value: AnyObject) { + dispatch_sync(updateQueue) { + self.sentWrites[characteristic] = value + } + } + + /** + Synchronously removes the characteristic-value pair from the `sentWrites` map. + + - parameter characteristic: The `HMCharacteristic` to remove. + - parameter value: The value of the `characteristic` (unused, but included for clarity). + */ + func didCompleteWrite(characteristic: HMCharacteristic, value: AnyObject) { + dispatch_sync(updateQueue) { + self.sentWrites.removeValueForKey(characteristic) + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsTableViewDataSource.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsTableViewDataSource.swift new file mode 100644 index 00000000..b41e83ac --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsTableViewDataSource.swift @@ -0,0 +1,109 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ControlsTableViewDataSource` provides data for the `ControlsViewController`. +*/ + +import UIKit +import HomeKit + +/// A `UITableViewDataSource` that populates the table in `ControlsViewController`. +class ControlsTableViewDataSource: NSObject, UITableViewDataSource { + // MARK: Types + + struct Identifiers { + static let serviceCell = "ServiceCell" + static let unreachableServiceCell = "UnreachableServiceCell" + } + + // MARK: Properties + + var serviceTable: [String: [HMService]]? + var sortedKeys: [String]? + + let tableView: UITableView + var home: HMHome? { + return HomeStore.sharedStore.home + } + + /// Initializes the table view and data source. + required init(tableView: UITableView) { + self.tableView = tableView + super.init() + self.tableView.dataSource = self + } + + /** + Reloads the table, sets the table's dataSource to self, + regenerated the service table, creates a sorted list of keys, + sets the home's delegate, and reloads the table. + */ + func reloadTable() { + if let home = home { + serviceTable = home.serviceTable + sortedKeys = serviceTable!.keys.sort() + tableView.reloadData() + } + else { + serviceTable = nil + sortedKeys = nil + } + + tableView.reloadData() + } + + /// - returns: The localized description of the service type for that section. + func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return sortedKeys?[section] + } + + func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return sortedKeys?.count ?? 0 + } + + /** + - returns: A message that corresponds to the current most important reason + that there are no services in the table. Either "No Accessories" + or "No Services". + */ + func emptyMessage() -> String { + if home?.accessories.count == 0 { + return NSLocalizedString("No Accessories", comment: "No Accessories") + } + else { + return NSLocalizedString("No Services", comment: "No Services") + } + } + + /// - returns: The number of services matching the service type in that section. + func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return serviceTable![sortedKeys![section]]!.count + } + + /// - returns: A `ServiceCell` set for the service at the provided index path. + func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let service = serviceForIndexPath(indexPath)! + + let reuseIdentifier = service.accessory!.reachable ? Identifiers.serviceCell : Identifiers.unreachableServiceCell + + let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! ServiceCell + + cell.service = service + + return cell + } + + /// - returns: The service represented at the index path in the table. + func serviceForIndexPath(indexPath: NSIndexPath) -> HMService? { + if let sortedKeys = sortedKeys, + serviceTable = serviceTable, + services = serviceTable[sortedKeys[indexPath.section]] { + return services[indexPath.row] + } + + return nil + } + +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsViewController.swift new file mode 100644 index 00000000..c00f26f6 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/ControlsViewController.swift @@ -0,0 +1,119 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ControlsViewController` lists services in the selected home. +*/ + +import UIKit +import HomeKit + +/// A view controller which displays a list of `HMServices`, separated by Service Type. +class ControlsViewController: HMCatalogViewController, HMAccessoryDelegate { + // MARK: Types + + struct Identifiers { + static let showServiceSegue = "Show Service" + } + + // MARK: Properties + + var tableViewDataSource: ControlsTableViewDataSource! + var cellController = AccessoryUpdateController() + + @IBOutlet weak var addButton: UIBarButtonItem! + + // MARK: View Methods + + /// Sends the selected service into the destination view controller. + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + super.prepareForSegue(segue, sender: sender) + if segue.identifier == Identifiers.showServiceSegue { + if let indexPath = tableView.indexPathForCell(sender as! UITableViewCell) { + let characteristicsViewController = segue.intendedDestinationViewController as! CharacteristicsViewController + + if let selectedService = tableViewDataSource.serviceForIndexPath(indexPath) { + characteristicsViewController.service = selectedService + } + + characteristicsViewController.cellDelegate = cellController + } + } + } + + /// Initializes the table view data source. + override func viewDidLoad() { + super.viewDidLoad() + tableViewDataSource = ControlsTableViewDataSource(tableView: tableView) + } + + /// Reloads the view. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + navigationItem.title = home.name + reloadData() + } + + // MARK: Helper Methods + + private func reloadData() { + tableViewDataSource.reloadTable() + let sections = tableViewDataSource.numberOfSectionsInTableView(tableView) + + if sections == 0 { + setBackgroundMessage(tableViewDataSource.emptyMessage()) + } + else { + setBackgroundMessage(nil) + } + } + + // MARK: Delegate Registration + + /// Registers as the delegate for the current home and all accessories in the home. + override func registerAsDelegate() { + super.registerAsDelegate() + for accessory in home.accessories { + accessory.delegate = self + } + } + + /* + Any delegate methods which could change data will reload the + table view data source. + */ + + // MARK: HMHomeDelegate Methods + + func home(home: HMHome, didAddAccessory accessory: HMAccessory) { + accessory.delegate = self + reloadData() + } + + func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) { + reloadData() + } + + // MARK: HMAccessoryDelegate Methods + + func accessoryDidUpdateReachability(accessory: HMAccessory) { + reloadData() + } + + func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) { + reloadData() + } + + func accessory(accessory: HMAccessory, didUpdateAssociatedServiceTypeForService service: HMService) { + reloadData() + } + + func accessoryDidUpdateServices(accessory: HMAccessory) { + reloadData() + } + + func accessoryDidUpdateName(accessory: HMAccessory) { + reloadData() + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/ModifyAccessoryViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/ModifyAccessoryViewController.swift new file mode 100644 index 00000000..a43b65ec --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/ModifyAccessoryViewController.swift @@ -0,0 +1,388 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ModifyAccessoryViewController` allows the user to modify a HomeKit accessory. +*/ + +import UIKit +import HomeKit + +/// Represents the sections in the `ModifyAccessoryViewController`. +enum AddAccessoryTableViewSection: Int { + case Name, Rooms, Identify + + static let count = 3 +} + +/// Contains a method for notifying the delegate that the accessory was saved. +protocol ModifyAccessoryDelegate { + func accessoryViewController(accessoryViewController: ModifyAccessoryViewController, didSaveAccessory accessory: HMAccessory) +} + +/// A view controller that allows for renaming, reassigning, and identifying accessories before and after they've been added to a home. +class ModifyAccessoryViewController: HMCatalogViewController, HMAccessoryDelegate { + // MARK: Types + + struct Identifiers { + static let roomCell = "RoomCell" + } + + // MARK: Properties + + // Update this if the acessory failed in any way. + private var didEncounterError = false + + private var selectedIndexPath: NSIndexPath? + private var selectedRoom: HMRoom! + + @IBOutlet weak var nameField: UITextField! + private lazy var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .Gray) + + private let saveAccessoryGroup = dispatch_group_create() + + private var editingExistingAccessory = false + + // Strong reference, because we will replace the button with an activity indicator. + @IBOutlet /* strong */ var addButton: UIBarButtonItem! + var delegate: ModifyAccessoryDelegate? + var rooms = [HMRoom]() + + var accessory: HMAccessory! + + // MARK: View Methods + + /// Configures the table view and initializes view elements. + override func viewDidLoad() { + super.viewDidLoad() + + tableView.estimatedRowHeight = 44.0 + tableView.rowHeight = UITableViewAutomaticDimension + + selectedRoom = accessory.room ?? home.roomForEntireHome() + + // If the accessory belongs to the home already, we are in 'edit' mode. + editingExistingAccessory = accessoryHasBeenAddedToHome() + if editingExistingAccessory { + // Show 'save' instead of 'add.' + addButton.title = NSLocalizedString("Save", comment: "Save") + } + else { + /* + If we're not editing an existing accessory, then let the back + button show in the left. + */ + navigationItem.leftBarButtonItem = nil + } + + // Put the accessory's name in the 'name' field. + resetNameField() + + // Register a cell for the rooms. + tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.roomCell) + } + + /** + Registers as the delegate for the current home + and the accessory. + */ + override func registerAsDelegate() { + super.registerAsDelegate() + accessory.delegate = self + } + + /// Replaces the activity indicator with the 'Add' or 'Save' button. + func hideActivityIndicator() { + activityIndicator.stopAnimating() + navigationItem.rightBarButtonItem = addButton + } + + /// Temporarily replaces the 'Add' or 'Save' button with an activity indicator. + func showActivityIndicator() { + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: activityIndicator) + activityIndicator.startAnimating() + } + + /** + Called whenever the user taps the 'add' button. + + This method: + 1. Adds the accessory to the home, if not already added. + 2. Updates the accessory's name, if necessary. + 3. Assigns the accessory to the selected room, if necessary. + */ + @IBAction func didTapAddButton() { + let name = trimmedName + showActivityIndicator() + + if editingExistingAccessory { + home(home, assignAccessory: accessory, toRoom: selectedRoom) + updateName(name, forAccessory: accessory) + } + else { + dispatch_group_enter(saveAccessoryGroup) + home.addAccessory(accessory) { error in + if let error = error { + self.hideActivityIndicator() + self.displayError(error) + self.didEncounterError = true + } + else { + // Once it's successfully added to the home, add it to the room that's selected. + self.home(self.home, assignAccessory:self.accessory, toRoom: self.selectedRoom) + self.updateName(name, forAccessory: self.accessory) + } + dispatch_group_leave(self.saveAccessoryGroup) + } + } + + dispatch_group_notify(saveAccessoryGroup, dispatch_get_main_queue()) { + self.hideActivityIndicator() + if !self.didEncounterError { + self.dismiss(nil) + } + } + } + + /** + Informs the delegate that the accessory has been saved, and + dismisses the view controller. + */ + @IBAction func dismiss(sender: AnyObject?) { + delegate?.accessoryViewController(self, didSaveAccessory: accessory) + if editingExistingAccessory { + presentingViewController?.dismissViewControllerAnimated(true, completion: nil) + } + else { + navigationController?.popViewControllerAnimated(true) + } + } + + /** + - returns: `true` if the accessory has already been added to + the home; `false` otherwise. + */ + func accessoryHasBeenAddedToHome() -> Bool { + return home.accessories.contains(accessory) + } + + /** + Updates the accessories name. This function will enter and leave the saved dispatch group. + If the accessory's name is already equal to the passed-in name, this method does nothing. + + - parameter name: The new name for the accessory. + - parameter accessory: The accessory to rename. + */ + func updateName(name: String, forAccessory accessory: HMAccessory) { + if accessory.name == name { + return + } + dispatch_group_enter(saveAccessoryGroup) + accessory.updateName(name) { error in + if let error = error { + self.displayError(error) + self.didEncounterError = true + } + dispatch_group_leave(self.saveAccessoryGroup) + } + } + + /** + Assigns the given accessory to the provided room. This method will enter and leave the saved dispatch group. + + - parameter home: The home to assign. + - parameter accessory: The accessory to be assigned. + - parameter room: The room to which to assign the accessory. + */ + func home(home: HMHome, assignAccessory accessory: HMAccessory, toRoom room: HMRoom) { + if accessory.room == room { + return + } + dispatch_group_enter(saveAccessoryGroup) + home.assignAccessory(accessory, toRoom: room) { error in + if let error = error { + self.displayError(error) + self.didEncounterError = true + } + dispatch_group_leave(self.saveAccessoryGroup) + } + } + + /// Tells the current accessory to identify itself. + func identifyAccessory() { + accessory.identifyWithCompletionHandler { error in + if let error = error { + self.displayError(error) + } + } + } + + /// Enables the name field if the accessory's name changes. + func resetNameField() { + var action: String + if editingExistingAccessory { + action = NSLocalizedString("Edit %@", comment: "Edit Accessory") + } + else { + action = NSLocalizedString("Add %@", comment: "Add Accessory") + } + navigationItem.title = NSString(format: action, accessory.name) as String + nameField.text = accessory.name + nameField.enabled = home.isAdmin + enableAddButtonIfApplicable() + } + + /// Enables the save button if the name field is not empty. + func enableAddButtonIfApplicable() { + addButton.enabled = home.isAdmin && trimmedName.characters.count > 0 + } + + /// - returns: The `nameField`'s text, trimmed of newline and whitespace characters. + var trimmedName: String { + return nameField.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) + } + + /// Enables or disables the add button. + @IBAction func nameFieldDidChange(sender: AnyObject) { + enableAddButtonIfApplicable() + } + + // MARK: Table View Methods + + /// - returns: The number of `AddAccessoryTableViewSection`s. + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return AddAccessoryTableViewSection.count + } + + /// - returns: The number rows for the rooms section. All other sections are static. + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch AddAccessoryTableViewSection(rawValue: section) { + case .Rooms?: + return home.allRooms.count + + case nil: + fatalError("Unexpected `AddAccessoryTableViewSection` raw value.") + + default: + return super.tableView(tableView, numberOfRowsInSection: section) + } + } + + /// - returns: `UITableViewAutomaticDimension` for dynamic cell, super otherwise. + override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + switch AddAccessoryTableViewSection(rawValue: indexPath.section) { + case .Rooms?: + return UITableViewAutomaticDimension + + case nil: + fatalError("Unexpected `AddAccessoryTableViewSection` raw value.") + + default: + return super.tableView(tableView, heightForRowAtIndexPath: indexPath) + } + } + + /// - returns: A 'room cell' for the rooms section, super otherwise. + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + switch AddAccessoryTableViewSection(rawValue: indexPath.section) { + case .Rooms?: + return self.tableView(tableView, roomCellForRowAtIndexPath: indexPath) + + case nil: + fatalError("Unexpected `AddAccessoryTableViewSection` raw value.") + + default: + return super.tableView(tableView, cellForRowAtIndexPath: indexPath) + } + } + + /** + Creates a cell with the name of each room within the home, displaying a checkmark if the room + is the currently selected room. + */ + func tableView(tableView: UITableView, roomCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.roomCell, forIndexPath: indexPath) + let room = home.allRooms[indexPath.row] as HMRoom + + cell.textLabel?.text = home.nameForRoom(room) + + // Put a checkmark on the selected room. + cell.accessoryType = room == selectedRoom ? .Checkmark : .None + if !home.isAdmin { + cell.selectionStyle = .None + } + return cell + } + + + /// Handles row selection based on the section. + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + tableView.deselectRowAtIndexPath(indexPath, animated: true) + switch AddAccessoryTableViewSection(rawValue: indexPath.section) { + case .Rooms?: + guard home.isAdmin else { return } + + selectedRoom = home.allRooms[indexPath.row] + + let sections = NSIndexSet(index: AddAccessoryTableViewSection.Rooms.rawValue) + + tableView.reloadSections(sections, withRowAnimation: .Automatic) + + case .Identify?: + identifyAccessory() + + case nil: + fatalError("Unexpected `AddAccessoryTableViewSection` raw value.") + + default: break + } + } + + /// Required override. + override func tableView(tableView: UITableView, indentationLevelForRowAtIndexPath indexPath: NSIndexPath) -> Int { + return super.tableView(tableView, indentationLevelForRowAtIndexPath: NSIndexPath(forRow: 0, inSection: indexPath.section)) + } + + // MARK: HMHomeDelegate Methods + + // All home changes reload the view. + + func home(home: HMHome, didUpdateNameForRoom room: HMRoom) { + tableView.reloadData() + } + + func home(home: HMHome, didAddRoom room: HMRoom) { + tableView.reloadData() + } + + func home(home: HMHome, didRemoveRoom room: HMRoom) { + if selectedRoom == room { + // Reset the selected room if ours was deleted. + selectedRoom = homeStore.home!.roomForEntireHome() + } + tableView.reloadData() + } + + func home(home: HMHome, didAddAccessory accessory: HMAccessory) { + /* + Bridged accessories don't call the original completion handler if their + bridges are added to the home. We must respond to `HMHomeDelegate`'s + `home(_:didAddAccessory:)` and assign bridged accessories properly. + */ + if selectedRoom != nil { + self.home(home, assignAccessory: accessory, toRoom: selectedRoom) + } + } + + func home(home: HMHome, didUnblockAccessory accessory: HMAccessory) { + tableView.reloadData() + } + + // MARK: HMAccessoryDelegate Methods + + /// If the accessory's name changes, we update the name field. + func accessoryDidUpdateName(accessory: HMAccessory) { + resetNameField() + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.swift new file mode 100644 index 00000000..eb9e5641 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.swift @@ -0,0 +1,182 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + `CharacteristicCell` is a superclass which represents the state of a HomeKit characteristic. +*/ + +import UIKit +import HomeKit + +/// Methods for handling cell reads and updates. +protocol CharacteristicCellDelegate { + + /** + Called whenever the control within the cell updates its value. + + - parameter cell: The cell which has updated its value. + - parameter newValue: The new value represented by the cell's control. + - parameter characteristic: The characteristic the cell represents. + - parameter immediate: Whether or not to update external values immediately. + + For example, Slider cells should not update immediately upon value change, + so their values are cached and updates are coalesced. Subclasses can decide + whether or not their values are meant to be updated immediately. + */ + func characteristicCell(cell: CharacteristicCell, didUpdateValue value: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool) + + /** + Called when the characteristic cell needs to reload its value from an external source. + Consider using this call to look up values in memory or query them from an accessory. + + - parameter cell: The cell requesting a value update. + - parameter characteristic: The characteristic for whose value the cell is asking. + - parameter completion: The closure that the cell provides to be called when values have been read successfully. + */ + func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void) +} + +/** + A `UITableViewCell` subclass that displays the current value of an `HMCharacteristic` and + notifies its delegate of changes. Subclasses of this class will provide additional controls + to display different kinds of data. +*/ +class CharacteristicCell: UITableViewCell { + /// An alpha percentage used when disabling cells. + static let DisabledAlpha: CGFloat = 0.4 + + /// Required init. + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + /// Subclasses can return false if they have many frequent updates that should be deferred. + class var updatesImmediately: Bool { + return true + } + + // MARK: Properties + + @IBOutlet weak var typeLabel: UILabel! + @IBOutlet weak var valueLabel: UILabel! + @IBOutlet weak var favoriteButton: UIButton! + + @IBOutlet weak var favoriteButtonWidthConstraint: NSLayoutConstraint! + @IBOutlet weak var favoriteButtonHeightContraint: NSLayoutConstraint! + + /** + Show / hide the favoriteButton and adjust the constraints + to ensure proper layout. + */ + var showsFavorites = false { + didSet { + if showsFavorites { + favoriteButton.hidden = false + favoriteButtonWidthConstraint.constant = favoriteButtonHeightContraint.constant + } + else { + favoriteButton.hidden = true + favoriteButtonWidthConstraint.constant = 15.0 + } + } + } + + /** + - returns: `true` if the represented characteristic is reachable; + `false` otherwise. + */ + var enabled: Bool { + return (characteristic.service?.accessory?.reachable ?? false) + } + + /** + The value currently represented by the cell. + + This is not necessarily the value of this cell's characteristic, + because the cell's value changes independently of the characteristic. + */ + var value: AnyObject? + + /// The delegate that will respond to cell value changes. + var delegate: CharacteristicCellDelegate? + + /** + The characteristic represented by this cell. + + When this is set, the cell populates based on + the characteristic's value and requests an initial value + from its delegate. + */ + var characteristic: HMCharacteristic! { + didSet { + typeLabel.text = characteristic.localizedCharacteristicType + + selectionStyle = characteristic.isIdentify ? .Default : .None + + setValue(characteristic.value, notify: false) + + if characteristic.isWriteOnly { + // Don't read the value for write-only characteristics. + return + } + + // Set initial state of the favorite button + favoriteButton.selected = characteristic.isFavorite + + // "Enable" the cell if the accessory is reachable or we are displaying the favorites. + + // Configure the views. + typeLabel.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha + valueLabel?.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha + + if enabled { + delegate?.characteristicCell(self, readInitialValueForCharacteristic: characteristic) { value, error in + if let error = error { + print("HomeKit: Error reading value for characteristic \(self.characteristic): \(error.localizedDescription).") + } + else { + self.setValue(value, notify: false) + } + } + } + + } + } + + /// Resets the value label to the localized description from HMCharacteristic+Readability. + func resetValueLabel() { + if let value = value { + valueLabel?.text = characteristic.localizedDescriptionForValue(value) + } + } + + /** + Toggles the star button and saves the favorite status + of the characteristic in the FavoriteManager. + */ + @IBAction func didTapFavoriteButton(sender: UIButton) { + sender.selected = !sender.selected + characteristic.isFavorite = sender.selected + } + + /** + Sets the cell's value and resets the label. + + - parameter newValue: The new value. + - parameter notify: If true, the cell notifies its delegate of the change. + */ + func setValue(newValue: AnyObject?, notify: Bool) { + value = newValue + if let newValue = newValue { + resetValueLabel() + /* + We do not allow the setting of nil values from the app, + but we do have to deal with incoming nil values. + */ + if notify { + delegate?.characteristicCell(self, didUpdateValue: newValue, forCharacteristic: characteristic, immediate: self.dynamicType.updatesImmediately) + } + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.xib b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.xib new file mode 100644 index 00000000..de349de2 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/CharacteristicCell.xib @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SegmentedControlCharacteristicCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SegmentedControlCharacteristicCell.swift new file mode 100644 index 00000000..cac8b6e9 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SegmentedControlCharacteristicCell.swift @@ -0,0 +1,86 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `SegmentedControlCharacteristicCell` displays characteristics with associated values. +*/ + +import UIKit +import HomeKit + +/** + A `CharacteristicCell` subclass that contains a `UISegmentedControl`. + + Used for `HMCharacteristic`s which have associated, non-numeric values, like Lock Management State. +*/ +class SegmentedControlCharacteristicCell: CharacteristicCell { + // MARK: Properties + + @IBOutlet weak var segmentedControl: UISegmentedControl! + + /** + Calls the super class's didSet, and also computes a list + of possible values. + */ + override var characteristic: HMCharacteristic! { + didSet { + segmentedControl.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha + segmentedControl.userInteractionEnabled = enabled + + if let values = characteristic.allPossibleValues as? [Int] { + possibleValues = values + } + } + } + + /** + The possible values for this characteristic. + When this is set, adds localized descriptions to the segmented control. + */ + var possibleValues = [Int]() { + didSet { + segmentedControl.removeAllSegments() + for index in 0.. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.swift new file mode 100644 index 00000000..bfd68d65 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.swift @@ -0,0 +1,77 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `SliderCharacteristicCell` displays characteristics with a continuous range of options. +*/ + +import UIKit +import HomeKit + +/** + A `CharacteristicCell` subclass that contains a slider. + Used for numeric characteristics that have a continuous range of options. +*/ +class SliderCharacteristicCell: CharacteristicCell { + // MARK: Properties + + @IBOutlet weak var valueSlider: UISlider! + + override var characteristic: HMCharacteristic! { + didSet { + valueSlider.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha + valueSlider.userInteractionEnabled = enabled + } + + willSet(newCharacteristic) { + // These are sane defaults in case the max and min are not set. + valueSlider.minimumValue = newCharacteristic.metadata?.minimumValue as? Float ?? 0.0 + valueSlider.maximumValue = newCharacteristic.metadata?.maximumValue as? Float ?? 100.0 + } + } + + /// If notify is false, sets the valueSlider's represented value. + override func setValue(newValue: AnyObject?, notify: Bool) { + super.setValue(newValue, notify: notify) + if let newValue = newValue as? NSNumber where !notify { + valueSlider.value = newValue.floatValue + } + } + + /** + Restricts a value to the step value provided in the cell's + characteristic's metadata. + + - parameter sliderValue: The provided value. + + - returns: The value adjusted to align with a step value. + */ + func roundedValueForSliderValue(value: Float) -> Float { + if let metadata = characteristic.metadata, + stepValue = metadata.stepValue as? Float + where stepValue > 0 { + let newStep = roundf(value / stepValue) + let stepped = newStep * stepValue + return stepped + } + + return value + } + + // Sliders don't update immediately, because sliders generate many updates. + override class var updatesImmediately: Bool { + return false + } + + /** + Responds to a slider change and sets the cell's value. + + - parameter slider: The slider that changed. + */ + func didChangeSliderValue(slider: UISlider) { + let value = roundedValueForSliderValue(slider.value) + setValue(value, notify: true) + } + +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.xib b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.xib new file mode 100644 index 00000000..9caf5733 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SliderCharacteristicCell.xib @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.swift new file mode 100644 index 00000000..5bde39b6 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.swift @@ -0,0 +1,49 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `SwitchCharacteristicCell` displays Boolean characteristics. +*/ + +import UIKit +import HomeKit + +/** + A `CharacteristicCell` subclass that contains a single switch. + Used for Boolean characteristics. +*/ +class SwitchCharacteristicCell: CharacteristicCell { + // MARK: Properties + + @IBOutlet weak var valueSwitch: UISwitch! + + override var characteristic: HMCharacteristic! { + didSet { + valueSwitch.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha + valueSwitch.userInteractionEnabled = enabled + } + } + + /// If notify is false, sets the switch to the value. + override func setValue(newValue: AnyObject?, notify: Bool) { + super.setValue(newValue, notify: notify) + + if !notify { + if let boolValue = newValue as? Bool { + valueSwitch.setOn(boolValue, animated: true) + } + } + } + + /** + Responds to the switch updating and sets the + value to the switch's value. + + - parameter valueSwitch: The switch that updated. + */ + func didChangeSwitchValue(valueSwitch: UISwitch) { + setValue(valueSwitch.on, notify: true) + } + +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.xib b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.xib new file mode 100644 index 00000000..04b433d0 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/SwitchCharacteristicCell.xib @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.swift new file mode 100644 index 00000000..ab84aa86 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.swift @@ -0,0 +1,48 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `TextCharacteristicCell` represents text-input characteristics. +*/ + +import UIKit +import HomeKit + +/** + A `CharacteristicCell` subclass that contains a text field. + Used for text-input characteristics. +*/ +class TextCharacteristicCell: CharacteristicCell, UITextFieldDelegate { + // MARK: Properties + + @IBOutlet weak var textField: UITextField! + + override var characteristic: HMCharacteristic! { + didSet { + textField.alpha = enabled ? 1.0 : CharacteristicCell.DisabledAlpha + textField.userInteractionEnabled = enabled + } + } + + /// If notify is false, sets the text field's text from the value. + override func setValue(newValue: AnyObject?, notify: Bool) { + super.setValue(newValue, notify: notify) + if !notify { + if let newStringValue = newValue as? String { + textField.text = newStringValue + } + } + } + + /// Dismiss the keyboard when "Go" is clicked + func textFieldShouldReturn(textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + + /// Sets the value of the characteristic when editing is complete. + func textFieldDidEndEditing(textField: UITextField) { + setValue(textField.text, notify: true) + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.xib b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.xib new file mode 100644 index 00000000..743a3f89 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/Characteristic Cells/TextCharacteristicCell.xib @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsTableViewDataSource.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsTableViewDataSource.swift new file mode 100644 index 00000000..a3a3ecc9 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsTableViewDataSource.swift @@ -0,0 +1,207 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `CharacteristicsTableViewDataSource` provides the data for the `CharacteristicsViewController`. +*/ + +import UIKit +import HomeKit + +/// Represents the sections in the `CharacteristicsViewController`. +enum CharacteristicTableViewSection: Int { + case Characteristics, AssociatedServiceType +} + +/// A `UITableViewDataSource` that populates a `CharacteristicsViewController`. +class CharacteristicsTableViewDataSource: NSObject, UITableViewDelegate, UITableViewDataSource { + // MARK: Types + + struct Identifiers { + static let characteristicCell = "CharacteristicCell" + static let sliderCharacteristicCell = "SliderCharacteristicCell" + static let switchCharacteristicCell = "SwitchCharacteristicCell" + static let segmentedControlCharacteristicCell = "SegmentedControlCharacteristicCell" + static let textCharacteristicCell = "TextCharacteristicCell" + static let serviceTypeCell = "ServiceTypeCell" + } + + // MARK: Properties + + var service: HMService + var tableView: UITableView + var delegate: CharacteristicCellDelegate + var showsFavorites: Bool + var allowsAllWrites: Bool + + /// Sets up properties from specified values, configures the table view, and cell reuse identifiers. + required init(service: HMService, tableView: UITableView, delegate: CharacteristicCellDelegate, showsFavorites: Bool = false, allowsAllWrites: Bool = false) { + self.service = service + self.tableView = tableView + self.delegate = delegate + self.showsFavorites = showsFavorites + self.allowsAllWrites = allowsAllWrites + super.init() + tableView.dataSource = self + tableView.rowHeight = UITableViewAutomaticDimension + tableView.estimatedRowHeight = 50.0 + registerReuseIdentifiers() + } + + /// Registers all of the characteristic cell reuse identifiers with this table. + func registerReuseIdentifiers() { + let characteristicNib = UINib(nibName: Identifiers.characteristicCell, bundle: nil) + tableView.registerNib(characteristicNib, forCellReuseIdentifier: Identifiers.characteristicCell) + + let sliderNib = UINib(nibName: Identifiers.sliderCharacteristicCell, bundle: nil) + tableView.registerNib(sliderNib, forCellReuseIdentifier: Identifiers.sliderCharacteristicCell) + + let switchNib = UINib(nibName: Identifiers.switchCharacteristicCell, bundle: nil) + tableView.registerNib(switchNib, forCellReuseIdentifier: Identifiers.switchCharacteristicCell) + + let segmentedNib = UINib(nibName: Identifiers.segmentedControlCharacteristicCell, bundle: nil) + tableView.registerNib(segmentedNib, forCellReuseIdentifier: Identifiers.segmentedControlCharacteristicCell) + + let textNib = UINib(nibName: Identifiers.textCharacteristicCell, bundle: nil) + tableView.registerNib(textNib, forCellReuseIdentifier: Identifiers.textCharacteristicCell) + + tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.serviceTypeCell) + } + + /** + - returns: The number of sections, computed from whether or not + the services supports an associated service type. + */ + func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return service.supportsAssociatedServiceType ? 2 : 1 + } + + /** + The characteristics section uses the services count to generate the number of rows. + The associated service type uses the valid associated service types. + */ + func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch CharacteristicTableViewSection(rawValue: section) { + case .Characteristics?: + return service.characteristics.count + + case .AssociatedServiceType?: + // For 'None'. + return HMService.validAssociatedServiceTypes.count + 1 + + case nil: + fatalError("Unexpected `CharacteristicTableViewSection` raw value.") + } + } + + /** + Looks up the appropriate service type for the row in the list and returns a localized version, + or 'None' if the row doesn't correspond to any valid service type. + + - parameter row: The row to look up. + + - returns: The localized service type in that row, or 'None'. + */ + func displayedServiceTypeForRow(row: Int) -> String { + let serviceTypes = HMService.validAssociatedServiceTypes + if row < serviceTypes.count { + return HMService.localizedDescriptionForServiceType(serviceTypes[row]) + } + + return NSLocalizedString("None", comment: "None") + } + + /** + Evaluates whether or not a service type is selected for a given row. + + - parameter row: The selected row. + + - returns: `true` if the current row is a valid service type, `false` otherwise + */ + func serviceTypeIsSelectedForRow(row: Int) -> Bool { + let serviceTypes = HMService.validAssociatedServiceTypes + if row >= serviceTypes.count { + return service.associatedServiceType == nil + } + + if let associatedServiceType = service.associatedServiceType { + return serviceTypes[row] == associatedServiceType + } + + return false + } + + /// Generates a cell for an associated service. + private func tableView(tableView: UITableView, associatedServiceTypeCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceTypeCell, forIndexPath: indexPath) + + cell.textLabel?.text = displayedServiceTypeForRow(indexPath.row) + cell.accessoryType = serviceTypeIsSelectedForRow(indexPath.row) ? .Checkmark : .None + + return cell + } + + /** + Generates a characteristic cell based on the type of characteristic + located at the specified index path. + */ + private func tableView(tableView: UITableView, characteristicCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let characteristic = service.characteristics[indexPath.row] + + var reuseIdentifier = Identifiers.characteristicCell + + if (characteristic.isReadOnly || characteristic.isWriteOnly) && !allowsAllWrites { + reuseIdentifier = Identifiers.characteristicCell + } + else if characteristic.isBoolean { + reuseIdentifier = Identifiers.switchCharacteristicCell + } + else if characteristic.hasPredeterminedValueDescriptions { + reuseIdentifier = Identifiers.segmentedControlCharacteristicCell + } + else if characteristic.isNumeric { + reuseIdentifier = Identifiers.sliderCharacteristicCell + } + else if characteristic.isTextWritable { + reuseIdentifier = Identifiers.textCharacteristicCell + } + + let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CharacteristicCell + + cell.showsFavorites = showsFavorites + cell.delegate = delegate + cell.characteristic = characteristic + + return cell + } + + /// Uses convenience methods to generate a cell based on the index path's section. + func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + switch CharacteristicTableViewSection(rawValue: indexPath.section) { + case .Characteristics?: + return self.tableView(tableView, characteristicCellForRowAtIndexPath: indexPath) + + case .AssociatedServiceType?: + return self.tableView(tableView, associatedServiceTypeCellForRowAtIndexPath: indexPath) + + case nil: + fatalError("Unexpected `CharacteristicTableViewSection` raw value.") + } + } + + /// - returns: A localized string for the section. + func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch CharacteristicTableViewSection(rawValue: section) { + case .Characteristics?: + return NSLocalizedString("Characteristics", comment: "Characteristics") + + case .AssociatedServiceType?: + return NSLocalizedString("Associated Service Type", comment: "Associated Service Type") + + case nil: + fatalError("Unexpected `CharacteristicTableViewSection` raw value.") + } + } + +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsViewController.swift new file mode 100644 index 00000000..14045ae6 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/CharacteristicsViewController.swift @@ -0,0 +1,167 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `CharacteristicsViewController` displays characteristics within a service. +*/ + +import UIKit +import HomeKit + +/// A view controller that displays a list of characteristics within an `HMService`. +class CharacteristicsViewController: HMCatalogViewController, HMAccessoryDelegate { + // MARK: Properties + + var service: HMService! + var cellDelegate: CharacteristicCellDelegate! + private var tableViewDataSource: CharacteristicsTableViewDataSource! + var showsFavorites = false + var allowsAllWrites = false + + // MARK: View Methods + + /// Initializes the data source. + override func viewDidLoad() { + super.viewDidLoad() + + tableViewDataSource = CharacteristicsTableViewDataSource(service: service, tableView: tableView, delegate: cellDelegate, showsFavorites: showsFavorites, allowsAllWrites: allowsAllWrites) + } + + /// Reloads the view and enabled notifications for all relevant characteristics. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + title = service.name + setNotificationsEnabled(true) + reloadTableView() + } + + /// Disables notifications for characteristics. + override func viewWillDisappear(animated: Bool) { + super.viewWillDisappear(animated) + setNotificationsEnabled(false) + } + + /** + Registers as the delegate for the current home and + the service's accessory. + */ + override func registerAsDelegate() { + super.registerAsDelegate() + service.accessory?.delegate = self + } + + /** + Enables or disables notifications on all characteristics within this service. + + - parameter notificationsEnabled: A `Bool`; whether to enable or disable. + */ + func setNotificationsEnabled(notificationsEnabled: Bool) { + for characteristic in service.characteristics { + if characteristic.supportsEventNotification { + characteristic.enableNotification(notificationsEnabled) { error in + if let error = error { + print("HomeKit: Error enabling notification on charcteristic '\(characteristic)': \(error.localizedDescription)") + } + } + } + } + } + + /// Reloads the table view and stops the refresh control. + func reloadTableView() { + setNotificationsEnabled(true) + tableViewDataSource.service = service + refreshControl?.endRefreshing() + tableView.reloadData() + } + + // MARK: Table View Methods + + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + switch CharacteristicTableViewSection(rawValue: indexPath.section) { + case .Characteristics?: + let characteristic = service.characteristics[indexPath.row] + didSelectCharacteristic(characteristic, atIndexPath: indexPath) + + case .AssociatedServiceType?: + didSelectAssociatedServiceTypeAtIndexPath(indexPath) + + case nil: + fatalError("Unexpected `CharacteristicTableViewSection` raw value.") + } + } + + /** + If a characteristic is selected, and it is the 'Identify' characteristic, + perform an identify on that accessory. + */ + private func didSelectCharacteristic(characteristic: HMCharacteristic, atIndexPath indexPath: NSIndexPath) { + if characteristic.isIdentify { + service.accessory?.identifyWithCompletionHandler { error in + if let error = error { + self.displayError(error) + return + } + } + } + } + + /** + Handles selection of one of the associated service types in the list. + + - parameter indexPath: The selected index path. + */ + private func didSelectAssociatedServiceTypeAtIndexPath(indexPath: NSIndexPath) { + let serviceTypes = HMService.validAssociatedServiceTypes + var newServiceType: String? + if indexPath.row < serviceTypes.count { + newServiceType = serviceTypes[indexPath.row] + } + service.updateAssociatedServiceType(newServiceType) { error in + if let error = error { + self.displayError(error) + return + } + + self.didUpdateAssociatedServiceType() + } + } + + /// Reloads the associated service section in the table view. + private func didUpdateAssociatedServiceType() { + let associatedServiceTypeIndexSet = NSIndexSet(index: CharacteristicTableViewSection.AssociatedServiceType.rawValue) + + tableView.reloadSections(associatedServiceTypeIndexSet, withRowAnimation: .Automatic) + } + + // MARK: HMHomeDelegate Methods + + /// If our accessory was removed, pop to root view controller. + func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) { + if accessory == service.accessory { + navigationController?.popToRootViewControllerAnimated(true) + } + } + + // MARK: HMAccessoryDelegate Methods + + /// If our accessory becomes unreachable, pop to root view controller. + func accessoryDidUpdateReachability(accessory: HMAccessory) { + if accessory == service.accessory && !accessory.reachable { + navigationController?.popToRootViewControllerAnimated(true) + } + } + + /** + Search for the cell corresponding to that characteristic and + update its value. + */ + func accessory(accessory: HMAccessory, service: HMService, didUpdateValueForCharacteristic characteristic: HMCharacteristic) { + if let index = service.characteristics.indexOf(characteristic) { + let indexPath = NSIndexPath(forRow: index, inSection: 0) + let cell = tableView.cellForRowAtIndexPath(indexPath) as! CharacteristicCell + cell.setValue(characteristic.value, notify: false) + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServiceCell.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServiceCell.swift new file mode 100644 index 00000000..edb5c6ec --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServiceCell.swift @@ -0,0 +1,47 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ServiceCell` displays a service and information about it. +*/ + +import UIKit +import HomeKit + +/// A `UITableViewCell` subclass for displaying a service and the room and accessory where it resides. +class ServiceCell: UITableViewCell { + + // MARK: Properties + var includeAccessoryText = true + + /// Required init. + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + /** + The cell's service. + + When the service is set, the cell's `textLabel` will contain the service's name + or the accessory's name if the service has no name. + The detail text will contain information about where this service lives. + */ + var service: HMService? { + didSet { + if let service = service, + accessory = service.accessory { + textLabel?.text = service.name ?? accessory.name + let accessoryName = accessory.name + let roomName = accessory.room!.name + if includeAccessoryText { + let inIdentifier = NSLocalizedString("%@ in %@", comment: "Accessory in Room") + detailTextLabel?.text = String(format: inIdentifier, accessoryName, roomName) + } + else { + detailTextLabel?.text = "" + } + } + } + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServicesViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServicesViewController.swift new file mode 100644 index 00000000..d12a706f --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Accessories/Services/ServicesViewController.swift @@ -0,0 +1,259 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ServicesViewController` displays an accessory's services. +*/ + +import UIKit +import HomeKit + +/// Represents the sections in the `ServicesViewController`. +enum AccessoryTableViewSection: Int { + case Services, BridgedAccessories +} + +/** + A view controller which displays all the services of a provided accessory, and + passes its cell delegate onto a `CharacteristicsViewController`. +*/ +class ServicesViewController: HMCatalogViewController, HMAccessoryDelegate { + // MARK: Types + + struct Identifiers { + static let accessoryCell = "AccessoryCell" + static let serviceCell = "ServiceCell" + static let showServiceSegue = "Show Service" + } + + // MARK: Properties + + var accessory: HMAccessory! + lazy var cellDelegate: CharacteristicCellDelegate = AccessoryUpdateController() + var showsFavorites = false + var allowsAllWrites = false + var onlyShowsControlServices = false + var displayedServices = [HMService]() + var bridgedAccessories = [HMAccessory]() + + // MARK: View Methods + + /// Configures table view. + override func viewDidLoad() { + super.viewDidLoad() + tableView.estimatedRowHeight = 44.0 + tableView.rowHeight = UITableViewAutomaticDimension + } + + /// Reloads the view. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + updateTitle() + reloadData() + } + + /// Pops the view controller, if required. + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + if shouldPopViewController() { + navigationController?.popToRootViewControllerAnimated(true) + } + } + + /** + Passes the `CharacteristicsViewController` the service from the cell and + configures the view controller. + */ + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + super.prepareForSegue(segue, sender: sender) + + guard segue.identifier == Identifiers.showServiceSegue else { return } + + if let indexPath = tableView.indexPathForCell(sender as! UITableViewCell) { + let selectedService = displayedServices[indexPath.row] + let characteristicsViewController = segue.intendedDestinationViewController as! CharacteristicsViewController + characteristicsViewController.showsFavorites = showsFavorites + characteristicsViewController.allowsAllWrites = allowsAllWrites + characteristicsViewController.service = selectedService + characteristicsViewController.cellDelegate = cellDelegate + } + } + + /** + - returns: `true` if our accessory is no longer in the + current home's list of accessories. + */ + private func shouldPopViewController() -> Bool { + for accessory in homeStore.home!.accessories { + if accessory == accessory { + return false + } + } + return true + } + + // MARK: Delegate Registration + + /** + Registers as the delegate for the current home + and for the current accessory. + */ + override func registerAsDelegate() { + super.registerAsDelegate() + accessory.delegate = self + } + + // MARK: Table View Methods + + /// Two sections if we're showing bridged accessories. + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + if accessory.uniqueIdentifiersForBridgedAccessories != nil { + return 2 + } + return 1 + } + + /** + Section 1 contains the services within the accessory. + Section 2 contains the bridged accessories. + */ + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch AccessoryTableViewSection(rawValue: section) { + case .Services?: + return displayedServices.count + + case .BridgedAccessories?: + return bridgedAccessories.count + + case nil: + fatalError("Unexpected `AccessoryTableViewSection` raw value.") + } + } + + /** + - returns: A Service or Bridged Accessory Cell based + on the section. + */ + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + switch AccessoryTableViewSection(rawValue: indexPath.section) { + case .Services?: + return self.tableView(tableView, serviceCellForRowAtIndexPath: indexPath) + + case .BridgedAccessories?: + return self.tableView(tableView, bridgedAccessoryCellForRowAtIndexPath: indexPath) + + case nil: + fatalError("Unexpected `AccessoryTableViewSection` raw value.") + } + } + + /** + - returns: A cell containing the name of a bridged + accessory at a given index path. + */ + func tableView(tableView: UITableView, bridgedAccessoryCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.accessoryCell, forIndexPath: indexPath) + let accessory = bridgedAccessories[indexPath.row] + cell.textLabel?.text = accessory.name + return cell + } + + /** + - returns: A cell containing the name of a service at + a given index path, as well as a localized + description of its service type. + */ + func tableView(tableView: UITableView, serviceCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceCell, forIndexPath: indexPath) + let service = displayedServices[indexPath.row] + + // Inherit the name from the accessory if the Service doesn't have one. + cell.textLabel?.text = service.name ?? service.accessory?.name + cell.detailTextLabel?.text = service.localizedDescription + return cell + } + + /// - returns: A title string for the section. + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch AccessoryTableViewSection(rawValue: section) { + case .Services?: + return NSLocalizedString("Services", comment: "Services") + + case .BridgedAccessories?: + return NSLocalizedString("Bridged Accessories", comment: "Bridged Accessories") + + case nil: + fatalError("Unexpected `AccessoryTableViewSection` raw value.") + } + } + + /// - returns: A localized description of the accessories bridged status. + override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + if accessory.bridged && AccessoryTableViewSection(rawValue: section)! == .Services { + let formatString = NSLocalizedString("This accessory is being bridged into HomeKit by %@.", comment: "Bridge Description") + if let bridge = home.bridgeForAccessory(accessory) { + return String(format: formatString, bridge.name) + } + else { + return NSLocalizedString("This accessory is being bridged into HomeKit.", comment: "Bridge Description Without Bridge") + } + } + return nil + } + + // MARK: Helper Methods + + /// Updates the navigation bar's title. + func updateTitle() { + navigationItem.title = accessory.name + } + + /** + Updates the title, resets the displayed services based on + view controller configurations, reloads the bridge accessory + array and reloads the table view. + */ + private func reloadData() { + displayedServices = accessory.services.sortByLocalizedName() + if onlyShowsControlServices { + // We are configured to only show control services, filter the array. + displayedServices = displayedServices.filter { service -> Bool in + return service.isControlType + } + } + + if let identifiers = accessory.uniqueIdentifiersForBridgedAccessories { + bridgedAccessories = home.accessoriesWithIdentifiers(identifiers).sortByLocalizedName() + } + tableView.reloadData() + } + + // MARK: HMAccessoryDelegate Methods + + /// Reloads the title based on the accessories new name. + func accessoryDidUpdateName(accessory: HMAccessory) { + updateTitle() + } + + /// Reloads the cell for the specified service. + func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) { + if let index = displayedServices.indexOf(service) { + let path = NSIndexPath(forRow: index, inSection: AccessoryTableViewSection.Services.rawValue) + tableView.reloadRowsAtIndexPaths([path], withRowAnimation: .Automatic) + } + } + + /// Reloads the view. + func accessoryDidUpdateServices(accessory: HMAccessory) { + reloadData() + } + + /// If our accessory has become unreachable, go back the previous view. + func accessoryDidUpdateReachability(accessory: HMAccessory) { + if self.accessory == accessory { + navigationController?.popViewControllerAnimated(true) + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionCell.swift b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionCell.swift new file mode 100644 index 00000000..c8016f2b --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionCell.swift @@ -0,0 +1,47 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ActionCell` displays a characteristic and a target value. +*/ + +import UIKit +import HomeKit + +/// A `UITableViewCell` subclass that displays a characteristic's 'target value'. +class ActionCell: UITableViewCell { + /// Ignores the passed-in style and overrides it with `.Subtitle`. + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: .Subtitle, reuseIdentifier: reuseIdentifier) + selectionStyle = .None + detailTextLabel?.textColor = UIColor.lightGrayColor() + accessoryType = .None + } + + /// Required init. + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + /** + Sets the cell's text to represent a characteristic and target value. + For example, "Brightness → 60%" + Sets the subtitle to the service and accessory that this characteristic represents. + + - parameter characteristic: The characteristic this cell represents. + - parameter targetValue: The target value from this action. + */ + func setCharacteristic(characteristic: HMCharacteristic, targetValue: AnyObject) { + let targetDescription = "\(characteristic.localizedDescription) → \(characteristic.localizedDescriptionForValue(targetValue))" + textLabel?.text = targetDescription + + let contextDescription = NSLocalizedString("%@ in %@", comment: "Service in Accessory") + if let service = characteristic.service, accessory = service.accessory { + detailTextLabel?.text = String(format: contextDescription, service.name, accessory.name) + } + else { + detailTextLabel?.text = NSLocalizedString("Unknown Characteristic", comment: "Unknown Characteristic") + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetCreator.swift new file mode 100644 index 00000000..3da23850 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetCreator.swift @@ -0,0 +1,294 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ActionSetCreator` builds `HMActionSet`s. +*/ + +import HomeKit + +/// A `CharacteristicCellDelegate` that builds an `HMActionSet` when it receives delegate callbacks. +class ActionSetCreator: CharacteristicCellDelegate { + // MARK: Properties + + var actionSet: HMActionSet? + var home: HMHome + + var saveError: NSError? + + /// The structure we're going to use to hold the target values. + let targetValueMap = NSMapTable.strongToStrongObjectsMapTable() + + /// A dispatch group to wait for all of the individual components of the saving process. + let saveActionSetGroup = dispatch_group_create() + + required init(actionSet: HMActionSet?, home: HMHome) { + self.actionSet = actionSet + self.home = home + } + + /** + If there is an action set, saves the action set and then updates its name. + Otherwise creates a new action set and adds all actions to it. + + - parameter name: The new name for the action set. + - parameter completionHandler: A closure to call once the action set has been completely saved. + */ + func saveActionSetWithName(name: NSString, completionHandler: (error: NSError?) -> Void) { + if let actionSet = actionSet { + saveActionSet(actionSet) + updateNameIfNecessary(name) + } + else { + createActionSetWithName(name) + } + dispatch_group_notify(saveActionSetGroup, dispatch_get_main_queue()) { + completionHandler(error: self.saveError) + self.saveError = nil + } + } + + /** + Adds all of the actions that have been requested to the Action Set, then runs a completion block. + + - parameter completion: A closure to be called when all of the actions have been added. + */ + func saveActionSet(actionSet: HMActionSet) { + let actions = actionsFromMapTable(targetValueMap) + for action in actions { + dispatch_group_enter(saveActionSetGroup) + addAction(action, toActionSet: actionSet) { error in + if let error = error { + print("HomeKit: Error adding action: \(error.localizedDescription)") + self.saveError = error + } + dispatch_group_leave(self.saveActionSetGroup) + } + } + } + + /** + Sets the name of an existing action set. + + - parameter name: The new name for the action set. + */ + func updateNameIfNecessary(name: NSString) { + if actionSet?.name == name { + return + } + dispatch_group_enter(saveActionSetGroup) + actionSet?.updateName(name as String) { error in + if let error = error { + print("HomeKit: Error updating name: \(error.localizedDescription)") + self.saveError = error + } + dispatch_group_leave(self.saveActionSetGroup) + } + } + + /** + Creates and saves an action set with the provided name. + + - parameter name: The name for the new action set. + */ + func createActionSetWithName(name: NSString) { + dispatch_group_enter(saveActionSetGroup) + home.addActionSetWithName(name as String) { actionSet, error in + if let error = error { + print("HomeKit: Error creating action set: \(error.localizedDescription)") + self.saveError = error + } + else { + // There is no error, so the action set has a value. + self.saveActionSet(actionSet!) + } + dispatch_group_leave(self.saveActionSetGroup) + } + } + + /** + Checks to see if an action already exists to modify the same characteristic + as the action passed in. If such an action exists, the method tells the + existing action to update its target value. Otherwise, the new action is + simply added to the action set. + + - parameter action: The action to add or update. + - parameter actionSet: The action set to which to add the action. + - parameter completion: A closure to call when the addition has finished. + */ + func addAction(action: HMCharacteristicWriteAction, toActionSet actionSet: HMActionSet, completion: (NSError?) -> Void) { + if let existingAction = existingActionInActionSetMatchingAction(action) { + existingAction.updateTargetValue(action.targetValue, completionHandler: completion) + } + else { + actionSet.addAction(action, completionHandler: completion) + } + } + + /** + Checks to see if there is already an HMCharacteristicWriteAction in + the action set that matches the provided action. + + - parameter action: The action in question. + + - returns: The existing action that matches the characteristic or nil if + there is no existing action. + */ + func existingActionInActionSetMatchingAction(action: HMCharacteristicWriteAction) -> HMCharacteristicWriteAction? { + if let actionSet = actionSet { + for existingAction in Array(actionSet.actions) as! [HMCharacteristicWriteAction] { + if action.characteristic == existingAction.characteristic { + return existingAction + } + } + } + return nil + } + + /** + Iterates over a map table of HMCharacteristic -> id objects and creates + an array of HMCharacteristicWriteActions based on those targets. + + - parameter table: An NSMapTable mapping HMCharacteristics to id's. + + - returns: An array of HMCharacteristicWriteActions. + */ + func actionsFromMapTable(table: NSMapTable) -> [HMCharacteristicWriteAction] { + return targetValueMap.keyEnumerator().allObjects.map { characteristic in + let targetValue = targetValueMap.objectForKey(characteristic) as! NSCopying + return HMCharacteristicWriteAction(characteristic: characteristic as! HMCharacteristic, targetValue: targetValue) + } + } + + /** + - returns: `true` if the characteristic count is greater than zero; + `false` otherwise. + */ + var containsActions: Bool { + return !allCharacteristics.isEmpty + } + + /** + All existing characteristics within `HMCharacteristiWriteActions` + and target values in the target value map. + */ + var allCharacteristics: [HMCharacteristic] { + var characteristics = Set() + + if let actionSet = actionSet, actions = Array(actionSet.actions) as? [HMCharacteristicWriteAction] { + let actionSetCharacteristics = actions.map { action -> HMCharacteristic in + return action.characteristic + } + characteristics.unionInPlace(actionSetCharacteristics) + } + + characteristics.unionInPlace(targetValueMap.keyEnumerator().allObjects as! [HMCharacteristic]) + + return Array(characteristics) + } + + /** + Searches through the target value map and existing `HMCharacteristicWriteActions` + to find the target value for the characteristic in question. + + - parameter characteristic: The characteristic in question. + + - returns: The target value for this characteristic, or nil if there is no target. + */ + func targetValueForCharacteristic(characteristic: HMCharacteristic) -> AnyObject? { + if let value = targetValueMap.objectForKey(characteristic) { + return value + } + else if let actions = actionSet?.actions { + for action in actions { + if let writeAction = action as? HMCharacteristicWriteAction + where writeAction.characteristic == characteristic { + return writeAction.targetValue + } + } + } + + return nil + } + + /** + First removes the characteristic from the `targetValueMap`. + Then removes any `HMCharacteristicWriteAction`s from the action set + which set the specified characteristic. + + - parameter characteristic: The `HMCharacteristic` to remove. + - parameter completion: The closure to invoke when the characteristic has been removed. + */ + func removeTargetValueForCharacteristic(characteristic: HMCharacteristic, completion: () -> Void) { + /* + We need to create a dispatch group here, because in many cases + there will be one characteristic saved in the Action Set, and one + in the target value map. We want to run the completion closure only one time, + to ensure we've removed both. + */ + let group = dispatch_group_create() + if targetValueMap.objectForKey(characteristic) != nil { + // Remove the characteristic from the target value map. + dispatch_group_async(group, dispatch_get_main_queue()) { + self.targetValueMap.removeObjectForKey(characteristic) + } + } + if let actions = actionSet?.actions as? Set { + for action in Array(actions) { + if action.characteristic == characteristic { + /* + Also remove the action, and only relinquish the dispatch group + once the action set has finished. + */ + dispatch_group_enter(group) + actionSet?.removeAction(action) { error in + if let error = error { + print(error.localizedDescription) + } + dispatch_group_leave(group) + } + } + } + } + // Once we're positive both have finished, run the completion closure on the main queue. + dispatch_group_notify(group, dispatch_get_main_queue(), completion) + } + + // MARK: Characteristic Cell Delegate + + /** + Receives a callback from a `CharacteristicCell` with a value change. + Adds this value change into the targetValueMap, overwriting other value changes. + */ + func characteristicCell(cell: CharacteristicCell, didUpdateValue newValue: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool) { + targetValueMap.setObject(newValue, forKey: characteristic) + } + + /** + Receives a callback from a `CharacteristicCell`, requesting an initial value for + a given characteristic. + + It checks to see if we have an action in this Action Set that matches the characteristic. + If so, calls the completion closure with the target value. + */ + func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void) { + if let value = targetValueForCharacteristic(characteristic) { + completion(value, nil) + return + } + + characteristic.readValueWithCompletionHandler { error in + /* + The user may have updated the cell value while the + read was happening. We check the map one more time. + */ + if let value = self.targetValueForCharacteristic(characteristic) { + completion(value, nil) + } + else { + completion(characteristic.value, error) + } + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetViewController.swift new file mode 100644 index 00000000..51e41f3e --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Action Sets/ActionSetViewController.swift @@ -0,0 +1,334 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ActionSetViewController` allows users to create and modify action sets. +*/ + + +import UIKit +import HomeKit + +/// Represents table view sections of the `ActionSetViewController`. +enum ActionSetTableViewSection: Int { + case Name, Actions, Accessories +} + +/** + A view controller that facilitates creation of Action Sets. + + It contains a cell for a name, and lists accessories within a home. + If there are actions within the action set, it also displays a list of ActionCells displaying those actions. + It owns an `ActionSetCreator` and routes events to the creator as appropriate. +*/ +class ActionSetViewController: HMCatalogViewController { + // MARK: Types + + struct Identifiers { + static let accessoryCell = "AccessoryCell" + static let unreachableAccessoryCell = "UnreachableAccessoryCell" + static let actionCell = "ActionCell" + static let showServiceSegue = "Show Services" + } + + // MARK: Properties + + @IBOutlet weak var nameField: UITextField! + @IBOutlet weak var saveButton: UIBarButtonItem! + + var actionSet: HMActionSet? + var actionSetCreator: ActionSetCreator! + var displayedAccessories = [HMAccessory]() + + // MARK: View Methods + + /** + Creates the action set creator, registers the appropriate reuse identifiers in the table, + and sets the `nameField` if appropriate. + */ + override func viewDidLoad() { + super.viewDidLoad() + actionSetCreator = ActionSetCreator(actionSet: actionSet, home: home) + displayedAccessories = home.sortedControlAccessories + + tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.accessoryCell) + tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.unreachableAccessoryCell) + tableView.registerClass(ActionCell.self, forCellReuseIdentifier: Identifiers.actionCell) + + tableView.rowHeight = UITableViewAutomaticDimension + + tableView.estimatedRowHeight = 44.0 + + if let actionSet = actionSet { + nameField.text = actionSet.name + nameFieldDidChange(nameField) + } + + if !home.isAdmin { + nameField.enabled = false + } + } + + /// Reloads the data and view. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + tableView.reloadData() + enableSaveButtonIfNecessary() + } + + /// Dismisses the view controller if our data is invalid. + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + if shouldPopViewController() { + dismissViewControllerAnimated(true, completion: nil) + } + } + + /// Dismisses the keyboard when we dismiss. + override func viewWillDisappear(animated: Bool) { + super.viewWillDisappear(animated) + resignFirstResponder() + } + + /// Passes our accessory into the `ServicesViewController`. + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + super.prepareForSegue(segue, sender: sender) + if segue.identifier == Identifiers.showServiceSegue { + let servicesViewController = segue.intendedDestinationViewController as! ServicesViewController + servicesViewController.onlyShowsControlServices = true + servicesViewController.cellDelegate = actionSetCreator + + let index = tableView.indexPathForCell(sender as! UITableViewCell)!.row + + servicesViewController.accessory = displayedAccessories[index] + servicesViewController.cellDelegate = actionSetCreator + } + } + + // MARK: IBAction Methods + + /// Dismisses the view controller. + @IBAction func dismiss() { + dismissViewControllerAnimated(true, completion: nil) + } + + /// Saves the action set, adds it to the home, and dismisses the view. + @IBAction func saveAndDismiss() { + saveButton.enabled = false + + actionSetCreator.saveActionSetWithName(trimmedName) { error in + self.saveButton.enabled = true + + if let error = error { + self.displayError(error) + } + else { + self.dismiss() + } + } + } + + /// Prompts an update to the save button enabled state. + @IBAction func nameFieldDidChange(field: UITextField) { + enableSaveButtonIfNecessary() + } + + // MARK: Table View Methods + + /// We do not allow the creation of action sets in a shared home. + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return home.isAdmin ? 3 : 2 + } + + /** + - returns: In the Actions section: the number of actions this set will contain upon saving. + In the Accessories section: The number of accessories in the home. + */ + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch ActionSetTableViewSection(rawValue: section) { + case .Name?: + return super.tableView(tableView, numberOfRowsInSection: section) + + case .Actions?: + return max(actionSetCreator.allCharacteristics.count, 1) + + case .Accessories?: + return displayedAccessories.count + + case nil: + fatalError("Unexpected `ActionSetTableViewSection` raw value.") + } + } + + /** + Required override to allow for a tableView with both static and dynamic content. + Basically, since the superclass's indentationLevelForRowAtIndexPath is only + expecting 1 row per section, just call the super class's implementation + for the first row. + */ + override func tableView(tableView: UITableView, indentationLevelForRowAtIndexPath indexPath: NSIndexPath) -> Int { + return super.tableView(tableView, indentationLevelForRowAtIndexPath: NSIndexPath(forRow: 0, inSection: indexPath.section)) + } + + /// Removes the action associated with the index path. + override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { + if editingStyle == .Delete { + let characteristic = actionSetCreator.allCharacteristics[indexPath.row] + actionSetCreator.removeTargetValueForCharacteristic(characteristic) { + if self.actionSetCreator.containsActions { + tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + } + else { + tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + } + } + } + } + + /// - returns: `true` for the Actions section; `false` otherwise. + override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { + return ActionSetTableViewSection(rawValue: indexPath.section) == .Actions && home.isAdmin + } + + /// - returns: `UITableViewAutomaticDimension` for dynamic sections, otherwise the superclass's implementation. + override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + switch ActionSetTableViewSection(rawValue: indexPath.section) { + case .Name?: + return super.tableView(tableView, heightForRowAtIndexPath: indexPath) + + case .Actions?, .Accessories?: + return UITableViewAutomaticDimension + + case nil: + fatalError("Unexpected `ActionSetTableViewSection` raw value.") + } + } + + /// - returns: An action cell for the actions section, an accessory cell for the accessory section, or the superclass's implementation. + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + switch ActionSetTableViewSection(rawValue: indexPath.section) { + case .Name?: + return super.tableView(tableView, cellForRowAtIndexPath: indexPath) + + case .Actions?: + if actionSetCreator.containsActions { + return self.tableView(tableView, actionCellForRowAtIndexPath: indexPath) + } + else { + return super.tableView(tableView, cellForRowAtIndexPath: indexPath) + } + + case .Accessories?: + return self.tableView(tableView, accessoryCellForRowAtIndexPath: indexPath) + + case nil: + fatalError("Unexpected `ActionSetTableViewSection` raw value.") + } + } + + // MARK: Helper Methods + + /// Enables the save button if there is a valid name and at least one action. + private func enableSaveButtonIfNecessary() { + saveButton.enabled = home.isAdmin && trimmedName.characters.count > 0 && actionSetCreator.containsActions + } + + /// - returns: The contents of the nameField, with whitespace trimmed from the beginning and end. + private var trimmedName: String { + return nameField.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) + } + + /** + - returns: `true` if there are no accessories in the home, we have no set action set, + or if our home no longer exists; `false` otherwise + */ + private func shouldPopViewController() -> Bool { + if homeStore.home?.accessories.count == 0 && actionSet == nil { + return true + } + + return !homeStore.homeManager.homes.contains { $0 == homeStore.home } + } + + /// - returns: An `ActionCell` instance with the target value for the characteristic at the specified index path. + private func tableView(tableView: UITableView, actionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.actionCell, forIndexPath: indexPath) as! ActionCell + let characteristic = actionSetCreator.allCharacteristics[indexPath.row] as HMCharacteristic + + if let target = actionSetCreator.targetValueForCharacteristic(characteristic) { + cell.setCharacteristic(characteristic, targetValue: target) + } + + return cell + } + + /// - returns: An Accessory cell that contains an accessory's name. + private func tableView(tableView: UITableView, accessoryCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + /* + These cells are static, the identifiers are defined in the Storyboard, + but they're not recognized here. In viewDidLoad:, we're registering + `UITableViewCell` as the class for "AccessoryCell" and "UnreachableAccessoryCell". + We must configure these cells manually, the cells in the Storyboard + are just for reference. + */ + + let accessory = displayedAccessories[indexPath.row] + let cellIdentifier = accessory.reachable ? Identifiers.accessoryCell : Identifiers.unreachableAccessoryCell + + let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) + cell.textLabel?.text = accessory.name + + if accessory.reachable { + cell.textLabel?.textColor = UIColor.darkTextColor() + cell.accessoryType = .DisclosureIndicator + cell.selectionStyle = .Default + } + else { + cell.textLabel?.textColor = UIColor.lightGrayColor() + cell.accessoryType = .None + cell.selectionStyle = .None + } + + return cell + } + + /// Shows the services in the selected accessory. + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + let cell = tableView.cellForRowAtIndexPath(indexPath)! + if cell.selectionStyle == .None { + return + } + + if ActionSetTableViewSection(rawValue: indexPath.section) == .Accessories { + performSegueWithIdentifier(Identifiers.showServiceSegue, sender: cell) + } + } + + // MARK: HMHomeDelegate Methods + + /** + Pops the view controller if our configuration is invalid; + reloads the view otherwise. + */ + func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) { + if shouldPopViewController() { + dismissViewControllerAnimated(true, completion: nil) + } + else { + tableView.reloadData() + } + } + + /// Reloads the table view data. + func home(home: HMHome, didAddAccessory accessory: HMAccessory) { + tableView.reloadData() + } + + /// If our action set was removed, dismiss the view. + func home(home: HMHome, didRemoveActionSet actionSet: HMActionSet) { + if actionSet == self.actionSet { + dismissViewControllerAnimated(true, completion: nil) + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/HomeKitObjectCollection.swift b/HomeKitCatalog/HMCatalog/Homes/HomeKitObjectCollection.swift new file mode 100644 index 00000000..1dfc22eb --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/HomeKitObjectCollection.swift @@ -0,0 +1,236 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `HomeKitObjectCollection` is a model object for the `HomeViewController`. + It manages arrays of HomeKit objects. +*/ + +import HomeKit + +/// Represents the all different types of HomeKit objects. +enum HomeKitObjectSection: Int { + case Accessory, Room, Zone, User, ActionSet, Trigger, ServiceGroup + + static let count = 7 +} + +/** + Manages internal lists of HomeKit objects to allow for + save insertion into a table view. +*/ +class HomeKitObjectCollection { + // MARK: Properties + + var accessories = [HMAccessory]() + var rooms = [HMRoom]() + var zones = [HMZone]() + var actionSets = [HMActionSet]() + var triggers = [HMTrigger]() + var serviceGroups = [HMServiceGroup]() + + /** + Adds an object to the collection by finding its corresponding + array and appending the object to it. + + - parameter object: The HomeKit object to append. + */ + func append(object: AnyObject) { + switch object { + case let actionSet as HMActionSet: + actionSets.append(actionSet) + actionSets = actionSets.sortByTypeAndLocalizedName() + + case let accessory as HMAccessory: + accessories.append(accessory) + accessories = accessories.sortByLocalizedName() + + case let room as HMRoom: + rooms.append(room) + rooms = rooms.sortByLocalizedName() + + case let zone as HMZone: + zones.append(zone) + zones = zones.sortByLocalizedName() + + case let trigger as HMTrigger: + triggers.append(trigger) + triggers = triggers.sortByLocalizedName() + + case let serviceGroup as HMServiceGroup: + serviceGroups.append(serviceGroup) + serviceGroups = serviceGroups.sortByLocalizedName() + + default: + break + } + } + + /** + Creates an `NSIndexPath` representing the location of the + HomeKit object in the table view. + + - parameter object: The HomeKit object to find. + + - returns: The `NSIndexPath` representing the location of + the HomeKit object in the table view. + */ + func indexPathOfObject(object: AnyObject) -> NSIndexPath? { + switch object { + case let actionSet as HMActionSet: + if let index = actionSets.indexOf(actionSet) { + return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.ActionSet.rawValue) + } + + case let accessory as HMAccessory: + if let index = accessories.indexOf(accessory) { + return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Accessory.rawValue) + } + + case let room as HMRoom: + if let index = rooms.indexOf(room) { + return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Room.rawValue) + } + + case let zone as HMZone: + if let index = zones.indexOf(zone) { + return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Zone.rawValue) + } + + case let trigger as HMTrigger: + if let index = triggers.indexOf(trigger) { + return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.Trigger.rawValue) + } + + case let serviceGroup as HMServiceGroup: + if let index = serviceGroups.indexOf(serviceGroup) { + return NSIndexPath(forRow: index, inSection: HomeKitObjectSection.ServiceGroup.rawValue) + } + + default: break + } + + return nil + } + + /** + Removes a HomeKit object from the collection. + + - parameter object: The HomeKit object to remove. + */ + func remove(object: AnyObject) { + switch object { + case let actionSet as HMActionSet: + if let index = actionSets.indexOf(actionSet) { + actionSets.removeAtIndex(index) + } + + case let accessory as HMAccessory: + if let index = accessories.indexOf(accessory) { + accessories.removeAtIndex(index) + } + + case let room as HMRoom: + if let index = rooms.indexOf(room) { + rooms.removeAtIndex(index) + } + + case let zone as HMZone: + if let index = zones.indexOf(zone) { + zones.removeAtIndex(index) + } + + case let trigger as HMTrigger: + if let index = triggers.indexOf(trigger) { + triggers.removeAtIndex(index) + } + + case let serviceGroup as HMServiceGroup: + if let index = serviceGroups.indexOf(serviceGroup) { + serviceGroups.removeAtIndex(index) + } + + default: + break + } + } + + /** + Provides the array of `NSObject`s corresponding to the provided section. + + - parameter section: A `HomeKitObjectSection`. + + - returns: An array of `NSObject`s corresponding to the provided section. + */ + func objectsForSection(section: HomeKitObjectSection) -> [NSObject] { + switch section { + case .ActionSet: + return actionSets + + case .Accessory: + return accessories + + case .Room: + return rooms + + case .Zone: + return zones + + case .Trigger: + return triggers + + case .ServiceGroup: + return serviceGroups + + default: + return [] + } + } + + /** + Provides an `HomeKitObjectSection` for a given object. + + - parameter object: A HomeKit object. + + - returns: The corrosponding `HomeKitObjectSection` + */ + class func sectionForObject(object: AnyObject?) -> HomeKitObjectSection? { + switch object { + case is HMActionSet: + return .ActionSet + + case is HMAccessory: + return .Accessory + + case is HMZone: + return .Zone + + case is HMRoom: + return .Room + + case is HMTrigger: + return .Trigger + + case is HMServiceGroup: + return .ServiceGroup + + default: + return nil + } + } + + /** + Reloads all internal structures based on the provided home. + + - parameter home: The `HMHome` with which to reset the collection. + */ + func resetWithHome(home: HMHome) { + accessories = home.accessories.sortByLocalizedName() + rooms = home.allRooms + zones = home.zones.sortByLocalizedName() + actionSets = home.actionSets.sortByTypeAndLocalizedName() + triggers = home.triggers.sortByLocalizedName() + serviceGroups = home.serviceGroups.sortByLocalizedName() + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/HomeListConfigurationViewController.swift b/HomeKitCatalog/HMCatalog/Homes/HomeListConfigurationViewController.swift new file mode 100644 index 00000000..5a7e9ea1 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/HomeListConfigurationViewController.swift @@ -0,0 +1,306 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `HomeListConfigurationViewController` allows for the creation and deletion of homes. +*/ + +import UIKit +import HomeKit + +// Represents the sections in the `HomeListConfigurationViewController`. +enum HomeListSection: Int { + case Homes, PrimaryHome + + static let count = 2 +} + +/** + A `HomeListViewController` subclass which allows the user to add and remove + homes and set the primary home. +*/ +class HomeListConfigurationViewController: HomeListViewController { + // MARK: Types + + struct Identifiers { + static let addHomeCell = "AddHomeCell" + static let noHomesCell = "NoHomesCell" + static let primaryHomeCell = "PrimaryHomeCell" + } + + // MARK: Table View Methods + + /// - returns: The number of sections in the `HomeListSection` enum. + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return HomeListSection.count + } + + /// Provides the number of rows in the section using the internal home's list. + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch HomeListSection(rawValue: section) { + // Add row. + case .Homes?: + return homes.count + 1 + + // 'No homes' row. + case .PrimaryHome?: + return max(homes.count, 1) + + case nil: fatalError("Unexpected `HomeListSection` raw value.") + } + } + + /** + Generates and configures either a content cell or an add cell using the + provided index path. + */ + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + if indexPathIsAdd(indexPath) { + return tableView.dequeueReusableCellWithIdentifier(Identifiers.addHomeCell, forIndexPath: indexPath) + } + else if homes.isEmpty { + return tableView.dequeueReusableCellWithIdentifier(Identifiers.noHomesCell, forIndexPath: indexPath) + } + + let reuseIdentifier: String + + switch HomeListSection(rawValue: indexPath.section) { + case .Homes?: + reuseIdentifier = Identifiers.homeCell + + case .PrimaryHome?: + reuseIdentifier = Identifiers.primaryHomeCell + + case nil: fatalError("Unexpected `HomeListSection` raw value.") + } + + let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) + let home = homes[indexPath.row] + + cell.textLabel!.text = home.name + cell.detailTextLabel?.text = sharedTextForHome(home) + + // Mark the primary home with checkmark. + if HomeListSection(rawValue: indexPath.section) == .PrimaryHome { + if home == homeManager.primaryHome { + cell.accessoryType = .Checkmark + } + else { + cell.accessoryType = .None + } + } + + return cell + } + + /// Homes in the list section can be deleted. The add row cannot be deleted. + override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { + return HomeListSection(rawValue: indexPath.section) == .Homes && !indexPathIsAdd(indexPath) + } + + /// Only the 'primary home' section has a title. + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + if HomeListSection(rawValue: section) == .PrimaryHome { + return NSLocalizedString("Primary Home", comment: "Primary Home") + } + + return nil + } + + /// Provides subtext about the use of designating a "primary home". + override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + if section == HomeListSection.PrimaryHome.rawValue { + return NSLocalizedString("The primary home is used by Siri to route commands if the home is not specified.", comment: "Primary Home Description") + } + return nil + } + + /** + If selecting a regular home, a segue will be performed. + If this method is called, the user either selected the 'add' row, + a primary home cell, or the `No Homes` cell. + */ + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + tableView.deselectRowAtIndexPath(indexPath, animated: true) + if indexPathIsAdd(indexPath) { + addNewHome() + } + else if indexPathIsNone(indexPath) { + return + } + else if HomeListSection(rawValue: indexPath.section) == .PrimaryHome { + let newPrimaryHome = homes[indexPath.row] + updatePrimaryHome(newPrimaryHome) + } + } + + /// Removes the home from HomeKit if the row is deleted. + override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { + if editingStyle == .Delete { + removeHomeAtIndexPath(indexPath) + } + } + + // MARK: Helper Methods + + /** + Updates the primary home in HomeKit and reloads the view. + If the home is already selected, no action is taken. + + - parameter newPrimaryHome: The new `HMHome` to set as the primary home. + */ + private func updatePrimaryHome(newPrimaryHome: HMHome) { + guard newPrimaryHome != homeManager.primaryHome else { return } + + homeManager.updatePrimaryHome(newPrimaryHome) { error in + if let error = error { + self.displayError(error) + return + } + + self.didUpdatePrimaryHome() + } + } + + /// Reloads the 'primary home' section. + private func didUpdatePrimaryHome() { + let primaryIndexSet = NSIndexSet(index: HomeListSection.PrimaryHome.rawValue) + + tableView.reloadSections(primaryIndexSet, withRowAnimation: .Automatic) + } + + /** + Removed the home at the specified index path from HomeKit and updates the view. + + - parameter indexPath: The `NSIndexPath` of the home to remove. + */ + private func removeHomeAtIndexPath(indexPath: NSIndexPath) { + let home = homes[indexPath.row] + + // Remove the home from the data structure. If it fails, put it back. + didRemoveHome(home) + homeManager.removeHome(home) { error in + if let error = error { + self.displayError(error) + self.didAddHome(home) + return + } + } + } + + /** + Presents an alert controller so the user can provide a name. If committed, + the home is created. + */ + private func addNewHome() { + let attributedType = NSLocalizedString("Home", comment: "Home") + let placeholder = NSLocalizedString("Apartment", comment: "Apartment") + + presentAddAlertWithAttributeType(attributedType, placeholder: placeholder) { name in + self.addHomeWithName(name) + } + } + + /** + Removes a home from the internal structure and updates the view. + + - parameter home: The `HMHome` to remove. + */ + override func didRemoveHome(home: HMHome) { + guard let index = homes.indexOf(home) else { return } + + let indexPath = NSIndexPath(forRow: index, inSection: HomeListSection.Homes.rawValue) + homes.removeAtIndex(index) + let primaryIndexPath = NSIndexPath(forRow: index, inSection: HomeListSection.PrimaryHome.rawValue) + + /* + If there aren't any homes, we still want one cell to display 'No Homes'. + Just reload. + */ + tableView.beginUpdates() + if homes.isEmpty { + tableView.reloadRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Fade) + } + else { + tableView.deleteRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Automatic) + } + tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + tableView.endUpdates() + + } + + /// Adds the home to the internal structure and updates the view. + override func didAddHome(home: HMHome) { + homes.append(home) + sortHomes() + guard let newHomeIndex = homes.indexOf(home) else { return } + + let indexPath = NSIndexPath(forRow: newHomeIndex, inSection: HomeListSection.Homes.rawValue) + + let primaryIndexPath = NSIndexPath(forRow: newHomeIndex, inSection: HomeListSection.PrimaryHome.rawValue) + + tableView.beginUpdates() + + if homes.count == 1 { + tableView.reloadRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Fade) + } + else { + tableView.insertRowsAtIndexPaths([primaryIndexPath], withRowAnimation: .Automatic) + } + + tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + tableView.endUpdates() + } + + /** + Creates a new home with the provided name, adds the home to HomeKit + and reloads the view. + */ + private func addHomeWithName(name: String) { + homeManager.addHomeWithName(name) { newHome, error in + if let error = error { + self.displayError(error) + return + } + + self.didAddHome(newHome!) + } + } + + + /// - returns: `true` if the index path is the 'add row'; `false` otherwise. + private func indexPathIsAdd(indexPath: NSIndexPath) -> Bool { + return HomeListSection(rawValue: indexPath.section) == .Homes && + indexPath.row == homes.count + } + + /// - returns: `true` if the index path is the 'No Homes' cell; `false` otherwise. + private func indexPathIsNone(indexPath: NSIndexPath) -> Bool { + return HomeListSection(rawValue: indexPath.section) == .PrimaryHome && homes.isEmpty + } + + // MARK: HMHomeDelegate Methods + + /// Finds the home in the internal structure and reloads the corresponding row. + override func homeDidUpdateName(home: HMHome) { + if let index = homes.indexOf(home) { + let listIndexPath = NSIndexPath(forRow: index, inSection: HomeListSection.Homes.rawValue) + + let primaryIndexPath = NSIndexPath(forRow: index, inSection: HomeListSection.PrimaryHome.rawValue) + + tableView.reloadRowsAtIndexPaths([listIndexPath, primaryIndexPath], withRowAnimation: .Automatic) + } + else { + // Just reload the data since we don't know the index path. + tableView.reloadData() + } + } + + // MARK: HMHomeManagerDelegate Methods + + /// Reloads the 'primary home' section. + func homeManagerDidUpdatePrimaryHome(manager: HMHomeManager) { + didUpdatePrimaryHome() + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/HomeListViewController.swift b/HomeKitCatalog/HMCatalog/Homes/HomeListViewController.swift new file mode 100644 index 00000000..8500b063 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/HomeListViewController.swift @@ -0,0 +1,257 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `HomeListViewController` is a superclass that lists the user's homes. +*/ + +import UIKit +import HomeKit + + +/// A generic view controller for displaying a list of homes in a home manager. +class HomeListViewController: HMCatalogViewController, HMHomeManagerDelegate { + // MARK: Types + + struct Identifiers { + static let homeCell = "HomeCell" + static let showHomeSegue = "Show Home" + } + + // MARK: Properties + + var homes = [HMHome]() + + var homeManager: HMHomeManager { + return homeStore.homeManager + } + + // MARK: View Methods + + /// Configures the table view. + override func viewDidLoad() { + super.viewDidLoad() + + tableView.estimatedRowHeight = 44.0 + + tableView.rowHeight = UITableViewAutomaticDimension + } + + /// Resets the list of homes (which will update the view). + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + resetHomesList() + } + + // MARK: Delegate Registration + + /** + Registers as the delegate for the home manager and all homes in the internal + homes list. + */ + override func registerAsDelegate() { + homeManager.delegate = self + + for home in homes { + home.delegate = self + } + } + + /// Sets the home store's current home based on which cell was selected. + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + super.prepareForSegue(segue, sender: sender) + + if segue.identifier == Identifiers.showHomeSegue { + if sender === self { + // Don't update the selected home if we sent ourselves here. + return + } + + if let indexPath = tableView.indexPathForCell(sender as! UITableViewCell) { + homeStore.home = homes[indexPath.row] + } + } + } + + // MARK: Table View Methods + + /** + Provides the number of sections based on the home array count. + Updates the background message for the table view. + + - returns: The number of homes in the internal array. + */ + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let rows = homes.count + + if rows == 0 { + let message = NSLocalizedString("No Homes", comment: "No Homes") + setBackgroundMessage(message) + } + else { + setBackgroundMessage(nil) + } + + return rows + } + + /** + Generates a basic cell for a home. + Subtext is provided to tell the user if the home is shared or owned by the user. + */ + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.homeCell, forIndexPath: indexPath) + let home = homes[indexPath.row] + + cell.textLabel?.text = home.name + cell.detailTextLabel?.text = sharedTextForHome(home) + + return cell + } + + // MARK: Helper Methods + + /** + Provides an ordering for homes. + + Homes are first ordered by their 'shared' status, then by name. + + - parameter home1: The first `HMHome`. + - parameter home2: The second `HMHome`. + + - returns: `true` if `home1` is ordered before `home2`; `false` otherwise. + */ + private func orderHomes(home1: HMHome, home2: HMHome) -> Bool { + if home1.isAdmin == home2.isAdmin { + /* + We are comparing two shared homes or two of our homes, just compare + names. + */ + return home1.name.localizedCompare(home2.name) == .OrderedAscending + } + else { + /* + We are comparing a shared home and one of our homes, if home1 is + ours, put it first. + */ + return home1.isAdmin + } + } + + /** + Regenerates the list of homes using list provided by the home manager. + The list is then sorted and the view is reloaded. + */ + private func resetHomesList() { + homes = homeManager.homes.sort(orderHomes) + tableView.reloadData() + } + + /// Sorts the list of homes (without reloading from the home manager). + func sortHomes() { + homes.sortInPlace(orderHomes) + } + + /** + Adds a new home into the internal homes array and inserts the new + row into the table view. + + - parameter home: The new `HMHome` that's been added. + */ + func didAddHome(home: HMHome) { + homes.append(home) + + sortHomes() + + if let newHomeIndex = homes.indexOf(home) { + let indexPathOfNewHome = NSIndexPath(forRow: newHomeIndex, inSection: 0) + + tableView.insertRowsAtIndexPaths([indexPathOfNewHome], withRowAnimation: .Automatic) + } + } + + /** + Removes a home from the internal homes array (if it exists) and + deletes corresponding row from the table view. + + - parameter home: The `HMHome` to remove. + */ + func didRemoveHome(home: HMHome) { + guard let removedHomeIndex = homes.indexOf(home) else { return } + + homes.removeAtIndex(removedHomeIndex) + let indexPath = NSIndexPath(forRow: removedHomeIndex, inSection: 0) + tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + } + + /** + - returns: A localized description of who owns the provided home. + + - parameter home: The `HMHome` to describe. + */ + func sharedTextForHome(home: HMHome) -> String { + if !home.isAdmin { + return NSLocalizedString("Shared with Me", comment: "Shared with Me") + } + else { + return NSLocalizedString("My Home", comment: "My Home") + } + } + + // MARK: HMHomeDelegate Methods + + /// Finds the cell with corresponds to the provided home and reloads it. + func homeDidUpdateName(home: HMHome) { + if let homeIndex = homes.indexOf(home) { + let indexPath = NSIndexPath(forRow: homeIndex, inSection: 0) + + tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + } + } + + // MARK: HMHomeManagerDelegate Methods + + /** + Reloads data and view. + + This view controller, in most cases, will remain the home manager delegate. + For this reason, this method will close all modal views and pop all detail views + if the home store's current home is no longer in the home manager's list of homes. + */ + func homeManagerDidUpdateHomes(manager: HMHomeManager) { + registerAsDelegate() + resetHomesList() + + if let home = homeStore.home where !manager.homes.contains(home) { + // Close all modal and detail views. + dismissViewControllerAnimated(true, completion: nil) + navigationController?.popToRootViewControllerAnimated(true) + } + } + + /// Registers for the delegate of the new home and updates the view. + func homeManager(manager: HMHomeManager, didAddHome home: HMHome) { + home.delegate = self + + didAddHome(home) + } + + /** + Removes the home from the current list of homes and updates the view. + + If the removed home was the current home, this view controller will dismiss + all modals views and pop all detail views. + */ + func homeManager(manager: HMHomeManager, didRemoveHome home: HMHome) { + didRemoveHome(home) + + guard let selectedHome = homeStore.home where home == selectedHome else { return } + + homeStore.home = nil + + // Close all modal and detail views. + dismissViewControllerAnimated(true, completion: nil) + navigationController?.popToRootViewControllerAnimated(true) + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/HomeStore.swift b/HomeKitCatalog/HMCatalog/Homes/HomeStore.swift new file mode 100644 index 00000000..97d6a8f8 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/HomeStore.swift @@ -0,0 +1,22 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `HomeStore` class is a simple singleton class which holds a home manager and the current selected home. +*/ + +import HomeKit + +/// A static, singleton class which holds a home manager and the current home. +class HomeStore: NSObject, HMHomeManagerDelegate { + static let sharedStore = HomeStore() + + // MARK: Properties + + /// The current 'selected' home. + var home: HMHome? + + /// The singleton home manager. + var homeManager = HMHomeManager() +} diff --git a/HomeKitCatalog/HMCatalog/Homes/HomeViewController.swift b/HomeKitCatalog/HMCatalog/Homes/HomeViewController.swift new file mode 100644 index 00000000..9604f814 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/HomeViewController.swift @@ -0,0 +1,929 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `HomeViewController` displays all of the HomeKit objects in a selected home. +*/ + +import Foundation +import UIKit +import HomeKit + +/// Distinguishes between the three types of cells in the `HomeViewController`. +enum HomeCellType { + /// Represents an actual object in HomeKit. + case Object + + /// Represents an "Add" row for users to select to create an object in HomeKit. + case Add + + /// The cell is displaying text to show the user that no objects exist in this section. + case None +} + +/** + A view controller that displays all elements within a home. + It contains separate sections for Accessories, Rooms, Zones, Action Sets, + Triggers, and Service Groups. +*/ +class HomeViewController: HMCatalogViewController, HMAccessoryDelegate { + // MARK: Types + + struct Identifiers { + static let addCell = "AddCell" + static let disabledAddCell = "DisabledAddCell" + static let accessoryCell = "AccessoryCell" + static let unreachableAccessoryCell = "UnreachableAccessoryCell" + static let roomCell = "RoomCell" + static let zoneCell = "ZoneCell" + static let userCell = "UserCell" + static let actionSetCell = "ActionSetCell" + static let triggerCell = "TriggerCell" + static let serviceGroupCell = "ServiceGroupCell" + static let addTimerTriggerSegue = "Add Timer Trigger" + static let addCharacteristicTriggerSegue = "Add Characteristic Trigger" + static let addLocationTriggerSegue = "Add Location Trigger" + static let addActionSetSegue = "Add Action Set" + static let addAccessoriesSegue = "Add Accessories" + static let showRoomSegue = "Show Room" + static let showZoneSegue = "Show Zone" + static let showActionSetSegue = "Show Action Set" + static let showServiceGroupSegue = "Show Service Group" + static let showAccessorySegue = "Show Accessory" + static let modifyAccessorySegue = "Modify Accessory" + static let showTimerTriggerSegue = "Show Timer Trigger" + static let showLocationTriggerSegue = "Show Location Trigger" + static let showCharacteristicTriggerSegue = "Show Characteristic Trigger" + } + + // MARK: Properties + + /// A structure to maintain internal arrays of HomeKit objects. + private var objectCollection = HomeKitObjectCollection() + + // MARK: View Methods + + /** + Determines the destination of the segue and passes the correct + HomeKit object onto the next view controller. + */ + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + guard let sender = sender as? UITableViewCell else { return } + guard let indexPath = tableView.indexPathForCell(sender) else { return } + + let homeKitObject = homeKitObjectAtIndexPath(indexPath) + let destinationViewController = segue.intendedDestinationViewController + + switch segue.identifier! { + case Identifiers.showRoomSegue: + let roomVC = destinationViewController as! RoomViewController + roomVC.room = homeKitObject as? HMRoom + + case Identifiers.showZoneSegue: + let zoneViewController = destinationViewController as! ZoneViewController + zoneViewController.homeZone = homeKitObject as? HMZone + + case Identifiers.showActionSetSegue: + let actionSetVC = destinationViewController as! ActionSetViewController + actionSetVC.actionSet = homeKitObject as? HMActionSet + + case Identifiers.showServiceGroupSegue: + let serviceGroupVC = destinationViewController as! ServiceGroupViewController + serviceGroupVC.serviceGroup = homeKitObject as? HMServiceGroup + + case Identifiers.showAccessorySegue: + let detailVC = destinationViewController as! ServicesViewController + /* + The services view controller is generic, we need to provide + `showsFavorites` to display the stars next to characteristics. + */ + detailVC.accessory = homeKitObject as? HMAccessory + detailVC.showsFavorites = true + detailVC.cellDelegate = AccessoryUpdateController() + + case Identifiers.modifyAccessorySegue: + let addAccessoryVC = destinationViewController as! ModifyAccessoryViewController + addAccessoryVC.accessory = homeKitObject as? HMAccessory + + case Identifiers.showTimerTriggerSegue: + let triggerVC = destinationViewController as! TimerTriggerViewController + triggerVC.trigger = homeKitObject as? HMTimerTrigger + + case Identifiers.showLocationTriggerSegue: + let triggerVC = destinationViewController as! LocationTriggerViewController + triggerVC.trigger = homeKitObject as? HMEventTrigger + + case Identifiers.showCharacteristicTriggerSegue: + let triggerVC = destinationViewController as! CharacteristicTriggerViewController + triggerVC.trigger = homeKitObject as? HMEventTrigger + + default: + print("Received unknown segue identifier: \(segue.identifier).") + } + } + + /// Configures the table view. + override func awakeFromNib() { + super.awakeFromNib() + tableView.estimatedRowHeight = 44.0 + tableView.rowHeight = UITableViewAutomaticDimension + } + + /// Sets the navigation title and reloads view. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + navigationItem.title = home.name + reloadTable() + } + + // MARK: Delegate Registration + + /** + Registers as the delegate for the home store's current home + and all accessories in the home. + */ + override func registerAsDelegate() { + super.registerAsDelegate() + + for accessory in home.accessories { + accessory.delegate = self + } + } + + // MARK: Helper Methods + + /// Resets the object collection and reloads the view. + private func reloadTable() { + objectCollection.resetWithHome(home) + tableView.reloadData() + } + + /** + Determines the type of the cell based on the index path. + + - parameter indexPath: The `NSIndexPath` of the cell. + + - returns: The `HomeCellType` for cell. + */ + private func cellTypeForIndexPath(indexPath: NSIndexPath) -> HomeCellType { + guard let section = HomeKitObjectSection(rawValue: indexPath.section) else { return .None } + + let objectCount = objectCollection.objectsForSection(section).count + + if objectCount == 0 { + // No objects -- this is either an 'Add Row' or a 'None Row'. + return home.isAdmin ? .Add : .None + } + else if indexPath.row == objectCount { + return .Add + } + else { + return .Object + } + } + + /// Reloads the trigger section. + private func updateTriggerAddRow() { + let triggerSection = NSIndexSet(index: HomeKitObjectSection.Trigger.rawValue) + + tableView.reloadSections(triggerSection, withRowAnimation: .Automatic) + } + + /// Reloads the action set section. + private func updateActionSetSection() { + let actionSetSection = NSIndexSet(index: HomeKitObjectSection.ActionSet.rawValue) + + tableView.reloadSections(actionSetSection, withRowAnimation: .Automatic) + + updateTriggerAddRow() + } + + /// - returns: `true` if there are accessories within the home; `false` otherwise. + private var canAddActionSet: Bool { + return !objectCollection.accessories.isEmpty + } + + /// - returns: `true` if there are action sets (with actions) within the home; `false` otherwise. + private var canAddTrigger: Bool { + return objectCollection.actionSets.contains { actionSet in + return !actionSet.actions.isEmpty + } + } + + /** + Provides the 'HomeKit object' (`AnyObject?`) at the specified index path. + + - parameter indexPath: The `NSIndexPath` of the object. + + - returns: The HomeKit object. + */ + private func homeKitObjectAtIndexPath(indexPath: NSIndexPath) -> AnyObject? { + if cellTypeForIndexPath(indexPath) != .Object { + return nil + } + + if let section = HomeKitObjectSection(rawValue: indexPath.section) { + return objectCollection.objectsForSection(section)[indexPath.row] + } + + return nil + } + + // MARK: Table View Methods + + /// - returns: The number of `HomeKitObjectSection`s. + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return HomeKitObjectSection.count + } + + /// - returns: Localized titles for each of the HomeKit sections. + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch HomeKitObjectSection(rawValue: section) { + case .Accessory?: + return NSLocalizedString("Accessories", comment: "Accessories") + + case .Room?: + return NSLocalizedString("Rooms", comment: "Rooms") + + case .Zone?: + return NSLocalizedString("Zones", comment: "Zones") + + case .User?: + return NSLocalizedString("Users", comment: "Users") + + case .ActionSet?: + return NSLocalizedString("Scenes", comment: "Scenes") + + case .Trigger?: + return NSLocalizedString("Triggers", comment: "Triggers") + + case .ServiceGroup?: + return NSLocalizedString("Service Groups", comment: "Service Groups") + + case nil: + fatalError("Unexpected `HomeKitObjectSection` raw value.") + } + + } + + /// - returns: Localized text for the 'add row'. + private func titleForAddRowInSection(section: HomeKitObjectSection) -> String { + switch section { + case .Accessory: + return NSLocalizedString("Add Accessory…", comment: "Add Accessory") + + case .Room: + return NSLocalizedString("Add Room…", comment: "Add Room") + + case .Zone: + return NSLocalizedString("Add Zone…", comment: "Add Zone") + + case .User: + return NSLocalizedString("Manage Users…", comment: "Manage Users") + + case .ActionSet: + return NSLocalizedString("Add Scene…", comment: "Add Scene") + + case .Trigger: + return NSLocalizedString("Add Trigger…", comment: "Add Trigger") + + case .ServiceGroup: + return NSLocalizedString("Add Service Group…", comment: "Add Service Group") + } + } + + /// - returns: Localized text for the 'none row'. + private func titleForNoneRowInSection(section: HomeKitObjectSection) -> String { + switch section { + case .Accessory: + return NSLocalizedString("No Accessories…", comment: "No Accessories") + + case .Room: + return NSLocalizedString("No Rooms…", comment: "No Rooms") + + case .Zone: + return NSLocalizedString("No Zones…", comment: "No Zones") + + case .User: + // We only ever list 'Manage Users'. + return NSLocalizedString("Manage Users…", comment: "Manage Users") + + case .ActionSet: + return NSLocalizedString("No Scenes…", comment: "No Scenes") + + case .Trigger: + return NSLocalizedString("No Triggers…", comment: "No Triggers") + + case .ServiceGroup: + return NSLocalizedString("No Service Groups…", comment: "No Service Groups") + } + } + + /// - returns: Localized descriptions for HomeKit object types. + override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + switch HomeKitObjectSection(rawValue: section) { + case .Zone?: + return NSLocalizedString("Zones are optional collections of rooms.", comment: "Zones Description") + + case .User?: + return NSLocalizedString("Users can control the accessories in your home. You can share your home with anybody with an iCloud account.", comment: "Users Description") + + case .ActionSet?: + return NSLocalizedString("Scenes (action sets) represent a state of your home. You must have at least one paired accessory to create a scene.", comment: "Scenes Description") + + case .Trigger?: + return NSLocalizedString("Triggers set scenes at specific times, when you get to locations, or when a characteristic is in a specific state. You must have created at least one scene with an action to create a trigger.", comment: "Trigger Description") + + case .ServiceGroup?: + return NSLocalizedString("Service groups organize services in a custom way. For example, add a subset of lights in your living room to control them without controlling all the lights in the living room.", comment: "Service Group Description") + + case nil: + fatalError("Unexpected `HomeKitObjectSection` raw value.") + + default: + return nil + } + } + + /** + Provides the number of rows in each HomeKit object section. + Most sections just return the object count, but we also handle special cases. + */ + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let sectionEnum = HomeKitObjectSection(rawValue: section)! + + // Only "Manage Users" button is in the Users section + if sectionEnum == .User { + return 1 + } + + let objectCount = objectCollection.objectsForSection(sectionEnum).count + if home.isAdmin { + // For add row. + return objectCount + 1 + } + else { + // Always show at least one row in the section. + return max(objectCount, 1) + } + } + + /// Generates a cell based on it's computed type. + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + switch cellTypeForIndexPath(indexPath) { + case .Add: + return self.tableView(tableView, addCellForRowAtIndexPath: indexPath) + + case .Object: + return self.tableView(tableView, homeKitObjectCellForRowAtIndexPath: indexPath) + + case .None: + return self.tableView(tableView, noneCellForRowAtIndexPath: indexPath) + } + } + + /// Generates a 'none cell' with a localized title. + private func tableView(tableView: UITableView, noneCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.disabledAddCell, forIndexPath: indexPath) + + let section = HomeKitObjectSection(rawValue: indexPath.section)! + + cell.textLabel!.text = titleForNoneRowInSection(section) + + return cell + } + + /** + Generates an 'add cell' with a localized title. + + In some cases, the 'add cell' will be 'disabled' because the user is not + allowed to perform the action. + */ + private func tableView(tableView: UITableView, addCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + var reuseIdentifier = Identifiers.addCell + + let section = HomeKitObjectSection(rawValue: indexPath.section) + + if (!canAddActionSet && section == .ActionSet) || + (!canAddTrigger && section == .Trigger) || !home.isAdmin { + reuseIdentifier = Identifiers.disabledAddCell + } + + let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) + + cell.textLabel!.text = titleForAddRowInSection(section!) + + return cell + } + + /** + Produces the cell reuse identifier based on the section. + + - parameter indexPath: The `NSIndexPath` of the cell. + + - returns: The cell reuse identifier. + */ + private func reuseIdentifierForIndexPath(indexPath: NSIndexPath) -> String { + switch HomeKitObjectSection(rawValue: indexPath.section) { + case .Accessory?: + let accessory = homeKitObjectAtIndexPath(indexPath) as! HMAccessory + return accessory.reachable ? Identifiers.accessoryCell : Identifiers.unreachableAccessoryCell + + case .Room?: + return Identifiers.roomCell + + case .Zone?: + return Identifiers.zoneCell + + case .User?: + return Identifiers.userCell + + case .ActionSet?: + return Identifiers.actionSetCell + + case .Trigger?: + return Identifiers.triggerCell + + case .ServiceGroup?: + return Identifiers.serviceGroupCell + + case nil: + fatalError("Unexpected `HomeKitObjectSection` raw value.") + } + } + + /// Generates a cell for the HomeKit object at the specified index path. + private func tableView(tableView: UITableView, homeKitObjectCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + // Grab the object associated with this indexPath. + let homeKitObject = homeKitObjectAtIndexPath(indexPath) + + // Get the name of the object. + let name: String + switch HomeKitObjectSection(rawValue: indexPath.section) { + case .Accessory?: + let accessory = homeKitObject as! HMAccessory + name = accessory.name + + case .Room?: + let room = homeKitObject as! HMRoom + name = self.home.nameForRoom(room) + + case .Zone?: + let zone = homeKitObject as! HMZone + name = zone.name + case .User?: + name = "" + + case .ActionSet?: + let actionSet = homeKitObject as! HMActionSet + name = actionSet.name + + case .Trigger?: + let trigger = homeKitObject as! HMTrigger + name = trigger.name + + case .ServiceGroup?: + let serviceGroup = homeKitObject as! HMServiceGroup + name = serviceGroup.name + + case nil: + fatalError("Unexpected `HomeKitObjectSection` raw value.") + } + + + // Grab the appropriate reuse identifier for this index path. + let reuseIdentifier = reuseIdentifierForIndexPath(indexPath) + + let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) + cell.textLabel?.text = name + + return cell + } + + /// Allows users to remove HomeKit object rows if they are the admin of the home. + override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { + let homeKitObject = homeKitObjectAtIndexPath(indexPath) + + if !home.isAdmin { + return false + } + + if let actionSet = homeKitObject as? HMActionSet where actionSet.isBuiltIn { + // We cannot remove built-in action sets. + return false + } + + // Any row that is not an 'add' row, and is not the roomForEntireHome, can be removed. + return !(homeKitObject as? NSObject == home.roomForEntireHome() || cellTypeForIndexPath(indexPath) == .Add) + } + + /// Removes the HomeKit object at the specified index path. + override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { + if editingStyle == .Delete { + let homeKitObject = homeKitObjectAtIndexPath(indexPath)! + + // Remove the object from the data structure. If it fails put it back. + didRemoveHomeKitObject(homeKitObject) + removeHomeKitObject(homeKitObject) { error in + guard let error = error else { return } + + self.displayError(error) + self.didAddHomeKitObject(homeKitObject) + } + } + } + + /// Handles cell selection based on the cell type. + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + tableView.deselectRowAtIndexPath(indexPath, animated: true) + let cell = tableView.cellForRowAtIndexPath(indexPath)! + + guard cell.selectionStyle != .None else { return } + + guard let section = HomeKitObjectSection(rawValue: indexPath.section) else { + fatalError("Unexpected `HomeKitObjectSection` raw value.") + } + + if cellTypeForIndexPath(indexPath) == .Add{ + switch section { + case .Accessory: + browseForAccessories() + + case .Room: + addNewRoom() + + case .Zone: + addNewZone() + + case .User: + manageUsers() + + case .ActionSet: + addNewActionSet() + + case .Trigger: + addNewTrigger() + + case .ServiceGroup: + addNewServiceGroup() + } + } + else if section == .ActionSet { + let selectedActionSet = homeKitObjectAtIndexPath(indexPath) as! HMActionSet + executeActionSet(selectedActionSet) + } + } + + /// Handles an accessory button tap based on the section. + override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) { + let cell = tableView.cellForRowAtIndexPath(indexPath) + + if HomeKitObjectSection(rawValue: indexPath.section) == .Trigger { + let trigger = homeKitObjectAtIndexPath(indexPath) + + switch trigger { + case is HMTimerTrigger: + performSegueWithIdentifier(Identifiers.showTimerTriggerSegue, sender: cell) + + case let eventTrigger as HMEventTrigger: + if eventTrigger.isLocationEvent { + performSegueWithIdentifier(Identifiers.showLocationTriggerSegue, sender: cell) + } + else { + performSegueWithIdentifier(Identifiers.showCharacteristicTriggerSegue, sender: cell) + } + + default: break + } + } + } + + // MARK: Action Methods + + /// Presents an alert controller to allow the user to choose a trigger type. + private func addNewTrigger() { + let title = NSLocalizedString("Add Trigger", comment: "Add Trigger") + let alertController = UIAlertController(title: title, message: nil, preferredStyle: .ActionSheet) + + // Timer trigger + let timeAction = UIAlertAction(title: NSLocalizedString("Time", comment: "Time"), style: .Default) { _ in + self.performSegueWithIdentifier(Identifiers.addTimerTriggerSegue, sender: self) + } + alertController.addAction(timeAction) + + // Characteristic trigger + let eventAction = UIAlertAction(title: NSLocalizedString("Characteristic", comment: "Characteristic"), style: .Default) { _ in + self.performSegueWithIdentifier(Identifiers.addCharacteristicTriggerSegue, sender: self) + } + alertController.addAction(eventAction) + + // Location trigger + let locationAction = UIAlertAction(title: NSLocalizedString("Location", comment: "Location"), style: .Default) { _ in + self.performSegueWithIdentifier(Identifiers.addLocationTriggerSegue, sender: self) + } + alertController.addAction(locationAction) + + // Cancel + let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "Cancel"), style: .Cancel, handler: nil) + alertController.addAction(cancelAction) + + // Present alert + presentViewController(alertController, animated: true, completion: nil) + } + + /// Navigates into the action set view controller. + private func addNewActionSet() { + performSegueWithIdentifier(Identifiers.addActionSetSegue, sender: self) + } + + /// Navigates into the browse accessory view controller. + private func browseForAccessories() { + performSegueWithIdentifier(Identifiers.addAccessoriesSegue, sender: self) + } + + // MARK: Dialog Creation Methods + + /// Presents a dialog to name a new room and generates the HomeKit object if committed. + private func addNewRoom() { + presentAddAlertWithAttributeType(NSLocalizedString("Room", comment: "Room"), + placeholder: NSLocalizedString("Living Room", comment: "Living Room")) { roomName in + self.addRoomWithName(roomName) + } + } + + /// Presents a dialog to name a new service group and generates the HomeKit object if committed. + private func addNewServiceGroup() { + presentAddAlertWithAttributeType(NSLocalizedString("Service Group", comment: "Service Group"), + placeholder: NSLocalizedString("Group", comment: "Group")) { groupName in + self.addServiceGroupWithName(groupName) + } + } + + /// Presents a dialog to name a new zone and generates the HomeKit object if committed. + private func addNewZone() { + presentAddAlertWithAttributeType(NSLocalizedString("Zone", comment: "Zone"), + placeholder: NSLocalizedString("Upstairs", comment: "Upstairs")) { zoneName in + self.addZoneWithName(zoneName) + } + } + + // MARK: HomeKit Object Creation and Deletion + + /** + Switches based on the type of object attempts to remove the HomeKit object + from the curret home. + + - parameter object: The HomeKit object to remove. + - parameter completionHandler: The closure to invote when the removal has been completed. + */ + private func removeHomeKitObject(object: AnyObject, completionHandler: NSError? -> Void) { + switch object { + case let actionSet as HMActionSet: + home.removeActionSet(actionSet) { error in + completionHandler(error) + self.updateActionSetSection() + } + + case let accessory as HMAccessory: + home.removeAccessory(accessory, completionHandler: completionHandler) + + case let room as HMRoom: + home.removeRoom(room, completionHandler: completionHandler) + + case let zone as HMZone: + home.removeZone(zone, completionHandler: completionHandler) + + case let trigger as HMTrigger: + home.removeTrigger(trigger, completionHandler: completionHandler) + + case let serviceGroup as HMServiceGroup: + home.removeServiceGroup(serviceGroup, completionHandler: completionHandler) + + default: + fatalError("Attempted to remove unknown HomeKit object.") + } + } + + /** + Adds a room to the current home. + + - parameter name: The name of the new room. + */ + private func addRoomWithName(name: String) { + home.addRoomWithName(name) { newRoom, error in + if let error = error { + self.displayError(error) + return + } + + self.didAddHomeKitObject(newRoom) + } + } + + /** + Adds a service group to the current home. + + - parameter name: The name of the new service group. + */ + private func addServiceGroupWithName(name: String) { + home.addServiceGroupWithName(name) { newGroup, error in + if let error = error { + self.displayError(error) + return + } + + self.didAddHomeKitObject(newGroup) + } + } + + /** + Adds a zone to the current home. + + - parameter name: The name of the new zone. + */ + private func addZoneWithName(name: String) { + home.addZoneWithName(name) { newZone, error in + if let error = error { + self.displayError(error) + return + } + + self.didAddHomeKitObject(newZone) + } + } + + /// Presents modal view for managing users. + private func manageUsers() { + home.manageUsersWithCompletionHandler { error in + if let error = error { + self.displayError(error) + } + } + } + + /** + Checks to see if an action set has any actions. + If actions exists, the action set will be executed. + Otherwise, the user will be alerted. + + - parameter actionSet: The `HMActionSet` to evaluate and execute. + */ + private func executeActionSet(actionSet: HMActionSet) { + if actionSet.actions.isEmpty { + let alertTitle = NSLocalizedString("Empty Scene", comment: "Empty Scene") + + let alertMessage = NSLocalizedString("This scene is empty. To set this scene, first add some actions to it.", comment: "Empty Scene Description") + + displayMessage(alertTitle, message: alertMessage) + + return + } + + home.executeActionSet(actionSet) { error in + guard let error = error else { return } + + self.displayError(error) + } + } + + /** + Adds the HomeKit object into the object collection and inserts the new row into the section. + + - parameter object: The HomeKit object to add. + */ + private func didAddHomeKitObject(object: AnyObject?) { + if let object = object { + objectCollection.append(object) + if let newObjectIndexPath = objectCollection.indexPathOfObject(object) { + tableView.insertRowsAtIndexPaths([newObjectIndexPath], withRowAnimation: .Automatic) + } + } + } + + /** + Finds the `NSIndexPath` of the specified object and reloads it in the table view. + + - parameter object: The HomeKit object that was modified. + */ + private func didModifyHomeKitObject(object: AnyObject?) { + if let object = object, + objectIndexPath = objectCollection.indexPathOfObject(object) { + tableView.reloadRowsAtIndexPaths([objectIndexPath], withRowAnimation: .Automatic) + } + } + + /** + Removes the HomeKit object from the object collection and then deletes the row from the section. + + - parameter object: The HomeKit object to remove. + */ + private func didRemoveHomeKitObject(object: AnyObject?) { + if let object = object, + objectIndexPath = objectCollection.indexPathOfObject(object) { + objectCollection.remove(object) + tableView.deleteRowsAtIndexPaths([objectIndexPath], withRowAnimation: .Automatic) + } + } + + /* + The following methods call the above helper methds to handle + the addition, removal, and modification of HomeKit objects. + */ + + // MARK: HMHomeDelegate Methods + + func homeDidUpdateName(home: HMHome) { + navigationItem.title = home.name + reloadTable() + } + + func home(home: HMHome, didAddAccessory accessory: HMAccessory) { + didAddHomeKitObject(accessory) + accessory.delegate = self + } + + func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) { + didRemoveHomeKitObject(accessory) + } + + // MARK: Triggers + + func home(home: HMHome, didAddTrigger trigger: HMTrigger) { + didAddHomeKitObject(trigger) + } + + func home(home: HMHome, didRemoveTrigger trigger: HMTrigger) { + didRemoveHomeKitObject(trigger) + } + + func home(home: HMHome, didUpdateNameForTrigger trigger: HMTrigger) { + didModifyHomeKitObject(trigger) + } + + // MARK: Service Groups + + func home(home: HMHome, didAddServiceGroup group: HMServiceGroup) { + didAddHomeKitObject(group) + } + + func home(home: HMHome, didRemoveServiceGroup group: HMServiceGroup) { + didRemoveHomeKitObject(group) + } + + func home(home: HMHome, didUpdateNameForServiceGroup group: HMServiceGroup) { + didModifyHomeKitObject(group) + } + + // MARK: Action Sets + + func home(home: HMHome, didAddActionSet actionSet: HMActionSet) { + didAddHomeKitObject(actionSet) + } + + func home(home: HMHome, didRemoveActionSet actionSet: HMActionSet) { + didRemoveHomeKitObject(actionSet) + } + + func home(home: HMHome, didUpdateNameForActionSet actionSet: HMActionSet) { + didModifyHomeKitObject(actionSet) + } + + // MARK: Zones + + func home(home: HMHome, didAddZone zone: HMZone) { + didAddHomeKitObject(zone) + } + + func home(home: HMHome, didRemoveZone zone: HMZone) { + didRemoveHomeKitObject(zone) + } + + func home(home: HMHome, didUpdateNameForZone zone: HMZone) { + didModifyHomeKitObject(zone) + } + + // MARK: Rooms + + func home(home: HMHome, didAddRoom room: HMRoom) { + didAddHomeKitObject(room) + } + + func home(home: HMHome, didRemoveRoom room: HMRoom) { + didRemoveHomeKitObject(room) + } + + func home(home: HMHome, didUpdateNameForRoom room: HMRoom) { + didModifyHomeKitObject(room) + } + + // MARK: Accessories + + func accessoryDidUpdateReachability(accessory: HMAccessory) { + didModifyHomeKitObject(accessory) + } + + func accessoryDidUpdateName(accessory: HMAccessory) { + didModifyHomeKitObject(accessory) + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Rooms/RoomViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Rooms/RoomViewController.swift new file mode 100644 index 00000000..f0ccec8b --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Rooms/RoomViewController.swift @@ -0,0 +1,266 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `RoomViewController` lists the accessory within a room. +*/ + + +import UIKit +import HomeKit + +/// A view controller that lists the accessories within a room. +class RoomViewController: HMCatalogViewController, HMAccessoryDelegate { + // MARK: Types + + struct Identifiers { + static let accessoryCell = "AccessoryCell" + static let unreachableAccessoryCell = "UnreachableAccessoryCell" + static let modifyAccessorySegue = "Modify Accessory" + } + + // MARK: Properties + + var room: HMRoom! { + didSet { + navigationItem.title = room.name + } + } + + var accessories = [HMAccessory]() + + // MARK: View Methods + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + reloadData() + } + + // MARK: Table View Methods + + /// - returns: The number of accessories within this room. + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let rows = accessories.count + if rows == 0 { + let message = NSLocalizedString("No Accessories", comment: "No Accessories") + setBackgroundMessage(message) + } + else { + setBackgroundMessage(nil) + } + + return rows + } + + /// - returns: `true` if the current room is not the home's roomForEntireHome; `false` otherwise. + override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { + return room != home.roomForEntireHome() + } + + /// - returns: Localized "Unassign". + override func tableView(tableView: UITableView, titleForDeleteConfirmationButtonForRowAtIndexPath indexPath: NSIndexPath) -> String? { + return NSLocalizedString("Unassign", comment: "Unassign") + } + + /// Assigns the 'deleted' room to the home's roomForEntireHome. + override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { + if editingStyle == .Delete { + unassignAccessory(accessories[indexPath.row]) + } + } + + /// - returns: A cell representing an accessory. + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let accessory = accessories[indexPath.row] + + var reuseIdentifier = Identifiers.accessoryCell + + if !accessory.reachable { + reuseIdentifier = Identifiers.unreachableAccessoryCell + } + + let cell = tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) + + cell.textLabel?.text = accessory.name + + return cell + } + + /// - returns: A localized description, "Accessories" if there are accessories to list. + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + if accessories.isEmpty { + return nil + } + + return NSLocalizedString("Accessories", comment: "Accessories") + } + + // MARK: Helper Methods + + /// Updates the internal array of accessories and reloads the table view. + private func reloadData() { + accessories = room.accessories.sortByLocalizedName() + tableView.reloadData() + } + + /// Sorts the internal list of accessories by localized name. + private func sortAccessories() { + accessories = accessories.sortByLocalizedName() + } + + /** + Registers as the delegate for the current home and + all accessories in our room. + */ + override func registerAsDelegate() { + super.registerAsDelegate() + for accessory in room.accessories { + accessory.delegate = self + } + } + + /// Sets the accessory and home of the modifyAccessoryViewController that will be presented. + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + super.prepareForSegue(segue, sender: sender) + let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)! + if segue.identifier == Identifiers.modifyAccessorySegue { + let modifyViewController = segue.intendedDestinationViewController as! ModifyAccessoryViewController + modifyViewController.accessory = room.accessories[indexPath.row] + } + } + + /** + Adds an accessory into the internal list of accessories + and inserts the row into the table view. + + - parameter accessory: The `HMAccessory` to add. + */ + private func didAssignAccessory(accessory: HMAccessory) { + accessories.append(accessory) + sortAccessories() + if let newAccessoryIndex = accessories.indexOf(accessory) { + let newAccessoryIndexPath = NSIndexPath(forRow: newAccessoryIndex, inSection: 0) + tableView.insertRowsAtIndexPaths([newAccessoryIndexPath], withRowAnimation: .Automatic) + } + } + + /** + Removes an accessory from the internal list of accessory (if it + exists) and deletes the row from the table view. + + - parameter accessory: The `HMAccessory` to remove. + */ + private func didUnassignAccessory(accessory: HMAccessory) { + if let accessoryIndex = accessories.indexOf(accessory) { + accessories.removeAtIndex(accessoryIndex) + let accessoryIndexPath = NSIndexPath(forRow: accessoryIndex, inSection: 0) + tableView.deleteRowsAtIndexPaths([accessoryIndexPath], withRowAnimation: .Automatic) + } + } + + /** + Assigns an accessory to the current room. + + - parameter accessory: The `HMAccessory` to assign to the room. + */ + private func assignAccessory(accessory: HMAccessory) { + didAssignAccessory(accessory) + home.assignAccessory(accessory, toRoom: room) { error in + if let error = error { + self.displayError(error) + self.didUnassignAccessory(accessory) + } + } + } + + /** + Assigns the current room back into `roomForEntireHome`. + + - parameter accessory: The `HMAccessory` to reassign. + */ + private func unassignAccessory(accessory: HMAccessory) { + didUnassignAccessory(accessory) + home.assignAccessory(accessory, toRoom: home.roomForEntireHome()) { error in + if let error = error { + self.displayError(error) + self.didAssignAccessory(accessory) + } + } + } + + /** + Finds an accessory in the internal array of accessories + and updates its row in the table view. + + - parameter accessory: The `HMAccessory` to reload. + */ + func didModifyAccessory(accessory: HMAccessory){ + if let index = accessories.indexOf(accessory) { + let indexPaths = [ + NSIndexPath(forRow: index, inSection: 0) + ] + + tableView.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic) + } + } + + // MARK: HMHomeDelegate Methods + + /// If the accessory was added to this room, insert it. + func home(home: HMHome, didAddAccessory accessory: HMAccessory) { + if accessory.room == room { + accessory.delegate = self + didAssignAccessory(accessory) + } + } + + /// Remove the accessory from our room, if required. + func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) { + didUnassignAccessory(accessory) + } + + /** + Handles the update. + + We act based on one of three options: + + 1. A new accessory is being added to this room. + 2. An accessory is being assigned from this room to another room. + 3. We can ignore this message. + */ + func home(home: HMHome, didUpdateRoom room: HMRoom, forAccessory accessory: HMAccessory) { + if room == self.room { + didAssignAccessory(accessory) + } + else if accessories.contains(accessory) { + didUnassignAccessory(accessory) + } + } + + /// If our room was removed, pop back. + func home(home: HMHome, didRemoveRoom room: HMRoom) { + if room == self.room { + navigationController!.popViewControllerAnimated(true) + } + } + + /// If our room was renamed, reload our title. + func home(home: HMHome, didUpdateNameForRoom room: HMRoom) { + if room == self.room { + navigationItem.title = room.name + } + } + + // MARK: HMAccessoryDelegate Methods + + // Accessory updates will reload the cell for the accessory. + + func accessoryDidUpdateReachability(accessory: HMAccessory) { + didModifyAccessory(accessory) + } + + func accessoryDidUpdateName(accessory: HMAccessory) { + didModifyAccessory(accessory) + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Service Groups/AddServicesViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Service Groups/AddServicesViewController.swift new file mode 100644 index 00000000..fefe63cc --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Service Groups/AddServicesViewController.swift @@ -0,0 +1,209 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `AddServicesViewController` allows users to add services to a service group. +*/ + +import UIKit +import HomeKit + +/** + A view controller that provides a list of services and lets the user select services to be added to the provided Service Group. + + The services are not added to the service group until the 'Done' button is pressed. +*/ +class AddServicesViewController: HMCatalogViewController, HMAccessoryDelegate { + // MARK: Types + + struct Identifiers { + static let serviceCell = "ServiceCell" + } + + // MARK: Properties + + lazy private var displayedAccessories = [HMAccessory]() + lazy private var displayedServicesForAccessory = [HMAccessory: [HMService]]() + lazy private var selectedServices = [HMService]() + + var serviceGroup: HMServiceGroup! + + // MARK: View Methods + + /// Reloads internal data and view. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + selectedServices = [] + reloadTable() + } + + /// Registers as the delegate for the home and all accessories. + override func registerAsDelegate() { + super.registerAsDelegate() + for accessory in homeStore.home!.accessories { + accessory.delegate = self + } + } + + // MARK: Table View Methods + + /// - returns: The number of displayed accessories. + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return displayedAccessories.count + } + + /// - returns: The number of displayed services for the provided accessory. + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let accessory = displayedAccessories[section] + return displayedServicesForAccessory[accessory]!.count + } + + /// - returns: A configured `ServiceCell`. + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceCell, forIndexPath: indexPath) as! ServiceCell + + let service = serviceAtIndexPath(indexPath) + + cell.includeAccessoryText = false + cell.service = service + cell.accessoryType = selectedServices.contains(service) ? .Checkmark : .None + + return cell + } + + /** + When an indexPath is selected, this function either adds or removes the selected service from the + service group. + */ + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + // Get the service associated with this index. + let service = serviceAtIndexPath(indexPath) + + // Call the appropriate add/remove operation with the closure from above. + if let index = selectedServices.indexOf(service) { + selectedServices.removeAtIndex(index) + } + else { + selectedServices.append(service) + } + + tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + } + + /// - returns: The name of the displayed accessory at the given section. + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return displayedAccessories[section].name + } + + // MARK: Helper Methods + + /** + Adds the selected services to the service group. + + Calls the provided completion handler once all services have been added. + */ + func addSelectedServicesWithCompletionHandler(completion: () -> Void) { + // Create a dispatch group for each of the service additions. + let addServicesGroup = dispatch_group_create() + for service in selectedServices { + dispatch_group_enter(addServicesGroup) + serviceGroup.addService(service) { error in + if let error = error { + self.displayError(error) + } + dispatch_group_leave(addServicesGroup) + } + } + dispatch_group_notify(addServicesGroup, dispatch_get_main_queue(), completion) + } + + /** + Finds the service at a specific index path. + + - parameter indexPath: An `NSIndexPath` + + - returns: The `HMService` at the given index path. + */ + private func serviceAtIndexPath(indexPath: NSIndexPath) -> HMService { + let accessory = displayedAccessories[indexPath.section] + let services = displayedServicesForAccessory[accessory]! + return services[indexPath.row] + } + + /** + Commits the changes to the service group + and dismisses the view. + */ + @IBAction func dismiss() { + addSelectedServicesWithCompletionHandler { + self.dismissViewControllerAnimated(true, completion: nil) + } + } + + /// Resets internal data and view. + func reloadTable() { + resetDisplayedServices() + tableView.reloadData() + } + + /** + Updates internal array of accessories and the mapping + of accessories to selected services. + */ + func resetDisplayedServices() { + displayedAccessories = [] + let allAccessories = home.accessories.sortByLocalizedName() + displayedServicesForAccessory = [:] + for accessory in allAccessories { + var displayedServices = [HMService]() + for service in accessory.services { + if !serviceGroup.services.contains(service) && service.serviceType != HMServiceTypeAccessoryInformation { + displayedServices.append(service) + } + } + + // Only add the accessory if it has displayed services. + if !displayedServices.isEmpty { + displayedServicesForAccessory[accessory] = displayedServices.sortByLocalizedName() + displayedAccessories.append(accessory) + } + } + } + + // MARK: HMHomeDelegate Methods + + /// Dismisses the view controller if our service group was removed. + func home(home: HMHome, didRemoveServiceGroup group: HMServiceGroup) { + if serviceGroup == group { + dismissViewControllerAnimated(true, completion: nil) + } + } + + /// Reloads the view if an accessory was added to HomeKit. + func home(home: HMHome, didAddAccessory accessory: HMAccessory) { + reloadTable() + accessory.delegate = self + } + + /// Dismisses the view controller if we no longer have accesories. + func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) { + if home.accessories.isEmpty { + navigationController?.dismissViewControllerAnimated(true, completion: nil) + } + + reloadTable() + } + + // MARK: HMAccessoryDelegate Methods + + // Accessory changes reload the data and view. + + func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) { + reloadTable() + } + + func accessoryDidUpdateServices(accessory: HMAccessory) { + reloadTable() + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Service Groups/ServiceGroupViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Service Groups/ServiceGroupViewController.swift new file mode 100644 index 00000000..6ac6337f --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Service Groups/ServiceGroupViewController.swift @@ -0,0 +1,246 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ServiceGroupViewController` allows users to modify service groups. +*/ + +import UIKit +import HomeKit + +/// A view controller that allows the user to add services to a service group. +class ServiceGroupViewController: HMCatalogViewController, HMAccessoryDelegate { + // MARK: Types + + struct Identifiers { + static let serviceCell = "ServiceCell" + static let addServicesSegue = "Add Services Plus" + } + + // MARK: Properties + + @IBOutlet weak var plusButton: UIBarButtonItem! + + var serviceGroup: HMServiceGroup! + lazy private var accessories = [HMAccessory]() + lazy private var servicesForAccessory = [HMAccessory: [HMService]]() + + // MARK: View Methods + + /// Reloads the view. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + title = serviceGroup.name + reloadData() + } + + /// Pops the view controller if our data is invalid. + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + if shouldPopViewController() { + navigationController?.popViewControllerAnimated(true) + } + } + + // MARK: Table View Methods + + /** + Generates the number of sections and adds a table view + back ground message, if required. + + - returns: The number of accessories in the service group. + */ + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + let sections = accessories.count + if sections == 0 { + setBackgroundMessage(NSLocalizedString("No Services", comment: "No Services")) + } + else { + setBackgroundMessage(nil) + } + + return sections + } + + /// - returns: The number of services for the accessory at the specified section. + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let accessory = accessories[section] + let services = servicesForAccessory[accessory] + + return services?.count ?? 0 + } + + /// - returns: The name of the accessory at the specified section. + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return accessories[section].name + } + + /// All cells in the table view represent services and can be deleted. + override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { + return true + } + + /// - returns: A `ServiceCell` with the service at the given index path. + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.serviceCell, forIndexPath: indexPath) as! ServiceCell + let service = serviceAtIndexPath(indexPath) + cell.includeAccessoryText = false + cell.service = service + return cell + } + + /** + - returns: `true` if there are any services not already in the service group; + `false` otherwise. + */ + private func shouldEnableAdd() -> Bool { + let unAddedServices = home.servicesNotAlreadyInServiceGroup(serviceGroup) + return unAddedServices.count != 0 + } + + /// Deleting a cell removes the corresponding service from the service group. + override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { + if editingStyle == .Delete { + removeServiceAtIndexPath(indexPath) + } + } + + /** + Removes the service associated with the cell at a given index path. + + - parameters indexPath: The `NSIndexPath` to remove. + */ + private func removeServiceAtIndexPath(indexPath: NSIndexPath) { + let service = serviceAtIndexPath(indexPath) + serviceGroup.removeService(service) { error in + if let error = error { + self.displayError(error) + } + + self.reloadData() + } + } + + /** + Finds the service at a given index path. + + - parameter indexPath: An `NSIndexPath`. + + - returns: The service at the given index path + */ + private func serviceAtIndexPath(indexPath: NSIndexPath) -> HMService { + let accessory = accessories[indexPath.section] + let services = servicesForAccessory[accessory]! + return services[indexPath.row] + } + + /// Passes the service group into the `AddServicesViewController` + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + super.prepareForSegue(segue, sender: sender) + if segue.identifier == Identifiers.addServicesSegue { + let addServicesVC = segue.intendedDestinationViewController as! AddServicesViewController + addServicesVC.serviceGroup = serviceGroup + } + } + + // MARK: Helper Methods + + /** + Resets accessory and service lists, resets the plus + button's enabled status and reloads the table view. + */ + private func reloadData() { + resetLists() + plusButton.enabled = shouldEnableAdd() + tableView.reloadData() + } + + /** + Resets the accessories array and the service-accessory mapping + using the original HomeKit objects. + */ + private func resetLists() { + accessories = [] + servicesForAccessory = [:] + + for service in serviceGroup.services { + if let accessory = service.accessory { + if servicesForAccessory[accessory] == nil { + accessories.append(accessory) + servicesForAccessory[accessory] = [service] + } + else { + servicesForAccessory[accessory]?.append(service) + } + } + } + + // Sort all service lists. + for accessory in accessories { + servicesForAccessory[accessory] = servicesForAccessory[accessory]?.sortByLocalizedName() + } + + // Sort accessory list. + accessories = accessories.sortByLocalizedName() + } + + /** + - returns: `true` if our service group is not + in the home any more; `false` otherwise. + */ + private func shouldPopViewController() -> Bool { + guard let home = homeStore.home else { return true } + + return !home.serviceGroups.contains { group in + return group == serviceGroup + } + } + + /** + Registers as the delegate for the home and + all accessories which are related to our service group. + */ + override func registerAsDelegate() { + super.registerAsDelegate() + + for service in serviceGroup.services { + service.accessory?.delegate = self + } + } + + // MARK: HMHomeDelegate Methods + + /// Pops the view controller if our service group has been deleted. + func home(home: HMHome, didRemoveServiceGroup group: HMServiceGroup) { + if group == serviceGroup { + navigationController?.popViewControllerAnimated(true) + } + } + + // Home and accessory changes result in a full data reload. + + func home(home: HMHome, didAddService service: HMService, toServiceGroup group: HMServiceGroup) { + if serviceGroup == group { + reloadData() + } + } + + func home(home: HMHome, didRemoveService service: HMService, fromServiceGroup group: HMServiceGroup) { + if serviceGroup == group { + reloadData() + } + } + + func home(home: HMHome, didRemoveAccessory accessory: HMAccessory) { + reloadData() + } + + func accessoryDidUpdateServices(accessory: HMAccessory) { + reloadData() + } + + func accessory(accessory: HMAccessory, didUpdateNameForService service: HMService) { + reloadData() + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicSelectionViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicSelectionViewController.swift new file mode 100644 index 00000000..2f5028e6 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicSelectionViewController.swift @@ -0,0 +1,110 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `CharacteristicSelectionViewController` allows for the selection of characteristics. + This is mainly used for creating characteristic events and conditions +*/ + +import UIKit +import HomeKit + +/** + Allows for the selection of characteristics. + This is mainly used for creating characteristic events and conditions +*/ +class CharacteristicSelectionViewController: HMCatalogViewController { + // MARK: Types + + struct Identifiers { + static let accessoryCell = "AccessoryCell" + static let unreachableAccessoryCell = "UnreachableAccessoryCell" + static let showServicesSegue = "Show Services" + } + + // MARK: Properties + + var eventTrigger: HMEventTrigger? + var triggerCreator: EventTriggerCreator! + + /// An internal copy of all controllable accessories in the home. + private var accessories = [HMAccessory]() + + @IBOutlet weak var saveButton: UIBarButtonItem! + + // MARK: View Methods + + /// Resets the internal array of accessories from the home. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + // Only take accessories which have one control service. + accessories = home.sortedControlAccessories + } + + /// Configures the `ServicesViewController` and passes it the correct accessory. + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if segue.identifier == Identifiers.showServicesSegue { + let senderCell = sender as! UITableViewCell + let servicesVC = segue.intendedDestinationViewController as! ServicesViewController + let cellIndex = tableView.indexPathForCell(senderCell)!.row + servicesVC.allowsAllWrites = true + servicesVC.onlyShowsControlServices = true + servicesVC.accessory = accessories[cellIndex] + servicesVC.cellDelegate = triggerCreator + } + } + + // MARK: IBAction Methods + + /** + Updates the predicates in the trigger creator and then + dismisses the view controller. + */ + @IBAction func didTapSave(sender: UIBarButtonItem) { + /* + We should not save the trigger completely, the user still has a chance to bail out. + Instead, we generate all of the predicates that were in the map. + */ + triggerCreator.updatePredicates() + dismissViewControllerAnimated(true, completion: nil) + } + + // MARK: Table View Methods + + /// Single section view controller. + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return 1 + } + + /// - returns: The number of accessories. If there are none, will return 1 (for the 'none row'). + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return max(accessories.count, 1) + } + + /// - returns: An Accessory cell that contains an accessory's name. + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let accessory = accessories.sortByLocalizedName()[indexPath.row] + let cellIdentifier = accessory.reachable ? Identifiers.accessoryCell : Identifiers.unreachableAccessoryCell + + let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) + cell.textLabel?.text = accessory.name + + return cell + } + + /// Shows the services in the selected accessory. + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + tableView.deselectRowAtIndexPath(indexPath, animated: true) + let cell = tableView.cellForRowAtIndexPath(indexPath)! + if cell.selectionStyle == .None { + return + } + performSegueWithIdentifier(Identifiers.showServicesSegue, sender: cell) + } + + /// - returns: Localized "Accessories" string. + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return NSLocalizedString("Accessories", comment: "Accessories") + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerCreator.swift new file mode 100644 index 00000000..e4eb4e1c --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerCreator.swift @@ -0,0 +1,254 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `CharacteristicTriggerCreator` creates characteristic triggers. +*/ + +import UIKit +import HomeKit + +/// Represents modes for a `CharacteristicTriggerCreator`. +enum CharacteristicTriggerCreatorMode: Int { + case Event, Condition +} + +/** + An `EventTriggerCreator` subclass which allows for the creation + of characteristic triggers. +*/ +class CharacteristicTriggerCreator: EventTriggerCreator { + // MARK: Properties + + var eventTrigger: HMEventTrigger? { + return self.trigger as? HMEventTrigger + } + + /** + This object will be a characteristic cell delegate and will therefore + be receiving updates when UI elements change value. However, this object + can construct both characteristic events and characteristic triggers. + Setting the `mode` determines how this trigger creator will handle + cell delegate callbacks. + */ + var mode: CharacteristicTriggerCreatorMode = .Event + + /** + Contains the new pending mapping of `HMCharacteristic`s to their trigger (`NSCopying`) values. + When `saveTriggerWithName(name:completion:)` is called, all of these mappings will be converted + into `HMCharacteristicEvent`s and added to the `HMEventTrigger`. + */ + private let targetValueMap = NSMapTable.strongToStrongObjectsMapTable() + + /// `HMCharacteristicEvent`s that should be removed if `saveTriggerWithName(name:completion:)` is called. + private var removalCharacteristicEvents = [HMCharacteristicEvent]() + + // MARK: Trigger Creator Methods + + /// Syncs the stored event trigger using internal values. + override func updateTrigger() { + guard let eventTrigger = eventTrigger else { return } + matchEventsFromTriggerIfNecessary() + removePendingEventsFromTrigger() + for (characteristic, triggerValue) in pairsFromMapTable(targetValueMap) { + let newEvent = HMCharacteristicEvent(characteristic: characteristic, triggerValue: triggerValue) + dispatch_group_enter(self.saveTriggerGroup) + eventTrigger.addEvent(newEvent) { error in + if let error = error { + self.errors.append(error) + } + dispatch_group_leave(self.saveTriggerGroup) + } + } + savePredicate() + } + + /** + - returns: A new `HMEventTrigger` with the pending + characteristic events and constructed predicate. + */ + override func newTrigger() -> HMTrigger? { + return HMEventTrigger(name: name, events: pendingCharacteristicEvents, predicate: newPredicate()) + } + + /** + Remove all objects from the map so they don't show up + in the `events` computed array. + */ + override func cleanUp() { + targetValueMap.removeAllObjects() + } + + /** + Removes an event from the map table if it's a new event and + queues it for removal if it already existed in the event trigger. + + - parameter event: `HMCharacteristicEvent` to be removed. + */ + func removeEvent(event: HMCharacteristicEvent) { + if targetValueMap.objectForKey(event.characteristic) != nil { + // Remove the characteristic from the target value map. + targetValueMap.removeObjectForKey(event.characteristic) + } + + if let characteristicEvents = eventTrigger?.characteristicEvents where characteristicEvents.contains(event) { + // If the given event is in the event array, queue it for removal. + removalCharacteristicEvents.append(event) + } + } + + // MARK: Helper Methods + + /** + Any characteristic events in the map table that have not yet been + added to the trigger. + */ + var pendingCharacteristicEvents: [HMCharacteristicEvent] { + return pairsFromMapTable(targetValueMap).map { (characteristic, triggerValue) -> HMCharacteristicEvent in + return HMCharacteristicEvent(characteristic: characteristic, triggerValue: triggerValue) + } + } + + /** + Loops through the characteristic events in the trigger. + If any characteristics in our map table are also in the event, + replace the value with the one we have stored and remove that entry from + our map table. + */ + private func matchEventsFromTriggerIfNecessary() { + guard let eventTrigger = eventTrigger else { return } + for event in eventTrigger.characteristicEvents { + // Find events who's characteristic is in our map table. + if let triggerValue = targetValueMap.objectForKey(event.characteristic) as? NSCopying { + dispatch_group_enter(self.saveTriggerGroup) + event.updateTriggerValue(triggerValue) { error in + if let error = error { + self.errors.append(error) + } + dispatch_group_leave(self.saveTriggerGroup) + } + } + } + } + + /** + Removes all `HMCharacteristicEvent`s from the `removalCharacteristicEvents` + array and stores any errors that accumulate. + */ + private func removePendingEventsFromTrigger() { + guard let eventTrigger = eventTrigger else { return } + for event in removalCharacteristicEvents { + dispatch_group_enter(saveTriggerGroup) + eventTrigger.removeEvent(event) { error in + if let error = error { + self.errors.append(error) + } + dispatch_group_leave(self.saveTriggerGroup) + } + } + removalCharacteristicEvents.removeAll() + } + + + + /// All `HMCharacteristic`s in the `targetValueMap`. + private var allCharacteristics: [HMCharacteristic] { + var characteristics = Set() + for characteristic in targetValueMap.keyEnumerator().allObjects as! [HMCharacteristic] { + characteristics.insert(characteristic) + } + return Array(characteristics) + } + + /** + Saves a characteristic and value into the pending map + of characteristic events. + + - parameter value: The value of the characteristic. + - parameter characteristic: The `HMCharacteristic` that has been updated. + */ + private func updateEventValue(value: AnyObject, forCharacteristic characteristic: HMCharacteristic) { + for (index, event) in removalCharacteristicEvents.enumerate() { + if event.characteristic == characteristic { + /* + We have this event pending for deletion, + but we are going to want to update it. + remove it from the removal array. + */ + removalCharacteristicEvents.removeAtIndex(index) + break + } + } + targetValueMap.setObject(value, forKey: characteristic) + } + + /** + The current, sorted collection of `HMCharacteristicEvent`s accumulated by + filtering out the events pending removal from the original trigger events and + then adding new pending events. + */ + var events: [HMCharacteristicEvent] { + let characteristicEvents = eventTrigger?.characteristicEvents ?? [] + + let originalEvents = characteristicEvents.filter { + return !removalCharacteristicEvents.contains($0) + } + + let allEvents = originalEvents + pendingCharacteristicEvents + + return allEvents.sort { (event1: HMCharacteristicEvent, event2: HMCharacteristicEvent) in + let type1 = event1.characteristic.localizedCharacteristicType + let type2 = event2.characteristic.localizedCharacteristicType + return type1.localizedCompare(type2) == .OrderedAscending + } + } + + // MARK: CharacteristicCellDelegate Methods + + /** + If the mode is event, update the event value. + Otherwise, default to super implementation + */ + override func characteristicCell(cell: CharacteristicCell, didUpdateValue value: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool) { + switch mode { + case .Event: + updateEventValue(value, forCharacteristic: characteristic) + + default: + super.characteristicCell(cell, didUpdateValue: value, forCharacteristic: characteristic, immediate: immediate) + } + } + + /** + Tries to find the characteristic in either the event map or the + condition map (based on the current mode). Then calls read value. + When the value comes back, we check the selected map for the value + */ + override func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void) { + if mode == .Condition { + // This is a condition, fall back to the `EventTriggerCreator` read. + super.characteristicCell(cell, readInitialValueForCharacteristic: characteristic, completion: completion) + return + } + + if let value = targetValueMap.objectForKey(characteristic) { + completion(value, nil) + return + } + + characteristic.readValueWithCompletionHandler { error in + /* + The user may have updated the cell value while the + read was happening. We check the map one more time. + */ + if let value = self.targetValueMap.objectForKey(characteristic) { + completion(value, nil) + } + else { + completion(characteristic.value, error) + } + } + } + +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerViewController.swift new file mode 100644 index 00000000..15927e7e --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Characteristic/CharacteristicTriggerViewController.swift @@ -0,0 +1,265 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `CharacteristicTriggerViewController` allows the user to create a characteristic trigger. +*/ + +import UIKit +import HomeKit + +/// A view controller which facilitates the creation of characteristic triggers. +class CharacteristicTriggerViewController: EventTriggerViewController { + // MARK: Types + + struct Identifiers { + static let selectCharacteristicSegue = "Select Characteristic" + } + + // MARK: Properties + + private var characteristicTriggerCreator: CharacteristicTriggerCreator { + return triggerCreator as! CharacteristicTriggerCreator + } + + var eventTrigger: HMEventTrigger? { + return trigger as? HMEventTrigger + } + + /// An internal array of `HMCharacteristicEvent`s to save into the trigger. + private var events = [HMCharacteristicEvent]() + + // MARK: View Methods + + /// Creates the trigger creator. + override func viewDidLoad() { + super.viewDidLoad() + triggerCreator = CharacteristicTriggerCreator(trigger: eventTrigger, home: home) + } + + /// Reloads the internal data. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + reloadData() + } + + /// Passes our event trigger and trigger creator to the `CharacteristicSelectionViewController` + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + super.prepareForSegue(segue, sender: sender) + if segue.identifier == Identifiers.selectCharacteristicSegue { + if let destinationVC = segue.intendedDestinationViewController as? CharacteristicSelectionViewController { + destinationVC.eventTrigger = eventTrigger + destinationVC.triggerCreator = characteristicTriggerCreator + } + } + } + + // MARK: Table View Methods + + /** + - returns: The characteristic events for the Characteristics section. + Defaults to super implementation. + */ + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch sectionForIndex(section) { + case .Characteristics?: + // Plus one for the add row. + return events.count + 1 + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, numberOfRowsInSection: section) + } + } + + /** + Switches based on cell type to generate the correct cell for the index path. + Defaults to super implementation. + */ + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + if indexPathIsAdd(indexPath) { + return self.tableView(tableView, addCellForRowAtIndexPath: indexPath) + } + + switch sectionForIndex(indexPath.section) { + case .Characteristics?: + return self.tableView(tableView, conditionCellForRowAtIndexPath: indexPath) + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, cellForRowAtIndexPath: indexPath) + } + } + + /// - returns: A 'condition cell' with the event at the specified index path. + private func tableView(tableView: UITableView, conditionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.conditionCell, forIndexPath: indexPath) as! ConditionCell + let event = events[indexPath.row] + cell.setCharacteristic(event.characteristic, targetValue: event.triggerValue!) + return cell + } + + /** + - returns: An 'add cell' with localized text. + Defaults to super implementation. + */ + override func tableView(tableView: UITableView, addCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + switch sectionForIndex(indexPath.section) { + case .Characteristics?: + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.addCell, forIndexPath: indexPath) + cell.textLabel?.text = NSLocalizedString("Add Characteristic…", comment: "Add Characteristic") + cell.textLabel?.textColor = UIColor.editableBlueColor() + return cell + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, addCellForRowAtIndexPath: indexPath) + } + } + + /** + Handles the selection of characteristic events. + Defaults to super implementation for other sections. + */ + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + tableView.deselectRowAtIndexPath(indexPath, animated: true) + switch sectionForIndex(indexPath.section) { + case .Characteristics?: + if indexPathIsAdd(indexPath) { + addEvent() + return + } + let cell = tableView.cellForRowAtIndexPath(indexPath) + performSegueWithIdentifier(Identifiers.selectCharacteristicSegue, sender: cell) + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + super.tableView(tableView, didSelectRowAtIndexPath: indexPath) + } + } + + /** + - returns: `true` for characteristic cells, + otherwise defaults to super implementation. + */ + override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { + if indexPathIsAdd(indexPath) { + return false + } + switch sectionForIndex(indexPath.section) { + case .Characteristics?: + return true + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, canEditRowAtIndexPath: indexPath) + } + } + + /** + Removes events from the trigger creator. + Defaults to super implementation for other sections. + */ + override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { + if editingStyle == .Delete { + switch sectionForIndex(indexPath.section) { + case .Characteristics?: + characteristicTriggerCreator.removeEvent(events[indexPath.row]) + events = characteristicTriggerCreator.events + tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + super.tableView(tableView, commitEditingStyle: editingStyle, forRowAtIndexPath: indexPath) + } + } + } + + /** + - returns: A localized description of characteristic events + Defaults to super implementation for other sections. + */ + override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + switch sectionForIndex(section) { + case .Characteristics?: + return NSLocalizedString("This trigger will activate when any of these characteristics change to their value. For example, 'run when the garage door is opened'.", comment: "Characteristic Trigger Description") + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, titleForFooterInSection: section) + } + } + + // MARK: Helper Methods + + /// Resets the internal events array from the trigger creator. + private func reloadData() { + events = characteristicTriggerCreator.events + tableView.reloadData() + } + + /// Performs a segue to the `CharacteristicSelectionViewController`. + private func addEvent() { + characteristicTriggerCreator.mode = .Event + self.performSegueWithIdentifier(Identifiers.selectCharacteristicSegue, sender: nil) + } + + /// - returns: `true` if the section is the Characteristic 'add row'; otherwise defaults to super implementation. + override func indexPathIsAdd(indexPath: NSIndexPath) -> Bool { + switch sectionForIndex(indexPath.section) { + case .Characteristics?: + return indexPath.row == events.count + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.indexPathIsAdd(indexPath) + } + } + + // MARK: Trigger Controller Methods + + /** + - parameter index: The section index. + + - returns: The `TriggerTableViewSection` for the given index. + */ + override func sectionForIndex(index: Int) -> TriggerTableViewSection? { + switch index { + case 0: + return .Name + + case 1: + return .Enabled + + case 2: + return .Characteristics + + case 3: + return .Conditions + + case 4: + return .ActionSets + + default: + return nil + } + } + +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/ConditionCell.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/ConditionCell.swift new file mode 100644 index 00000000..a84cc631 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/ConditionCell.swift @@ -0,0 +1,116 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ConditionCell` displays characteristic and location conditions. +*/ + +import UIKit +import HomeKit + +/// A `UITableViewCell` subclass that displays a trigger condition. +class ConditionCell: UITableViewCell { + /// A static, short date formatter. + static let dateFormatter: NSDateFormatter = { + let dateFormatter = NSDateFormatter() + dateFormatter.dateStyle = .NoStyle + dateFormatter.timeStyle = .ShortStyle + dateFormatter.locale = NSLocale.currentLocale() + return dateFormatter + }() + + /// Ignores the passed-in style and overrides it with .Subtitle. + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: .Subtitle, reuseIdentifier: reuseIdentifier) + selectionStyle = .None + detailTextLabel?.textColor = UIColor.lightGrayColor() + accessoryType = .None + } + + /// Required because we overwrote a designated initializer. + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + /** + Sets the cell's text to represent a characteristic and target value. + For example, "Brightness → 60%" + Sets the subtitle to the service and accessory that this characteristic represents. + + - parameter characteristic: The characteristic this cell represents. + - parameter targetValue: The target value from this action. + */ + func setCharacteristic(characteristic: HMCharacteristic, targetValue: AnyObject) { + let targetDescription = "\(characteristic.localizedDescription) → \(characteristic.localizedDescriptionForValue(targetValue))" + textLabel?.text = targetDescription + + let contextDescription = NSLocalizedString("%@ in %@", comment: "Service in Accessory") + if let service = characteristic.service, accessory = service.accessory { + detailTextLabel?.text = String(format: contextDescription, service.name, accessory.name) + } + else { + detailTextLabel?.text = NSLocalizedString("Unknown Characteristic", comment: "Unknown Characteristic") + } + } + + /** + Sets the cell's text to represent an ordered time with a set context string. + + - parameter order: A `TimeConditionOrder` which will map to a localized string. + - parameter timeString: The localized time string. + - parameter contextString: A localized string describing the time type. + */ + private func setOrder(order: TimeConditionOrder, timeString: String, contextString: String) { + let formatString: String + switch order { + case .Before: + formatString = NSLocalizedString("Before %@", comment: "Before Time") + + case .After: + formatString = NSLocalizedString("After %@", comment: "After Time") + + case .At: + formatString = NSLocalizedString("At %@", comment: "At Time") + } + textLabel?.text = String(format: formatString, timeString) + detailTextLabel?.text = contextString + } + + /** + Sets the cell's text to represent an exact time condition. + + - parameter order: A `TimeConditionOrder` which will map to a localized string. + - parameter dateComponents: The date components of the exact time. + */ + func setOrder(order: TimeConditionOrder, dateComponents: NSDateComponents) { + let date = NSCalendar.currentCalendar().dateFromComponents(dateComponents) + let timeString = ConditionCell.dateFormatter.stringFromDate(date!) + setOrder(order, timeString: timeString, contextString: NSLocalizedString("Relative to Time", comment: "Relative to Time")) + } + + /** + Sets the cell's text to represent a solar event time condition. + + - parameter order: A `TimeConditionOrder` which will map to a localized string. + - parameter sunState: A `TimeConditionSunState` which will map to localized string. + */ + func setOrder(order: TimeConditionOrder, sunState: TimeConditionSunState) { + let timeString: String + switch sunState { + case .Sunrise: + timeString = NSLocalizedString("Sunrise", comment: "Sunrise") + + case .Sunset: + timeString = NSLocalizedString("Sunset", comment: "Sunset") + } + setOrder(order, timeString: timeString , contextString: NSLocalizedString("Relative to sun", comment: "Relative to Sun")) + } + + /// Sets the cell's text to indicate the given condition is not handled by the app. + func setUnknown() { + let unknownString = NSLocalizedString("Unknown Condition", comment: "Unknown Condition") + detailTextLabel?.text = unknownString + textLabel?.text = unknownString + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/SegmentedTimeCell.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/SegmentedTimeCell.swift new file mode 100644 index 00000000..b0540a12 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/SegmentedTimeCell.swift @@ -0,0 +1,15 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `SegmentedTimeCell` has a segmented control, used for selecting the time type. +*/ + +import UIKit +/// A `UITableViewCell` subclass with a `UISegmentedControl`, used for selecting the time type. +class SegmentedTimeCell: UITableViewCell { + // MARK: Properties + + @IBOutlet weak var segmentedControl: UISegmentedControl! +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimeConditionViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimeConditionViewController.swift new file mode 100644 index 00000000..d859a9cc --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimeConditionViewController.swift @@ -0,0 +1,350 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `TimeConditionViewController` allows the user to create a new time condition. +*/ + +import UIKit +import HomeKit + +/// Represents a section in the `TimeConditionViewController`. +enum TimeConditionTableViewSection: Int { + /** + This section contains the segmented control to + choose a time condition type. + */ + case TimeOrSun + + /** + This section contains cells to allow the selection + of 'before', 'after', or 'at'. 'At' is only available + when the exact time is specified. + */ + case BeforeOrAfter + + /** + If the condition type is exact time, this section will + only have one cell, the date picker cell. + + If the condition type is relative to a solar event, + this section will have two cells, one for 'sunrise' and + one for 'sunset. + */ + case Value + + static let count = 3 +} + +/** + Represents the type of time condition. + + The condition can be an exact time, or relative to a solar event. +*/ +enum TimeConditionType: Int { + case Time, Sun +} + +/** + Represents the type of solar event. + + This can be sunrise or sunset. +*/ +enum TimeConditionSunState: Int { + case Sunrise, Sunset +} + +/** + Represents the condition order. + + Conditions can be before, after, or exactly at a given time. +*/ +enum TimeConditionOrder: Int { + case Before, After, At +} + +/// A view controller that facilitates the creation of time conditions for triggers. +class TimeConditionViewController: HMCatalogViewController { + // MARK: Types + + struct Identifiers { + static let selectionCell = "SelectionCell" + static let timePickerCell = "TimePickerCell" + static let segmentedTimeCell = "SegmentedTimeCell" + } + + static let timeOrSunTitles = [ + NSLocalizedString("Relative to time", comment: "Relative to time"), + NSLocalizedString("Relative to sun", comment: "Relative to sun") + ] + + static let beforeOrAfterTitles = [ + NSLocalizedString("Before", comment: "Before"), + NSLocalizedString("After", comment: "After"), + NSLocalizedString("At", comment: "At") + ] + + static let sunriseSunsetTitles = [ + NSLocalizedString("Sunrise", comment: "Sunrise"), + NSLocalizedString("Sunset", comment: "Sunset") + ] + + // MARK: Properties + + private var timeType: TimeConditionType = .Time + private var order: TimeConditionOrder = .Before + private var sunState: TimeConditionSunState = .Sunrise + + private var datePicker: UIDatePicker? + + var triggerCreator: EventTriggerCreator? + + // MARK: View Methods + + /// Configures the table view. + override func viewDidLoad() { + super.viewDidLoad() + tableView.rowHeight = UITableViewAutomaticDimension + tableView.estimatedRowHeight = 44.0 + } + + // MARK: Table View Methods + + /// - returns: The number of `TimeConditionTableViewSection`s. + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return TimeConditionTableViewSection.count + } + + /** + - returns: The number rows based on the `TimeConditionTableViewSection` + and the `timeType`. + */ + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch TimeConditionTableViewSection(rawValue: section) { + case .TimeOrSun?: + return 1 + + case .BeforeOrAfter?: + // If we're choosing an exact time, we add the 'At' row. + return (timeType == .Time) ? 3 : 2 + + case .Value?: + // Date picker cell or sunrise/sunset selection cells + return (timeType == .Time) ? 1 : 2 + + case nil: + fatalError("Unexpected `TimeConditionTableViewSection` raw value.") + } + } + + /// Switches based on the section to generate a cell. + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + switch TimeConditionTableViewSection(rawValue: indexPath.section) { + case .TimeOrSun?: + return self.tableView(tableView, segmentedCellForRowAtIndexPath: indexPath) + + case .BeforeOrAfter?: + return self.tableView(tableView, selectionCellForRowAtIndexPath: indexPath) + + case .Value?: + switch timeType { + case .Time: + return self.tableView(tableView, datePickerCellForRowAtIndexPath: indexPath) + case .Sun: + return self.tableView(tableView, selectionCellForRowAtIndexPath: indexPath) + } + + case nil: + fatalError("Unexpected `TimeConditionTableViewSection` raw value.") + } + } + + /// - returns: A localized string describing the section. + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch TimeConditionTableViewSection(rawValue: section) { + case .TimeOrSun?: + return NSLocalizedString("Condition Type", comment: "Condition Type") + + case .BeforeOrAfter?: + return nil + + case .Value?: + if timeType == .Time { + return NSLocalizedString("Time", comment: "Time") + } + else { + return NSLocalizedString("Event", comment: "Event") + } + + case nil: + fatalError("Unexpected `TimeConditionTableViewSection` raw value.") + } + } + + /// - returns: A localized description for condition type section; `nil` otherwise. + override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + switch TimeConditionTableViewSection(rawValue: section) { + case .TimeOrSun?: + return NSLocalizedString("Time conditions can relate to specific times or special events, like sunrise and sunset.", comment: "Condition Type Description") + + case .BeforeOrAfter?: + return nil + + case .Value?: + return nil + + case nil: + fatalError("Unexpected `TimeConditionTableViewSection` raw value.") + } + } + + /// Updates internal values based on row selection. + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + let cell = tableView.cellForRowAtIndexPath(indexPath)! + if cell.selectionStyle == .None { + return + } + + tableView.deselectRowAtIndexPath(indexPath, animated: true) + + switch TimeConditionTableViewSection(rawValue: indexPath.section) { + case .TimeOrSun?: + timeType = TimeConditionType(rawValue: indexPath.row)! + reloadDynamicSections() + return + + case .BeforeOrAfter?: + order = TimeConditionOrder(rawValue: indexPath.row)! + tableView.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: .Automatic) + + case .Value?: + if timeType == .Sun { + sunState = TimeConditionSunState(rawValue: indexPath.row)! + } + tableView.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: .Automatic) + + case nil: + fatalError("Unexpected `TimeConditionTableViewSection` raw value.") + } + } + + // MARK: Helper Methods + + /** + Generates a selection cell based on the section. + Ordering and sun-state sections have selections. + */ + private func tableView(tableView: UITableView, selectionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.selectionCell, forIndexPath: indexPath) + switch TimeConditionTableViewSection(rawValue: indexPath.section) { + case .BeforeOrAfter?: + cell.textLabel?.text = TimeConditionViewController.beforeOrAfterTitles[indexPath.row] + cell.accessoryType = (order.rawValue == indexPath.row) ? .Checkmark : .None + + case .Value?: + if timeType == .Sun { + cell.textLabel?.text = TimeConditionViewController.sunriseSunsetTitles[indexPath.row] + cell.accessoryType = (sunState.rawValue == indexPath.row) ? .Checkmark : .None + } + + case nil: + fatalError("Unexpected `TimeConditionTableViewSection` raw value.") + + default: + break + } + return cell + } + + /// Generates a date picker cell and sets the internal date picker when created. + private func tableView(tableView: UITableView, datePickerCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.timePickerCell, forIndexPath: indexPath) as! TimePickerCell + // Save the date picker so we can get the result later. + datePicker = cell.datePicker + return cell + } + + /// Generates a segmented cell and sets its target when created. + private func tableView(tableView: UITableView, segmentedCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.segmentedTimeCell, forIndexPath: indexPath) as! SegmentedTimeCell + cell.segmentedControl.selectedSegmentIndex = timeType.rawValue + cell.segmentedControl.removeTarget(nil, action: nil, forControlEvents: .AllEvents) + cell.segmentedControl.addTarget(self, action: #selector(TimeConditionViewController.segmentedControlDidChange(_:)), forControlEvents: .ValueChanged) + return cell + } + + /// Creates date components from the date picker's date. + var dateComponents: NSDateComponents? { + guard let datePicker = datePicker else { return nil } + let flags: NSCalendarUnit = [.Hour, .Minute] + return NSCalendar.currentCalendar().components(flags, fromDate: datePicker.date) + } + + /** + Updates the time type and reloads dynamic sections. + + - parameter segmentedControl: The segmented control that changed. + */ + func segmentedControlDidChange(segmentedControl: UISegmentedControl) { + if let segmentedControlType = TimeConditionType(rawValue: segmentedControl.selectedSegmentIndex) { + timeType = segmentedControlType + } + reloadDynamicSections() + } + + /// Reloads the BeforeOrAfter and Value section. + private func reloadDynamicSections() { + if timeType == .Sun && order == .At { + order = .Before + } + let reloadIndexSet = NSIndexSet(indexesInRange: NSMakeRange(TimeConditionTableViewSection.BeforeOrAfter.rawValue, 2)) + tableView.reloadSections(reloadIndexSet, withRowAnimation: .Automatic) + } + + // MARK: IBAction Methods + + /** + Generates a predicate based on the stored values, adds + the condition to the trigger, then dismisses the view. + */ + @IBAction func saveAndDismiss(sender: UIBarButtonItem) { + var predicate: NSPredicate? + switch timeType { + case .Time: + switch order { + case .Before: + predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringBeforeDateWithComponents(dateComponents!) + + case .After: + predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringAfterDateWithComponents(dateComponents!) + + case .At: + predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringOnDateWithComponents(dateComponents!) + } + + case .Sun: + let significantEventString = (sunState == .Sunrise) ? HMSignificantEventSunrise : HMSignificantEventSunset + switch order { + case .Before: + predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringBeforeSignificantEvent(significantEventString, applyingOffset: nil) + + case .After: + predicate = HMEventTrigger.predicateForEvaluatingTriggerOccurringAfterSignificantEvent(significantEventString, applyingOffset: nil) + + case .At: + // Significant events must be specified 'before' or 'after'. + break + } + } + if let predicate = predicate { + triggerCreator?.addCondition(predicate) + } + dismissViewControllerAnimated(true, completion: nil) + } + + /// Cancels the creation of the conditions and exits. + @IBAction func dismiss(sender: UIBarButtonItem) { + dismissViewControllerAnimated(true, completion: nil) + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimePickerCell.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimePickerCell.swift new file mode 100644 index 00000000..b1219dc1 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Conditions/TimePickerCell.swift @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `TimePickerCell` has a date picker, used for selecting a specific time of day. +*/ + +import UIKit + +/// A `UITableViewCell` subclass with a `UIDatePicker`, used for selecting a specific time of day. +class TimePickerCell: UITableViewCell { + // MARK: Properties + + @IBOutlet weak var datePicker: UIDatePicker! +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerCreator.swift new file mode 100644 index 00000000..e78e1171 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerCreator.swift @@ -0,0 +1,140 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `EventTriggerCreator` is a superclass that creates Characteristic and Location triggers. +*/ + +import HomeKit + +/** + A superclass for event trigger creators. + + These classes manage the state for characteristic trigger conditions. +*/ +class EventTriggerCreator: TriggerCreator, CharacteristicCellDelegate { + // MARK: Properties + + /// A mapping of `HMCharacteristic`s to their values. + private let conditionValueMap = NSMapTable.strongToStrongObjectsMapTable() + + private var eventTrigger: HMEventTrigger? { + return trigger as? HMEventTrigger + } + + /** + An array of top-level `NSPredicate` objects. + + Currently, HMCatalog only supports top-level `NSPredicate`s + which have type `AndPredicateType`. + */ + var originalConditions: [NSPredicate] { + if let compoundPredicate = eventTrigger?.predicate as? NSCompoundPredicate, + subpredicates = compoundPredicate.subpredicates as? [NSPredicate] { + return subpredicates + } + + return [] + } + + /// An array of new conditions which will be written when the trigger is saved. + lazy var conditions: [NSPredicate] = self.originalConditions + + /** + Adds a predicate to the pending conditions. + + - parameter predicate: The new `NSPredicate` to add. + */ + func addCondition(predicate: NSPredicate) { + conditions.append(predicate) + } + + /** + Removes a predicate from the pending conditions. + + - parameter predicate: The `NSPredicate` to remove. + */ + func removeCondition(predicate: NSPredicate) { + if let index = conditions.indexOf(predicate) { + conditions.removeAtIndex(index) + } + } + + /** + - returns: The new `NSCompoundPredicate`, generated from + the pending conditions. + */ + func newPredicate() -> NSPredicate { + return NSCompoundPredicate(type: .AndPredicateType, subpredicates: conditions) + } + + /// Handles the value update and stores the value in the condition map. + func characteristicCell(cell: CharacteristicCell, didUpdateValue value: AnyObject, forCharacteristic characteristic: HMCharacteristic, immediate: Bool) { + conditionValueMap.setObject(value, forKey: characteristic) + } + + /** + Tries to use the value from the condition-value map, but falls back + to reading the characteristic's value from HomeKit. + */ + func characteristicCell(cell: CharacteristicCell, readInitialValueForCharacteristic characteristic: HMCharacteristic, completion: (AnyObject?, NSError?) -> Void) { + if let value = conditionValueMap.objectForKey(characteristic) { + completion(value, nil) + return + } + + characteristic.readValueWithCompletionHandler { error in + /* + The user may have updated the cell value while the + read was happening. We check the map one more time. + */ + if let value = self.conditionValueMap.objectForKey(characteristic) { + completion(value, nil) + } + else { + completion(characteristic.value, error) + } + } + } + + // MARK: Helper Methods + + /** + Updates the predicates and saves the new, generated + predicate to the event trigger. + */ + func savePredicate() { + updatePredicates() + dispatch_group_enter(saveTriggerGroup) + eventTrigger?.updatePredicate(newPredicate()) { error in + if let error = error { + self.errors.append(error) + } + dispatch_group_leave(self.saveTriggerGroup) + } + } + + /// Generates predicates from the characteristic-value map and adds them to the pending conditions. + func updatePredicates() { + for (characteristic, value) in pairsFromMapTable(conditionValueMap) { + let predicate = HMEventTrigger.predicateForEvaluatingTriggerWithCharacteristic(characteristic, relatedBy: .EqualToPredicateOperatorType, toValue: value) + addCondition(predicate) + } + + conditionValueMap.removeAllObjects() + } + + /** + - parameter table: The `NSMapTable` from which to generate the pairs. + + - returns: Tuples representing `HMCharacteristic`s and their associated return trigger values. + */ + func pairsFromMapTable(table: NSMapTable) -> [(HMCharacteristic, NSCopying)] { + return table.keyEnumerator().allObjects.map { object in + let characteristic = object as! HMCharacteristic + let triggerValue = table.objectForKey(object) as! NSCopying + return (characteristic, triggerValue) + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerViewController.swift new file mode 100644 index 00000000..66fc81bf --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/EventTriggerViewController.swift @@ -0,0 +1,250 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `EventTriggerViewController` is a superclass that helps users create Characteristic and Location triggers. +*/ + +import UIKit +import HomeKit + +/** + A superclass for all event-based view controllers. + + It handles the process of creating and managing trigger conditions. +*/ +class EventTriggerViewController: TriggerViewController { + // MARK: Types + + struct Identifiers { + static let addCell = "AddCell" + static let conditionCell = "ConditionCell" + static let showTimeConditionSegue = "Show Time Condition" + } + + // MARK: Properties + + private var eventTriggerCreator: EventTriggerCreator { + return triggerCreator as! EventTriggerCreator + } + + // MARK: View Methods + + /// Registers table view for cells. + override func viewDidLoad() { + super.viewDidLoad() + tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier:Identifiers.addCell) + tableView.registerClass(ConditionCell.self, forCellReuseIdentifier:Identifiers.conditionCell) + } + + /// Hands off the trigger creator to the condition view controllers. + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + switch segue.intendedDestinationViewController { + case let timeVC as TimeConditionViewController: + timeVC.triggerCreator = eventTriggerCreator + + case let characteristicEventVC as CharacteristicSelectionViewController: + let characteristicTriggerCreator = triggerCreator as! EventTriggerCreator + characteristicEventVC.triggerCreator = characteristicTriggerCreator + + default: + break + } + } + + // MARK: Table View Methods + + /** + - returns: In the conditions section: the number of conditions, plus one + for the add row. Defaults to the super implementation. + */ + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch sectionForIndex(section) { + case .Conditions?: + // Add row. + return eventTriggerCreator.conditions.count + 1 + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, numberOfRowsInSection: section) + } + } + + /** + Launchs "Add Condition" if the 'add index path' is selected. + Defaults to the super implementation. + */ + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + tableView.deselectRowAtIndexPath(indexPath, animated: true) + switch sectionForIndex(indexPath.section) { + case .Conditions?: + if indexPathIsAdd(indexPath) { + addCondition() + } + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + super.tableView(tableView, didSelectRowAtIndexPath: indexPath) + } + } + + /** + Switches to select the correct type of cell for the section. + Defaults to the super implementation. + */ + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + if indexPathIsAdd(indexPath) { + return self.tableView(tableView, addCellForRowAtIndexPath: indexPath) + } + + switch sectionForIndex(indexPath.section) { + case .Conditions?: + return self.tableView(tableView, conditionCellForRowAtIndexPath: indexPath) + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, cellForRowAtIndexPath: indexPath) + } + } + + /** + The conditions can be removed, the 'add index path' cannot. + For all others, default to super implementation. + */ + override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { + if indexPathIsAdd(indexPath) { + return false + } + + switch sectionForIndex(indexPath.section) { + case .Conditions?: + return true + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return false + } + } + + /// Remove the selected condition from the trigger creator. + override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { + if editingStyle == .Delete { + let predicate = eventTriggerCreator.conditions[indexPath.row] + eventTriggerCreator.removeCondition(predicate) + tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + } + } + + /// - returns: An 'add cell' with 'Add Condition' text. + func tableView(tableView: UITableView, addCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.addCell, forIndexPath: indexPath) + let cellText: String + switch sectionForIndex(indexPath.section) { + case .Conditions?: + cellText = NSLocalizedString("Add Condition…", comment: "Add Condition") + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + cellText = NSLocalizedString("Add…", comment: "Add") + } + + cell.textLabel?.text = cellText + cell.textLabel?.textColor = UIColor.editableBlueColor() + + return cell + } + + /// - returns: A localized description of a trigger. Falls back to super implementation. + override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + switch sectionForIndex(section) { + case .Conditions?: + return NSLocalizedString("When a trigger is activated by an event, it checks these conditions. If all of them are true, it will set its scenes.", comment: "Trigger Conditions Description") + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, titleForFooterInSection: section) + } + } + + // MARK: Helper Methods + + /// - returns: A 'condition cell', which displays information about the condition. + private func tableView(tableView: UITableView, conditionCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.conditionCell) as! ConditionCell + let condition = eventTriggerCreator.conditions[indexPath.row] + + switch condition.homeKitConditionType { + case .Characteristic(let characteristic, let value): + cell.setCharacteristic(characteristic, targetValue: value) + + case .ExactTime(let order, let dateComponents): + cell.setOrder(order, dateComponents: dateComponents) + + case .SunTime(let order, let sunState): + cell.setOrder(order, sunState: sunState) + + case .Unknown: + cell.setUnknown() + } + + return cell + } + + /// Presents an alert controller to choose the type of trigger. + private func addCondition() { + let title = NSLocalizedString("Add Condition", comment: "Add Condition") + let alertController = UIAlertController(title: title, message: nil, preferredStyle: .ActionSheet) + + // Time Condition. + let timeAction = UIAlertAction(title: NSLocalizedString("Time", comment: "Time"), style: .Default) { _ in + self.performSegueWithIdentifier(Identifiers.showTimeConditionSegue, sender: self) + } + alertController.addAction(timeAction) + + // Characteristic trigger. + let eventActionTitle = NSLocalizedString("Characteristic", comment: "Characteristic") + + let eventAction = UIAlertAction(title: eventActionTitle, style: .Default, handler: { _ in + if let triggerCreator = self.triggerCreator as? CharacteristicTriggerCreator { + triggerCreator.mode = .Condition + } + self.performSegueWithIdentifier("Select Characteristic", sender: self) + }) + + alertController.addAction(eventAction) + + // Cancel. + let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "Cancel"), style: .Cancel, handler: nil) + alertController.addAction(cancelAction) + + // Present alert. + presentViewController(alertController, animated: true, completion: nil) + } + + /// - returns: `true` if the index path is the 'add row'; `false` otherwise. + func indexPathIsAdd(indexPath: NSIndexPath) -> Bool { + switch sectionForIndex(indexPath.section) { + case .Conditions?: + return indexPath.row == eventTriggerCreator.conditions.count + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return false + } + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerCreator.swift new file mode 100644 index 00000000..72c46a76 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerCreator.swift @@ -0,0 +1,94 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `LocationTriggerCreator` creates Location triggers. +*/ + +import HomeKit +import MapKit + +/** + An `EventTriggerCreator` subclass which allows for the creation + of location triggers. +*/ +class LocationTriggerCreator: EventTriggerCreator, MapViewControllerDelegate { + // MARK: Properties + + var eventTrigger: HMEventTrigger? { + return trigger as? HMEventTrigger + } + var locationEvent: HMLocationEvent? + var targetRegion: CLCircularRegion? + var targetRegionStateIndex = 0 + + // MARK: Trigger Creator Methods + + /// Initializes location event, target region, and region state. + required init(trigger: HMTrigger?, home: HMHome) { + super.init(trigger: trigger, home: home) + if let eventTrigger = eventTrigger { + self.locationEvent = eventTrigger.locationEvent + if let region = locationEvent?.region as? CLCircularRegion { + self.targetRegion = region + } + self.targetRegionStateIndex = (self.targetRegion?.notifyOnEntry ?? true) ? 0 : 1 + + } + } + + /// Generates a new region and updates the location event. + override func updateTrigger() { + if let region = targetRegion { + prepareRegion() + if let locationEvent = locationEvent { + dispatch_group_enter(saveTriggerGroup) + locationEvent.updateRegion(region) { error in + if let error = error { + self.errors.append(error) + } + dispatch_group_leave(self.saveTriggerGroup) + } + } + } + + self.savePredicate() + } + + /** + - returns: A new `HMEventTrigger` with a new generated + location event and predicate. + */ + override func newTrigger() -> HMTrigger? { + var events = [HMLocationEvent]() + if let region = targetRegion { + prepareRegion() + events.append(HMLocationEvent(region: region)) + } + return HMEventTrigger(name: name, events: events, predicate: newPredicate()) + } + + // MARK: Helper Methods + + /** + Sets the `notifyOnEntry` and `notifyOnExit` region + properties based on the selected state. + */ + private func prepareRegion() { + if let region = targetRegion { + region.notifyOnEntry = (targetRegionStateIndex == 0) + region.notifyOnExit = !region.notifyOnEntry + } + } + + /** + Updates the target region from the one provided + by the delegate. + + - parameter region: A new `CLCircularRegion`, provided by the delegate. + */ + func mapViewDidUpdateRegion(region: CLCircularRegion) { + targetRegion = region + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerViewController.swift new file mode 100644 index 00000000..04de5847 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/LocationTriggerViewController.swift @@ -0,0 +1,248 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `LocationTriggerViewController` allows the user to modify and create Location triggers. +*/ + +import UIKit +import MapKit +import HomeKit +import AddressBookUI +import Contacts + +/// A view controller which facilitates the creation of a location trigger. +class LocationTriggerViewController: EventTriggerViewController { + + struct Identifiers { + static let locationCell = "LocationCell" + static let regionStatusCell = "RegionStatusCell" + static let selectLocationSegue = "Select Location" + } + + static let geocoder = CLGeocoder() + + static let regionStatusTitles = [ + NSLocalizedString("When I Enter The Area", comment: "When I Enter The Area"), + NSLocalizedString("When I Leave The Area", comment: "When I Leave The Area") + ] + + var locationTriggerCreator: LocationTriggerCreator { + return triggerCreator as! LocationTriggerCreator + } + + var localizedAddress: String? + + var viewIsDisplayed = false + + // MARK: View Methods + + /// Initializes a trigger creator and registers for table view cells. + override func viewDidLoad() { + super.viewDidLoad() + triggerCreator = LocationTriggerCreator(trigger: trigger, home: home) + tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.locationCell) + tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.regionStatusCell) + } + + /** + Generates an address string for the current region location and + reloads the table view. + */ + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + viewIsDisplayed = true + if let region = locationTriggerCreator.targetRegion { + let centerLocation = CLLocation(latitude: region.center.latitude, longitude: region.center.longitude) + LocationTriggerViewController.geocoder.reverseGeocodeLocation(centerLocation) { placemarks, error in + if !self.viewIsDisplayed { + // The geocoder took too long, we're not on this view any more. + return + } + if let error = error { + self.displayError(error) + return + } + if let mostLikelyPlacemark = placemarks?.first { + let address = CNMutablePostalAddress(placemark: mostLikelyPlacemark) + let addressFormatter = CNPostalAddressFormatter() + let addressString = addressFormatter.stringFromPostalAddress(address) + self.localizedAddress = addressString.stringByReplacingOccurrencesOfString("\n", withString: ", ") + let section = NSIndexSet(index: 2) + self.tableView.reloadSections(section, withRowAnimation: .Automatic) + } + } + } + tableView.reloadData() + } + + /// Passes the trigger creator and region into the `MapViewController`. + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + super.prepareForSegue(segue, sender: sender) + if segue.identifier == Identifiers.selectLocationSegue { + guard let destinationVC = segue.intendedDestinationViewController as? MapViewController else { return } + // Give the map the previous target region (if exists). + destinationVC.targetRegion = locationTriggerCreator.targetRegion + destinationVC.delegate = locationTriggerCreator + } + } + + override func viewWillDisappear(animated: Bool) { + super.viewWillDisappear(animated) + viewIsDisplayed = false + } + + // MARK: Table View Methods + + /** + - returns: The number of rows in the Region section; + defaults to the super implementation for other sections. + */ + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch sectionForIndex(section) { + case .Region?: + return 2 + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, numberOfRowsInSection: section) + } + } + + /** + Generates a cell based on the section. + Handles Region and Location sections, defaults to + super implementations for other sections. + */ + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + switch sectionForIndex(indexPath.section) { + case .Region?: + return self.tableView(tableView, regionStatusCellForRowAtIndexPath: indexPath) + + case .Location?: + return self.tableView(tableView, locationCellForRowAtIndexPath: indexPath) + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, cellForRowAtIndexPath: indexPath) + } + } + + /// Generates the single location cell. + private func tableView(tableView: UITableView, locationCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.locationCell, forIndexPath: indexPath) + cell.accessoryType = .DisclosureIndicator + + if locationTriggerCreator.targetRegion != nil { + cell.textLabel?.text = localizedAddress ?? NSLocalizedString("Update Location", comment: "Update Location") + } + else { + cell.textLabel?.text = NSLocalizedString("Set Location", comment: "Set Location") + } + return cell + } + + /// Generates the cell which allow the user to select either 'on enter' or 'on exit'. + private func tableView(tableView: UITableView, regionStatusCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.regionStatusCell, forIndexPath: indexPath) + cell.textLabel?.text = LocationTriggerViewController.regionStatusTitles[indexPath.row] + cell.accessoryType = (locationTriggerCreator.targetRegionStateIndex == indexPath.row) ? .Checkmark : .None + return cell + } + + /** + Allows the user to select a location or change the region status. + Defaults to the super implmentation for other sections. + */ + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + switch sectionForIndex(indexPath.section) { + case .Location?: + performSegueWithIdentifier(Identifiers.selectLocationSegue, sender: self) + + case .Region?: + locationTriggerCreator.targetRegionStateIndex = indexPath.row + let reloadIndexSet = NSIndexSet(index: indexPath.section) + tableView.reloadSections(reloadIndexSet, withRowAnimation: .Automatic) + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + super.tableView(tableView, didSelectRowAtIndexPath: indexPath) + } + } + + /** + - returns: A localized title for the Location and Region sections. + Defaults to the super implmentation for other sections. + */ + override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch sectionForIndex(section) { + case .Location?: + return NSLocalizedString("Location", comment: "Location") + + case .Region?: + return NSLocalizedString("Region Status", comment: "Region Status") + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, titleForHeaderInSection: section) + } + } + + /** + - returns: A localized description of the region status. + Defaults to the super implmentation for other sections. + */ + override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + switch sectionForIndex(section) { + case .Region?: + return NSLocalizedString("This trigger can activate when you enter or leave a region. For example, when you arrive at home or when you leave work.", comment: "Location Region Description") + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, titleForFooterInSection: section) + } + } + + // MARK: Trigger Controller Methods + + /** + - parameter index: The section index. + + - returns: The `TriggerTableViewSection` for the given index. + */ + override func sectionForIndex(index: Int) -> TriggerTableViewSection? { + switch index { + case 0: + return .Name + + case 1: + return .Enabled + + case 2: + return .Location + + case 3: + return .Region + + case 4: + return .Conditions + + case 5: + return .ActionSets + + default: + return nil + } + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapOverlayView.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapOverlayView.swift new file mode 100644 index 00000000..dbae61ed --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapOverlayView.swift @@ -0,0 +1,44 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `MapOverlayView` draws the circle over the `MapViewController`. +*/ + +import MapKit + +/** + A simple `UIView` subclass to draw a selection circle over + a MKMapView of the same size. +*/ +class MapOverlayView: UIView { + + /** + Draws a dashed circle in the center of the `rect` with + a radius 1/4th of the `rect`'s smallest side. + */ + override func drawRect(rect: CGRect) { + super.drawRect(rect) + let context = UIGraphicsGetCurrentContext() + + let strokeColor = UIColor.blueColor() + + let circleDiameter: CGFloat = min(rect.width, rect.height) / 2.0 + let circleRadius = circleDiameter / 2.0 + let cirlceRect = CGRect(x: rect.midX - circleRadius, y: rect.midY - circleRadius, width: circleDiameter, height: circleDiameter) + let circlePath = UIBezierPath(ovalInRect: cirlceRect) + + strokeColor.setStroke() + circlePath.lineWidth = 3 + CGContextSaveGState(context!) + CGContextSetLineDash(context!, 0, [6, 6], 2) + circlePath.stroke() + CGContextRestoreGState(context!) + } + + /// - returns: `false` to accept no touches. + override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { + return false + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapViewController.swift new file mode 100644 index 00000000..caaea012 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Event/Location/Mapping/MapViewController.swift @@ -0,0 +1,227 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `MapViewController` allow the user to select a location using the map. + This location will be passed back to the sender when the user saves the view. +*/ + +import UIKit +import MapKit + +/** + Allows the sender to get notified when there + have been changes to the region. +*/ +protocol MapViewControllerDelegate { + /** + Notifies the delegate that the `MapViewController`'s + region has been updated. + */ + func mapViewDidUpdateRegion(region: CLCircularRegion) +} + +/** + A view controller which allows the selection of a + circular region on a map. +*/ +class MapViewController: UIViewController, UISearchBarDelegate, CLLocationManagerDelegate, MKMapViewDelegate { + // MARK: Types + + struct Identifiers { + static let circularRegion = "MapViewController.Region" + } + + /// When the view loads, we'll zoom to this longitude/latitude span delta. + static let InitialZoomDelta: Double = 0.0015 + + /// When the view loads, we'll zoom into this span. + static let InitialZoomSpan = MKCoordinateSpan(latitudeDelta: MapViewController.InitialZoomDelta, longitudeDelta: MapViewController.InitialZoomDelta) + + // The inverse of the percentage of the map view that should be captured in the region. + static let MapRegionFraction: Double = 4.0 + + // The size of the query region with respect to the map's zoom. + static let RegionQueryDegreeMultiplier: Double = 5.0 + + // MARK: Properties + + @IBOutlet weak var overlayView: MapOverlayView! + @IBOutlet weak var searchBar: UISearchBar! + @IBOutlet weak var mapView: MKMapView! + + var delegate: MapViewControllerDelegate? + + var targetRegion: CLCircularRegion? + + var circleOverlay: MKCircle? { + didSet { + // Remove the old overlay (if exists) + if let oldOverlay = oldValue { + mapView.removeOverlay(oldOverlay) + } + + // Add the new overlay (if exists) + if let overlay = circleOverlay { + mapView.addOverlay(overlay) + } + } + } + + var locationManager = CLLocationManager() + + // MARK: View Methods + + /// Configures the map view, search bar and location manager. + override func viewDidLoad() { + super.viewDidLoad() + searchBar.delegate = self + mapView.delegate = self + mapView.showsUserLocation = true + mapView.pitchEnabled = false + locationManager.delegate = self + } + + /// Loads the user's location and zooms the target region. + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + locationManager.requestWhenInUseAuthorization() + locationManager.requestLocation() + + if let region = targetRegion { + annotateAndZoomToRegion(region) + } + } + + /// Updates the overlay when the orientation changes. + override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) { + overlayView.setNeedsDisplay() + } + + // MARK: Button Actions + + /** + Generates a map region based on the map's position + and zoom, then notifies the delegate that the region has changed. + This will dismiss the view. + */ + @IBAction func didTapSaveButton(sender: UIBarButtonItem) { + let circleDegreeDelta: CLLocationDegrees + let pointOnCircle: CLLocation + + if mapView.region.span.latitudeDelta > mapView.region.span.longitudeDelta { + circleDegreeDelta = mapView.region.span.longitudeDelta / MapViewController.MapRegionFraction + pointOnCircle = CLLocation(latitude: mapView.region.center.latitude, longitude: mapView.region.center.longitude - circleDegreeDelta) + } + else { + circleDegreeDelta = mapView.region.span.latitudeDelta / MapViewController.MapRegionFraction + pointOnCircle = CLLocation(latitude: mapView.region.center.latitude - circleDegreeDelta, longitude: mapView.region.center.longitude) + } + + + let mapCenterLocation = CLLocation(latitude: mapView.region.center.latitude, longitude: mapView.region.center.longitude) + let distance = pointOnCircle.distanceFromLocation(mapCenterLocation) + let genericRegion = CLCircularRegion(center: mapView.region.center, radius: distance, identifier: Identifiers.circularRegion) + + circleOverlay = MKCircle(centerCoordinate: genericRegion.center, radius: genericRegion.radius) + delegate?.mapViewDidUpdateRegion(genericRegion) + dismissViewControllerAnimated(true, completion: nil) + } + + /// Dismisses the view without notifying the delegate. + @IBAction func didTapCancelButton(sender: UIBarButtonItem) { + dismissViewControllerAnimated(true, completion: nil) + } + + // MARK: Search Bar Methods + + /** + Dismisses the keyboard and runs a new search from the + search bar. + */ + func searchBarSearchButtonClicked(searchBar: UISearchBar) { + searchBar.resignFirstResponder() + mapView.removeAnnotations(mapView.annotations) + performSearch() + } + + // MARK: Location Manager Methods + + /// Zooms to the user's location if the region is not set. + func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + guard let lastLocation = locations.last else { return } + if targetRegion != nil { + // Do not zoom to the user's location if there is already a target region. + return + } + let newRegion = MKCoordinateRegion(center: lastLocation.coordinate, span: MapViewController.InitialZoomSpan) + mapView.setRegion(newRegion, animated: true) + } + + /** + The method is required. + Simply logs the error. + */ + func locationManager(manager: CLLocationManager, didFailWithError error: NSError) { + print("System: Location Manager Error: \(error)") + } + + /** + When the user updates the authorization status, we want to + zoom to their current location by asking for it. + */ + func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) { + locationManager.requestLocation() + } + + // MARK: Helper Methods + + /** + Updates the region overlay and zooms the map region + + - parameter region: The new `CLCircularRegion`. + */ + private func annotateAndZoomToRegion(region: CLCircularRegion) { + circleOverlay = MKCircle(centerCoordinate: region.center, radius: region.radius) + let multiplier = MapViewController.MapRegionFraction + let mapRegion = MKCoordinateRegionMakeWithDistance(region.center, region.radius*multiplier, region.radius*multiplier) + mapView.setRegion(mapRegion, animated: false) + } + + /** + Performs a natural language search for locations + in the map's region that match the `searchBar`'s text. + */ + private func performSearch() { + let request = MKLocalSearchRequest() + request.naturalLanguageQuery = searchBar.text + let multiplier = MapViewController.RegionQueryDegreeMultiplier + let querySpan = MKCoordinateSpan(latitudeDelta: mapView.region.span.latitudeDelta*multiplier, longitudeDelta: mapView.region.span.longitudeDelta*multiplier) + request.region = MKCoordinateRegion(center: mapView.region.center, span: querySpan) + + let search = MKLocalSearch(request: request) + + var matchingItems = [MKMapItem]() + + search.startWithCompletionHandler { response, error in + let mapItems: [MKMapItem] = response?.mapItems ?? [] + for item in mapItems { + matchingItems.append(item) + let annotation = MKPointAnnotation() + annotation.coordinate = item.placemark.coordinate + annotation.title = item.name + self.mapView.addAnnotation(annotation) + } + } + } + + /// - returns: An `MKOverlayRenderer` with our custom stroke and fill. + func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer { + let circleRenderer = MKCircleRenderer(overlay: overlay) + circleRenderer.fillColor = UIColor.blueColor().colorWithAlphaComponent(0.2) + circleRenderer.strokeColor = UIColor.blackColor() + circleRenderer.lineWidth = 2.0 + return circleRenderer + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerCreator.swift new file mode 100644 index 00000000..1361496c --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerCreator.swift @@ -0,0 +1,153 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `TimerTriggerCreator` creates Timer triggers. +*/ + +import HomeKit + +/** + A `TriggerCreator` subclass which allows for the creation + of timer triggers. +*/ +class TimerTriggerCreator: TriggerCreator { + static let RecurrenceComponents: [NSCalendarUnit] = [ + .Hour, + .Day, + .WeekOfYear + ] + + // MARK: Properties + + var timerTrigger: HMTimerTrigger? { + return trigger as? HMTimerTrigger + } + + var selectedRecurrenceIndex = NSNotFound + + var rawFireDate = NSDate() + var fireDate: NSDate { + let flags: NSCalendarUnit = [.Year, .Weekday, .Month, .Day, .Hour, .Minute] + let dateComponents = NSCalendar.currentCalendar().components(flags, fromDate: self.rawFireDate) + let probableDate = NSCalendar.currentCalendar().dateFromComponents(dateComponents) + return probableDate ?? rawFireDate + } + + // MARK: Trigger Creator Methods + + /// Configures raw fire date and selected recurrence index. + required init(trigger: HMTrigger?, home: HMHome) { + super.init(trigger: trigger, home: home) + if let timerTrigger = timerTrigger { + rawFireDate = timerTrigger.fireDate + selectedRecurrenceIndex = recurrenceIndexFromDateComponents(timerTrigger.recurrence) + } + } + + /// - returns: A new `HMTimerTrigger` with the stored configurations. + override func newTrigger() -> HMTrigger? { + return HMTimerTrigger(name: name, fireDate: fireDate, timeZone: NSCalendar.currentCalendar().timeZone, recurrence: recurrenceComponents, recurrenceCalendar: nil) + } + + /// Updates the fire date and recurrence of the trigger. + override func updateTrigger() { + updateFireDateIfNecessary() + updateRecurrenceIfNecessary() + } + + // MARK: Helper Methods + + /** + Creates an NSDateComponent for the selected recurrence type. + + - returns: An NSDateComponent where either `weekOfYear`, + `hour`, or `day` is set to 1. + */ + var recurrenceComponents:NSDateComponents? { + if selectedRecurrenceIndex == NSNotFound { + return nil + } + let recurrenceComponents = NSDateComponents() + let unit = TimerTriggerCreator.RecurrenceComponents[selectedRecurrenceIndex] + switch unit { + case NSCalendarUnit.WeekOfYear: + recurrenceComponents.weekOfYear = 1 + + case NSCalendarUnit.Hour: + recurrenceComponents.hour = 1 + + case NSCalendarUnit.Day: + recurrenceComponents.day = 1 + + default: + break + } + return recurrenceComponents + } + + /** + Maps the possible calendar units associated with recurrence titles, so we can properly + set our recurrenceUnit when an index is selected. + + - parameter components: An optional `NSDateComponents` to query. + + - returns: An index for the date components. + */ + func recurrenceIndexFromDateComponents(components: NSDateComponents?) -> Int { + guard let components = components else { return NSNotFound } + var unit: NSCalendarUnit? + if components.day == 1 { + unit = NSCalendarUnit.Day + } + else if components.weekOfYear == 1 { + unit = NSCalendarUnit.WeekOfYear + } + else if components.hour == 1 { + unit = NSCalendarUnit.Hour + } + if let unit = unit { + return TimerTriggerCreator.RecurrenceComponents.indexOf(unit) ?? NSNotFound + } + return NSNotFound + } + + /** + Updates the trigger's fire date, entering and leaving the dispatch group if necessary. + If the trigger's fire date is already equal to the passed-in fire date, this method does nothing. + + - parameter fireDate: The trigger's new fire date. + */ + private func updateFireDateIfNecessary() { + if timerTrigger?.fireDate == fireDate { + return + } + dispatch_group_enter(saveTriggerGroup) + timerTrigger?.updateFireDate(fireDate) { error in + if let error = error { + self.errors.append(error) + } + dispatch_group_leave(self.saveTriggerGroup) + } + } + + /** + Updates the trigger's recurrence components, entering and leaving the dispatch group if necessary. + If the trigger's components are already equal to the passed-in components, this method does nothing. + + - parameter recurrenceComponents: The trigger's new recurrence components. + */ + private func updateRecurrenceIfNecessary() { + if recurrenceComponents == timerTrigger?.recurrence { + return + } + dispatch_group_enter(saveTriggerGroup) + timerTrigger?.updateRecurrence(recurrenceComponents) { error in + if let error = error { + self.errors.append(error) + } + dispatch_group_leave(self.saveTriggerGroup) + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerViewController.swift new file mode 100644 index 00000000..0c42b9c3 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/Timer/TimerTriggerViewController.swift @@ -0,0 +1,195 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `TimerTriggerViewController` allows the user to create Timer triggers. +*/ + +import UIKit +import HomeKit + +/// A view controller which facilitates the creation of timer triggers. +class TimerTriggerViewController: TriggerViewController { + // MARK: Types + + struct Identifiers { + static let recurrenceCell = "RecurrenceCell" + } + + static let RecurrenceTitles = [ + NSLocalizedString("Every Hour", comment: "Every Hour"), + NSLocalizedString("Every Day", comment: "Every Day"), + NSLocalizedString("Every Week", comment: "Every Week") + ] + + // MARK: Properties + + @IBOutlet weak var datePicker: UIDatePicker! + + /** + Sets the stored fireDate to the new value. + HomeKit only accepts dates aligned with minute boundaries, + so we use NSDateComponents to only get the appropriate pieces of information from that date. + Eventually we will end up with a date following this format: "MM/dd/yyyy hh:mm" + */ + + var timerTrigger: HMTimerTrigger? { + return trigger as? HMTimerTrigger + } + + var timerTriggerCreator: TimerTriggerCreator { + return triggerCreator as! TimerTriggerCreator + } + + // MARK: View Methods + + /// Configures the views and registers for table view cells. + override func viewDidLoad() { + super.viewDidLoad() + tableView.rowHeight = UITableViewAutomaticDimension + tableView.estimatedRowHeight = 44.0 + triggerCreator = TimerTriggerCreator(trigger: trigger, home: home) + datePicker.date = timerTriggerCreator.fireDate + tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.recurrenceCell) + } + + // MARK: IBAction Methods + + /// Reset our saved fire date to the date in the picker. + @IBAction func didChangeDate(picker: UIDatePicker) { + timerTriggerCreator.rawFireDate = picker.date + } + + // MARK: Table View Methods + + /** + - returns: The number of rows in the Recurrence section; + defaults to the super implementation for other sections + */ + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch sectionForIndex(section) { + case .Recurrence?: + return TimerTriggerViewController.RecurrenceTitles.count + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, numberOfRowsInSection: section) + } + } + + /** + Generates a recurrence cell. + Defaults to the super implementation for other sections + */ + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + switch sectionForIndex(indexPath.section) { + case .Recurrence?: + return self.tableView(tableView, recurrenceCellForRowAtIndexPath: indexPath) + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, cellForRowAtIndexPath: indexPath) + } + } + + /// Creates a cell that represents a recurrence type. + func tableView(tableView: UITableView, recurrenceCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.recurrenceCell, forIndexPath: indexPath) + let title = TimerTriggerViewController.RecurrenceTitles[indexPath.row] + cell.textLabel?.text = title + + // The current preferred recurrence style should have a check mark. + if indexPath.row == timerTriggerCreator.selectedRecurrenceIndex { + cell.accessoryType = .Checkmark + } + else { + cell.accessoryType = .None + } + return cell + } + + /** + Tell the tableView to automatically size the custom rows, while using the superclass's + static sizing for the static cells. + */ + override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + switch sectionForIndex(indexPath.section) { + case .Recurrence?: + return UITableViewAutomaticDimension + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, heightForRowAtIndexPath: indexPath) + } + } + + /** + Handles recurrence cell selection. + Defaults to the super implementation for other sections + */ + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + tableView.deselectRowAtIndexPath(indexPath, animated: true) + switch sectionForIndex(indexPath.section) { + case .Recurrence?: + self.tableView(tableView, didSelectRecurrenceComponentAtIndexPath: indexPath) + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + super.tableView(tableView, didSelectRowAtIndexPath: indexPath) + } + } + + /** + Handles selection of a recurrence cell. + + If the newly selected recurrence component is the previously selected + recurrence component, reset the current selected component to `NSNotFound` + and deselect that row. + */ + func tableView(tableView: UITableView, didSelectRecurrenceComponentAtIndexPath indexPath: NSIndexPath) { + if indexPath.row == timerTriggerCreator.selectedRecurrenceIndex { + timerTriggerCreator.selectedRecurrenceIndex = NSNotFound + } + else { + timerTriggerCreator.selectedRecurrenceIndex = indexPath.row + } + tableView.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: .Automatic) + } + + /** + - parameter index: The section index. + + - returns: The `TriggerTableViewSection` for the given index. + */ + override func sectionForIndex(index: Int) -> TriggerTableViewSection? { + switch index { + case 0: + return .Name + + case 1: + return .Enabled + + case 2: + return .DateAndTime + + case 3: + return .Recurrence + + case 4: + return .ActionSets + + default: + return nil + } + } + +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerCreator.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerCreator.swift new file mode 100644 index 00000000..2a5d5d25 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerCreator.swift @@ -0,0 +1,161 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `TriggerCreator` is a superclass that builds triggers. +*/ + +import HomeKit + +/** + A superclass for all trigger creators. + + These classes manage the temporary state of the trigger + and unify some of the saving processes. +*/ +class TriggerCreator { + // MARK: Properties + + internal var home: HMHome + internal var trigger: HMTrigger? + internal var name = "" + internal let saveTriggerGroup = dispatch_group_create() + internal var errors = [NSError]() + + /** + Initializes a trigger creator from an existing trigger (if it exists), + and the current home. + + - parameter trigger: An `HMTrigger` or `nil`, if creation is desired. + - parameter home: The `HMHome` into which this trigger will go. + */ + required init(trigger: HMTrigger?, home: HMHome) { + self.home = home + self.trigger = trigger + } + + /** + Completes one of two actions based on the current status of the `trigger` object: + + 1. Updates the existing trigger. + 2. Creates a new trigger. + + - parameter name: The name to set for the new or updated trigger. + - parameter actionSets: The new list of action sets to set for the trigger + - parameter completion: The closure to call when all configurations have been completed. + */ + func saveTriggerWithName(name: String, actionSets: [HMActionSet], completion: (HMTrigger?, [NSError]) -> Void) { + self.name = name + if trigger != nil { + // Let the subclass update the trigger. + updateTrigger() + updateNameIfNecessary() + configureWithActionSets(actionSets) + } + else { + self.trigger = newTrigger() + dispatch_group_enter(saveTriggerGroup) + home.addTrigger(trigger!) { error in + if let error = error { + self.errors.append(error) + } + else { + self.configureWithActionSets(actionSets) + } + dispatch_group_leave(self.saveTriggerGroup) + } + } + + /* + Call the completion block with our event trigger and any accumulated errors + from the saving process. + */ + dispatch_group_notify(saveTriggerGroup, dispatch_get_main_queue()) { + self.cleanUp() + completion(self.trigger, self.errors) + } + } + + /** + Updates the trigger's internals. + Action sets and the trigger name need not be configured. + + Implemented by subclasses. + */ + internal func updateTrigger() { } + + /** + Creates a new trigger to be added to the home. + Action sets and the trigger name need not be configured. + + Implemented by subclasses. + + - returns: A new, generated `HMTrigger`. + */ + internal func newTrigger() -> HMTrigger? { + return nil + } + + /** + Cleans up an internal structures after the trigger has been saved. + + Implemented by subclasses. + */ + internal func cleanUp() {} + + + // MARK: Helper Methods + + /** + Syncs the trigger's action sets with the specified array of action sets. + + - parameter actionSets: Array of `HMActionSet`s to match. + */ + private func configureWithActionSets(actionSets: [HMActionSet]) { + guard let trigger = trigger else { return } + /* + Save a standard completion handler to use when we either add or remove + an action set. + */ + let defaultCompletion: NSError? -> Void = { error in + // Leave the dispatch group, to notify that we've finished this task. + if let error = error { + self.errors.append(error) + } + dispatch_group_leave(self.saveTriggerGroup) + } + + // First pass, remove the action sets that have been deselected. + for actionSet in trigger.actionSets { + if actionSets.contains(actionSet) { + continue + } + dispatch_group_enter(saveTriggerGroup) + trigger.removeActionSet(actionSet, completionHandler: defaultCompletion) + } + + // Second pass, add the new action sets that were just selected. + for actionSet in actionSets { + if trigger.actionSets.contains(actionSet) { + continue + } + dispatch_group_enter(saveTriggerGroup) + trigger.addActionSet(actionSet, completionHandler: defaultCompletion) + } + } + + /// Updates the trigger's name from the stored name, entering and leaving the dispatch group if necessary. + func updateNameIfNecessary() { + if trigger?.name == self.name { + return + } + dispatch_group_enter(saveTriggerGroup) + trigger?.updateName(name) { error in + if let error = error { + self.errors.append(error) + } + dispatch_group_leave(self.saveTriggerGroup) + } + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerViewController.swift new file mode 100644 index 00000000..185cd232 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Triggers/TriggerViewController.swift @@ -0,0 +1,304 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `TriggerViewController` is a superclass which allows users to create triggers. +*/ + +import UIKit +import HomeKit + +/// Represents all possible sections in a `TriggerViewController` subclass. +enum TriggerTableViewSection: Int { + // All triggers have these sections. + case Name, Enabled, ActionSets + + // Timer triggers only. + case DateAndTime, Recurrence + + // Location and Characteristic triggers only. + case Conditions + + // Location triggers only. + case Location, Region + + // Characteristic triggers only. + case Characteristics +} + +/** + A superclass for all trigger view controllers. + + It manages the name, enabled state, and action set components of the view, + as these are shared components. +*/ +class TriggerViewController: HMCatalogViewController { + // MARK: Types + + struct Identifiers { + static let actionSetCell = "ActionSetCell" + } + + // MARK: Properties + + @IBOutlet weak var saveButton: UIBarButtonItem! + @IBOutlet weak var nameField: UITextField! + @IBOutlet weak var enabledSwitch: UISwitch! + + var trigger: HMTrigger? + var triggerCreator: TriggerCreator? + + /// An internal array of all action sets in the home. + var actionSets: [HMActionSet]! + + /** + An array of all action sets that the user has selected. + This will be used to save the trigger when it is finalized. + */ + lazy var selectedActionSets = [HMActionSet]() + + // MARK: View Methods + + /// Resets internal data, sets initial UI, and configures the table view. + override func viewDidLoad() { + super.viewDidLoad() + let filteredActionSets = home.actionSets.filter { actionSet in + return !actionSet.actions.isEmpty + } + + actionSets = filteredActionSets.sortByTypeAndLocalizedName() + tableView.rowHeight = UITableViewAutomaticDimension + tableView.estimatedRowHeight = 44.0 + + /* + If we have a trigger, set the saved properties to the current properties + of the passed-in trigger. + */ + if let trigger = trigger { + selectedActionSets = trigger.actionSets + nameField.text = trigger.name + enabledSwitch.on = trigger.enabled + } + + enableSaveButtonIfApplicable() + + tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: Identifiers.actionSetCell) + } + + // MARK: IBAction Methods + + /** + Any time the name field changed, reevaluate whether or not + to enable the save button. + */ + @IBAction func nameFieldDidChange(sender: UITextField) { + enableSaveButtonIfApplicable() + } + + /// Saves the trigger and dismisses this view controller. + @IBAction func saveAndDismiss() { + saveButton.enabled = false + triggerCreator?.saveTriggerWithName(trimmedName, actionSets: selectedActionSets) { trigger, errors in + self.trigger = trigger + self.saveButton.enabled = true + + if !errors.isEmpty { + self.displayErrors(errors) + return + } + + self.enableTrigger(self.trigger!) { + self.dismiss() + } + } + } + + @IBAction func dismiss() { + dismissViewControllerAnimated(true, completion: nil) + } + + // MARK: Subclass Methods + + /** + Generates the section for the index. + + This allows for the subclasses to lay out their content in different sections + while still maintaining common code in the `TriggerViewController`. + + - parameter index: The index of the section + + - returns: The `TriggerTableViewSection` for the provided index. + */ + func sectionForIndex(index: Int) -> TriggerTableViewSection? { + return nil + } + + // MARK: Helper Methods + + /// Enable the trigger if necessary. + func enableTrigger(trigger: HMTrigger, completion: Void -> Void) { + if trigger.enabled == enabledSwitch.on { + completion() + return + } + + trigger.enable(enabledSwitch.on) { error in + if let error = error { + self.displayError(error) + } + else { + completion() + } + } + } + + /** + Enables the save button if: + + 1. The name field is not empty, and + 2. There will be at least one action set in the trigger after saving. + */ + private func enableSaveButtonIfApplicable() { + saveButton.enabled = !trimmedName.characters.isEmpty && + (!selectedActionSets.isEmpty || trigger?.actionSets.count > 0) + } + + /// - returns: The name from the `nameField`, stripping newline and whitespace characters. + var trimmedName: String { + return nameField.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) + } + + // MARK: Table View Methods + + /// Creates a cell that represents either a selected or unselected action set cell. + private func tableView(tableView: UITableView, actionSetCellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.actionSetCell, forIndexPath: indexPath) + let actionSet = actionSets[indexPath.row] + + if selectedActionSets.contains(actionSet) { + cell.accessoryType = .Checkmark + } + else { + cell.accessoryType = .None + } + + cell.textLabel?.text = actionSet.name + + return cell + } + + + /// Only handles the ActionSets case, defaults to super. + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if sectionForIndex(section) == .ActionSets { + return actionSets.count ?? 0 + } + + return super.tableView(tableView, numberOfRowsInSection: section) + } + + /// Only handles the ActionSets case, defaults to super. + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + if sectionForIndex(indexPath.section) == .ActionSets { + return self.tableView(tableView, actionSetCellForRowAtIndexPath: indexPath) + } + + return super.tableView(tableView, cellForRowAtIndexPath: indexPath) + } + + /** + This is necessary for mixing static and dynamic table view cells. + We return a fake index path because otherwise the superclass's implementation (which does not + know about the extra cells we're adding) will cause an error. + + - returns: The superclass's indentationLevel for the first row in the provided section, + instead of the provided row. + */ + override func tableView(tableView: UITableView, indentationLevelForRowAtIndexPath indexPath: NSIndexPath) -> Int { + let newIndexPath = NSIndexPath(forRow: 0, inSection: indexPath.section) + + return super.tableView(tableView, indentationLevelForRowAtIndexPath: newIndexPath) + } + + /** + Tell the tableView to automatically size the custom rows, while using the superclass's + static sizing for the static cells. + */ + override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { + switch sectionForIndex(indexPath.section) { + case .Name?, .Enabled?: + return super.tableView(tableView, heightForRowAtIndexPath: indexPath) + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return UITableViewAutomaticDimension + } + } + + /// Handles row selction for action sets, defaults to super implementation. + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + tableView.deselectRowAtIndexPath(indexPath, animated: true) + if sectionForIndex(indexPath.section) == .ActionSets { + self.tableView(tableView, didSelectActionSetAtIndexPath: indexPath) + } + } + + /** + Manages footer titles for higher-level sections. Superclasses should fall back + on this implementation after attempting to handle any special trigger sections. + */ + override func tableView(tableView: UITableView, titleForFooterInSection section: Int) -> String? { + switch sectionForIndex(section) { + case .ActionSets?: + return NSLocalizedString("When this trigger is activated, it will set these scenes. You can only select scenes which have at least one action.", comment: "Scene Trigger Description") + + case .Enabled?: + return NSLocalizedString("This trigger will only activate if it is enabled. You can disable triggers to temporarily stop them from running.", comment: "Trigger Enabled Description") + + case nil: + fatalError("Unexpected `TriggerTableViewSection` raw value.") + + default: + return super.tableView(tableView, titleForFooterInSection: section) + } + } + + /** + Handle selection of an action set cell. If the action set is already part of the selected action sets, + then remove it from the selected list. Otherwise, add it to the selected list. + */ + func tableView(tableView: UITableView, didSelectActionSetAtIndexPath indexPath: NSIndexPath) { + let actionSet = actionSets[indexPath.row] + if let index = selectedActionSets.indexOf(actionSet) { + selectedActionSets.removeAtIndex(index) + } + else { + selectedActionSets.append(actionSet) + } + + enableSaveButtonIfApplicable() + tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + } + + // MARK: HMHomeDelegate Methods + + /** + If our trigger has been removed from the home, + dismiss the view controller. + */ + func home(home: HMHome, didRemoveTrigger trigger: HMTrigger) { + if self.trigger == trigger{ + dismissViewControllerAnimated(true, completion: nil) + } + } + + /// If our trigger has been updated, reload our data. + func home(home: HMHome, didUpdateTrigger trigger: HMTrigger) { + if self.trigger == trigger{ + tableView.reloadData() + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Zones/AddRoomViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Zones/AddRoomViewController.swift new file mode 100644 index 00000000..33aff7b3 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Zones/AddRoomViewController.swift @@ -0,0 +1,143 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `AddRoomViewController` allows the user to add rooms to a zone. +*/ + +import UIKit +import HomeKit + +/// A view controller that lists rooms within a home and allows the user to add the rooms to a provided zone. +class AddRoomViewController: HMCatalogViewController { + // MARK: Types + + struct Identifiers { + static let roomCell = "RoomCell" + } + + // MARK: Properties + + var homeZone: HMZone! + + lazy var displayedRooms = [HMRoom]() + lazy var selectedRooms = [HMRoom]() + + // MARK: View Methods + + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + title = homeZone.name + resetDisplayedRooms() + } + + /// Adds the selected rooms to the zone and dismisses the view. + @IBAction func dismiss(sender: AnyObject) { + addSelectedRoomsToZoneWithCompletionHandler { + self.dismissViewControllerAnimated(true, completion: nil) + } + } + + /** + Creates a dispatch group, adds all of the rooms to the zone, + and runs the provided completion once all rooms have been added. + + - parameter completion: A closure to call once all rooms have been added. + */ + func addSelectedRoomsToZoneWithCompletionHandler(completion: () -> Void) { + let group = dispatch_group_create() + for room in selectedRooms { + dispatch_group_enter(group) + homeZone.addRoom(room) { error in + if let error = error { + self.displayError(error) + } + dispatch_group_leave(group) + } + } + dispatch_group_notify(group, dispatch_get_main_queue(), completion) + } + + // MARK: Table View Methods + + /// - returns: The number of displayed rooms. + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return displayedRooms.count + } + + /// - returns: A cell that includes the name of a room and a checkmark if it's intended to be added to the zone. + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.roomCell, forIndexPath: indexPath) + + let room = displayedRooms[indexPath.row] + + cell.textLabel?.text = room.name + cell.accessoryType = selectedRooms.contains(room) ? .Checkmark : .None + + return cell + } + + /// Adds the selected room to the selected rooms array and reloads that cell + override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + let room = displayedRooms[indexPath.row] + + if let index = selectedRooms.indexOf(room) { + selectedRooms.removeAtIndex(index) + } + else { + selectedRooms.append(room) + } + + tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + } + + /// Resets the list of displayed rooms and reloads the table. + func resetDisplayedRooms() { + displayedRooms = home.roomsNotAlreadyInZone(homeZone, includingRooms: selectedRooms) + if displayedRooms.isEmpty { + dismissViewControllerAnimated(true, completion: nil) + } + else { + tableView.reloadData() + } + } + + // MARK: HMHomeDelegate Methods + + /// If our zone was removed, dismiss this view. + func home(home: HMHome, didRemoveZone zone: HMZone) { + if zone == homeZone { + dismissViewControllerAnimated(true, completion: nil) + } + } + + /// If our zone was renamed, reset our title. + func home(home: HMHome, didUpdateNameForZone zone: HMZone) { + if zone == homeZone { + title = zone.name + } + } + + // All home updates reset the displayed homes and reload the view. + + func home(home: HMHome, didUpdateNameForRoom room: HMRoom) { + resetDisplayedRooms() + } + + func home(home: HMHome, didAddRoom room: HMRoom) { + resetDisplayedRooms() + } + + func home(home: HMHome, didRemoveRoom room: HMRoom) { + resetDisplayedRooms() + } + + func home(home: HMHome, didAddRoom room: HMRoom, toZone zone: HMZone) { + resetDisplayedRooms() + } + + func home(home: HMHome, didRemoveRoom room: HMRoom, fromZone zone: HMZone) { + resetDisplayedRooms() + } +} diff --git a/HomeKitCatalog/HMCatalog/Homes/Zones/ZoneViewController.swift b/HomeKitCatalog/HMCatalog/Homes/Zones/ZoneViewController.swift new file mode 100644 index 00000000..485fec36 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Homes/Zones/ZoneViewController.swift @@ -0,0 +1,264 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `ZoneViewController` lists the rooms in a zone. +*/ + +import UIKit +import HomeKit + +/// A view controller that lists the rooms within a provided zone. +class ZoneViewController: HMCatalogViewController { + // MARK: Types + + struct Identifiers { + static let roomCell = "RoomCell" + static let addCell = "AddCell" + static let disabledAddCell = "DisabledAddCell" + static let addRoomsSegue = "Add Rooms" + } + + // MARK: Properties + + var homeZone: HMZone! + var rooms = [HMRoom]() + + // MARK: View Methods + + /// Reload the data and configure the view. + override func viewWillAppear(animated: Bool) { + super.viewWillAppear(animated) + title = homeZone.name + reloadData() + } + + /// If our data is invalid, pop the view controller. + override func viewDidAppear(animated: Bool) { + super.viewDidAppear(animated) + if shouldPopViewController() { + navigationController?.popViewControllerAnimated(true) + } + } + + /// Provide the zone to `AddRoomViewController`. + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + super.prepareForSegue(segue, sender: sender) + if segue.identifier == Identifiers.addRoomsSegue { + let addViewController = segue.intendedDestinationViewController as! AddRoomViewController + addViewController.homeZone = homeZone + } + } + + // MARK: Helper Methods + + /// Resets the internal list of rooms and reloads the table view. + private func reloadData() { + rooms = homeZone.rooms.sortByLocalizedName() + tableView.reloadData() + } + + /// Sorts the internal list of rooms by localized name. + private func sortRooms() { + rooms = rooms.sortByLocalizedName() + } + + /// - returns: The `NSIndexPath` where the 'Add Cell' should be located. + private var addIndexPath: NSIndexPath { + return NSIndexPath(forRow: rooms.count, inSection: 0) + } + + /** + - parameter indexPath: The index path in question. + + - returns: `true` if the indexPath should contain + an 'add' cell, `false` otherwise + */ + private func indexPathIsAdd(indexPath: NSIndexPath) -> Bool { + return indexPath.row == addIndexPath.row + } + + /** + Reloads the `addIndexPath`. + + This is typically used when something has changed to allow + the user to add a room. + */ + private func reloadAddIndexPath() { + tableView.reloadRowsAtIndexPaths([addIndexPath], withRowAnimation: .Automatic) + } + + /** + Adds a room to the internal array of rooms and inserts new row + into the table view. + + - parameter room: The new `HMRoom` to add. + */ + private func didAddRoom(room: HMRoom) { + rooms.append(room) + + sortRooms() + + if let newRoomIndex = rooms.indexOf(room) { + let newRoomIndexPath = NSIndexPath(forRow: newRoomIndex, inSection: 0) + tableView.insertRowsAtIndexPaths([newRoomIndexPath], withRowAnimation: .Automatic) + } + + reloadAddIndexPath() + } + + /** + Removes a room from the internal array of rooms and deletes + the row from the table view. + + - parameter room: The `HMRoom` to remove. + */ + private func didRemoveRoom(room: HMRoom) { + if let roomIndex = rooms.indexOf(room) { + rooms.removeAtIndex(roomIndex) + let roomIndexPath = NSIndexPath(forRow: roomIndex, inSection: 0) + tableView.deleteRowsAtIndexPaths([roomIndexPath], withRowAnimation: .Automatic) + } + + reloadAddIndexPath() + } + + /** + Reloads the cell corresponding a given room. + + - parameter room: The `HMRoom` to reload. + */ + private func didUpdateRoom(room: HMRoom) { + if let roomIndex = rooms.indexOf(room) { + let roomIndexPath = NSIndexPath(forRow: roomIndex, inSection: 0) + tableView.reloadRowsAtIndexPaths([roomIndexPath], withRowAnimation: .Automatic) + } + } + + /** + Removes a room from HomeKit and updates the view. + + - parameter room: The `HMRoom` to remove. + */ + private func removeRoom(room: HMRoom) { + didRemoveRoom(room) + homeZone.removeRoom(room) { error in + if let error = error { + self.displayError(error) + self.didAddRoom(room) + } + } + } + + /** + - returns: `true` if our current home no longer + exists, `false` otherwise. + */ + private func shouldPopViewController() -> Bool { + for zone in home.zones { + if zone == homeZone { + return false + } + } + return true + } + + /** + - returns: `true` if more rooms can be added to this zone; + `false` otherwise. + */ + private var canAddRoom: Bool { + return rooms.count < home.rooms.count + } + + // MARK: Table View Methods + + /// - returns: The number of rooms in the zone, plus 1 for the 'add' row. + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return rooms.count + 1 + } + + /// - returns: A cell containing the name of an HMRoom. + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + if indexPathIsAdd(indexPath) { + let reuseIdentifier = home.isAdmin && canAddRoom ? Identifiers.addCell : Identifiers.disabledAddCell + + return tableView.dequeueReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) + } + + let cell = tableView.dequeueReusableCellWithIdentifier(Identifiers.roomCell, forIndexPath: indexPath) + + cell.textLabel?.text = rooms[indexPath.row].name + + return cell + } + + /** + - returns: `true` if the cell is anything but an 'add' cell; + `false` otherwise. + */ + override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { + return home.isAdmin && !indexPathIsAdd(indexPath) + } + + /// Deletes the room at the provided index path. + override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { + if editingStyle == .Delete { + let room = rooms[indexPath.row] + + removeRoom(room) + } + } + + // MARK: HMHomeDelegate Methods + + /// If our zone was removed, pop the view controller. + func home(home: HMHome, didRemoveZone zone: HMZone) { + if zone == homeZone{ + navigationController?.popViewControllerAnimated(true) + } + } + + /// If our zone was renamed, update the title. + func home(home: HMHome, didUpdateNameForZone zone: HMZone) { + if zone == homeZone { + title = zone.name + } + } + + /// Update the row for the room. + func home(home: HMHome, didUpdateNameForRoom room: HMRoom) { + didUpdateRoom(room) + } + + /** + A room has been added, we may be able to add it to the zone. + Reload the 'addIndexPath' + */ + func home(home: HMHome, didAddRoom room: HMRoom) { + reloadAddIndexPath() + } + + /** + A room has been removed, attempt to remove it from the room. + This will always reload the 'addIndexPath'. + */ + func home(home: HMHome, didRemoveRoom room: HMRoom) { + didRemoveRoom(room) + } + + /// If the room was added to our zone, add it to the view. + func home(home: HMHome, didAddRoom room: HMRoom, toZone zone: HMZone) { + if zone == homeZone { + didAddRoom(room) + } + } + + /// If the room was removed from our zone, remove it from the view. + func home(home: HMHome, didRemoveRoom room: HMRoom, fromZone zone: HMZone) { + if zone == homeZone { + didRemoveRoom(room) + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..6aff7f55 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,139 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-Small-40@2x.png", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "Icon.png", + "scale" : "1x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "Icon@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small-1.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small@2x-1.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-Small-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-Small-40@2x-1.png", + "scale" : "2x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "Icon-Small-50.png", + "scale" : "1x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "Icon-Small-50@2x.png", + "scale" : "2x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "Icon-72.png", + "scale" : "1x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "Icon-72@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-83_5@2x.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "iTunesArtwork.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "iTunesArtwork@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png new file mode 100644 index 00000000..362a93a6 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png new file mode 100644 index 00000000..b775c601 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72.png new file mode 100644 index 00000000..05293876 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png new file mode 100644 index 00000000..e41d2860 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-72@2x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76.png new file mode 100644 index 00000000..4a9ce07b Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png new file mode 100644 index 00000000..326c688e Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-83_5@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-83_5@2x.png new file mode 100644 index 00000000..8e158097 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-83_5@2x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-1.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-1.png new file mode 100644 index 00000000..b07196be Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-1.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png new file mode 100644 index 00000000..74dc7c6d Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png new file mode 100644 index 00000000..b114c7a3 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png new file mode 100644 index 00000000..12a43145 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png new file mode 100644 index 00000000..da97d757 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png new file mode 100644 index 00000000..13c5af8f Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small.png new file mode 100644 index 00000000..b07196be Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png new file mode 100644 index 00000000..6113e73f Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png new file mode 100644 index 00000000..6113e73f Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png new file mode 100644 index 00000000..2e8d2304 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon.png new file mode 100644 index 00000000..b1aa344a Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon@2x.png new file mode 100644 index 00000000..5ea17591 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/Icon@2x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork.png new file mode 100644 index 00000000..84891266 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png new file mode 100644 index 00000000..84c50b5b Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/AppIcon.appiconset/iTunesArtwork@2x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Configure.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Configure.pdf new file mode 100644 index 00000000..8a53bc5a Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Configure.pdf differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Contents.json new file mode 100644 index 00000000..44e2bdb0 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Images.xcassets/ConfigureTabIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Configure.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Contents.json new file mode 100644 index 00000000..9d6c89ff --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Control.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Control.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Control.pdf new file mode 100644 index 00000000..09af6d0a Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/ControlTabIcon.imageset/Control.pdf differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/Contents.json new file mode 100644 index 00000000..db480e28 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FavoriteTabIcon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/FavoriteTabIcon.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/FavoriteTabIcon.pdf new file mode 100644 index 00000000..23eeb586 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/FavoritesTabIcon.imageset/FavoriteTabIcon.pdf differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/Contents.json new file mode 100644 index 00000000..2058f2a2 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "SelectedConfigureTabIcon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/SelectedConfigureTabIcon.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/SelectedConfigureTabIcon.pdf new file mode 100644 index 00000000..245876a4 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedConfigureTabIcon.imageset/SelectedConfigureTabIcon.pdf differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/Contents.json new file mode 100644 index 00000000..ef1356bd --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "SelectedControlTabIcon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/SelectedControlTabIcon.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/SelectedControlTabIcon.pdf new file mode 100644 index 00000000..5a5e5a1e Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedControlTabIcon.imageset/SelectedControlTabIcon.pdf differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/Contents.json new file mode 100644 index 00000000..043acf3d --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "SelectedFavoriteTabIcon.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/SelectedFavoriteTabIcon.pdf b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/SelectedFavoriteTabIcon.pdf new file mode 100644 index 00000000..ba2dc48b Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/SelectedFavoritesTabIcon.imageset/SelectedFavoriteTabIcon.pdf differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/Contents.json new file mode 100644 index 00000000..775fd030 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "stariosfilled.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stariosfilled@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "stariosfilled@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled.png new file mode 100644 index 00000000..29d49ced Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@2x.png new file mode 100644 index 00000000..0538ad84 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@2x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@3x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@3x.png new file mode 100644 index 00000000..57a5535b Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarFavorite.imageset/stariosfilled@3x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/Contents.json b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/Contents.json new file mode 100644 index 00000000..531a670e --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "starios.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "starios@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "starios@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios.png new file mode 100644 index 00000000..116373f5 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@2x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@2x.png new file mode 100644 index 00000000..3d6e3d15 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@2x.png differ diff --git a/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@3x.png b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@3x.png new file mode 100644 index 00000000..814f6c77 Binary files /dev/null and b/HomeKitCatalog/HMCatalog/Images.xcassets/StarNotFavorite.imageset/starios@3x.png differ diff --git a/HomeKitCatalog/HMCatalog/Launch Screen.storyboard b/HomeKitCatalog/HMCatalog/Launch Screen.storyboard new file mode 100644 index 00000000..b5f6eac5 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Launch Screen.storyboard @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HomeKitCatalog/HMCatalog/Main.storyboard b/HomeKitCatalog/HMCatalog/Main.storyboard new file mode 100644 index 00000000..747c8113 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Main.storyboard @@ -0,0 +1,2288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/Array+Sorting.swift b/HomeKitCatalog/HMCatalog/Supporting Files/Array+Sorting.swift new file mode 100644 index 00000000..4234debb --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/Array+Sorting.swift @@ -0,0 +1,68 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `Array+Sorting` extension allows for easy sorting of HomeKit objects. +*/ + +import HomeKit + +/// A protocol for objects which have a property called `name`. +protocol Nameable { + var name: String { get } +} + +/* + All of these HomeKit objects have names and can conform + to this protocol without modification. +*/ + +extension HMHome: Nameable {} +extension HMAccessory: Nameable {} +extension HMRoom: Nameable {} +extension HMZone: Nameable {} +extension HMActionSet: Nameable {} +extension HMService: Nameable {} +extension HMServiceGroup: Nameable {} +extension HMTrigger: Nameable {} + +extension CollectionType where Generator.Element: Nameable { + /** + Generates a new array from the original collection, + sorted by localized name. + + - returns: New array sorted by localized name. + */ + func sortByLocalizedName() -> [Generator.Element] { + return sort { return $0.name.localizedCompare($1.name) == .OrderedAscending } + } +} + +extension CollectionType where Generator.Element: HMActionSet { + /** + Generates a new array from the original collection, + sorted by built-in first, then user-defined sorted + by localized name. + + - returns: New array sorted by localized name. + */ + func sortByTypeAndLocalizedName() -> [HMActionSet] { + return sort { (actionSet1, actionSet2) -> Bool in + if actionSet1.isBuiltIn != actionSet2.isBuiltIn { + // If comparing a built-in and a user-defined, the built-in is ranked first. + return actionSet1.isBuiltIn + } + else if actionSet1.isBuiltIn && actionSet2.isBuiltIn { + // If comparing two built-ins, we follow a standard ranking + let firstIndex = HMActionSet.Constants.builtInActionSetTypes.indexOf(actionSet1.actionSetType) ?? NSNotFound + let secondIndex = HMActionSet.Constants.builtInActionSetTypes.indexOf(actionSet2.actionSetType) ?? NSNotFound + return firstIndex < secondIndex + } + else { + // If comparing two user-defines, sort by localized name. + return actionSet1.name.localizedCompare(actionSet2.name) == .OrderedAscending + } + } + } +} diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/CNMutablePostalAddress+Convenience.swift b/HomeKitCatalog/HMCatalog/Supporting Files/CNMutablePostalAddress+Convenience.swift new file mode 100644 index 00000000..df46b09e --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/CNMutablePostalAddress+Convenience.swift @@ -0,0 +1,22 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `CNMutablePostalAddress+Convenience` method creates `CNMutablePostalAddress` from a `CLPlacemark`. +*/ + +import MapKit +import Contacts + +extension CNMutablePostalAddress { + /// Constructs a `CNMutablePostalAddress` from a `CLPlacemark` + convenience init(placemark: CLPlacemark) { + self.init() + self.street = (placemark.subThoroughfare ?? "") + " " + (placemark.thoroughfare ?? "") + self.city = placemark.locality ?? "" + self.state = placemark.administrativeArea ?? "" + self.postalCode = placemark.postalCode ?? "" + self.country = placemark.country ?? "" + } +} diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/HMActionSet+BuiltIn.swift b/HomeKitCatalog/HMCatalog/Supporting Files/HMActionSet+BuiltIn.swift new file mode 100644 index 00000000..77421c1c --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/HMActionSet+BuiltIn.swift @@ -0,0 +1,20 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `HMActionSet+BuiltIn` extension provides a method for determining whether or not an action set is built-in. +*/ + +import HomeKit + +extension HMActionSet { + struct Constants { + static let builtInActionSetTypes = [HMActionSetTypeWakeUp, HMActionSetTypeHomeDeparture, HMActionSetTypeHomeArrival, HMActionSetTypeSleep] + } + + /// - returns: `true` if the action set is built-in; `false` otherwise. + var isBuiltIn: Bool { + return Constants.builtInActionSetTypes.contains(self.actionSetType) + } +} diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/HMCharacteristic+Properties.swift b/HomeKitCatalog/HMCatalog/Supporting Files/HMCharacteristic+Properties.swift new file mode 100644 index 00000000..7954b26f --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/HMCharacteristic+Properties.swift @@ -0,0 +1,428 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `HMCharacteristic+Properties` methods are used to generate localized strings related to a characteristic or to evaluate a characteristic's type. +*/ + +import HomeKit + +extension HMCharacteristic { + + private struct Constants { + static let valueFormatter = NSNumberFormatter() + static let numericFormats = [ + HMCharacteristicMetadataFormatInt, + HMCharacteristicMetadataFormatFloat, + HMCharacteristicMetadataFormatUInt8, + HMCharacteristicMetadataFormatUInt16, + HMCharacteristicMetadataFormatUInt32, + HMCharacteristicMetadataFormatUInt64 + ] + } + + /** + Returns the localized description for a provided value, taking the characteristic's metadata and possible + values into account. + + - parameter value: The value to look up. + + - returns: A string representing the value in a localized way, e.g. `"24%"` or `"354º"` + */ + func localizedDescriptionForValue(value: AnyObject) -> String { + if self.isWriteOnly { + return NSLocalizedString("Write-Only Characteristic", comment: "Write-Only Characteristic") + } + else if self.isBoolean { + if let boolValue = value.boolValue { + return boolValue ? NSLocalizedString("On", comment: "On") : NSLocalizedString("Off", comment: "Off") + } + } + if let number = value as? Int { + if let predeterminedValueString = self.predeterminedValueDescriptionForNumber(number) { + return predeterminedValueString + } + + if let stepValue = self.metadata?.stepValue { + Constants.valueFormatter.minimumFractionDigits = Int(log10(1.0 / stepValue.doubleValue)) + if let string = Constants.valueFormatter.stringFromNumber(number) { + return string + self.localizedUnitDecoration + } + } + } + return "\(value)" + } + + /** + - parameter number: The value of this characteristic. + + - returns: An optional, localized string for the value. + */ + func predeterminedValueDescriptionForNumber(number: Int) -> String? { + switch self.characteristicType { + case HMCharacteristicTypePowerState, HMCharacteristicTypeInputEvent, HMCharacteristicTypeOutputState: + if Bool(number) { + return NSLocalizedString("On", comment: "On") + } + else { + return NSLocalizedString("Off", comment: "Off") + } + + case HMCharacteristicTypeOutletInUse, HMCharacteristicTypeMotionDetected, HMCharacteristicTypeAdminOnlyAccess, HMCharacteristicTypeAudioFeedback, HMCharacteristicTypeObstructionDetected: + if Bool(number) { + return NSLocalizedString("Yes", comment: "Yes") + } + else { + return NSLocalizedString("No", comment: "No") + } + + case HMCharacteristicTypeTargetDoorState, HMCharacteristicTypeCurrentDoorState: + if let doorState = HMCharacteristicValueDoorState(rawValue: number) { + switch doorState { + case .Open: + return NSLocalizedString("Open", comment: "Open") + + case .Opening: + return NSLocalizedString("Opening", comment: "Opening") + + case .Closed: + return NSLocalizedString("Closed", comment: "Closed") + + case .Closing: + return NSLocalizedString("Closing", comment: "Closing") + + case .Stopped: + return NSLocalizedString("Stopped", comment: "Stopped") + } + } + + case HMCharacteristicTypeTargetHeatingCooling: + if let mode = HMCharacteristicValueHeatingCooling(rawValue: number) { + switch mode { + case .Off: + return NSLocalizedString("Off", comment: "Off") + + case .Heat: + return NSLocalizedString("Heat", comment: "Heat") + + case .Cool: + return NSLocalizedString("Cool", comment: "Cool") + + case .Auto: + return NSLocalizedString("Auto", comment: "Auto") + } + } + + case HMCharacteristicTypeCurrentHeatingCooling: + if let mode = HMCharacteristicValueHeatingCooling(rawValue: number) { + switch mode { + case .Off: + return NSLocalizedString("Off", comment: "Off") + + case .Heat: + return NSLocalizedString("Heating", comment: "Heating") + + case .Cool: + return NSLocalizedString("Cooling", comment: "Cooling") + + case .Auto: + return NSLocalizedString("Auto", comment: "Auto") + } + } + + case HMCharacteristicTypeTargetLockMechanismState, HMCharacteristicTypeCurrentLockMechanismState: + if let lockState = HMCharacteristicValueLockMechanismState(rawValue: number) { + switch lockState { + case .Unsecured: + return NSLocalizedString("Unsecured", comment: "Unsecured") + + case .Secured: + return NSLocalizedString("Secured", comment: "Secured") + + case .Unknown: + return NSLocalizedString("Unknown", comment: "Unknown") + + case .Jammed: + return NSLocalizedString("Jammed", comment: "Jammed") + } + } + + case HMCharacteristicTypeTemperatureUnits: + if let unit = HMCharacteristicValueTemperatureUnit(rawValue: number) { + switch unit { + case .Celsius: + return NSLocalizedString("Celsius", comment: "Celsius") + + case .Fahrenheit: + return NSLocalizedString("Fahrenheit", comment: "Fahrenheit") + } + } + + case HMCharacteristicTypeLockMechanismLastKnownAction: + if let lastKnownAction = HMCharacteristicValueLockMechanismLastKnownAction(rawValue: number) { + switch lastKnownAction { + case .SecuredUsingPhysicalMovementInterior: + return NSLocalizedString("Interior Secured", comment: "Interior Secured") + + case .UnsecuredUsingPhysicalMovementInterior: + return NSLocalizedString("Exterior Unsecured", comment: "Exterior Unsecured") + + case .SecuredUsingPhysicalMovementExterior: + return NSLocalizedString("Exterior Secured", comment: "Exterior Secured") + + case .UnsecuredUsingPhysicalMovementExterior: + return NSLocalizedString("Exterior Unsecured", comment: "Exterior Unsecured") + + case .SecuredWithKeypad: + return NSLocalizedString("Keypad Secured", comment: "Keypad Secured") + + case .UnsecuredWithKeypad: + return NSLocalizedString("Keypad Unsecured", comment: "Keypad Unsecured") + + case .SecuredRemotely: + return NSLocalizedString("Secured Remotely", comment: "Secured Remotely") + + case .UnsecuredRemotely: + return NSLocalizedString("Unsecured Remotely", comment: "Unsecured Remotely") + + case .SecuredWithAutomaticSecureTimeout: + return NSLocalizedString("Secured Automatically", comment: "Secured Automatically") + + case .SecuredUsingPhysicalMovement: + return NSLocalizedString("Secured Using Physical Movement", comment: "Secured Using Physical Movement") + + case .UnsecuredUsingPhysicalMovement: + return NSLocalizedString("Unsecured Using Physical Movement", comment: "Unsecured Using Physical Movement") + } + } + + case HMCharacteristicTypeRotationDirection: + if let rotationDirection = HMCharacteristicValueRotationDirection(rawValue: number) { + switch rotationDirection { + case .Clockwise: + return NSLocalizedString("Clockwise", comment: "Clockwise") + + case .CounterClockwise: + return NSLocalizedString("Counter Clockwise", comment: "Counter Clockwise") + } + } + + case HMCharacteristicTypeAirParticulateSize: + if let size = HMCharacteristicValueAirParticulateSize(rawValue: number) { + switch size { + case .Size10: + return NSLocalizedString("Size 10", comment: "Size 10") + + case .Size2_5: + return NSLocalizedString("Size 2.5", comment: "Size 2.5") + } + } + + case HMCharacteristicTypePositionState: + if let state = HMCharacteristicValuePositionState(rawValue: number) { + switch state { + case .Opening: + return NSLocalizedString("Opening", comment: "Opening") + + case .Closing: + return NSLocalizedString("Closing", comment: "Closing") + + case .Stopped: + return NSLocalizedString("Stopped", comment: "Stopped") + } + } + + case HMCharacteristicTypeCurrentSecuritySystemState: + if let state = HMCharacteristicValueCurrentSecuritySystemState(rawValue: number) { + switch state { + case .AwayArm: + return NSLocalizedString("Away", comment: "Away") + + case .StayArm: + return NSLocalizedString("Home", comment: "Home") + + case .NightArm: + return NSLocalizedString("Night", comment: "Night") + + case .Disarmed: + return NSLocalizedString("Disarm", comment: "Disarm") + + case .Triggered: + return NSLocalizedString("Triggered", comment: "Triggered") + } + } + + case HMCharacteristicTypeTargetSecuritySystemState: + if let state = HMCharacteristicValueTargetSecuritySystemState(rawValue: number) { + switch state { + case .AwayArm: + return NSLocalizedString("Away", comment: "Away") + + case .StayArm: + return NSLocalizedString("Home", comment: "Home") + + case .NightArm: + return NSLocalizedString("Night", comment: "Night") + + case .Disarm: + return NSLocalizedString("Disarm", comment: "Disarm") + } + } + + default: + break + } + return nil + } + + var supportsEventNotification: Bool { + return self.properties.contains(HMCharacteristicPropertySupportsEventNotification) + } + + /// - returns: A string representing the value in a localized way, e.g. `"24%"` or `"354º"` + var localizedValueDescription: String { + if let value = value { + return self.localizedDescriptionForValue(value) + } + return "" + } + + /// - returns: The decoration for the characteristic's units, localized, e.g. `"%"` or `"º"` + var localizedUnitDecoration: String { + if let units = self.metadata?.units { + switch units { + case HMCharacteristicMetadataUnitsCelsius: + return NSLocalizedString("℃", comment: "Degrees Celsius") + + case HMCharacteristicMetadataUnitsArcDegree: + return NSLocalizedString("º", comment: "Arc Degrees") + + case HMCharacteristicMetadataUnitsFahrenheit: + return NSLocalizedString("℉", comment: "Degrees Fahrenheit") + + case HMCharacteristicMetadataUnitsPercentage: + return NSLocalizedString("%", comment: "Percentage") + + default: break + } + } + return "" + } + + /// - returns: The type of the characteristic, e.g. `"Current Lock Mechanism State"` + var localizedCharacteristicType: String { + var type = self.localizedDescription + + var localizedDescription: NSString? = nil + if isReadOnly { + localizedDescription = NSLocalizedString("Read Only", comment: "Read Only") + } + else if isWriteOnly { + localizedDescription = NSLocalizedString("Write Only", comment: "Write Only") + } + + if let localizedDescription = localizedDescription { + type = type + " (\(localizedDescription))" + } + + return type + } + + /// - returns: `true` if this characteristic has numeric values that are all integers; `false` otherwise. + var isInteger: Bool { + return self.isNumeric && !self.isFloatingPoint + } + + /** + - returns: `true` if this characteristic has numeric values; + `false` otherwise. + */ + var isNumeric: Bool { + guard let metadata = metadata else { return false } + guard let format = metadata.format else { return false } + return Constants.numericFormats.contains(format) + } + + /// - returns: `true` if this characteristic is boolean; `false` otherwise. + var isBoolean: Bool { + guard let metadata = metadata else { return false } + return metadata.format == HMCharacteristicMetadataFormatBool + } + + /** + - returns: `true` if this characteristic is text-writable; + `false` otherwise. + */ + var isTextWritable: Bool { + guard let metadata = metadata else { return false } + return metadata.format == HMCharacteristicMetadataFormatString && properties.contains(HMCharacteristicPropertyWritable) + } + + /** + - returns: `true` if this characteristic has numeric values + that are all floating point; `false` otherwise. + */ + var isFloatingPoint: Bool { + guard let metadata = metadata else { return false } + return metadata.format == HMCharacteristicMetadataFormatFloat + } + + /// - returns: `true` if characteristic is read only; `false` otherwise. + var isReadOnly: Bool { + return !properties.contains(HMCharacteristicPropertyWritable) && + properties.contains(HMCharacteristicPropertyReadable) + } + + /** + - returns: `true` if this characteristic is write only; + `false` otherwise. + */ + var isWriteOnly: Bool { + return !properties.contains(HMCharacteristicPropertyReadable) && + properties.contains(HMCharacteristicPropertyWritable) + } + + /** + - returns: `true` if this characteristic is the 'Identify' + characteristic; `false` otherwise. + */ + var isIdentify: Bool { + return self.characteristicType == HMCharacteristicTypeIdentify + } + + /** + - returns: The number of possible values that this characteristic can contain. + The standard formula for the number of values between two numbers is + `((greater - lesser) + 1)`, and this takes step value into account. + */ + var numberOfChoices: Int { + guard let metadata = metadata, minimumValue = metadata.minimumValue as? Int else { return 0 } + guard let maximumValue = metadata.maximumValue as? Int else { return 0 } + var range = maximumValue - minimumValue + if let stepValue = metadata.stepValue as? Double { + range = Int(Double(range) / stepValue) + } + return range + 1 + } + + /// - returns: All of the possible values that this characteristic can contain. + var allPossibleValues: [AnyObject]? { + guard self.isInteger else { return nil } + guard let metadata = metadata, stepValue = metadata.stepValue as? Double else { return nil } + let choices = Array(0.. [HMService] in + return accumulator + accessory.services.filter { return !accumulator.contains($0) } + }) + } + + /// All the characteristics within all of the services within the home. + var allCharacteristics: [HMCharacteristic] { + return allServices.reduce([], combine: { (accumulator, service) -> [HMCharacteristic] in + return accumulator + service.characteristics.filter { return !accumulator.contains($0) } + }) + } + + /** + - returns: A dictionary mapping localized service types to an array + of all services of that type. + */ + var serviceTable: [String: [HMService]] { + var serviceDictionary = [String: [HMService]]() + for service in self.allServices { + if !service.isControlType { + continue + } + + let serviceType = service.localizedDescription + var existingServices: [HMService] = serviceDictionary[serviceType] ?? [HMService]() + existingServices.append(service) + serviceDictionary[service.localizedDescription] = existingServices + } + + for (serviceType, services) in serviceDictionary { + serviceDictionary[serviceType] = services.sort { + return $0.accessory!.name.localizedCompare($1.accessory!.name) == .OrderedAscending + } + } + return serviceDictionary + } + + /// - returns: All rooms in the home, including `roomForEntireHome`. + var allRooms: [HMRoom] { + let allRooms = [self.roomForEntireHome()] + self.rooms + return allRooms.sortByLocalizedName() + } + + /// - returns: `true` if the current user is the admin of this home; `false` otherwise. + var isAdmin: Bool { + return self.homeAccessControlForUser(currentUser).administrator + } + + /// - returns: All accessories which are 'control accessories'. + var sortedControlAccessories: [HMAccessory] { + let filteredAccessories = self.accessories.filter { accessory -> Bool in + for service in accessory.services { + if service.isControlType { + return true + } + } + return false + } + return filteredAccessories.sortByLocalizedName() + } + + /** + - parameter identifier: The UUID to look up. + + - returns: The accessory within the receiver that matches the given UUID, + or nil if there is no accessory with that UUID. + */ + func accessoryWithIdentifier(identifier: NSUUID) -> HMAccessory? { + for accessory in self.accessories { + if accessory.uniqueIdentifier == identifier { + return accessory + } + } + return nil + } + + /** + - parameter identifiers: An array of `NSUUID`s that match accessories in the receiver. + + - returns: An array of `HMAccessory` instances corresponding to + the UUIDs passed in. + */ + func accessoriesWithIdentifiers(identifiers: [NSUUID]) -> [HMAccessory] { + return self.accessories.filter { accessory in + identifiers.contains(accessory.uniqueIdentifier) + } + } + + /** + Searches through the home's accessories to find the accessory + that is bridging the provided accessory. + + - parameter accessory: The bridged accessory. + + - returns: The accessory bridging the bridged accessory. + */ + func bridgeForAccessory(accessory: HMAccessory) -> HMAccessory? { + if !accessory.bridged { + return nil + } + for bridge in self.accessories { + if let identifiers = bridge.uniqueIdentifiersForBridgedAccessories where identifiers.contains(accessory.uniqueIdentifier) { + return bridge + } + } + return nil + } + + /** + - parameter room: The room. + + - returns: The name of the room, appending "Default Room" if the room + is the home's `roomForEntireHome` + */ + func nameForRoom(room: HMRoom) -> String { + if room == self.roomForEntireHome() { + let defaultRoom = NSLocalizedString("Default Room", comment: "Default Room") + return room.name + " (\(defaultRoom))" + } + return room.name + } + + /** + - parameter zone: The zone. + - parameter rooms: A list of rooms to add to the final list. + + - returns: A list of rooms that exist in the home and have not + yet been added to this zone. + */ + func roomsNotAlreadyInZone(zone: HMZone, includingRooms rooms: [HMRoom]? = nil) -> [HMRoom] { + var filteredRooms = self.rooms.filter { room in + return !zone.rooms.contains(room) + } + if let rooms = rooms { + filteredRooms += rooms + } + return filteredRooms + } + + /** + - parameter home: The home. + - parameter serviceGroup: The service group. + - parameter services: A list of services to add to the final list. + + - returns: A list of services that exist in the home and have not yet been added to this service group. + */ + func servicesNotAlreadyInServiceGroup(serviceGroup: HMServiceGroup, includingServices services: [HMService]? = nil) -> [HMService] { + var filteredServices = self.allServices.filter { service in + /* + Exclude services that are already in the service group + and the accessory information service. + */ + return !serviceGroup.services.contains(service) && service.serviceType != HMServiceTypeAccessoryInformation + } + if let services = services { + filteredServices += services + } + return filteredServices + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/HMService+Properties.swift b/HomeKitCatalog/HMCatalog/Supporting Files/HMService+Properties.swift new file mode 100644 index 00000000..e97597b6 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/HMService+Properties.swift @@ -0,0 +1,47 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `HMService+Properties` methods provide convenience methods for deconstructing `HMService` objects. +*/ + +import HomeKit + +extension HMService { + struct Constants { + static let serviceMap = [ + HMServiceTypeLightbulb: NSLocalizedString("Lightbulb", comment: "Lightbulb"), + HMServiceTypeFan: NSLocalizedString("Fan", comment: "Fan") + ] + } + + /** + - parameter serviceType: The service type. + + - returns: A localized description of that service type or + the original `type` string if one cannot be found. + */ + class func localizedDescriptionForServiceType(type: String) -> String { + return Constants.serviceMap[type] ?? type + } + + /// - returns: `true` if this service supports the `associatedServiceType` property; `false` otherwise. + var supportsAssociatedServiceType: Bool { + return self.serviceType == HMServiceTypeOutlet || self.serviceType == HMServiceTypeSwitch + } + + /// - returns: `true` if this service is a 'control type'; `false` otherwise. + var isControlType: Bool { + let noncontrolTypes = [HMServiceTypeAccessoryInformation, HMServiceTypeLockManagement] + return !noncontrolTypes.contains(self.serviceType) + } + + /** + - returns: The valid associated service types for this service, + e.g. `HMServiceTypeFan` or `HMServiceTypeLightbulb` + */ + class var validAssociatedServiceTypes: [String] { + return [HMServiceTypeFan, HMServiceTypeLightbulb] + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/Info.plist b/HomeKitCatalog/HMCatalog/Supporting Files/Info.plist new file mode 100644 index 00000000..0357a312 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/Info.plist @@ -0,0 +1,58 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 2.0.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocationWhenInUseUsageDescription + HMCatalog needs your location to search for relevant places in your area. + NSHomeKitUsageDescription + HMCatalog needs access top your HomeKit devices. + UILaunchStoryboardName + Launch Screen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullscreen + + UIStatusBarHidden + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/NSPredicate+Condition.swift b/HomeKitCatalog/HMCatalog/Supporting Files/NSPredicate+Condition.swift new file mode 100644 index 00000000..67c116dd --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/NSPredicate+Condition.swift @@ -0,0 +1,162 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `NSPredicate+Condition` properties and methods are used to parse the conditions used in `HMEventTrigger`s. +*/ + +import HomeKit + +/// Represents condition type in HomeKit with associated values. +enum HomeKitConditionType { + /** + Represents a characteristic condition. + + The tuple represents the `HMCharacteristic` and its condition value. + For example, "Current gargage door is set to 'Open'". + */ + case Characteristic(HMCharacteristic, NSCopying) + + /** + Represents a time condition. + + The tuple represents the time ordering and the sun state. + For example, "Before sunset". + */ + case SunTime(TimeConditionOrder, TimeConditionSunState) + + /** + Represents an exact time condition. + + The tuple represents the time ordering and time. + For example, "At 12:00pm". + */ + case ExactTime(TimeConditionOrder, NSDateComponents) + + /// The predicate is not a HomeKit condition. + case Unknown +} + +extension NSPredicate { + + /** + Parses the predicate and attempts to generate a characteristic-value `HomeKitConditionType`. + + - returns: An optional characteristic-value tuple. + */ + private func characteristic() -> HomeKitConditionType? { + guard let predicate = self as? NSCompoundPredicate else { return nil } + guard let subpredicates = predicate.subpredicates as? [NSPredicate] else { return nil } + guard subpredicates.count == 2 else { return nil } + + var characteristicPredicate: NSComparisonPredicate? = nil + var valuePredicate: NSComparisonPredicate? = nil + + for subpredicate in subpredicates { + if let comparison = subpredicate as? NSComparisonPredicate where comparison.leftExpression.expressionType == .KeyPathExpressionType && comparison.rightExpression.expressionType == .ConstantValueExpressionType { + switch comparison.leftExpression.keyPath { + case HMCharacteristicKeyPath: + characteristicPredicate = comparison + + case HMCharacteristicValueKeyPath: + valuePredicate = comparison + + default: + break + } + } + } + + if let characteristic = characteristicPredicate?.rightExpression.constantValue as? HMCharacteristic, + characteristicValue = valuePredicate?.rightExpression.constantValue as? NSCopying { + return .Characteristic(characteristic, characteristicValue) + } + return nil + } + + /** + Parses the predicate and attempts to generate an order-sunstate `HomeKitConditionType`. + + - returns: An optional order-sunstate tuple. + */ + private func sunState() -> HomeKitConditionType? { + guard let comparison = self as? NSComparisonPredicate else { return nil } + guard comparison.leftExpression.expressionType == .KeyPathExpressionType else { return nil } + guard comparison.rightExpression.expressionType == .FunctionExpressionType else { return nil } + guard comparison.rightExpression.function == "now" else { return nil } + guard comparison.rightExpression.arguments?.count == 0 else { return nil } + + switch (comparison.leftExpression.keyPath, comparison.predicateOperatorType) { + case (HMSignificantEventSunrise, .LessThanPredicateOperatorType): + return .SunTime(.After, .Sunrise) + + case (HMSignificantEventSunrise, .LessThanOrEqualToPredicateOperatorType): + return .SunTime(.After, .Sunrise) + + case (HMSignificantEventSunrise, .GreaterThanPredicateOperatorType): + return .SunTime(.Before, .Sunrise) + + case (HMSignificantEventSunrise, .GreaterThanOrEqualToPredicateOperatorType): + return .SunTime(.Before, .Sunrise) + + case (HMSignificantEventSunset, .LessThanPredicateOperatorType): + return .SunTime(.After, .Sunset) + + case (HMSignificantEventSunset, .LessThanOrEqualToPredicateOperatorType): + return .SunTime(.After, .Sunset) + + case (HMSignificantEventSunset, .GreaterThanPredicateOperatorType): + return .SunTime(.Before, .Sunset) + + case (HMSignificantEventSunset, .GreaterThanOrEqualToPredicateOperatorType): + return .SunTime(.Before, .Sunset) + + default: + return nil + } + } + + /** + Parses the predicate and attempts to generate an order-exacttime `HomeKitConditionType`. + + - returns: An optional order-exacttime tuple. + */ + private func exactTime() -> HomeKitConditionType? { + guard let comparison = self as? NSComparisonPredicate else { return nil } + guard comparison.leftExpression.expressionType == .FunctionExpressionType else { return nil } + guard comparison.leftExpression.function == "now" else { return nil } + guard comparison.rightExpression.expressionType == .ConstantValueExpressionType else { return nil } + guard let dateComponents = comparison.rightExpression.constantValue as? NSDateComponents else { return nil } + + switch comparison.predicateOperatorType { + case .LessThanPredicateOperatorType, .LessThanOrEqualToPredicateOperatorType: + return .ExactTime(.Before, dateComponents) + + case .GreaterThanPredicateOperatorType, .GreaterThanOrEqualToPredicateOperatorType: + return .ExactTime(.After, dateComponents) + + case .EqualToPredicateOperatorType: + return .ExactTime(.At, dateComponents) + + default: + return nil + } + } + + /// - returns: The 'type' of HomeKit condition, with associated value, if applicable. + var homeKitConditionType: HomeKitConditionType { + if let characteristic = characteristic() { + return characteristic + } + else if let sunState = sunState() { + return sunState + } + else if let exactTime = exactTime() { + return exactTime + } + else { + return .Unknown + } + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/UIAlertController+Convenience.swift b/HomeKitCatalog/HMCatalog/Supporting Files/UIAlertController+Convenience.swift new file mode 100644 index 00000000..e35717bd --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/UIAlertController+Convenience.swift @@ -0,0 +1,61 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `UIAlertController+Convenience` methods allow for quick construction of `UIAlertController`s with common structures. +*/ + +import UIKit + +extension UIAlertController { + + /** + A simple `UIAlertController` that prompts for a name, then runs a completion block passing in the name. + + - parameter attributeType: The type of object that will be named. + - parameter completion: A block to call, passing in the provided text. + - parameter placeholder: An optional string used as text field's placeholder text. + - parameter shortType: An optional string used as to form the alert's action title. + + - returns: A `UIAlertController` instance with a UITextField, cancel button, and add button. + */ + convenience init(attributeType: String, completionHandler: (name: String) -> Void, placeholder: String? = nil, shortType: String? = nil) { + let title = NSLocalizedString("New", comment: "New") + " \(attributeType)" + let message = NSLocalizedString("Enter a name.", comment: "Enter a name.") + self.init(title: title, message: message, preferredStyle: .Alert) + self.addTextFieldWithConfigurationHandler { textField in + textField.placeholder = placeholder ?? attributeType + textField.autocapitalizationType = .Words + } + let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "Cancel"), style: .Cancel) { action in + self.dismissViewControllerAnimated(true, completion: nil) + } + let add = NSLocalizedString("Add", comment: "Add") + let actionTitle = "\(add) \(shortType ?? attributeType)" + let addNewObject = UIAlertAction(title: actionTitle, style: .Default) { action in + if let name = self.textFields!.first!.text { + let trimmedName = name.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) + completionHandler(name: trimmedName) + } + self.dismissViewControllerAnimated(true, completion: nil) + } + self.addAction(cancelAction) + self.addAction(addNewObject) + } + + /** + A simple `UIAlertController` made to show an error message that's passed in. + + - parameter body: The body of the alert. + + - returns: A `UIAlertController` with an 'Okay' button. + */ + convenience init(title: String, body: String) { + self.init(title: title, message: body, preferredStyle: .Alert) + let okayAction = UIAlertAction(title: NSLocalizedString("Okay", comment: "Okay"), style: .Default) { action in + self.dismissViewControllerAnimated(true, completion: nil) + } + self.addAction(okayAction) + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/UIColor+Custom.swift b/HomeKitCatalog/HMCatalog/Supporting Files/UIColor+Custom.swift new file mode 100644 index 00000000..2478408f --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/UIColor+Custom.swift @@ -0,0 +1,17 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `UIColor+Custom` method provides a generator method for a custom color. +*/ + +import UIKit + +extension UIColor { + + /// - returns: A nice blue color which suggests editability. + static func editableBlueColor() -> UIColor { + return UIColor(red:0/255.0, green:122/255.0, blue:255/255.0, alpha:1.0) + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/UIStoryboardSegue+IntendedDestination.swift b/HomeKitCatalog/HMCatalog/Supporting Files/UIStoryboardSegue+IntendedDestination.swift new file mode 100644 index 00000000..cf77e4e9 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/UIStoryboardSegue+IntendedDestination.swift @@ -0,0 +1,20 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `UIStoryboardSegue+IntendedDestination` method allows for the selection of a segue's destination. +*/ + +import UIKit + +extension UIStoryboardSegue { + + /// - returns: The intended `UIViewController` from the segue's destination. + var intendedDestinationViewController: UIViewController { + if let navigationController = self.destinationViewController as? UINavigationController { + return navigationController.topViewController! + } + return self.destinationViewController + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/UITableViewController+Convenience.swift b/HomeKitCatalog/HMCatalog/Supporting Files/UITableViewController+Convenience.swift new file mode 100644 index 00000000..23c92c6b --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/UITableViewController+Convenience.swift @@ -0,0 +1,39 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `UITableViewController+Convenience` methods allow for the configuration of a background label. +*/ + +import HomeKit +import UIKit + +extension UITableViewController { + + /** + Displays or hides a label in the background of the table view. + + - parameter message: The String message to display. The message is hidden + if `nil` is provided. + */ + func setBackgroundMessage(message: String?) { + if let message = message { + // Display a message when the table is empty + let messageLabel = UILabel() + + messageLabel.text = message + messageLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody) + messageLabel.textColor = UIColor.lightGrayColor() + messageLabel.textAlignment = .Center + messageLabel.sizeToFit() + + tableView.backgroundView = messageLabel + tableView.separatorStyle = .None + } + else { + tableView.backgroundView = nil + tableView.separatorStyle = .SingleLine + } + } +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/Supporting Files/UIViewController+Convenience.swift b/HomeKitCatalog/HMCatalog/Supporting Files/UIViewController+Convenience.swift new file mode 100644 index 00000000..76e8ebd4 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/Supporting Files/UIViewController+Convenience.swift @@ -0,0 +1,94 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `UIViewController+Convenience` methods allow for easy presentation of common views. +*/ + +import HomeKit +import UIKit + +extension UIViewController { + + /** + Displays a `UIAlertController` on the main thread with the error's `localizedDescription` at the body. + + - parameter error: The error to display. + */ + func displayError(error: NSError) { + if let errorCode = HMErrorCode(rawValue: error.code) { + if self.presentedViewController != nil || errorCode == .OperationCancelled || errorCode == .UserDeclinedAddingUser { + print(error.localizedDescription) + } + else { + self.displayErrorMessage(error.localizedDescription) + } + } + else { + self.displayErrorMessage(error.description) + } + } + + /** + Displays a collection of errors, separated by newlines. + + - parameter errors: An array of `NSError`s to display. + */ + func displayErrors(errors: [NSError]) { + var messages = [String]() + for error in errors { + if let errorCode = HMErrorCode(rawValue: error.code) { + if self.presentedViewController != nil || errorCode == .OperationCancelled || errorCode == .UserDeclinedAddingUser { + print(error.localizedDescription) + } + else { + messages.append(error.localizedDescription) + } + } + else { + messages.append(error.description) + } + } + + if messages.count > 0 { + // There were errors in the list, reduce the messages into a single one. + let collectedMessage = messages.reduce("", combine: { (accumulator, message) -> String in + return accumulator + "\n" + message + }) + self.displayErrorMessage(collectedMessage) + } + } + + /// Displays a `UIAlertController` with the passed-in text and an 'Okay' button. + func displayMessage(title: String, message: String) { + dispatch_async(dispatch_get_main_queue()) { + let alert = UIAlertController(title: title, body: message) + self.presentViewController(alert, animated: true, completion: nil) + } + } + + /** + Displays `UIAlertController` with a message and a localized "Error" title. + + - parameter message: The message to display. + */ + private func displayErrorMessage(message: String) { + let errorTitle = NSLocalizedString("Error", comment: "Error") + displayMessage(errorTitle, message: message) + } + + /** + Presents a simple `UIAlertController` with a textField, set up to + accept a name. Once the name is entered, the completion handler will + be called and the name will be passed in. + + - parameter attributeType: The kind of object being added + - parameter completion: The block to run when the user taps the add button. + */ + func presentAddAlertWithAttributeType(type: String, placeholder: String? = nil, shortType: String? = nil, completion: (String) -> Void) { + let alertController = UIAlertController(attributeType: type, completionHandler: completion, placeholder: placeholder, shortType: shortType) + self.presentViewController(alertController, animated: true, completion: nil) + } + +} \ No newline at end of file diff --git a/HomeKitCatalog/HMCatalog/TabBarController.swift b/HomeKitCatalog/HMCatalog/TabBarController.swift new file mode 100644 index 00000000..fd323fe5 --- /dev/null +++ b/HomeKitCatalog/HMCatalog/TabBarController.swift @@ -0,0 +1,43 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The `TabBarController` maintains the state of the tabs across app launches. +*/ +import UIKit + +/** + Saves the current state of the tab so that the app will always open to the + appropriate tab on launch. +*/ +class TabBarController: UITabBarController { + // MARK: Types + + static let startingTabIndexKey = "TabBarController-StartingTabIndexKey" + + // MARK: View Methods + + // Load the current tab from `NSUserDefaults`. + override func viewDidLoad() { + super.viewDidLoad() + + let userDefaults = NSUserDefaults.standardUserDefaults() + + let startingIndex = userDefaults.objectForKey(TabBarController.startingTabIndexKey) as? Int ?? 2 + + selectedIndex = startingIndex + } + + // MARK: Tab Bar Methods + + /// Save the current selected tab into defaults. + override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) { + if let tabBarItems = tabBar.items, index = tabBarItems.indexOf(item) { + + let userDefaults = NSUserDefaults.standardUserDefaults() + + userDefaults.setObject(index, forKey: TabBarController.startingTabIndexKey) + } + } +} diff --git a/HomeKitCatalog/LICENSE.txt b/HomeKitCatalog/LICENSE.txt new file mode 100644 index 00000000..7ab93e7f --- /dev/null +++ b/HomeKitCatalog/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: HomeKit Catalog: Creating Homes, Pairing and Controlling Accessories, and Setting Up Triggers +Version: 2.2 + +IMPORTANT: This Apple software is supplied to you by Apple +Inc. ("Apple") in consideration of your agreement to the following +terms, and your use, installation, modification or redistribution of +this Apple software constitutes acceptance of these terms. If you do +not agree with these terms, please do not use, install, modify or +redistribute this Apple software. + +In consideration of your agreement to abide by the following terms, and +subject to these terms, Apple grants you a personal, non-exclusive +license, under Apple's copyrights in this original Apple software (the +"Apple Software"), to use, reproduce, modify and redistribute the Apple +Software, with or without modifications, in source and/or binary forms; +provided that if you redistribute the Apple Software in its entirety and +without modifications, you must retain this notice and the following +text and disclaimers in all such redistributions of the Apple Software. +Neither the name, trademarks, service marks or logos of Apple Inc. may +be used to endorse or promote products derived from the Apple Software +without specific prior written permission from Apple. Except as +expressly stated in this notice, no other rights or licenses, express or +implied, are granted by Apple herein, including but not limited to any +patent rights that may be infringed by your derivative works or by other +works in which the Apple Software may be incorporated. + +The Apple Software is provided by Apple on an "AS IS" basis. APPLE +MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION +THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND +OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. + +IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, +MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED +AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), +STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +Copyright (C) 2016 Apple Inc. All Rights Reserved. diff --git a/HomeKitCatalog/README.md b/HomeKitCatalog/README.md new file mode 100644 index 00000000..d567efa4 --- /dev/null +++ b/HomeKitCatalog/README.md @@ -0,0 +1,50 @@ +# HomeKit Catalog + +HomeKit Catalog demonstrates how to use the HomeKit API, to create homes, to associate accessories with homes, to associate accessories with homes, to group the accessories into rooms and zones, to create actions sets to tie together multiple actions, to create timer triggers to fire actions sets at specific times, and to create service groups to group services into contexts. + +HomeKit Catalog requires Xcode 8 with the iOS 9.0 SDK to build the application. You can either run the sample code within the iOS Simulator or on a device with iOS 9.0 installed. You can use the HomeKit Accessory Simulator running under OS X to simulate accessories on your local Wi-Fi network. The HomeKit Accessory Simulator is available from the Apple Developer site as part of the Hardware IO Tools disk image. + + +## Using the Sample + +To use the sample, you should have HomeKit accessories already associated with the current WiFi LAN with which your device is attached. Alternatively, you can use the HomeKit Accessory Simulator running on you OS X System, to simulate the presence of a variety of HomeKit Accessories. When you launch the app, switch to the Configure tab to add new homes. + +You may then select a home and perform the following actions: + +1. Define the names of the rooms (Bedroom, Living Room, etc) in the home, define zones as a collection of rooms in the home (first floor), +2. Define Action Sets (turn off Kitchen lights), +3. Define Triggers (turn off lights at 10PM), +4. Define Service Groups (subset of accessories in a room), and +5. Define other users who can control the accessories in your home. + +Note: For information on using the HomeKit Accessory Simulator, please refer to the HomeKit Accessory Simulator Help under the Help menu. + +Use the Configure tab to set up the home, associate accessories with each room, and to perform the actions described above. Use the Control button to control the accessories in the home. + +## Considerations + +HomeKit operates asynchronously. Frequently, you will have to defer some UI response until all operations associated with a particular action are +finished. For example, when this sample wants to save a trigger, it must: + +1. Create a new trigger object +2. Add the trigger to the home +3. Add all of the specified Action Sets individually +4. Update its name +5. Enable it + +This sample makes heavy use of `dispatch_group`s to ensure all actions are completed before confirming with UI. + +This sample also includes many convenience functions implemented as categories on HomeKit classes, and provides a very basic, flexible UI that adapts based +on HMCharacteristic metadata. + +## Requirements + +### Build + +Xcode 8.0 or later; iOS 9.0 SDK or later + +### Runtime + +iOS 9.0 or later. + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/IceCreamBuilder/IceCreamBuilder.xcodeproj/project.pbxproj b/IceCreamBuilder/IceCreamBuilder.xcodeproj/project.pbxproj new file mode 100644 index 00000000..e9ac62b9 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilder.xcodeproj/project.pbxproj @@ -0,0 +1,672 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + B52828AD1CD1724C009CD776 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52828AC1CD1724C009CD776 /* AppDelegate.swift */; }; + B52828AF1CD1724C009CD776 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52828AE1CD1724C009CD776 /* ViewController.swift */; }; + B52828B21CD1724C009CD776 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B52828B01CD1724C009CD776 /* Main.storyboard */; }; + B52828B41CD1724C009CD776 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B52828B31CD1724C009CD776 /* Assets.xcassets */; }; + B52828B71CD1724C009CD776 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B52828B51CD1724C009CD776 /* LaunchScreen.storyboard */; }; + B55F96251CFF829D00B33E33 /* Messages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B52828C41CD17260009CD776 /* Messages.framework */; }; + B55F96281CFF829D00B33E33 /* MessagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F96271CFF829D00B33E33 /* MessagesViewController.swift */; }; + B55F962B1CFF829D00B33E33 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B55F96291CFF829D00B33E33 /* MainInterface.storyboard */; }; + B55F962D1CFF829D00B33E33 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B55F962C1CFF829D00B33E33 /* Assets.xcassets */; }; + B55F96311CFF829D00B33E33 /* IceCreamBuilderMessagesExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = B55F96241CFF829D00B33E33 /* IceCreamBuilderMessagesExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + B55F96411CFF82E100B33E33 /* BuildIceCreamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F96351CFF82E100B33E33 /* BuildIceCreamViewController.swift */; }; + B55F96421CFF82E100B33E33 /* IceCream.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F96361CFF82E100B33E33 /* IceCream.swift */; }; + B55F96431CFF82E100B33E33 /* IceCream+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F96371CFF82E100B33E33 /* IceCream+Image.swift */; }; + B55F96441CFF82E100B33E33 /* IceCreamCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F96381CFF82E100B33E33 /* IceCreamCell.swift */; }; + B55F96451CFF82E100B33E33 /* IceCreamHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F96391CFF82E100B33E33 /* IceCreamHistory.swift */; }; + B55F96461CFF82E100B33E33 /* IceCreamOutlineCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F963A1CFF82E100B33E33 /* IceCreamOutlineCell.swift */; }; + B55F96471CFF82E100B33E33 /* IceCreamPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F963B1CFF82E100B33E33 /* IceCreamPart.swift */; }; + B55F96481CFF82E100B33E33 /* IceCreamPartCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F963C1CFF82E100B33E33 /* IceCreamPartCell.swift */; }; + B55F96491CFF82E100B33E33 /* IceCreamPartCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F963D1CFF82E100B33E33 /* IceCreamPartCollectionViewLayout.swift */; }; + B55F964A1CFF82E100B33E33 /* IceCreamsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F963E1CFF82E100B33E33 /* IceCreamsViewController.swift */; }; + B55F964B1CFF82E100B33E33 /* IceCreamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F963F1CFF82E100B33E33 /* IceCreamView.swift */; }; + B55F964C1CFF82E100B33E33 /* QueryItemRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F96401CFF82E100B33E33 /* QueryItemRepresentable.swift */; }; + B55F964F1CFF9FB800B33E33 /* CompletedIceCreamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55F964E1CFF9FB800B33E33 /* CompletedIceCreamViewController.swift */; }; + B5A2FBA71D048F7800EB0205 /* sticker_placeholder.png in Resources */ = {isa = PBXBuildFile; fileRef = B5A2FBA61D048F7800EB0205 /* sticker_placeholder.png */; }; + B5B1E9F21D02717700BD5715 /* IceCreamStickerCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B1E9F11D02717700BD5715 /* IceCreamStickerCache.swift */; }; + B5E0A5691D01079C006A77D2 /* base01_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A54E1D01079C006A77D2 /* base01_sticker.png */; }; + B5E0A56A1D01079C006A77D2 /* base02_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A54F1D01079C006A77D2 /* base02_sticker.png */; }; + B5E0A56B1D01079C006A77D2 /* base03_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5501D01079C006A77D2 /* base03_sticker.png */; }; + B5E0A56C1D01079C006A77D2 /* base04_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5511D01079C006A77D2 /* base04_sticker.png */; }; + B5E0A56D1D01079C006A77D2 /* scoops01_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5521D01079C006A77D2 /* scoops01_sticker.png */; }; + B5E0A56E1D01079C006A77D2 /* scoops02_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5531D01079C006A77D2 /* scoops02_sticker.png */; }; + B5E0A56F1D01079C006A77D2 /* scoops03_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5541D01079C006A77D2 /* scoops03_sticker.png */; }; + B5E0A5701D01079C006A77D2 /* scoops04_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5551D01079C006A77D2 /* scoops04_sticker.png */; }; + B5E0A5711D01079C006A77D2 /* scoops05_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5561D01079C006A77D2 /* scoops05_sticker.png */; }; + B5E0A5721D01079C006A77D2 /* scoops06_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5571D01079C006A77D2 /* scoops06_sticker.png */; }; + B5E0A5731D01079C006A77D2 /* scoops07_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5581D01079C006A77D2 /* scoops07_sticker.png */; }; + B5E0A5741D01079C006A77D2 /* scoops08_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5591D01079C006A77D2 /* scoops08_sticker.png */; }; + B5E0A5751D01079C006A77D2 /* scoops09_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A55A1D01079C006A77D2 /* scoops09_sticker.png */; }; + B5E0A5761D01079C006A77D2 /* scoops10_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A55B1D01079C006A77D2 /* scoops10_sticker.png */; }; + B5E0A5781D01079C006A77D2 /* topping01_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A55D1D01079C006A77D2 /* topping01_sticker.png */; }; + B5E0A5791D01079C006A77D2 /* topping02_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A55E1D01079C006A77D2 /* topping02_sticker.png */; }; + B5E0A57A1D01079C006A77D2 /* topping03_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A55F1D01079C006A77D2 /* topping03_sticker.png */; }; + B5E0A57B1D01079C006A77D2 /* topping04_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5601D01079C006A77D2 /* topping04_sticker.png */; }; + B5E0A57C1D01079C006A77D2 /* topping05_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5611D01079C006A77D2 /* topping05_sticker.png */; }; + B5E0A57D1D01079C006A77D2 /* topping06_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5621D01079C006A77D2 /* topping06_sticker.png */; }; + B5E0A57E1D01079C006A77D2 /* topping07_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5631D01079C006A77D2 /* topping07_sticker.png */; }; + B5E0A57F1D01079C006A77D2 /* topping08_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5641D01079C006A77D2 /* topping08_sticker.png */; }; + B5E0A5801D01079C006A77D2 /* topping09_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5651D01079C006A77D2 /* topping09_sticker.png */; }; + B5E0A5811D01079C006A77D2 /* topping10_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5661D01079C006A77D2 /* topping10_sticker.png */; }; + B5E0A5821D01079C006A77D2 /* topping11_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5671D01079C006A77D2 /* topping11_sticker.png */; }; + B5E0A5831D01079C006A77D2 /* topping12_sticker.png in Resources */ = {isa = PBXBuildFile; fileRef = B5E0A5681D01079C006A77D2 /* topping12_sticker.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + B55F962F1CFF829D00B33E33 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B52828A11CD1724C009CD776 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B55F96231CFF829D00B33E33; + remoteInfo = IceCreamBuilderMessagesExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + B52828D51CD17260009CD776 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + B55F96311CFF829D00B33E33 /* IceCreamBuilderMessagesExtension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + B52828A91CD1724C009CD776 /* IceCreamBuilder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IceCreamBuilder.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B52828AC1CD1724C009CD776 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + B52828AE1CD1724C009CD776 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + B52828B11CD1724C009CD776 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + B52828B31CD1724C009CD776 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + B52828B61CD1724C009CD776 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + B52828B81CD1724C009CD776 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B52828C41CD17260009CD776 /* Messages.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Messages.framework; path = System/Library/Frameworks/Messages.framework; sourceTree = SDKROOT; }; + B55F96241CFF829D00B33E33 /* IceCreamBuilderMessagesExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IceCreamBuilderMessagesExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + B55F96271CFF829D00B33E33 /* MessagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesViewController.swift; sourceTree = ""; }; + B55F962A1CFF829D00B33E33 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + B55F962C1CFF829D00B33E33 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + B55F962E1CFF829D00B33E33 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B55F96351CFF82E100B33E33 /* BuildIceCreamViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildIceCreamViewController.swift; sourceTree = ""; }; + B55F96361CFF82E100B33E33 /* IceCream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IceCream.swift; sourceTree = ""; }; + B55F96371CFF82E100B33E33 /* IceCream+Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "IceCream+Image.swift"; sourceTree = ""; }; + B55F96381CFF82E100B33E33 /* IceCreamCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IceCreamCell.swift; sourceTree = ""; }; + B55F96391CFF82E100B33E33 /* IceCreamHistory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IceCreamHistory.swift; sourceTree = ""; }; + B55F963A1CFF82E100B33E33 /* IceCreamOutlineCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IceCreamOutlineCell.swift; sourceTree = ""; }; + B55F963B1CFF82E100B33E33 /* IceCreamPart.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IceCreamPart.swift; sourceTree = ""; }; + B55F963C1CFF82E100B33E33 /* IceCreamPartCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IceCreamPartCell.swift; sourceTree = ""; }; + B55F963D1CFF82E100B33E33 /* IceCreamPartCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IceCreamPartCollectionViewLayout.swift; sourceTree = ""; }; + B55F963E1CFF82E100B33E33 /* IceCreamsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IceCreamsViewController.swift; sourceTree = ""; }; + B55F963F1CFF82E100B33E33 /* IceCreamView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IceCreamView.swift; sourceTree = ""; }; + B55F96401CFF82E100B33E33 /* QueryItemRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryItemRepresentable.swift; sourceTree = ""; }; + B55F964E1CFF9FB800B33E33 /* CompletedIceCreamViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompletedIceCreamViewController.swift; sourceTree = ""; }; + B5A2FBA61D048F7800EB0205 /* sticker_placeholder.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = sticker_placeholder.png; sourceTree = ""; }; + B5B1E9F11D02717700BD5715 /* IceCreamStickerCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IceCreamStickerCache.swift; sourceTree = ""; }; + B5CE99031CF5AF210031942E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + B5E0A54E1D01079C006A77D2 /* base01_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = base01_sticker.png; sourceTree = ""; }; + B5E0A54F1D01079C006A77D2 /* base02_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = base02_sticker.png; sourceTree = ""; }; + B5E0A5501D01079C006A77D2 /* base03_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = base03_sticker.png; sourceTree = ""; }; + B5E0A5511D01079C006A77D2 /* base04_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = base04_sticker.png; sourceTree = ""; }; + B5E0A5521D01079C006A77D2 /* scoops01_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = scoops01_sticker.png; sourceTree = ""; }; + B5E0A5531D01079C006A77D2 /* scoops02_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = scoops02_sticker.png; sourceTree = ""; }; + B5E0A5541D01079C006A77D2 /* scoops03_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = scoops03_sticker.png; sourceTree = ""; }; + B5E0A5551D01079C006A77D2 /* scoops04_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = scoops04_sticker.png; sourceTree = ""; }; + B5E0A5561D01079C006A77D2 /* scoops05_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = scoops05_sticker.png; sourceTree = ""; }; + B5E0A5571D01079C006A77D2 /* scoops06_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = scoops06_sticker.png; sourceTree = ""; }; + B5E0A5581D01079C006A77D2 /* scoops07_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = scoops07_sticker.png; sourceTree = ""; }; + B5E0A5591D01079C006A77D2 /* scoops08_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = scoops08_sticker.png; sourceTree = ""; }; + B5E0A55A1D01079C006A77D2 /* scoops09_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = scoops09_sticker.png; sourceTree = ""; }; + B5E0A55B1D01079C006A77D2 /* scoops10_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = scoops10_sticker.png; sourceTree = ""; }; + B5E0A55D1D01079C006A77D2 /* topping01_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping01_sticker.png; sourceTree = ""; }; + B5E0A55E1D01079C006A77D2 /* topping02_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping02_sticker.png; sourceTree = ""; }; + B5E0A55F1D01079C006A77D2 /* topping03_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping03_sticker.png; sourceTree = ""; }; + B5E0A5601D01079C006A77D2 /* topping04_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping04_sticker.png; sourceTree = ""; }; + B5E0A5611D01079C006A77D2 /* topping05_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping05_sticker.png; sourceTree = ""; }; + B5E0A5621D01079C006A77D2 /* topping06_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping06_sticker.png; sourceTree = ""; }; + B5E0A5631D01079C006A77D2 /* topping07_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping07_sticker.png; sourceTree = ""; }; + B5E0A5641D01079C006A77D2 /* topping08_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping08_sticker.png; sourceTree = ""; }; + B5E0A5651D01079C006A77D2 /* topping09_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping09_sticker.png; sourceTree = ""; }; + B5E0A5661D01079C006A77D2 /* topping10_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping10_sticker.png; sourceTree = ""; }; + B5E0A5671D01079C006A77D2 /* topping11_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping11_sticker.png; sourceTree = ""; }; + B5E0A5681D01079C006A77D2 /* topping12_sticker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = topping12_sticker.png; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B52828A61CD1724C009CD776 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B55F96211CFF829D00B33E33 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B55F96251CFF829D00B33E33 /* Messages.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B52828A01CD1724C009CD776 = { + isa = PBXGroup; + children = ( + B5CE99031CF5AF210031942E /* README.md */, + B52828AB1CD1724C009CD776 /* IceCreamBuilder */, + B55F96261CFF829D00B33E33 /* IceCreamBuilderMessagesExtension */, + B52828C31CD17260009CD776 /* Frameworks */, + B52828AA1CD1724C009CD776 /* Products */, + ); + sourceTree = ""; + }; + B52828AA1CD1724C009CD776 /* Products */ = { + isa = PBXGroup; + children = ( + B52828A91CD1724C009CD776 /* IceCreamBuilder.app */, + B55F96241CFF829D00B33E33 /* IceCreamBuilderMessagesExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; + B52828AB1CD1724C009CD776 /* IceCreamBuilder */ = { + isa = PBXGroup; + children = ( + B52828AC1CD1724C009CD776 /* AppDelegate.swift */, + B52828AE1CD1724C009CD776 /* ViewController.swift */, + B52828B01CD1724C009CD776 /* Main.storyboard */, + B52828B31CD1724C009CD776 /* Assets.xcassets */, + B52828B51CD1724C009CD776 /* LaunchScreen.storyboard */, + B52828B81CD1724C009CD776 /* Info.plist */, + ); + path = IceCreamBuilder; + sourceTree = ""; + }; + B52828C31CD17260009CD776 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B52828C41CD17260009CD776 /* Messages.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + B55F96261CFF829D00B33E33 /* IceCreamBuilderMessagesExtension */ = { + isa = PBXGroup; + children = ( + B55F96271CFF829D00B33E33 /* MessagesViewController.swift */, + B55F963E1CFF82E100B33E33 /* IceCreamsViewController.swift */, + B55F96381CFF82E100B33E33 /* IceCreamCell.swift */, + B55F963A1CFF82E100B33E33 /* IceCreamOutlineCell.swift */, + B55F96351CFF82E100B33E33 /* BuildIceCreamViewController.swift */, + B55F963C1CFF82E100B33E33 /* IceCreamPartCell.swift */, + B55F963D1CFF82E100B33E33 /* IceCreamPartCollectionViewLayout.swift */, + B55F964E1CFF9FB800B33E33 /* CompletedIceCreamViewController.swift */, + B55F963F1CFF82E100B33E33 /* IceCreamView.swift */, + B55F964D1CFF830700B33E33 /* Model */, + B5E0A53C1D0103F0006A77D2 /* StickerPartImages */, + B55F96291CFF829D00B33E33 /* MainInterface.storyboard */, + B55F962C1CFF829D00B33E33 /* Assets.xcassets */, + B55F962E1CFF829D00B33E33 /* Info.plist */, + ); + path = IceCreamBuilderMessagesExtension; + sourceTree = ""; + }; + B55F964D1CFF830700B33E33 /* Model */ = { + isa = PBXGroup; + children = ( + B55F96361CFF82E100B33E33 /* IceCream.swift */, + B55F96371CFF82E100B33E33 /* IceCream+Image.swift */, + B55F96391CFF82E100B33E33 /* IceCreamHistory.swift */, + B55F963B1CFF82E100B33E33 /* IceCreamPart.swift */, + B55F96401CFF82E100B33E33 /* QueryItemRepresentable.swift */, + B5B1E9F11D02717700BD5715 /* IceCreamStickerCache.swift */, + ); + name = Model; + sourceTree = ""; + }; + B5E0A53C1D0103F0006A77D2 /* StickerPartImages */ = { + isa = PBXGroup; + children = ( + B5E0A5841D0109D1006A77D2 /* Bases */, + B5E0A5851D0109DB006A77D2 /* Scoops */, + B5E0A5861D0109E9006A77D2 /* Toppings */, + B5A2FBA61D048F7800EB0205 /* sticker_placeholder.png */, + ); + path = StickerPartImages; + sourceTree = ""; + }; + B5E0A5841D0109D1006A77D2 /* Bases */ = { + isa = PBXGroup; + children = ( + B5E0A54E1D01079C006A77D2 /* base01_sticker.png */, + B5E0A54F1D01079C006A77D2 /* base02_sticker.png */, + B5E0A5501D01079C006A77D2 /* base03_sticker.png */, + B5E0A5511D01079C006A77D2 /* base04_sticker.png */, + ); + name = Bases; + sourceTree = ""; + }; + B5E0A5851D0109DB006A77D2 /* Scoops */ = { + isa = PBXGroup; + children = ( + B5E0A5521D01079C006A77D2 /* scoops01_sticker.png */, + B5E0A5531D01079C006A77D2 /* scoops02_sticker.png */, + B5E0A5541D01079C006A77D2 /* scoops03_sticker.png */, + B5E0A5551D01079C006A77D2 /* scoops04_sticker.png */, + B5E0A5561D01079C006A77D2 /* scoops05_sticker.png */, + B5E0A5571D01079C006A77D2 /* scoops06_sticker.png */, + B5E0A5581D01079C006A77D2 /* scoops07_sticker.png */, + B5E0A5591D01079C006A77D2 /* scoops08_sticker.png */, + B5E0A55A1D01079C006A77D2 /* scoops09_sticker.png */, + B5E0A55B1D01079C006A77D2 /* scoops10_sticker.png */, + ); + name = Scoops; + sourceTree = ""; + }; + B5E0A5861D0109E9006A77D2 /* Toppings */ = { + isa = PBXGroup; + children = ( + B5E0A55D1D01079C006A77D2 /* topping01_sticker.png */, + B5E0A55E1D01079C006A77D2 /* topping02_sticker.png */, + B5E0A55F1D01079C006A77D2 /* topping03_sticker.png */, + B5E0A5601D01079C006A77D2 /* topping04_sticker.png */, + B5E0A5611D01079C006A77D2 /* topping05_sticker.png */, + B5E0A5621D01079C006A77D2 /* topping06_sticker.png */, + B5E0A5631D01079C006A77D2 /* topping07_sticker.png */, + B5E0A5641D01079C006A77D2 /* topping08_sticker.png */, + B5E0A5651D01079C006A77D2 /* topping09_sticker.png */, + B5E0A5661D01079C006A77D2 /* topping10_sticker.png */, + B5E0A5671D01079C006A77D2 /* topping11_sticker.png */, + B5E0A5681D01079C006A77D2 /* topping12_sticker.png */, + ); + name = Toppings; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B52828A81CD1724C009CD776 /* IceCreamBuilder */ = { + isa = PBXNativeTarget; + buildConfigurationList = B52828BB1CD1724C009CD776 /* Build configuration list for PBXNativeTarget "IceCreamBuilder" */; + buildPhases = ( + B52828A51CD1724C009CD776 /* Sources */, + B52828A61CD1724C009CD776 /* Frameworks */, + B52828A71CD1724C009CD776 /* Resources */, + B52828D51CD17260009CD776 /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + B55F96301CFF829D00B33E33 /* PBXTargetDependency */, + ); + name = IceCreamBuilder; + productName = IceCreamBuilder; + productReference = B52828A91CD1724C009CD776 /* IceCreamBuilder.app */; + productType = "com.apple.product-type.application"; + }; + B55F96231CFF829D00B33E33 /* IceCreamBuilderMessagesExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = B55F96321CFF829D00B33E33 /* Build configuration list for PBXNativeTarget "IceCreamBuilderMessagesExtension" */; + buildPhases = ( + B55F96201CFF829D00B33E33 /* Sources */, + B55F96211CFF829D00B33E33 /* Frameworks */, + B55F96221CFF829D00B33E33 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = IceCreamBuilderMessagesExtension; + productName = IceCreamBuilderMessagesExtension; + productReference = B55F96241CFF829D00B33E33 /* IceCreamBuilderMessagesExtension.appex */; + productType = "com.apple.product-type.app-extension.messages"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B52828A11CD1724C009CD776 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Apple; + TargetAttributes = { + B52828A81CD1724C009CD776 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + B55F96231CFF829D00B33E33 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = B52828A41CD1724C009CD776 /* Build configuration list for PBXProject "IceCreamBuilder" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B52828A01CD1724C009CD776; + productRefGroup = B52828AA1CD1724C009CD776 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B52828A81CD1724C009CD776 /* IceCreamBuilder */, + B55F96231CFF829D00B33E33 /* IceCreamBuilderMessagesExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B52828A71CD1724C009CD776 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B52828B71CD1724C009CD776 /* LaunchScreen.storyboard in Resources */, + B52828B41CD1724C009CD776 /* Assets.xcassets in Resources */, + B52828B21CD1724C009CD776 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B55F96221CFF829D00B33E33 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B5E0A57D1D01079C006A77D2 /* topping06_sticker.png in Resources */, + B5E0A57A1D01079C006A77D2 /* topping03_sticker.png in Resources */, + B5E0A5721D01079C006A77D2 /* scoops06_sticker.png in Resources */, + B5E0A5751D01079C006A77D2 /* scoops09_sticker.png in Resources */, + B5E0A57E1D01079C006A77D2 /* topping07_sticker.png in Resources */, + B55F962D1CFF829D00B33E33 /* Assets.xcassets in Resources */, + B5E0A5761D01079C006A77D2 /* scoops10_sticker.png in Resources */, + B5E0A5791D01079C006A77D2 /* topping02_sticker.png in Resources */, + B5E0A5691D01079C006A77D2 /* base01_sticker.png in Resources */, + B5E0A56C1D01079C006A77D2 /* base04_sticker.png in Resources */, + B5E0A5731D01079C006A77D2 /* scoops07_sticker.png in Resources */, + B5E0A56A1D01079C006A77D2 /* base02_sticker.png in Resources */, + B5E0A5711D01079C006A77D2 /* scoops05_sticker.png in Resources */, + B5E0A5701D01079C006A77D2 /* scoops04_sticker.png in Resources */, + B5E0A5821D01079C006A77D2 /* topping11_sticker.png in Resources */, + B5E0A57F1D01079C006A77D2 /* topping08_sticker.png in Resources */, + B5E0A57B1D01079C006A77D2 /* topping04_sticker.png in Resources */, + B5E0A56E1D01079C006A77D2 /* scoops02_sticker.png in Resources */, + B5E0A56F1D01079C006A77D2 /* scoops03_sticker.png in Resources */, + B5E0A57C1D01079C006A77D2 /* topping05_sticker.png in Resources */, + B5E0A56B1D01079C006A77D2 /* base03_sticker.png in Resources */, + B5E0A5831D01079C006A77D2 /* topping12_sticker.png in Resources */, + B55F962B1CFF829D00B33E33 /* MainInterface.storyboard in Resources */, + B5E0A5741D01079C006A77D2 /* scoops08_sticker.png in Resources */, + B5A2FBA71D048F7800EB0205 /* sticker_placeholder.png in Resources */, + B5E0A5781D01079C006A77D2 /* topping01_sticker.png in Resources */, + B5E0A5811D01079C006A77D2 /* topping10_sticker.png in Resources */, + B5E0A5801D01079C006A77D2 /* topping09_sticker.png in Resources */, + B5E0A56D1D01079C006A77D2 /* scoops01_sticker.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B52828A51CD1724C009CD776 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B52828AF1CD1724C009CD776 /* ViewController.swift in Sources */, + B52828AD1CD1724C009CD776 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B55F96201CFF829D00B33E33 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B55F96461CFF82E100B33E33 /* IceCreamOutlineCell.swift in Sources */, + B55F964A1CFF82E100B33E33 /* IceCreamsViewController.swift in Sources */, + B55F96411CFF82E100B33E33 /* BuildIceCreamViewController.swift in Sources */, + B55F96281CFF829D00B33E33 /* MessagesViewController.swift in Sources */, + B55F96431CFF82E100B33E33 /* IceCream+Image.swift in Sources */, + B5B1E9F21D02717700BD5715 /* IceCreamStickerCache.swift in Sources */, + B55F964B1CFF82E100B33E33 /* IceCreamView.swift in Sources */, + B55F96491CFF82E100B33E33 /* IceCreamPartCollectionViewLayout.swift in Sources */, + B55F964F1CFF9FB800B33E33 /* CompletedIceCreamViewController.swift in Sources */, + B55F964C1CFF82E100B33E33 /* QueryItemRepresentable.swift in Sources */, + B55F96451CFF82E100B33E33 /* IceCreamHistory.swift in Sources */, + B55F96471CFF82E100B33E33 /* IceCreamPart.swift in Sources */, + B55F96441CFF82E100B33E33 /* IceCreamCell.swift in Sources */, + B55F96481CFF82E100B33E33 /* IceCreamPartCell.swift in Sources */, + B55F96421CFF82E100B33E33 /* IceCream.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + B55F96301CFF829D00B33E33 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B55F96231CFF829D00B33E33 /* IceCreamBuilderMessagesExtension */; + targetProxy = B55F962F1CFF829D00B33E33 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + B52828B01CD1724C009CD776 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B52828B11CD1724C009CD776 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + B52828B51CD1724C009CD776 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B52828B61CD1724C009CD776 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + B55F96291CFF829D00B33E33 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B55F962A1CFF829D00B33E33 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + B52828B91CD1724C009CD776 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = lossless; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + B52828BA1CD1724C009CD776 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = "respect-asset-catalog"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + B52828BC1CD1724C009CD776 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + INFOPLIST_FILE = IceCreamBuilder/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.IceCreamBuilder"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + B52828BD1CD1724C009CD776 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + INFOPLIST_FILE = IceCreamBuilder/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.IceCreamBuilder"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + B55F96331CFF829D00B33E33 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + INFOPLIST_FILE = IceCreamBuilderMessagesExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.IceCreamBuilder.IceCreamBuilderMessagesExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + B55F96341CFF829D00B33E33 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "iMessage App Icon"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + INFOPLIST_FILE = IceCreamBuilderMessagesExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.IceCreamBuilder.IceCreamBuilderMessagesExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B52828A41CD1724C009CD776 /* Build configuration list for PBXProject "IceCreamBuilder" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B52828B91CD1724C009CD776 /* Debug */, + B52828BA1CD1724C009CD776 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B52828BB1CD1724C009CD776 /* Build configuration list for PBXNativeTarget "IceCreamBuilder" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B52828BC1CD1724C009CD776 /* Debug */, + B52828BD1CD1724C009CD776 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B55F96321CFF829D00B33E33 /* Build configuration list for PBXNativeTarget "IceCreamBuilderMessagesExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B55F96331CFF829D00B33E33 /* Debug */, + B55F96341CFF829D00B33E33 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B52828A11CD1724C009CD776 /* Project object */; +} diff --git a/IceCreamBuilder/IceCreamBuilder/AppDelegate.swift b/IceCreamBuilder/IceCreamBuilder/AppDelegate.swift new file mode 100644 index 00000000..fd6a6290 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilder/AppDelegate.swift @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application delegate. +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? +} + diff --git a/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/Contents.json b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d05742be --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,82 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon_40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon_40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon_60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon_60x60@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon_40x40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon_40x40@2x-1.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon_76x76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon_76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "icon_83.5x83.5@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40.png b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40.png new file mode 100644 index 00000000..1d2753a0 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40.png differ diff --git a/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x-1.png b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x-1.png new file mode 100644 index 00000000..de3b92e0 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x-1.png differ diff --git a/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x.png b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x.png new file mode 100644 index 00000000..de3b92e0 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40@3x.png b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40@3x.png new file mode 100644 index 00000000..b93862a2 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_40x40@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_60x60@2x.png b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_60x60@2x.png new file mode 100644 index 00000000..e6395633 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_60x60@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_60x60@3x.png b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_60x60@3x.png new file mode 100644 index 00000000..56e30021 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_60x60@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_76x76.png b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_76x76.png new file mode 100644 index 00000000..b48e99c1 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_76x76.png differ diff --git a/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_76x76@2x.png b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_76x76@2x.png new file mode 100644 index 00000000..54a7def8 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_76x76@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_83.5x83.5@2x.png b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_83.5x83.5@2x.png new file mode 100644 index 00000000..f7ac2a41 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilder/Assets.xcassets/AppIcon.appiconset/icon_83.5x83.5@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilder/Base.lproj/LaunchScreen.storyboard b/IceCreamBuilder/IceCreamBuilder/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..2e721e18 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilder/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IceCreamBuilder/IceCreamBuilder/Base.lproj/Main.storyboard b/IceCreamBuilder/IceCreamBuilder/Base.lproj/Main.storyboard new file mode 100644 index 00000000..13652f72 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilder/Base.lproj/Main.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IceCreamBuilder/IceCreamBuilder/Info.plist b/IceCreamBuilder/IceCreamBuilder/Info.plist new file mode 100644 index 00000000..8e882a53 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilder/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Ice Cream + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/IceCreamBuilder/IceCreamBuilder/ViewController.swift b/IceCreamBuilder/IceCreamBuilder/ViewController.swift new file mode 100644 index 00000000..cd51230b --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilder/ViewController.swift @@ -0,0 +1,13 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application's view controller. +*/ + +import UIKit + +class ViewController: UIViewController { +} + diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/Contents.json new file mode 100644 index 00000000..3e1f78fb --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "base01.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "base01@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "base01@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/base01.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/base01.png new file mode 100644 index 00000000..40cd6274 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/base01.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/base01@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/base01@2x.png new file mode 100644 index 00000000..c19953ae Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/base01@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/base01@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/base01@3x.png new file mode 100644 index 00000000..717bd7d4 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base01.imageset/base01@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/Contents.json new file mode 100644 index 00000000..31f2f25f --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "base02.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "base02@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "base02@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/base02.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/base02.png new file mode 100644 index 00000000..842e917b Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/base02.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/base02@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/base02@2x.png new file mode 100644 index 00000000..89f204a8 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/base02@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/base02@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/base02@3x.png new file mode 100644 index 00000000..08daa7cd Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base02.imageset/base02@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/Contents.json new file mode 100644 index 00000000..843df71d --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "base03.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "base03@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "base03@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/base03.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/base03.png new file mode 100644 index 00000000..efff7d4b Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/base03.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/base03@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/base03@2x.png new file mode 100644 index 00000000..1225e1f5 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/base03@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/base03@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/base03@3x.png new file mode 100644 index 00000000..4a7a4415 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base03.imageset/base03@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/Contents.json new file mode 100644 index 00000000..5ae1e922 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "base04.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "base04@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "base04@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/base04.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/base04.png new file mode 100644 index 00000000..e3db1806 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/base04.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/base04@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/base04@2x.png new file mode 100644 index 00000000..c69b2ee4 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/base04@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/base04@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/base04@3x.png new file mode 100644 index 00000000..486fe125 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/base04.imageset/base04@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/Contents.json new file mode 100644 index 00000000..8f12c5c0 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "create.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "create@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "create@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/create.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/create.png new file mode 100644 index 00000000..d53a9c81 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/create.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/create@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/create@2x.png new file mode 100644 index 00000000..8fdf316f Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/create@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/create@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/create@3x.png new file mode 100644 index 00000000..11903c54 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/create.imageset/create@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/Contents.json new file mode 100644 index 00000000..54812be9 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/Contents.json @@ -0,0 +1,67 @@ +{ + "images" : [ + { + "size" : "60x45", + "idiom" : "iphone", + "filename" : "icon_60x45@2x.png", + "scale" : "2x" + }, + { + "size" : "60x45", + "idiom" : "iphone", + "filename" : "icon_60x45@3x.png", + "scale" : "3x" + }, + { + "size" : "67x50", + "idiom" : "ipad", + "filename" : "icon_67x50@2x.png", + "scale" : "2x" + }, + { + "size" : "74x55", + "idiom" : "ipad", + "filename" : "icon_74x55@2x.png", + "scale" : "2x" + }, + { + "size" : "27x20", + "idiom" : "universal", + "filename" : "icon_27x20@2x.png", + "scale" : "2x", + "platform" : "ios" + }, + { + "size" : "27x20", + "idiom" : "universal", + "filename" : "icon_27x20@3x.png", + "scale" : "3x", + "platform" : "ios" + }, + { + "size" : "32x24", + "idiom" : "universal", + "filename" : "icon_32x24@2x.png", + "scale" : "2x", + "platform" : "ios" + }, + { + "size" : "32x24", + "idiom" : "universal", + "filename" : "icon_32x24@3x.png", + "scale" : "3x", + "platform" : "ios" + }, + { + "size" : "1024x768", + "idiom" : "ios-marketing", + "filename" : "icon_1024x768.png", + "scale" : "1x", + "platform" : "ios" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_1024x768.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_1024x768.png new file mode 100644 index 00000000..d40dbd82 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_1024x768.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_27x20@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_27x20@2x.png new file mode 100644 index 00000000..3490a6d8 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_27x20@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_27x20@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_27x20@3x.png new file mode 100644 index 00000000..ce4b4330 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_27x20@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_32x24@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_32x24@2x.png new file mode 100644 index 00000000..a084a0b2 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_32x24@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_32x24@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_32x24@3x.png new file mode 100644 index 00000000..7d301ad3 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_32x24@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_60x45@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_60x45@2x.png new file mode 100644 index 00000000..48c1e429 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_60x45@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_60x45@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_60x45@3x.png new file mode 100644 index 00000000..73a1c924 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_60x45@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_67x50@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_67x50@2x.png new file mode 100644 index 00000000..67ea3c73 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_67x50@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_74x55@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_74x55@2x.png new file mode 100644 index 00000000..2bd5d788 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/iMessage App Icon.stickersiconset/icon_74x55@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/Contents.json new file mode 100644 index 00000000..6378381e --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "outline66x100.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "outline66x100@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "outline66x100@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/outline66x100.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/outline66x100.png new file mode 100644 index 00000000..43145983 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/outline66x100.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/outline66x100@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/outline66x100@2x.png new file mode 100644 index 00000000..ad3d493b Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/outline66x100@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/outline66x100@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/outline66x100@3x.png new file mode 100644 index 00000000..d0ce0161 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/icecream_outline.imageset/outline66x100@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/Contents.json new file mode 100644 index 00000000..2ffe85f9 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scoops01.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scoops01@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "scoops01@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/scoops01.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/scoops01.png new file mode 100644 index 00000000..024bfbc9 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/scoops01.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/scoops01@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/scoops01@2x.png new file mode 100644 index 00000000..97171bc9 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/scoops01@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/scoops01@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/scoops01@3x.png new file mode 100644 index 00000000..6b5e56df Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops01.imageset/scoops01@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/Contents.json new file mode 100644 index 00000000..59fc74da --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scoops02.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scoops02@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "scoops02@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/scoops02.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/scoops02.png new file mode 100644 index 00000000..36efc045 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/scoops02.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/scoops02@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/scoops02@2x.png new file mode 100644 index 00000000..18f117a9 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/scoops02@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/scoops02@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/scoops02@3x.png new file mode 100644 index 00000000..f94aa88b Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops02.imageset/scoops02@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/Contents.json new file mode 100644 index 00000000..c1da380a --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scoops03.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scoops03@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "scoops03@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/scoops03.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/scoops03.png new file mode 100644 index 00000000..9e006ff3 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/scoops03.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/scoops03@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/scoops03@2x.png new file mode 100644 index 00000000..e3406a74 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/scoops03@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/scoops03@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/scoops03@3x.png new file mode 100644 index 00000000..b1d7a12f Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops03.imageset/scoops03@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/Contents.json new file mode 100644 index 00000000..e5f0fea5 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scoops04.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scoops04@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "scoops04@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/scoops04.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/scoops04.png new file mode 100644 index 00000000..fe4765d0 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/scoops04.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/scoops04@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/scoops04@2x.png new file mode 100644 index 00000000..fd06f85d Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/scoops04@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/scoops04@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/scoops04@3x.png new file mode 100644 index 00000000..dc8137b7 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops04.imageset/scoops04@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/Contents.json new file mode 100644 index 00000000..ee7382c7 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scoops05.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scoops05@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "scoops05@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/scoops05.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/scoops05.png new file mode 100644 index 00000000..7d0895c8 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/scoops05.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/scoops05@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/scoops05@2x.png new file mode 100644 index 00000000..f986e117 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/scoops05@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/scoops05@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/scoops05@3x.png new file mode 100644 index 00000000..d9863e14 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops05.imageset/scoops05@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/Contents.json new file mode 100644 index 00000000..001a7ca7 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scoops06.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scoops06@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "scoops06@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/scoops06.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/scoops06.png new file mode 100644 index 00000000..10acc6fc Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/scoops06.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/scoops06@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/scoops06@2x.png new file mode 100644 index 00000000..aa232e3b Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/scoops06@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/scoops06@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/scoops06@3x.png new file mode 100644 index 00000000..dd03d437 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops06.imageset/scoops06@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/Contents.json new file mode 100644 index 00000000..3d410104 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scoops07.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scoops07@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "scoops07@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/scoops07.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/scoops07.png new file mode 100644 index 00000000..10c7a992 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/scoops07.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/scoops07@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/scoops07@2x.png new file mode 100644 index 00000000..6550920c Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/scoops07@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/scoops07@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/scoops07@3x.png new file mode 100644 index 00000000..9c8f0b19 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops07.imageset/scoops07@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/Contents.json new file mode 100644 index 00000000..886423c0 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scoops08.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scoops08@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "scoops08@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/scoops08.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/scoops08.png new file mode 100644 index 00000000..5318369d Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/scoops08.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/scoops08@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/scoops08@2x.png new file mode 100644 index 00000000..22c118be Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/scoops08@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/scoops08@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/scoops08@3x.png new file mode 100644 index 00000000..eb779f84 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops08.imageset/scoops08@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/Contents.json new file mode 100644 index 00000000..4d6149e9 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scoops09.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scoops09@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "scoops09@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/scoops09.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/scoops09.png new file mode 100644 index 00000000..d2aaa3b2 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/scoops09.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/scoops09@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/scoops09@2x.png new file mode 100644 index 00000000..0d58e9ac Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/scoops09@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/scoops09@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/scoops09@3x.png new file mode 100644 index 00000000..cda65ea9 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops09.imageset/scoops09@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/Contents.json new file mode 100644 index 00000000..a3d07c98 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scoops10.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scoops10@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "scoops10@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/scoops10.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/scoops10.png new file mode 100644 index 00000000..3bfbca98 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/scoops10.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/scoops10@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/scoops10@2x.png new file mode 100644 index 00000000..89c551bd Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/scoops10@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/scoops10@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/scoops10@3x.png new file mode 100644 index 00000000..1caaf224 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/scoops10.imageset/scoops10@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/Contents.json new file mode 100644 index 00000000..8cb6dd29 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping01.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping01@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping01@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/topping01.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/topping01.png new file mode 100644 index 00000000..74631504 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/topping01.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/topping01@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/topping01@2x.png new file mode 100644 index 00000000..b25bfc05 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/topping01@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/topping01@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/topping01@3x.png new file mode 100644 index 00000000..981edf72 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping01.imageset/topping01@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/Contents.json new file mode 100644 index 00000000..acf2406c --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping02.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping02@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping02@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/topping02.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/topping02.png new file mode 100644 index 00000000..1908331c Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/topping02.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/topping02@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/topping02@2x.png new file mode 100644 index 00000000..c02b9009 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/topping02@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/topping02@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/topping02@3x.png new file mode 100644 index 00000000..9a0eaec7 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping02.imageset/topping02@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/Contents.json new file mode 100644 index 00000000..a118eee1 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping03.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping03@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping03@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/topping03.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/topping03.png new file mode 100644 index 00000000..76609300 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/topping03.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/topping03@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/topping03@2x.png new file mode 100644 index 00000000..01d0fe8b Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/topping03@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/topping03@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/topping03@3x.png new file mode 100644 index 00000000..b5bec595 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping03.imageset/topping03@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/Contents.json new file mode 100644 index 00000000..d3b463c7 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping04.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping04@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping04@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/topping04.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/topping04.png new file mode 100644 index 00000000..059593f1 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/topping04.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/topping04@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/topping04@2x.png new file mode 100644 index 00000000..57dcfa1a Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/topping04@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/topping04@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/topping04@3x.png new file mode 100644 index 00000000..b5a31e6b Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping04.imageset/topping04@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/Contents.json new file mode 100644 index 00000000..3ebd679e --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping05.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping05@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping05@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/topping05.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/topping05.png new file mode 100644 index 00000000..ccdddea0 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/topping05.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/topping05@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/topping05@2x.png new file mode 100644 index 00000000..e376974c Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/topping05@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/topping05@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/topping05@3x.png new file mode 100644 index 00000000..cef826c6 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping05.imageset/topping05@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/Contents.json new file mode 100644 index 00000000..5b9bfefb --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping06.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping06@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping06@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/topping06.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/topping06.png new file mode 100644 index 00000000..132adf10 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/topping06.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/topping06@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/topping06@2x.png new file mode 100644 index 00000000..98fd935a Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/topping06@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/topping06@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/topping06@3x.png new file mode 100644 index 00000000..71719170 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping06.imageset/topping06@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/Contents.json new file mode 100644 index 00000000..cd3108cf --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping07.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping07@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping07@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/topping07.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/topping07.png new file mode 100644 index 00000000..f7524bf4 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/topping07.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/topping07@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/topping07@2x.png new file mode 100644 index 00000000..73524891 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/topping07@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/topping07@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/topping07@3x.png new file mode 100644 index 00000000..17dc8901 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping07.imageset/topping07@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/Contents.json new file mode 100644 index 00000000..35efcbeb --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping08.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping08@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping08@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/topping08.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/topping08.png new file mode 100644 index 00000000..ba587b1d Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/topping08.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/topping08@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/topping08@2x.png new file mode 100644 index 00000000..cb23f31c Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/topping08@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/topping08@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/topping08@3x.png new file mode 100644 index 00000000..01c69e52 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping08.imageset/topping08@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/Contents.json new file mode 100644 index 00000000..b1ed9db3 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping09.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping09@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping09@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/topping09.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/topping09.png new file mode 100644 index 00000000..89aa2d94 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/topping09.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/topping09@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/topping09@2x.png new file mode 100644 index 00000000..f5fd43a8 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/topping09@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/topping09@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/topping09@3x.png new file mode 100644 index 00000000..b90dc283 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping09.imageset/topping09@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/Contents.json new file mode 100644 index 00000000..2cacccec --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping10.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping10@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping10@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/topping10.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/topping10.png new file mode 100644 index 00000000..657e6c13 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/topping10.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/topping10@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/topping10@2x.png new file mode 100644 index 00000000..bb4e1c51 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/topping10@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/topping10@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/topping10@3x.png new file mode 100644 index 00000000..3d1aae96 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping10.imageset/topping10@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/Contents.json new file mode 100644 index 00000000..9f7793b2 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping11.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping11@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping11@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/topping11.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/topping11.png new file mode 100644 index 00000000..21314a7f Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/topping11.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/topping11@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/topping11@2x.png new file mode 100644 index 00000000..dafe8f01 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/topping11@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/topping11@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/topping11@3x.png new file mode 100644 index 00000000..de57a332 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping11.imageset/topping11@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/Contents.json b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/Contents.json new file mode 100644 index 00000000..8f8f5f73 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "topping12.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "topping12@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "topping12@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/topping12.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/topping12.png new file mode 100644 index 00000000..b08423c4 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/topping12.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/topping12@2x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/topping12@2x.png new file mode 100644 index 00000000..b141e731 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/topping12@2x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/topping12@3x.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/topping12@3x.png new file mode 100644 index 00000000..7acaeff0 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Assets.xcassets/topping12.imageset/topping12@3x.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Base.lproj/MainInterface.storyboard b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Base.lproj/MainInterface.storyboard new file mode 100644 index 00000000..a3c49c6e --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/BuildIceCreamViewController.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/BuildIceCreamViewController.swift new file mode 100644 index 00000000..4c3f8e0d --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/BuildIceCreamViewController.swift @@ -0,0 +1,153 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The view controller shown to select an ice-cream part for a partially built ice cream. +*/ + +import UIKit + +class BuildIceCreamViewController: UIViewController { + // MARK: Properties + + static let storyboardIdentifier = "BuildIceCreamViewController" + + weak var delegate: BuildIceCreamViewControllerDelegate? + + var iceCream: IceCream? { + didSet { + guard let iceCream = iceCream else { return } + + // Determine the ice cream parts to show in the collection view. + if iceCream.base == nil { + iceCreamParts = Base.all.map { $0 } + prompt = NSLocalizedString("Select a base", comment: "") + } + else if iceCream.scoops == nil { + iceCreamParts = Scoops.all.map { $0 } + prompt = NSLocalizedString("Add some scoops", comment: "") + } + else if iceCream.topping == nil { + iceCreamParts = Topping.all.map { $0 } + prompt = NSLocalizedString("Finish with a topping", comment: "") + } + } + } + + /// An array of `IceCreamPart`s to show in the collection view. + fileprivate var iceCreamParts = [IceCreamPart]() { + didSet { + // Update the collection view to show the new ice cream parts. + guard isViewLoaded else { return } + collectionView.reloadData() + } + } + + private var prompt: String? + + @IBOutlet weak var promptLabel: UILabel! + + @IBOutlet weak var iceCreamView: IceCreamView! + + @IBOutlet weak var iceCreamViewHeightConstraint: NSLayoutConstraint! + + @IBOutlet weak var collectionView: UICollectionView! + + @IBOutlet weak var collectionViewHeightConstraint: NSLayoutConstraint! + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + // Make sure the prompt and ice cream view are showing the correct information. + promptLabel.text = prompt + iceCreamView.iceCream = iceCream + + /* + We want the collection view to decelerate faster than normal so comes + to rests on a body part more quickly. + */ + collectionView.decelerationRate = UIScrollViewDecelerationRateFast + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // There is nothing to layout of there are no ice cream parts to pick from. + guard !iceCreamParts.isEmpty else { return } + guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { fatalError("Expected the collection view to have a UICollectionViewFlowLayout") } + + // The ideal cell width is 1/3 of the width of the collection view. + layout.itemSize.width = floor(view.bounds.size.width / 3.0) + + // Set the cell height using the aspect ratio of the ice cream part images. + let iceCreamPartImageSize = iceCreamParts[0].image.size + guard iceCreamPartImageSize.width > 0 else { return } + let imageAspectRatio = iceCreamPartImageSize.width / iceCreamPartImageSize.height + + layout.itemSize.height = floor(layout.itemSize.width / imageAspectRatio) + + // Set the collection view's height constraint to match the cell size. + collectionViewHeightConstraint.constant = layout.itemSize.height + + // Adjust the collection view's `contentInset` so the first item is centered. + var contentInset = collectionView.contentInset + contentInset.left = (view.bounds.size.width - layout.itemSize.width) / 2.0 + contentInset.right = contentInset.left + collectionView.contentInset = contentInset + + // Calculate the ideal height of the ice cream view. + let iceCreamViewContentHeight = iceCreamView.arrangedSubviews.reduce(0.0) { total, arrangedSubview in + return total + arrangedSubview.intrinsicContentSize.height + } + + let iceCreamPartImageScale = layout.itemSize.height / iceCreamPartImageSize.height + iceCreamViewHeightConstraint.constant = floor(iceCreamViewContentHeight * iceCreamPartImageScale) + } + + // MARK: Interface Builder actions + + @IBAction func didTapSelect(_: AnyObject) { + // Determine the index path of the centered cell in the collection view. + guard let layout = collectionView.collectionViewLayout as? IceCreamPartCollectionViewLayout else { fatalError("Expected the collection view to have a IceCreamPartCollectionViewLayout") } + + let halfWidth = collectionView.bounds.size.width / 2.0 + guard let indexPath = layout.indexPathForVisibleItemClosest(to: collectionView.contentOffset.x + halfWidth) else { return } + + // Call the delegate with the body part for the centered cell. + delegate?.buildIceCreamViewController(self, didSelect: iceCreamParts[indexPath.row]) + } +} + + + +/** + A delegate protocol for the `BuildIceCreamViewController` class. + */ +protocol BuildIceCreamViewControllerDelegate: class { + /// Called when the user taps to select an `IceCreamPart` in the `BuildIceCreamViewController`. + func buildIceCreamViewController(_ controller: BuildIceCreamViewController, didSelect iceCreamPart: IceCreamPart) +} + + + +/** + Extends `BuildIceCreamViewController` to conform to the `UICollectionViewDataSource` + protocol. + */ +extension BuildIceCreamViewController: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return iceCreamParts.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: IceCreamPartCell.reuseIdentifier, for: indexPath as IndexPath) as? IceCreamPartCell else { fatalError("Unable to dequeue a BodyPartCell") } + + let iceCreamPart = iceCreamParts[indexPath.row] + cell.imageView.image = iceCreamPart.image + + return cell + } +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/CompletedIceCreamViewController.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/CompletedIceCreamViewController.swift new file mode 100644 index 00000000..5d5215d1 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/CompletedIceCreamViewController.swift @@ -0,0 +1,39 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The view controller shown when a completed ice cream is selected in a conversation. +*/ + +import UIKit +import Messages + +class CompletedIceCreamViewController: UIViewController { + // MARK: Properties + + static let storyboardIdentifier = "CompletedIceCreamViewController" + + var iceCream: IceCream? + + @IBOutlet weak var stickerView: MSStickerView! + + // MARK: UIViewController + + override func viewDidLoad() { + guard let iceCream = iceCream else { fatalError("No ice cream has been set") } + super.viewDidLoad() + + // Update the sticker view + let cache = IceCreamStickerCache.cache + + stickerView.sticker = cache.placeholderSticker + cache.sticker(for: iceCream) { sticker in + OperationQueue.main.addOperation { + guard self.isViewLoaded else { return } + + self.stickerView.sticker = sticker + } + } + } +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCream+Image.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCream+Image.swift new file mode 100644 index 00000000..d1d97571 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCream+Image.swift @@ -0,0 +1,109 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extends `IceCream` to add methods to render the ice cream as a `UIImage`. +*/ + +import UIKit + +extension IceCream { + + private struct StickerProperties { + /// The desired size of an ice cream sticker image. + static let size = CGSize(width: 300.0, height: 300.0) + + /** + The amount of padding to apply to a sticker when drawn with an opaque + background. + */ + static let opaquePadding = CGSize(width: 60.0, height: 10.0) + } + + func renderSticker(opaque: Bool) -> UIImage? { + guard let partsImage = renderParts() else { return nil } + + // Determine the size to draw as a sticker. + let outputSize: CGSize + let iceCreamSize: CGSize + + if opaque { + // Scale the ice cream image to fit in the center of the sticker. + let scale = min((StickerProperties.size.width - StickerProperties.opaquePadding.width) / partsImage.size.height, + (StickerProperties.size.height - StickerProperties.opaquePadding.height) / partsImage.size.width) + iceCreamSize = CGSize(width: partsImage.size.width * scale, height: partsImage.size.height * scale) + outputSize = StickerProperties.size + } + else { + // Scale the ice cream to fit it's height into the sticker. + let scale = StickerProperties.size.width / partsImage.size.height + iceCreamSize = CGSize(width: partsImage.size.width * scale, height: partsImage.size.height * scale) + outputSize = iceCreamSize + } + + // Scale the ice cream image to the correct size. + let renderer = UIGraphicsImageRenderer(size: outputSize) + let image = renderer.image { context in + let backgroundColor: UIColor + if opaque { + // Give the image a colored background. + backgroundColor = UIColor(red: 250.0 / 255.0, green: 225.0 / 255.0, blue: 235.0 / 255.0, alpha: 1.0) + } + else { + // Give the image a clear background + backgroundColor = UIColor.clear + } + + // Draw the background + backgroundColor.setFill() + context.fill(CGRect(origin: CGPoint.zero, size: StickerProperties.size)) + + // Draw the scaled composited image. + var drawRect = CGRect.zero + drawRect.size = iceCreamSize + drawRect.origin.x = (outputSize.width / 2.0) - (iceCreamSize.width / 2.0) + drawRect.origin.y = (outputSize.height / 2.0) - (iceCreamSize.height / 2.0) + + partsImage.draw(in: drawRect) + } + + return image + } + + /// Composites the valid ice cream parts into a single `UIImage`. + private func renderParts() -> UIImage? { + // Determine which parts to draw. + let allParts: [IceCreamPart?] = [topping, scoops, base] + let partImages = allParts.flatMap { $0?.stickerImage } + + guard !partImages.isEmpty else { return nil } + + // Calculate the size of the composited ice cream parts image. + var outputImageSize = CGSize.zero + outputImageSize.width = partImages.reduce(0) { largestWidth, image in + return max(largestWidth, image.size.width) + } + outputImageSize.height = partImages.reduce(0) { totalHeight, image in + return totalHeight + image.size.height + } + + // Render the part images into a single composite image. + let renderer = UIGraphicsImageRenderer(size: outputImageSize) + let image = renderer.image { context in + // Draw each of the body parts in a vertica stack. + var nextYPosition = CGFloat(0.0) + for partImage in partImages { + var position = CGPoint.zero + position.x = outputImageSize.width / 2.0 - partImage.size.width / 2.0 + position.y = nextYPosition + + partImage.draw(at: position) + + nextYPosition += partImage.size.height + } + } + + return image + } +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCream.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCream.swift new file mode 100644 index 00000000..05448f72 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCream.swift @@ -0,0 +1,103 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Defines the `IceCream` struct that represents a complete or partially built ice cream. +*/ + +import Foundation +import Messages + +struct IceCream { + // MARK: Properties + + var base: Base? + + var scoops: Scoops? + + var topping: Topping? + + var isComplete: Bool { + return base != nil && scoops != nil && topping != nil + } +} + + + +/** + Extends `IceCream` to be able to be represented by and created with an array of + `NSURLQueryItem`s. + */ +extension IceCream { + // MARK: Computed properties + + var queryItems: [URLQueryItem] { + var items = [URLQueryItem]() + + if let part = base { + items.append(part.queryItem) + } + if let part = scoops { + items.append(part.queryItem) + } + if let part = topping { + items.append(part.queryItem) + } + + return items + } + + // MARK: Initialization + + init?(queryItems: [URLQueryItem]) { + var base: Base? + var scoops: Scoops? + var topping: Topping? + + for queryItem in queryItems { + guard let value = queryItem.value else { continue } + + if let decodedPart = Base(rawValue: value), queryItem.name == Base.queryItemKey { + base = decodedPart + } + if let decodedPart = Scoops(rawValue: value), queryItem.name == Scoops.queryItemKey { + scoops = decodedPart + } + if let decodedPart = Topping(rawValue: value), queryItem.name == Topping.queryItemKey { + topping = decodedPart + } + } + + guard let decodedBase = base else { return nil } + + self.base = decodedBase + self.scoops = scoops + self.topping = topping + } +} + + + +/** + Extends `IceCream` to be able to be created with the contents of an `MSMessage`. + */ +extension IceCream { + init?(message: MSMessage?) { + guard let messageURL = message?.url else { return nil } + guard let urlComponents = NSURLComponents(url: messageURL, resolvingAgainstBaseURL: false), let queryItems = urlComponents.queryItems else { return nil } + + self.init(queryItems: queryItems) + } +} + + + +/** + Extends `IceCream` to make it `Equatable`. + */ +extension IceCream: Equatable {} + +func ==(lhs: IceCream, rhs: IceCream) -> Bool { + return lhs.base == rhs.base && lhs.scoops == rhs.scoops && lhs.topping == rhs.topping +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamCell.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamCell.swift new file mode 100644 index 00000000..17ca28d2 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamCell.swift @@ -0,0 +1,19 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A `UICollectionViewCell` subclass used to display an `IceCream` in the `IceCreamsViewController`. +*/ + +import UIKit +import Messages + +class IceCreamCell: UICollectionViewCell { + + static let reuseIdentifier = "IceCreamCell" + + var representedIceCream: IceCream? + + @IBOutlet weak var stickerView: MSStickerView! +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamHistory.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamHistory.swift new file mode 100644 index 00000000..bdfb4a96 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamHistory.swift @@ -0,0 +1,117 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A struct that persists a history of ice creams to `UserDefaults`. +*/ + +import Foundation + +struct IceCreamHistory { + // MARK: Properties + + private static let maximumHistorySize = 50 + + private static let userDefaultsKey = "iceCreams" + + /// An array of previously created `IceCream`s. + fileprivate var iceCreams: [IceCream] + + var count: Int { + return iceCreams.count + } + + subscript(index: Int) -> IceCream { + return iceCreams[index] + } + + // MARK: Initialization + + /** + `IceCreamHistory`'s initializer is marked as private. Instead instances should + be loaded via the `load` method. + */ + private init(iceCreams: [IceCream]) { + self.iceCreams = iceCreams + } + + /// Loads previously created `IceCream`s and returns a `IceCreamHistory` instance. + static func load() -> IceCreamHistory { + var iceCreams = [IceCream]() + let defaults = UserDefaults.standard + + if let savedIceCreams = defaults.object(forKey: IceCreamHistory.userDefaultsKey) as? [String] { + iceCreams = savedIceCreams.flatMap { urlString in + guard let url = URL(string: urlString) else { return nil } + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = components.queryItems else { return nil } + + return IceCream(queryItems: queryItems) + } + } + + // If no ice creams have been loaded, create some tasty examples. + if iceCreams.isEmpty { + iceCreams.append(IceCream(base: .base01, scoops: .scoops01, topping: .topping01)) + iceCreams.append(IceCream(base: .base02, scoops: .scoops02, topping: .topping02)) + iceCreams.append(IceCream(base: .base03, scoops: .scoops03, topping: .topping03)) + iceCreams.append(IceCream(base: .base04, scoops: .scoops04, topping: .topping04)) + + let historyToSave = IceCreamHistory(iceCreams: iceCreams) + historyToSave.save() + } + + return IceCreamHistory(iceCreams: iceCreams) + } + + /// Saves the history. + func save() { + // Save a maximum number ice creams. + let iceCreamsToSave = iceCreams.suffix(IceCreamHistory.maximumHistorySize) + + // Map the ice creams to an array of URL strings. + let iceCreamURLStrings: [String] = iceCreamsToSave.flatMap { iceCream in + var components = URLComponents() + components.queryItems = iceCream.queryItems + + return components.url?.absoluteString + } + + let defaults = UserDefaults.standard + defaults.set(iceCreamURLStrings as AnyObject, forKey: IceCreamHistory.userDefaultsKey) + } + + mutating func append(_ iceCream: IceCream) { + /* + Filter any existing instances of the new ice cream from the current + history before adding it to the end of the history. + */ + var newIceCreams = self.iceCreams.filter { $0 != iceCream } + newIceCreams.append(iceCream) + + iceCreams = newIceCreams + } +} + + + +/** + Extends `IceCreamHistory` to conform to the `Sequence` protocol so it can be used + in for..in statements. + */ +extension IceCreamHistory: Sequence { + typealias Iterator = AnyIterator + + func makeIterator() -> Iterator { + var index = 0 + + return Iterator { + guard index < self.iceCreams.count else { return nil } + + let iceCream = self.iceCreams[index] + index += 1 + + return iceCream + } + } +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamOutlineCell.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamOutlineCell.swift new file mode 100644 index 00000000..e4b24d01 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamOutlineCell.swift @@ -0,0 +1,13 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A `UICollectionViewCell` subclass used to display the empty outline of an ice creeam in the `IceCreamsViewController`. +*/ + +import UIKit + +class IceCreamOutlineCell: UICollectionViewCell { + static let reuseIdentifier = "IceCreamOutlineCell" +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamPart.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamPart.swift new file mode 100644 index 00000000..4e271e87 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamPart.swift @@ -0,0 +1,75 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The protocol and enums that define the different types of body parts that are used to build a robot. +*/ + +import UIKit + +protocol IceCreamPart { + var rawValue: String { get } + + var image: UIImage { get } + + var stickerImage: UIImage { get } +} + + +enum Topping: String, IceCreamPart, QueryItemRepresentable { + case topping01, topping02, topping03, topping04, topping05, topping06, topping07, topping08, topping09, topping10, topping11, topping12 + + static let all: [Topping] = [.topping01, .topping02, .topping03, .topping04, .topping05, .topping06, .topping07, .topping08, .topping09, .topping10, .topping11, .topping12] + + static var queryItemKey: String { + return "Topping" + } +} + +enum Scoops: String, IceCreamPart, QueryItemRepresentable { + case scoops01, scoops02, scoops03, scoops04, scoops05, scoops06, scoops07, scoops08, scoops09, scoops10 + + static let all: [Scoops] = [.scoops01, .scoops02, .scoops03, .scoops04, .scoops05, .scoops06, .scoops07, .scoops08, .scoops09, .scoops10] + + static var queryItemKey: String { + return "Scoops" + } +} + +enum Base: String, IceCreamPart, QueryItemRepresentable { + case base01, base02, base03, base04 + + static let all: [Base] = [.base01, .base02, .base03, .base04] + + static var queryItemKey: String { + return "Base" + } +} + + + +/// Extends `IceCreamPart` to provide a default implementation of the `image` and `stickerImage` properties. +extension IceCreamPart { + var image: UIImage { + let imageName = self.rawValue + guard let image = UIImage(named: imageName) else { fatalError("Unable to find image named \(imageName)") } + return image + } + + var stickerImage: UIImage { + let imageName = "\(self.rawValue)_sticker" + guard let image = UIImage(named: imageName) else { fatalError("Unable to find image named \(imageName)") } + return image + } +} + +/** + Extends instances of `QueryItemRepresentable` that also conformt to `IceCreamPart` + to provide a default implementation of `queryItem`. + */ +extension QueryItemRepresentable where Self: IceCreamPart { + var queryItem: URLQueryItem { + return URLQueryItem(name: Self.queryItemKey, value: rawValue) + } +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamPartCell.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamPartCell.swift new file mode 100644 index 00000000..a0fd3a54 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamPartCell.swift @@ -0,0 +1,15 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A `UICollectionViewCell` subclass used to display an ice cream part in the `BuildIceCreamViewController`. +*/ + +import UIKit + +class IceCreamPartCell: UICollectionViewCell { + static let reuseIdentifier = "IceCreamPartCell" + + @IBOutlet weak var imageView: UIImageView! +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamPartCollectionViewLayout.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamPartCollectionViewLayout.swift new file mode 100644 index 00000000..61d99291 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamPartCollectionViewLayout.swift @@ -0,0 +1,36 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A custom `UICollectionViewFlowLayout` that allows the `UICollectionView` in the `BuildIceCreamViewController` to horizontally center selected ice cream parts. +*/ + +import UIKit + +class IceCreamPartCollectionViewLayout: UICollectionViewFlowLayout { + + override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint { + guard let collectionView = collectionView else { return proposedContentOffset } + let halfWidth = collectionView.bounds.width / 2.0 + + guard let targetIndexPath = indexPathForVisibleItemClosest(to: proposedContentOffset.x + halfWidth) else { return proposedContentOffset } + guard let itemAttributes = layoutAttributesForItem(at: targetIndexPath) else { return proposedContentOffset } + + return CGPoint(x: itemAttributes.center.x - halfWidth, y: proposedContentOffset.y) + } + + func indexPathForVisibleItemClosest(to offset: CGFloat) -> IndexPath? { + guard let collectionView = collectionView else { return nil } + guard let layoutAttributes = layoutAttributesForElements(in: collectionView.bounds), !layoutAttributes.isEmpty else { return nil } + + let closestAttributes = layoutAttributes.sorted { attributesA, attributesB in + let distanceA = abs(attributesA.center.x - offset) + let distanceB = abs(attributesB.center.x - offset) + + return distanceA < distanceB + }.first! + + return closestAttributes.indexPath + } +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamStickerCache.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamStickerCache.swift new file mode 100644 index 00000000..fe384fd5 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamStickerCache.swift @@ -0,0 +1,101 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An class that caches sticker images in a temporary directory. +*/ + +import UIKit +import Messages + +class IceCreamStickerCache { + + static let cache = IceCreamStickerCache() + + private let cacheURL: URL + + private let queue = OperationQueue() + + /** + An `MSSticker` that can be used as a placeholder while a real ice cream + sticker is being fetched from the cache. + */ + let placeholderSticker: MSSticker = { + let bundle = Bundle.main + guard let placeholderURL = bundle.url(forResource: "sticker_placeholder", withExtension: "png") else { fatalError("Unable to find placeholder sticker image") } + + do { + let description = NSLocalizedString("An ice cream sticker", comment: "") + return try MSSticker(contentsOfFileURL: placeholderURL, localizedDescription: description) + } + catch { + fatalError("Failed to create placeholder sticker: \(error)") + } + }() + + // MARK: Initialization + + private init() { + let fileManager = FileManager.default + let tempPath = NSTemporaryDirectory() + let directoryName = UUID().uuidString + + do { + cacheURL = URL(fileURLWithPath: tempPath).appendingPathComponent(directoryName) + try fileManager.createDirectory(at: cacheURL, withIntermediateDirectories: true, attributes: nil) + } + catch { + fatalError("Unable to create cache URL: \(error)") + } + } + + deinit { + let fileManager = FileManager.default + do { + try fileManager.removeItem(at: cacheURL) + } + catch { + print("Unable to remove cache directory: \(error)") + } + } + + // MARK + + func sticker(for iceCream: IceCream, completion: @escaping (_ sticker: MSSticker) -> Void) { + guard let base = iceCream.base, let scoops = iceCream.scoops, let topping = iceCream.topping else { fatalError("Stickers can only be created for completed ice creams") } + + // Determine the URL for the sticker. + let fileName = base.rawValue + scoops.rawValue + topping.rawValue + ".png" + let url = cacheURL.appendingPathComponent(fileName) + + // Create an operation to process the request. + let operation = BlockOperation { + // Check if the sticker already exists at the URL. + let fileManager = FileManager.default + guard !fileManager.fileExists(atPath: url.absoluteString) else { return } + + // Create the sticker image and write it to disk. + guard let image = iceCream.renderSticker(opaque: false), let imageData = UIImagePNGRepresentation(image) else { fatalError("Unable to build image for ice cream") } + + do { + try imageData.write(to: url, options: [.atomicWrite]) + } catch { + fatalError("Failed to write sticker image to cache: \(error)") + } + } + + // Set the operation's completion block to call the request's completion handler. + operation.completionBlock = { + do { + let sticker = try MSSticker(contentsOfFileURL: url, localizedDescription: "Ice Cream") + completion(sticker) + } catch { + print("Failed to write image to cache, error: \(error)") + } + } + + // Add the operation to the queue to start the work. + queue.addOperation(operation) + } +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamView.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamView.swift new file mode 100644 index 00000000..86cc97f3 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamView.swift @@ -0,0 +1,34 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A `UIStackView` containing the parts of a given `IceCream`. +*/ + +import UIKit + +class IceCreamView: UIStackView { + + var iceCream: IceCream? { + didSet { + // Remove any existing arranged subviews. + for view in arrangedSubviews { + removeArrangedSubview(view) + } + + // Do nothing more if the `iceCream` property is nil. + guard let unwrappedIceCream = iceCream else { return } + + // Add a `UIImageView` for each of the ice cream's valid parts. + let iceCreamParts: [IceCreamPart?] = [unwrappedIceCream.topping, unwrappedIceCream.scoops, unwrappedIceCream.base] + for iceCreamPart in iceCreamParts { + guard let iceCreamPart = iceCreamPart else { continue } + + let imageView = UIImageView(image: iceCreamPart.image) + imageView.contentMode = .scaleAspectFit + addArrangedSubview(imageView) + } + } + } +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamsViewController.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamsViewController.swift new file mode 100644 index 00000000..c1d349b8 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/IceCreamsViewController.swift @@ -0,0 +1,115 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A `UICollectionViewController` that displays the history of ice creams as well as a cell that can be tapped to start the process of creating a new ice cream. +*/ + +import UIKit + +class IceCreamsViewController: UICollectionViewController { + // MARK: Types + + /// An enumeration that represents an item in the collection view. + enum CollectionViewItem { + case iceCream(IceCream) + case create + } + + // MARK: Properties + + static let storyboardIdentifier = "IceCreamsViewController" + + weak var delegate: IceCreamsViewControllerDelegate? + + private let items: [CollectionViewItem] + + private let stickerCache = IceCreamStickerCache.cache + + // MARK: Initialization + + required init?(coder aDecoder: NSCoder) { + // Map the previously completed ice creams to an array of `CollectionViewItem`s. + let reversedHistory = IceCreamHistory.load().reversed() + var items: [CollectionViewItem] = reversedHistory.map { .iceCream($0) } + + // Add `CollectionViewItem` that the user can tap to start building a new ice cream. + items.insert(.create, at: 0) + + self.items = items + super.init(coder: aDecoder) + } + + // MARK: UICollectionViewDataSource + + override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return items.count + } + + override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let item = items[indexPath.row] + + // The item's type determines which type of cell to return. + switch item { + case .iceCream(let iceCream): + return dequeueIceCreamCell(for: iceCream, at: indexPath) + + case .create: + return dequeueIceCreamOutlineCell(at: indexPath) + } + } + + // MARK: UICollectionViewDelegate + + override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let item = items[indexPath.row] + + switch item { + case .create: + delegate?.iceCreamsViewControllerDidSelectAdd(self) + + default: + break + } + } + + // MARK: Convenience + + private func dequeueIceCreamCell(for iceCream: IceCream, at indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView?.dequeueReusableCell(withReuseIdentifier: IceCreamCell.reuseIdentifier, for: indexPath) as? IceCreamCell else { fatalError("Unable to dequeue am IceCreamCell") } + + cell.representedIceCream = iceCream + + // Use a placeholder sticker while we fetch the real one from the cache. + let cache = IceCreamStickerCache.cache + cell.stickerView.sticker = cache.placeholderSticker + + // Fetch the sticker for the ice cream from the cache. + cache.sticker(for: iceCream) { sticker in + OperationQueue.main.addOperation { + // If the cell is still showing the same ice cream, update its sticker view. + guard cell.representedIceCream == iceCream else { return } + cell.stickerView.sticker = sticker + } + } + + return cell + } + + private func dequeueIceCreamOutlineCell(at indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView?.dequeueReusableCell(withReuseIdentifier: IceCreamOutlineCell.reuseIdentifier, for: indexPath) as? IceCreamOutlineCell else { fatalError("Unable to dequeue a IceCreamOutlineCell") } + + return cell + } +} + + + +/** + A delegate protocol for the `IceCreamsViewController` class. + */ +protocol IceCreamsViewControllerDelegate: class { + /// Called when a user choses to add a new `IceCream` in the `IceCreamsViewController`. + func iceCreamsViewControllerDidSelectAdd(_ controller: IceCreamsViewController) +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/Info.plist b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Info.plist new file mode 100644 index 00000000..3dd7e26d --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/Info.plist @@ -0,0 +1,33 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Ice Cream + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSExtension + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.message-payload-provider + + + diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/MessagesViewController.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/MessagesViewController.swift new file mode 100644 index 00000000..6e14a317 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/MessagesViewController.swift @@ -0,0 +1,191 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The root view controller shown by the Messages app. +*/ + +import UIKit +import Messages + +class MessagesViewController: MSMessagesAppViewController { + // MARK: Properties + + override func willBecomeActive(with conversation: MSConversation) { + super.willBecomeActive(with: conversation) + + // Present the view controller appropriate for the conversation and presentation style. + presentViewController(for: conversation, with: presentationStyle) + } + + // MARK: MSMessagesAppViewController + + override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) { + guard let conversation = activeConversation else { fatalError("Expected an active converstation") } + + // Present the view controller appropriate for the conversation and presentation style. + presentViewController(for: conversation, with: presentationStyle) + } + + // MARK: Child view controller presentation + + private func presentViewController(for conversation: MSConversation, with presentationStyle: MSMessagesAppPresentationStyle) { + // Determine the controller to present. + let controller: UIViewController + if presentationStyle == .compact { + // Show a list of previously created ice creams. + controller = instantiateIceCreamsController() + } + else { + /* + Parse an `IceCream` from the conversation's `selectedMessage` or + create a new `IceCream` if there isn't one associated with the message. + */ + let iceCream = IceCream(message: conversation.selectedMessage) ?? IceCream() + + if iceCream.isComplete { + controller = instantiateCompletedIceCreamController(with: iceCream) + } + else { + controller = instantiateBuildIceCreamController(with: iceCream) + } + } + + // Remove any existing child controllers. + for child in childViewControllers { + child.willMove(toParentViewController: nil) + child.view.removeFromSuperview() + child.removeFromParentViewController() + } + + // Embed the new controller. + addChildViewController(controller) + + controller.view.frame = view.bounds + controller.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(controller.view) + + controller.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true + controller.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true + controller.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true + + controller.didMove(toParentViewController: self) + } + + private func instantiateIceCreamsController() -> UIViewController { + // Instantiate a `IceCreamsViewController` and present it. + guard let controller = storyboard?.instantiateViewController(withIdentifier: IceCreamsViewController.storyboardIdentifier) as? IceCreamsViewController else { fatalError("Unable to instantiate an IceCreamsViewController from the storyboard") } + + controller.delegate = self + + return controller + } + + private func instantiateBuildIceCreamController(with iceCream: IceCream) -> UIViewController { + // Instantiate a `BuildIceCreamViewController` and present it. + guard let controller = storyboard?.instantiateViewController(withIdentifier: BuildIceCreamViewController.storyboardIdentifier) as? BuildIceCreamViewController else { fatalError("Unable to instantiate a BuildIceCreamViewController from the storyboard") } + + controller.iceCream = iceCream + controller.delegate = self + + return controller + } + + private func instantiateCompletedIceCreamController(with iceCream: IceCream) -> UIViewController { + // Instantiate a `BuildIceCreamViewController` and present it. + guard let controller = storyboard?.instantiateViewController(withIdentifier: CompletedIceCreamViewController.storyboardIdentifier) as? CompletedIceCreamViewController else { fatalError("Unable to instantiate a CompletedIceCreamViewController from the storyboard") } + + controller.iceCream = iceCream + + return controller + } + + // MARK: Convenience + + fileprivate func composeMessage(with iceCream: IceCream, caption: String, session: MSSession? = nil) -> MSMessage { + var components = URLComponents() + components.queryItems = iceCream.queryItems + + let layout = MSMessageTemplateLayout() + layout.image = iceCream.renderSticker(opaque: true) + layout.caption = caption + + let message = MSMessage(session: session ?? MSSession()) + message.url = components.url! + message.layout = layout + + return message + } +} + + + +/** + Extends `MessagesViewController` to conform to the `IceCreamsViewControllerDelegate` + protocol. + */ +extension MessagesViewController: IceCreamsViewControllerDelegate { + func iceCreamsViewControllerDidSelectAdd(_ controller: IceCreamsViewController) { + /* + The user tapped the silhouette to start creating a new ice cream. + Change the presentation style to `.expanded`. + */ + requestPresentationStyle(.expanded) + } +} + + + +/** + Extends `MessagesViewController` to conform to the `BuildIceCreamViewControllerDelegate` + protocol. + */ +extension MessagesViewController: BuildIceCreamViewControllerDelegate { + func buildIceCreamViewController(_ controller: BuildIceCreamViewController, didSelect iceCreamPart: IceCreamPart) { + guard let conversation = activeConversation else { fatalError("Expected a conversation") } + guard var iceCream = controller.iceCream else { fatalError("Expected the controller to be displaying an ice cream") } + + /* + Update the ice cream with the selected body part and determine a caption + and description of the change. + */ + var messageCaption: String + + if let base = iceCreamPart as? Base { + iceCream.base = base + messageCaption = NSLocalizedString("Let's build an ice cream", comment: "") + } + else if let scoops = iceCreamPart as? Scoops { + iceCream.scoops = scoops + messageCaption = NSLocalizedString("I added some scoops", comment: "") + } + else if let topping = iceCreamPart as? Topping { + iceCream.topping = topping + messageCaption = NSLocalizedString("Our finished ice cream", comment: "") + } + else { + fatalError("Unexpected type of ice cream part selected.") + } + + // Create a new message with the same session as any currently selected message. + let message = composeMessage(with: iceCream, caption: messageCaption, session: conversation.selectedMessage?.session) + + // Add the message to the conversation. + conversation.insert(message) { error in + if let error = error { + print(error) + } + } + + // If the ice cream is complete, save it in the history. + if iceCream.isComplete { + var history = IceCreamHistory.load() + history.append(iceCream) + history.save() + } + + dismiss() + } +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/QueryItemRepresentable.swift b/IceCreamBuilder/IceCreamBuilderMessagesExtension/QueryItemRepresentable.swift new file mode 100644 index 00000000..6571e5a8 --- /dev/null +++ b/IceCreamBuilder/IceCreamBuilderMessagesExtension/QueryItemRepresentable.swift @@ -0,0 +1,15 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Types that conform to the QueryItemRepresentable protocol must implement properties that allow it to be saved as a query item in a URL. +*/ + +import Foundation + +protocol QueryItemRepresentable { + var queryItem: URLQueryItem { get } + + static var queryItemKey: String { get } +} diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base01_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base01_sticker.png new file mode 100644 index 00000000..717bd7d4 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base01_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base02_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base02_sticker.png new file mode 100644 index 00000000..08daa7cd Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base02_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base03_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base03_sticker.png new file mode 100644 index 00000000..4a7a4415 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base03_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base04_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base04_sticker.png new file mode 100644 index 00000000..486fe125 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/base04_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops01_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops01_sticker.png new file mode 100644 index 00000000..6b5e56df Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops01_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops02_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops02_sticker.png new file mode 100644 index 00000000..f94aa88b Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops02_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops03_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops03_sticker.png new file mode 100644 index 00000000..b1d7a12f Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops03_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops04_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops04_sticker.png new file mode 100644 index 00000000..dc8137b7 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops04_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops05_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops05_sticker.png new file mode 100644 index 00000000..d9863e14 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops05_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops06_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops06_sticker.png new file mode 100644 index 00000000..dd03d437 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops06_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops07_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops07_sticker.png new file mode 100644 index 00000000..9c8f0b19 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops07_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops08_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops08_sticker.png new file mode 100644 index 00000000..eb779f84 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops08_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops09_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops09_sticker.png new file mode 100644 index 00000000..cda65ea9 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops09_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops10_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops10_sticker.png new file mode 100644 index 00000000..1caaf224 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/scoops10_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/sticker_placeholder.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/sticker_placeholder.png new file mode 100644 index 00000000..d0ce0161 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/sticker_placeholder.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping01_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping01_sticker.png new file mode 100644 index 00000000..981edf72 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping01_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping02_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping02_sticker.png new file mode 100644 index 00000000..9a0eaec7 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping02_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping03_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping03_sticker.png new file mode 100644 index 00000000..b5bec595 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping03_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping04_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping04_sticker.png new file mode 100644 index 00000000..b5a31e6b Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping04_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping05_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping05_sticker.png new file mode 100644 index 00000000..cef826c6 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping05_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping06_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping06_sticker.png new file mode 100644 index 00000000..71719170 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping06_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping07_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping07_sticker.png new file mode 100644 index 00000000..17dc8901 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping07_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping08_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping08_sticker.png new file mode 100644 index 00000000..01c69e52 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping08_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping09_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping09_sticker.png new file mode 100644 index 00000000..b90dc283 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping09_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping10_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping10_sticker.png new file mode 100644 index 00000000..3d1aae96 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping10_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping11_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping11_sticker.png new file mode 100644 index 00000000..de57a332 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping11_sticker.png differ diff --git a/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping12_sticker.png b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping12_sticker.png new file mode 100644 index 00000000..7acaeff0 Binary files /dev/null and b/IceCreamBuilder/IceCreamBuilderMessagesExtension/StickerPartImages/topping12_sticker.png differ diff --git a/IceCreamBuilder/LICENSE.txt b/IceCreamBuilder/LICENSE.txt new file mode 100644 index 00000000..f20f5ece --- /dev/null +++ b/IceCreamBuilder/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: Ice Cream Builder: A simple Messages app extension +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) 2016 Apple Inc. All Rights Reserved. diff --git a/IceCreamBuilder/README.md b/IceCreamBuilder/README.md new file mode 100644 index 00000000..a8746ac4 --- /dev/null +++ b/IceCreamBuilder/README.md @@ -0,0 +1,17 @@ +# Ice Cream Builder: A simple Messages app extension + +This sample is a simple example of building an app extension that interacts with the Messages app and lets users send interactive messages and create stickers. + +The extension is based on a MSMessagesAppViewController subclass that then presents a child view controller depending on the current conversation, message state and presentation style. It also updates a conversation with new or updated messages and posts stickers to a conversation. + +## Requirements + +### Build + +Xcode 8.0 or later; iOS 10.0 SDK or later + +### Runtime + +iOS 10.0 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/IntentHandling/IntentHandling.xcworkspace/contents.xcworkspacedata b/IntentHandling/IntentHandling.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..78fe4ae9 --- /dev/null +++ b/IntentHandling/IntentHandling.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/IntentHandling/LICENSE.txt b/IntentHandling/LICENSE.txt new file mode 100644 index 00000000..8edddb7c --- /dev/null +++ b/IntentHandling/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: IntentHandling: Using the Intents framework to handle custom Siri request +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) 2016 Apple Inc. All Rights Reserved. diff --git a/IntentHandling/Projects/Ascent/Ascent.xcodeproj/project.pbxproj b/IntentHandling/Projects/Ascent/Ascent.xcodeproj/project.pbxproj new file mode 100644 index 00000000..0c9d223d --- /dev/null +++ b/IntentHandling/Projects/Ascent/Ascent.xcodeproj/project.pbxproj @@ -0,0 +1,687 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + B51776991CE517C2001C5955 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B51776981CE517C2001C5955 /* Intents.framework */; }; + B5511C441CD75A2F0084A751 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5511C431CD75A2F0084A751 /* AppDelegate.swift */; }; + B5511C461CD75A2F0084A751 /* WorkoutsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5511C451CD75A2F0084A751 /* WorkoutsController.swift */; }; + B5511C491CD75A2F0084A751 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B5511C471CD75A2F0084A751 /* Main.storyboard */; }; + B5511C4B1CD75A2F0084A751 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5511C4A1CD75A2F0084A751 /* Assets.xcassets */; }; + B5511C4E1CD75A2F0084A751 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B5511C4C1CD75A2F0084A751 /* LaunchScreen.storyboard */; }; + B5511C5C1CD75A930084A751 /* Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5511C5B1CD75A930084A751 /* Extension.swift */; }; + B5511C6F1CD75A930084A751 /* AscentIntentsExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = B5511C591CD75A930084A751 /* AscentIntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + B553C38B1CDF63380084BA9A /* AppIntentVocabulary.plist in Resources */ = {isa = PBXBuildFile; fileRef = B553C38A1CDF63380084BA9A /* AppIntentVocabulary.plist */; }; + B5C39F3B1CED0C9900E93902 /* AscentFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = B5C39F3A1CED0C9900E93902 /* AscentFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B5C39F3F1CED0C9900E93902 /* AscentFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5C39F381CED0C9900E93902 /* AscentFramework.framework */; }; + B5C39F401CED0C9900E93902 /* AscentFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B5C39F381CED0C9900E93902 /* AscentFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B5C39F4C1CED0CC300E93902 /* NSUserActivity+Ascent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C39F451CED0CC300E93902 /* NSUserActivity+Ascent.swift */; }; + B5C39F4D1CED0CC300E93902 /* Workout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C39F461CED0CC300E93902 /* Workout.swift */; }; + B5C39F4E1CED0CC300E93902 /* Workout+Descriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C39F471CED0CC300E93902 /* Workout+Descriptions.swift */; }; + B5C39F4F1CED0CC300E93902 /* Workout+DictionaryRepresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C39F481CED0CC300E93902 /* Workout+DictionaryRepresentation.swift */; }; + B5C39F501CED0CC300E93902 /* Workout+INStartWorkoutIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C39F491CED0CC300E93902 /* Workout+INStartWorkoutIntent.swift */; }; + B5C39F511CED0CC300E93902 /* WorkoutHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C39F4A1CED0CC300E93902 /* WorkoutHistory.swift */; }; + B5C39F521CED0CC300E93902 /* WorkoutHistory+ActiveWorkout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C39F4B1CED0CC300E93902 /* WorkoutHistory+ActiveWorkout.swift */; }; + B5C39F531CED0D9800E93902 /* AscentFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5C39F381CED0C9900E93902 /* AscentFramework.framework */; }; + B5D7B46F1CE495A200A19023 /* CancelWorkoutIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D7B4681CE495A200A19023 /* CancelWorkoutIntentHandler.swift */; }; + B5D7B4701CE495A200A19023 /* EndWorkoutIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D7B4691CE495A200A19023 /* EndWorkoutIntentHandler.swift */; }; + B5D7B4711CE495A200A19023 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D7B46A1CE495A200A19023 /* IntentHandler.swift */; }; + B5D7B4721CE495A200A19023 /* PauseWorkoutIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D7B46B1CE495A200A19023 /* PauseWorkoutIntentHandler.swift */; }; + B5D7B4731CE495A200A19023 /* ResumeWorkoutIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D7B46C1CE495A200A19023 /* ResumeWorkoutIntentHandler.swift */; }; + B5D7B4741CE495A200A19023 /* StartWorkoutIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D7B46D1CE495A200A19023 /* StartWorkoutIntentHandler.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + B5511C6D1CD75A930084A751 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B5511C381CD75A2F0084A751 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B5511C581CD75A930084A751; + remoteInfo = AscentIntentsExtension; + }; + B5C39F3D1CED0C9900E93902 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B5511C381CD75A2F0084A751 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B5C39F371CED0C9900E93902; + remoteInfo = AscentFramework; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + B5511C761CD75A930084A751 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + B5511C6F1CD75A930084A751 /* AscentIntentsExtension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + B59FA6C01CDF4A3500CF6A71 /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; + B5C39F441CED0C9900E93902 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + B5C39F401CED0C9900E93902 /* AscentFramework.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + B51776981CE517C2001C5955 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; + B517769A1CE517D1001C5955 /* IntentsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntentsUI.framework; path = System/Library/Frameworks/IntentsUI.framework; sourceTree = SDKROOT; }; + B52BA4471CE4C455000E6AF1 /* AscentIntentsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = AscentIntentsExtension.entitlements; sourceTree = ""; }; + B5511C401CD75A2F0084A751 /* Ascent.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ascent.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B5511C431CD75A2F0084A751 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + B5511C451CD75A2F0084A751 /* WorkoutsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkoutsController.swift; sourceTree = ""; }; + B5511C481CD75A2F0084A751 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + B5511C4A1CD75A2F0084A751 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + B5511C4D1CD75A2F0084A751 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + B5511C4F1CD75A2F0084A751 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B5511C591CD75A930084A751 /* AscentIntentsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = AscentIntentsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + B5511C5B1CD75A930084A751 /* Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extension.swift; sourceTree = ""; }; + B5511C5D1CD75A930084A751 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B553C38A1CDF63380084BA9A /* AppIntentVocabulary.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = AppIntentVocabulary.plist; sourceTree = ""; }; + B553C3CD1CDF7F720084BA9A /* Ascent.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Ascent.entitlements; sourceTree = ""; }; + B574C8D91D7B81E700F68E77 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../../README.md; sourceTree = ""; }; + B5C39F381CED0C9900E93902 /* AscentFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AscentFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B5C39F3A1CED0C9900E93902 /* AscentFramework.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AscentFramework.h; sourceTree = ""; }; + B5C39F3C1CED0C9900E93902 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B5C39F451CED0CC300E93902 /* NSUserActivity+Ascent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Ascent.swift"; sourceTree = ""; }; + B5C39F461CED0CC300E93902 /* Workout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Workout.swift; sourceTree = ""; }; + B5C39F471CED0CC300E93902 /* Workout+Descriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Workout+Descriptions.swift"; sourceTree = ""; }; + B5C39F481CED0CC300E93902 /* Workout+DictionaryRepresentation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Workout+DictionaryRepresentation.swift"; sourceTree = ""; }; + B5C39F491CED0CC300E93902 /* Workout+INStartWorkoutIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Workout+INStartWorkoutIntent.swift"; sourceTree = ""; }; + B5C39F4A1CED0CC300E93902 /* WorkoutHistory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WorkoutHistory.swift; sourceTree = ""; }; + B5C39F4B1CED0CC300E93902 /* WorkoutHistory+ActiveWorkout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WorkoutHistory+ActiveWorkout.swift"; sourceTree = ""; }; + B5D7B4681CE495A200A19023 /* CancelWorkoutIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CancelWorkoutIntentHandler.swift; sourceTree = ""; }; + B5D7B4691CE495A200A19023 /* EndWorkoutIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EndWorkoutIntentHandler.swift; sourceTree = ""; }; + B5D7B46A1CE495A200A19023 /* IntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + B5D7B46B1CE495A200A19023 /* PauseWorkoutIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PauseWorkoutIntentHandler.swift; sourceTree = ""; }; + B5D7B46C1CE495A200A19023 /* ResumeWorkoutIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResumeWorkoutIntentHandler.swift; sourceTree = ""; }; + B5D7B46D1CE495A200A19023 /* StartWorkoutIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartWorkoutIntentHandler.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B5511C3D1CD75A2F0084A751 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B5C39F3F1CED0C9900E93902 /* AscentFramework.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B5511C561CD75A930084A751 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B5C39F531CED0D9800E93902 /* AscentFramework.framework in Frameworks */, + B51776991CE517C2001C5955 /* Intents.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B5C39F341CED0C9900E93902 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B51776971CE517C2001C5955 /* System Frameworks */ = { + isa = PBXGroup; + children = ( + B517769A1CE517D1001C5955 /* IntentsUI.framework */, + B51776981CE517C2001C5955 /* Intents.framework */, + ); + name = "System Frameworks"; + sourceTree = ""; + }; + B5511C371CD75A2F0084A751 = { + isa = PBXGroup; + children = ( + B574C8D91D7B81E700F68E77 /* README.md */, + B5511C421CD75A2F0084A751 /* Ascent */, + B5511C5A1CD75A930084A751 /* AscentIntentsExtension */, + B5C39F391CED0C9900E93902 /* AscentFramework */, + B5511C411CD75A2F0084A751 /* Products */, + B51776971CE517C2001C5955 /* System Frameworks */, + ); + sourceTree = ""; + }; + B5511C411CD75A2F0084A751 /* Products */ = { + isa = PBXGroup; + children = ( + B5511C401CD75A2F0084A751 /* Ascent.app */, + B5511C591CD75A930084A751 /* AscentIntentsExtension.appex */, + B5C39F381CED0C9900E93902 /* AscentFramework.framework */, + ); + name = Products; + sourceTree = ""; + }; + B5511C421CD75A2F0084A751 /* Ascent */ = { + isa = PBXGroup; + children = ( + B5511C431CD75A2F0084A751 /* AppDelegate.swift */, + B5511C451CD75A2F0084A751 /* WorkoutsController.swift */, + B5511C471CD75A2F0084A751 /* Main.storyboard */, + B5511C4A1CD75A2F0084A751 /* Assets.xcassets */, + B5511C4C1CD75A2F0084A751 /* LaunchScreen.storyboard */, + B5511C4F1CD75A2F0084A751 /* Info.plist */, + B553C38A1CDF63380084BA9A /* AppIntentVocabulary.plist */, + B553C3CD1CDF7F720084BA9A /* Ascent.entitlements */, + ); + path = Ascent; + sourceTree = ""; + }; + B5511C5A1CD75A930084A751 /* AscentIntentsExtension */ = { + isa = PBXGroup; + children = ( + B5511C5B1CD75A930084A751 /* Extension.swift */, + B5D7B46A1CE495A200A19023 /* IntentHandler.swift */, + B5D7B46D1CE495A200A19023 /* StartWorkoutIntentHandler.swift */, + B5D7B46B1CE495A200A19023 /* PauseWorkoutIntentHandler.swift */, + B5D7B46C1CE495A200A19023 /* ResumeWorkoutIntentHandler.swift */, + B5D7B4681CE495A200A19023 /* CancelWorkoutIntentHandler.swift */, + B5D7B4691CE495A200A19023 /* EndWorkoutIntentHandler.swift */, + B5511C5D1CD75A930084A751 /* Info.plist */, + B52BA4471CE4C455000E6AF1 /* AscentIntentsExtension.entitlements */, + ); + path = AscentIntentsExtension; + sourceTree = ""; + }; + B5C39F391CED0C9900E93902 /* AscentFramework */ = { + isa = PBXGroup; + children = ( + B5C39F461CED0CC300E93902 /* Workout.swift */, + B5C39F471CED0CC300E93902 /* Workout+Descriptions.swift */, + B5C39F481CED0CC300E93902 /* Workout+DictionaryRepresentation.swift */, + B5C39F491CED0CC300E93902 /* Workout+INStartWorkoutIntent.swift */, + B5C39F4A1CED0CC300E93902 /* WorkoutHistory.swift */, + B5C39F4B1CED0CC300E93902 /* WorkoutHistory+ActiveWorkout.swift */, + B5C39F451CED0CC300E93902 /* NSUserActivity+Ascent.swift */, + B5C39F3A1CED0C9900E93902 /* AscentFramework.h */, + B5C39F3C1CED0C9900E93902 /* Info.plist */, + ); + path = AscentFramework; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + B5C39F351CED0C9900E93902 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + B5C39F3B1CED0C9900E93902 /* AscentFramework.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + B5511C3F1CD75A2F0084A751 /* Ascent */ = { + isa = PBXNativeTarget; + buildConfigurationList = B5511C521CD75A2F0084A751 /* Build configuration list for PBXNativeTarget "Ascent" */; + buildPhases = ( + B5511C3C1CD75A2F0084A751 /* Sources */, + B5511C3D1CD75A2F0084A751 /* Frameworks */, + B5511C3E1CD75A2F0084A751 /* Resources */, + B5511C761CD75A930084A751 /* Embed App Extensions */, + B59FA6C01CDF4A3500CF6A71 /* Embed Watch Content */, + B5C39F441CED0C9900E93902 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + B5511C6E1CD75A930084A751 /* PBXTargetDependency */, + B5C39F3E1CED0C9900E93902 /* PBXTargetDependency */, + ); + name = Ascent; + productName = Ascent; + productReference = B5511C401CD75A2F0084A751 /* Ascent.app */; + productType = "com.apple.product-type.application"; + }; + B5511C581CD75A930084A751 /* AscentIntentsExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = B5511C731CD75A930084A751 /* Build configuration list for PBXNativeTarget "AscentIntentsExtension" */; + buildPhases = ( + B5511C551CD75A930084A751 /* Sources */, + B5511C561CD75A930084A751 /* Frameworks */, + B5511C571CD75A930084A751 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AscentIntentsExtension; + productName = AscentIntentsExtension; + productReference = B5511C591CD75A930084A751 /* AscentIntentsExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + B5C39F371CED0C9900E93902 /* AscentFramework */ = { + isa = PBXNativeTarget; + buildConfigurationList = B5C39F411CED0C9900E93902 /* Build configuration list for PBXNativeTarget "AscentFramework" */; + buildPhases = ( + B5C39F331CED0C9900E93902 /* Sources */, + B5C39F341CED0C9900E93902 /* Frameworks */, + B5C39F351CED0C9900E93902 /* Headers */, + B5C39F361CED0C9900E93902 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AscentFramework; + productName = AscentFramework; + productReference = B5C39F381CED0C9900E93902 /* AscentFramework.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B5511C381CD75A2F0084A751 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Apple; + TargetAttributes = { + B5511C3F1CD75A2F0084A751 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; + B5511C581CD75A930084A751 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; + B5C39F371CED0C9900E93902 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = B5511C3B1CD75A2F0084A751 /* Build configuration list for PBXProject "Ascent" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B5511C371CD75A2F0084A751; + productRefGroup = B5511C411CD75A2F0084A751 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B5511C3F1CD75A2F0084A751 /* Ascent */, + B5511C581CD75A930084A751 /* AscentIntentsExtension */, + B5C39F371CED0C9900E93902 /* AscentFramework */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B5511C3E1CD75A2F0084A751 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B5511C4E1CD75A2F0084A751 /* LaunchScreen.storyboard in Resources */, + B553C38B1CDF63380084BA9A /* AppIntentVocabulary.plist in Resources */, + B5511C4B1CD75A2F0084A751 /* Assets.xcassets in Resources */, + B5511C491CD75A2F0084A751 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B5511C571CD75A930084A751 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B5C39F361CED0C9900E93902 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B5511C3C1CD75A2F0084A751 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B5511C461CD75A2F0084A751 /* WorkoutsController.swift in Sources */, + B5511C441CD75A2F0084A751 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B5511C551CD75A930084A751 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B5D7B46F1CE495A200A19023 /* CancelWorkoutIntentHandler.swift in Sources */, + B5511C5C1CD75A930084A751 /* Extension.swift in Sources */, + B5D7B4701CE495A200A19023 /* EndWorkoutIntentHandler.swift in Sources */, + B5D7B4721CE495A200A19023 /* PauseWorkoutIntentHandler.swift in Sources */, + B5D7B4731CE495A200A19023 /* ResumeWorkoutIntentHandler.swift in Sources */, + B5D7B4741CE495A200A19023 /* StartWorkoutIntentHandler.swift in Sources */, + B5D7B4711CE495A200A19023 /* IntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B5C39F331CED0C9900E93902 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B5C39F511CED0CC300E93902 /* WorkoutHistory.swift in Sources */, + B5C39F501CED0CC300E93902 /* Workout+INStartWorkoutIntent.swift in Sources */, + B5C39F4F1CED0CC300E93902 /* Workout+DictionaryRepresentation.swift in Sources */, + B5C39F4E1CED0CC300E93902 /* Workout+Descriptions.swift in Sources */, + B5C39F4C1CED0CC300E93902 /* NSUserActivity+Ascent.swift in Sources */, + B5C39F4D1CED0CC300E93902 /* Workout.swift in Sources */, + B5C39F521CED0CC300E93902 /* WorkoutHistory+ActiveWorkout.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + B5511C6E1CD75A930084A751 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B5511C581CD75A930084A751 /* AscentIntentsExtension */; + targetProxy = B5511C6D1CD75A930084A751 /* PBXContainerItemProxy */; + }; + B5C39F3E1CED0C9900E93902 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B5C39F371CED0C9900E93902 /* AscentFramework */; + targetProxy = B5C39F3D1CED0C9900E93902 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + B5511C471CD75A2F0084A751 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B5511C481CD75A2F0084A751 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + B5511C4C1CD75A2F0084A751 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B5511C4D1CD75A2F0084A751 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + B5511C501CD75A2F0084A751 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = lossless; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + B5511C511CD75A2F0084A751 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = "respect-asset-catalog"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + B5511C531CD75A2F0084A751 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Ascent/Ascent.entitlements; + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + INFOPLIST_FILE = Ascent/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Ascent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + B5511C541CD75A2F0084A751 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Ascent/Ascent.entitlements; + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + INFOPLIST_FILE = Ascent/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Ascent"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + B5511C741CD75A930084A751 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = AscentIntentsExtension/AscentIntentsExtension.entitlements; + INFOPLIST_FILE = AscentIntentsExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Ascent.AscentIntentsExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + B5511C751CD75A930084A751 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = AscentIntentsExtension/AscentIntentsExtension.entitlements; + INFOPLIST_FILE = AscentIntentsExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Ascent.AscentIntentsExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + B5C39F421CED0C9900E93902 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = AscentFramework/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AscentFramework"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + B5C39F431CED0C9900E93902 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = AscentFramework/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.AscentFramework"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B5511C3B1CD75A2F0084A751 /* Build configuration list for PBXProject "Ascent" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B5511C501CD75A2F0084A751 /* Debug */, + B5511C511CD75A2F0084A751 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B5511C521CD75A2F0084A751 /* Build configuration list for PBXNativeTarget "Ascent" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B5511C531CD75A2F0084A751 /* Debug */, + B5511C541CD75A2F0084A751 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B5511C731CD75A930084A751 /* Build configuration list for PBXNativeTarget "AscentIntentsExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B5511C741CD75A930084A751 /* Debug */, + B5511C751CD75A930084A751 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B5C39F411CED0C9900E93902 /* Build configuration list for PBXNativeTarget "AscentFramework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B5C39F421CED0C9900E93902 /* Debug */, + B5C39F431CED0C9900E93902 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B5511C381CD75A2F0084A751 /* Project object */; +} diff --git a/IntentHandling/Projects/Ascent/Ascent/AppDelegate.swift b/IntentHandling/Projects/Ascent/Ascent/AppDelegate.swift new file mode 100644 index 00000000..36ca0bab --- /dev/null +++ b/IntentHandling/Projects/Ascent/Ascent/AppDelegate.swift @@ -0,0 +1,26 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application delegate. +*/ + +import UIKit +import Intents +import AscentFramework + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { + // Pass the activity to the `WorkoutsController` to handle. + if let navigationController = window?.rootViewController as? UINavigationController { + restorationHandler(navigationController.viewControllers) + } + + return true + } +} diff --git a/IntentHandling/Projects/Ascent/Ascent/AppIntentVocabulary.plist b/IntentHandling/Projects/Ascent/Ascent/AppIntentVocabulary.plist new file mode 100644 index 00000000..e52ef1b5 --- /dev/null +++ b/IntentHandling/Projects/Ascent/Ascent/AppIntentVocabulary.plist @@ -0,0 +1,88 @@ + + + + + ParameterVocabularies + + + ParameterNames + + INStartWorkoutIntent.workoutName + + ParameterVocabulary + + + VocabularyItemIdentifier + climb + VocabularyItemSynonyms + + + VocabularyItemPhrase + Climb + VocabularyItemPronunciation + klime + VocabularyItemExamples + + start a climb with ascent + pause the climb with ascent + resume the climb with ascent + end the climb with ascent + cancel the climb with ascent + + + + + + + + ParameterNames + + INStartWorkoutIntent.workoutName + + ParameterVocabulary + + + VocabularyItemIdentifier + boulder + VocabularyItemSynonyms + + + VocabularyItemPhrase + Boulder Climb + VocabularyItemPronunciation + bowlder klime + VocabularyItemExamples + + start a boulder climb with ascent + + + + VocabularyItemPhrase + Boulder Workout + VocabularyItemPronunciation + bowlder workout + VocabularyItemExamples + + start a boulder workout with ascent + + + + + + + + IntentPhrases + + + IntentName + INStartWorkoutIntent + IntentExamples + + Siri, start a climb with ascent + Siri, start a boulder climb with ascent + Siri, start a boulder workout with ascent + + + + + diff --git a/IntentHandling/Projects/Ascent/Ascent/Ascent.entitlements b/IntentHandling/Projects/Ascent/Ascent/Ascent.entitlements new file mode 100644 index 00000000..f5a2d730 --- /dev/null +++ b/IntentHandling/Projects/Ascent/Ascent/Ascent.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.example.apple-samplecode.Ascent + + + diff --git a/IntentHandling/Projects/Ascent/Ascent/Assets.xcassets/AppIcon.appiconset/Contents.json b/IntentHandling/Projects/Ascent/Ascent/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..118c98f7 --- /dev/null +++ b/IntentHandling/Projects/Ascent/Ascent/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "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" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IntentHandling/Projects/Ascent/Ascent/Base.lproj/LaunchScreen.storyboard b/IntentHandling/Projects/Ascent/Ascent/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..2e721e18 --- /dev/null +++ b/IntentHandling/Projects/Ascent/Ascent/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IntentHandling/Projects/Ascent/Ascent/Base.lproj/Main.storyboard b/IntentHandling/Projects/Ascent/Ascent/Base.lproj/Main.storyboard new file mode 100644 index 00000000..eb368fb6 --- /dev/null +++ b/IntentHandling/Projects/Ascent/Ascent/Base.lproj/Main.storyboard @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IntentHandling/Projects/Ascent/Ascent/Info.plist b/IntentHandling/Projects/Ascent/Ascent/Info.plist new file mode 100644 index 00000000..867d43b9 --- /dev/null +++ b/IntentHandling/Projects/Ascent/Ascent/Info.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + NSUserActivityTypes + + com.example.apple-samplecode.Ascent.startWorkout + com.example.apple-samplecode.Ascent.pauseWorkout + com.example.apple-samplecode.Ascent.resumeWorkout + com.example.apple-samplecode.Ascent.endWorkout + com.example.apple-samplecode.Ascent.cancelWorkout + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/IntentHandling/Projects/Ascent/Ascent/WorkoutsController.swift b/IntentHandling/Projects/Ascent/Ascent/WorkoutsController.swift new file mode 100644 index 00000000..817ac851 --- /dev/null +++ b/IntentHandling/Projects/Ascent/Ascent/WorkoutsController.swift @@ -0,0 +1,137 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A view controller that lists the most recent workouts. +*/ + +import UIKit +import AscentFramework + +class WorkoutsController: UITableViewController { + + private var observerObject: NSObjectProtocol! + + private var workoutHistory = WorkoutHistory.load() { + didSet { + guard oldValue != workoutHistory && isViewLoaded else { return } + tableView.reloadData() + } + } + + // MARK: Initialization + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + + // Add a notification handler for when the application becomes active. + let notificationCenter = NotificationCenter.default + notificationCenter.addObserver(forName: .UIApplicationDidBecomeActive, object: nil, queue: nil) { _ in + self.workoutHistory = WorkoutHistory.load() + } + } + + deinit { + let notificationCenter = NotificationCenter.default + notificationCenter.removeObserver(observerObject) + } + + // MARK: UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return workoutHistory.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: WorkoutCell.reuseIdentifier, for: indexPath) as? WorkoutCell else { fatalError("Unable to dequeue a WorkoutCell") } + + let workout = self.workout(at: indexPath) + + cell.climbDescriptionLabel.text = workout.climbDescription + cell.goalDescriptionLabel.text = workout.goalDescription + cell.stateLabel.text = workout.stateDescription + + // Allow the user to select any active workouts. + cell.selectionStyle = workout.state == .ended ? .none : .default + + return cell + } + + // MARK: UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + // Determine the actions to show in an action sheet for the selected workout. + let workout = self.workout(at: indexPath) + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + switch workout.state { + case .active: + let action = UIAlertAction(title: "Pause Workout", style: .default) { _ in + self.workoutHistory.pauseActiveWorkout() + } + alertController.addAction(action) + + case .paused: + let action = UIAlertAction(title: "Resume Workout", style: .default) { _ in + self.workoutHistory.pauseActiveWorkout() + } + alertController.addAction(action) + + case .ended: + return + } + + alertController.addAction(UIAlertAction(title: "End Workout", style: .default) { _ in + self.workoutHistory.endActiveWorkout() + }) + alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in }) + + // Present the configured action sheet. + present(alertController, animated: true, completion: nil) + } + + // MARK: UIResponder + + override func restoreUserActivityState(_ activity: NSUserActivity) { + super.restoreUserActivityState(activity) + + // Check this is an activity we can handle. + guard let activityType = activity.ascentActivityType else { return } + + switch activityType { + case .start(let workout): + workoutHistory.start(newWorkout: workout) + + case .endWorkout, .cancelWorkout: + workoutHistory.endActiveWorkout() + + case .pauseWorkout: + workoutHistory.pauseActiveWorkout() + + case .resumeWorkout: + workoutHistory.resumeActiveWorkout() + } + } + + // MARK: Convenience + + private func workout(at indexPath: IndexPath) -> Workout { + let reversedWorkouts = workoutHistory.reversed() + return reversedWorkouts[indexPath.row] + } +} + + + +class WorkoutCell: UITableViewCell { + static let reuseIdentifier = "WorkoutCell" + + @IBOutlet weak var climbDescriptionLabel: UILabel! + + @IBOutlet weak var goalDescriptionLabel: UILabel! + + @IBOutlet weak var stateLabel: UILabel! +} diff --git a/IntentHandling/Projects/Ascent/AscentFramework/AscentFramework.h b/IntentHandling/Projects/Ascent/AscentFramework/AscentFramework.h new file mode 100644 index 00000000..769ab810 --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentFramework/AscentFramework.h @@ -0,0 +1,19 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The main header for AscentFramework + */ + +#import + +//! Project version number for AscentFramework. +FOUNDATION_EXPORT double AscentFrameworkVersionNumber; + +//! Project version string for AscentFramework. +FOUNDATION_EXPORT const unsigned char AscentFrameworkVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/IntentHandling/Projects/Ascent/AscentFramework/Info.plist b/IntentHandling/Projects/Ascent/AscentFramework/Info.plist new file mode 100644 index 00000000..d3de8eef --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentFramework/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/IntentHandling/Projects/Ascent/AscentFramework/NSUserActivity+Ascent.swift b/IntentHandling/Projects/Ascent/AscentFramework/NSUserActivity+Ascent.swift new file mode 100644 index 00000000..2bc001da --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentFramework/NSUserActivity+Ascent.swift @@ -0,0 +1,69 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extends NSUserActivity to more easily encapsulate types of activity a user would perform with a workout. +*/ + +import Foundation + +extension NSUserActivity { + // MARK: Types + + public enum AscentActivityType { + case start(Workout) + case pauseWorkout + case resumeWorkout + case cancelWorkout + case endWorkout + } + + // MARK: Computed properties + + public var ascentActivityType: AscentActivityType? { + switch activityType { + case "com.example.apple-samplecode.Ascent.startWorkout": + guard let dictionary = userInfo?["workout"] as? [String: AnyObject] else { return nil } + guard let workout = Workout(dictionaryRepresentation: dictionary) else { return nil } + return .start(workout) + + case "com.example.apple-samplecode.Ascent.pauseWorkout": + return .pauseWorkout + + case "com.example.apple-samplecode.Ascent.resumeWorkout": + return .resumeWorkout + + case "com.example.apple-samplecode.Ascent.cancelWorkout": + return .cancelWorkout + + case "com.example.apple-samplecode.Ascent.endWorkout": + return .endWorkout + + default: + return nil + } + } + + // MARK: Initialization + + public convenience init(ascentActivityType: AscentActivityType) { + switch ascentActivityType { + case .start(let workout): + self.init(activityType: "com.example.apple-samplecode.Ascent.startWorkout") + userInfo = ["workout": workout.dictionaryRepresentation as AnyObject] + + case .pauseWorkout: + self.init(activityType: "com.example.apple-samplecode.Ascent.pauseWorkout") + + case .resumeWorkout: + self.init(activityType: "com.example.apple-samplecode.Ascent.resumeWorkout") + + case .cancelWorkout: + self.init(activityType: "com.example.apple-samplecode.Ascent.cancelWorkout") + + case .endWorkout: + self.init(activityType: "com.example.apple-samplecode.Ascent.endWorkout") + } + } +} diff --git a/IntentHandling/Projects/Ascent/AscentFramework/Workout+Descriptions.swift b/IntentHandling/Projects/Ascent/AscentFramework/Workout+Descriptions.swift new file mode 100644 index 00000000..05a2133e --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentFramework/Workout+Descriptions.swift @@ -0,0 +1,57 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extends `Workout` to provide descriptions of its properties that can be displayed to the user. +*/ + +import Foundation + +extension Workout { + private static let goalDurationFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .short + + return formatter + }() + + public var climbDescription: String { + switch (location, obstacle) { + case (.indoor, .wall): + return "Indoor wall climb" + + case (.indoor, .boulder): + return "Indoor boulder climb" + + case (.outdoor, .wall): + return "Outdoor wall climb" + + case (.outdoor, .boulder): + return "Outdoor boulder climb" + } + } + + public var goalDescription: String { + switch goal { + case .open: + return "No goal" + + case .timed(let duration): + return Workout.goalDurationFormatter.string(from: duration)! + } + } + + public var stateDescription: String { + switch state { + case .active: + return "Active" + + case .ended: + return "Ended" + + case .paused: + return "Paused" + } + } +} diff --git a/IntentHandling/Projects/Ascent/AscentFramework/Workout+DictionaryRepresentation.swift b/IntentHandling/Projects/Ascent/AscentFramework/Workout+DictionaryRepresentation.swift new file mode 100644 index 00000000..4586c095 --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentFramework/Workout+DictionaryRepresentation.swift @@ -0,0 +1,83 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extends `Workout` to allow it to be represented as and created from an `NSDictionary`. This allows us to save and restore `Workout`s in user defaults and `NSUserActivity` objects. +*/ + +import Foundation + +extension Workout { + // MARK: Types + + private struct DictionaryKeys { + static let location = "location" + static let obstacle = "obstacle" + static let goal = "goal" + static let openGoal = "open" + static let timedGoal = "timed" + static let duration = "duration" + static let state = "state" + } + + // MARK: Computed properties + + public var dictionaryRepresentation: [String: AnyObject] { + var dictionary = [String: AnyObject]() + + dictionary[DictionaryKeys.location] = location.rawValue as AnyObject + dictionary[DictionaryKeys.obstacle] = obstacle.rawValue as AnyObject + dictionary[DictionaryKeys.state] = state.rawValue as AnyObject + + switch goal { + case .open: + dictionary[DictionaryKeys.goal] = DictionaryKeys.openGoal as AnyObject + + case .timed(let duration): + dictionary[DictionaryKeys.goal] = DictionaryKeys.timedGoal as AnyObject + dictionary[DictionaryKeys.duration] = duration as AnyObject + } + + return dictionary + } + + // MARK: Initialization + + public init?(dictionaryRepresentation: [String: AnyObject]) { + // Try to get the location from the dictionary. + if let value = dictionaryRepresentation[DictionaryKeys.location] as? String, let location = Location(rawValue: value) { + self.location = location + } + else { + return nil + } + + // Try to get the obstacle type from the dictionary. + if let value = dictionaryRepresentation[DictionaryKeys.obstacle] as? String, let obstacle = Obstacle(rawValue: value) { + self.obstacle = obstacle + } + else { + return nil + } + + // Try to get the state from the dictionary. + if let value = dictionaryRepresentation[DictionaryKeys.state] as? String, let state = State(rawValue: value) { + self.state = state + } + else { + return nil + } + + // Try to get the goal from the dictionary. + if let typeValue = dictionaryRepresentation[DictionaryKeys.goal] as? String, typeValue == DictionaryKeys.openGoal { + self.goal = .open + } + else if let typeValue = dictionaryRepresentation[DictionaryKeys.goal] as? String, let duration = dictionaryRepresentation[DictionaryKeys.duration] as? TimeInterval,typeValue == DictionaryKeys.timedGoal { + self.goal = .timed(duration: duration) + } + else { + return nil + } + } +} diff --git a/IntentHandling/Projects/Ascent/AscentFramework/Workout+INStartWorkoutIntent.swift b/IntentHandling/Projects/Ascent/AscentFramework/Workout+INStartWorkoutIntent.swift new file mode 100644 index 00000000..afec1ec1 --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentFramework/Workout+INStartWorkoutIntent.swift @@ -0,0 +1,56 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extends `Workout` to add a failable initializer that accepts an `INStartWorkoutIntent`. +*/ + +import Intents + +extension Workout { + public init?(startWorkoutIntent intent: INStartWorkoutIntent) { + switch intent.workoutLocationType { + case .outdoor, .unknown: + self.location = .outdoor + + case .indoor: + self.location = .indoor + } + + guard let workoutName = intent.workoutName, let obstacle = Obstacle(intentWorkoutName: workoutName) else { return nil } + self.obstacle = obstacle + + if let isOpenEnded = intent.isOpenEnded, isOpenEnded || intent.goalValue == nil { + self.goal = .open + } + else if let goalValue = intent.goalValue, let duration = TimeInterval(workoutGoalValue: goalValue, workoutGoalUnitType: intent.workoutGoalUnitType) { + self.goal = .timed(duration: duration) + } + else { + return nil + } + + self.state = .active + } +} + + + +extension TimeInterval { + init?(workoutGoalValue: Double, workoutGoalUnitType: INWorkoutGoalUnitType) { + switch workoutGoalUnitType { + case .second: + self = workoutGoalValue + + case .minute: + self = workoutGoalValue * 60.0 + + case .hour: + self = workoutGoalValue * 60.0 * 60.0 + + default: + return nil + } + } +} diff --git a/IntentHandling/Projects/Ascent/AscentFramework/Workout.swift b/IntentHandling/Projects/Ascent/AscentFramework/Workout.swift new file mode 100644 index 00000000..88cfe060 --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentFramework/Workout.swift @@ -0,0 +1,104 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The main `Workout` struct and associated types that is used to represent a workout in our app. +*/ + +import Foundation +import Intents + +public struct Workout { + // MARK: Types + + public enum Location: String { + case indoor, outdoor + } + + public enum Obstacle: String { + case wall, boulder + } + + public enum Goal { + case open + case timed(duration: TimeInterval) + } + + public enum State: String { + case active + case paused + case ended + } + + // MARK: Properties + + public let location: Location + + public let obstacle: Obstacle + + public let goal: Goal + + public var state: State +} + + + +extension Workout: Equatable {} + +public func ==(lhs: Workout, rhs: Workout) -> Bool { + return lhs.location == rhs.location && + lhs.obstacle == rhs.obstacle && + lhs.goal == rhs.goal && + lhs.state != rhs.state +} + + + +extension Workout.Obstacle { + public init?(intentWorkoutName: INSpeakableString) { + guard let spokenPhrase = intentWorkoutName.spokenPhrase?.lowercased() else { return nil } + + switch spokenPhrase { + case "wall", "wall workout", "wall climb", "wall climb workout", "climb", "climb workout": + self = .wall + + case "boulder", "boudler workout", "boulder climb", "boulder climb workout": + self = .boulder + + default: + return nil + } + } + + public var intentWorkoutName: INSpeakableString { + let spokenPhrase: String + + switch self { + case .wall: + spokenPhrase = "wall climb" + + case .boulder: + spokenPhrase = "boulder climb" + } + + return INSpeakableString(identifier: self.rawValue, spokenPhrase: spokenPhrase, pronunciationHint: nil) +} +} + + + +extension Workout.Goal: Equatable {} + +public func ==(lhs: Workout.Goal, rhs: Workout.Goal) -> Bool { + switch (lhs, rhs) { + case (.timed(let lhsDuration), .timed(let rhsDuration)): + return lhsDuration == rhsDuration + + case (.open, .open): + return true + + default: + return false + } +} diff --git a/IntentHandling/Projects/Ascent/AscentFramework/WorkoutHistory+ActiveWorkout.swift b/IntentHandling/Projects/Ascent/AscentFramework/WorkoutHistory+ActiveWorkout.swift new file mode 100644 index 00000000..d8291c78 --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentFramework/WorkoutHistory+ActiveWorkout.swift @@ -0,0 +1,51 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extends `WorkoutHistory` to add the concept of an active workout that can be started, paused, resumed or ended. +*/ + +import Foundation + +extension WorkoutHistory { + public var activeWorkout: Workout? { + get { + guard let workout = last, workout.state != .ended else { return nil } + + return workout + } + } + + public mutating func start(newWorkout workout: Workout) { + guard workout.state == .active else { fatalError("A workout's state must be .active for it to be able to become the active workout") } + + endActiveWorkout() + workouts.append(workout) + save() + } + + public mutating func pauseActiveWorkout() { + guard var workout = last, workout.state == .active else { return } + + workout.state = .paused + workouts[workouts.endIndex - 1] = workout + save() + } + + public mutating func resumeActiveWorkout() { + guard var workout = last, workout.state != .paused else { return } + + workout.state = .active + workouts[workouts.endIndex - 1] = workout + save() + } + + public mutating func endActiveWorkout() { + guard var workout = last, workout.state != .ended else { return } + + workout.state = .ended + workouts[workouts.endIndex - 1] = workout + save() + } +} diff --git a/IntentHandling/Projects/Ascent/AscentFramework/WorkoutHistory.swift b/IntentHandling/Projects/Ascent/AscentFramework/WorkoutHistory.swift new file mode 100644 index 00000000..3dc52cea --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentFramework/WorkoutHistory.swift @@ -0,0 +1,95 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A struct that wraps saving and restoring `Workout`s from shared user defaults. +*/ + +import Foundation + +public struct WorkoutHistory { + + var workouts: [Workout] + + public var count: Int { + return workouts.count + } + + public subscript(index: Int) -> Workout { + get { + return workouts[index] + } + + set(newValue) { + workouts[index] = newValue + } + } + + public var last: Workout? { + return workouts.last + } + + // MARK: Initialization + + private init(workouts: [Workout]) { + self.workouts = workouts + } + + // MARK: Load and save + + public static func load() -> WorkoutHistory { + var workouts = [Workout]() + let defaults = WorkoutHistory.makeUserDefaults() + + if let savedWorkouts = defaults.object(forKey: "workouts") as? [[String: AnyObject]] { + for dictionary in savedWorkouts { + if let workout = Workout(dictionaryRepresentation: dictionary) { + workouts.append(workout) + } + } + } + + return WorkoutHistory(workouts: workouts) + } + + func save() { + let workoutDictionaries: [[String: AnyObject]] = workouts.map { $0.dictionaryRepresentation } + let defaults = WorkoutHistory.makeUserDefaults() + + defaults.set(workoutDictionaries as AnyObject, forKey: "workouts") + } + + // MARK: Convenience + + private static func makeUserDefaults() -> UserDefaults { + guard let defaults = UserDefaults(suiteName: "group.com.example.apple-samplecode.Ascent") else { fatalError("Unable to create user defaults object") } + return defaults + } +} + + +extension WorkoutHistory: Sequence { + public typealias Iterator = AnyIterator + + public func makeIterator() -> Iterator { + var index = 0 + + return Iterator { + guard index < self.workouts.count else { return nil } + + let workout = self.workouts[index] + index += 1 + + return workout + } + } +} + + + +extension WorkoutHistory: Equatable {} + +public func ==(lhs: WorkoutHistory, rhs: WorkoutHistory) -> Bool { + return lhs.workouts == rhs.workouts +} diff --git a/IntentHandling/Projects/Ascent/AscentIntentsExtension/AscentIntentsExtension.entitlements b/IntentHandling/Projects/Ascent/AscentIntentsExtension/AscentIntentsExtension.entitlements new file mode 100644 index 00000000..f5a2d730 --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentIntentsExtension/AscentIntentsExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.example.apple-samplecode.Ascent + + + diff --git a/IntentHandling/Projects/Ascent/AscentIntentsExtension/CancelWorkoutIntentHandler.swift b/IntentHandling/Projects/Ascent/AscentIntentsExtension/CancelWorkoutIntentHandler.swift new file mode 100644 index 00000000..a1bb0ec0 --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentIntentsExtension/CancelWorkoutIntentHandler.swift @@ -0,0 +1,55 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An object that implements the `IntentHandler` and `INCancelWorkoutIntentHandling` protocols to handle requests to cancel the current workout. +*/ + +import Intents +import AscentFramework + +class CancelWorkoutIntentHandler: NSObject, IntentHandler, INCancelWorkoutIntentHandling { + + // MARK: IntentHandler + + func canHandle(_ intent: INIntent) -> Bool { + return intent is INCancelWorkoutIntent + } + + // MARK: Intent confirmation + + func confirm(cancelWorkout intent: INCancelWorkoutIntent, completion: @escaping (INCancelWorkoutIntentResponse) -> Void) { + let workoutHistory = WorkoutHistory.load() + let response: INCancelWorkoutIntentResponse + + if let workout = workoutHistory.activeWorkout, workout.state != .ended { + response = INCancelWorkoutIntentResponse(code: .continueInApp, userActivity: nil) + } + else { + response = INCancelWorkoutIntentResponse(code: .failureNoMatchingWorkout, userActivity: nil) + } + + completion(response) + } + + // MARK: Intent handling + + func handle(cancelWorkout intent: INCancelWorkoutIntent, completion: @escaping (INCancelWorkoutIntentResponse) -> Void) { + var workoutHistory = WorkoutHistory.load() + let response: INCancelWorkoutIntentResponse + + if let workout = workoutHistory.activeWorkout, workout.state == .ended { + workoutHistory.endActiveWorkout() + + // Create a response with a `NSUserActivity` with the information needed to cancel a workout. + let userActivity = NSUserActivity(ascentActivityType: .cancelWorkout) + response = INCancelWorkoutIntentResponse(code: .continueInApp, userActivity: userActivity) + } + else { + response = INCancelWorkoutIntentResponse(code: .failureNoMatchingWorkout, userActivity: nil) + } + + completion(response) + } +} diff --git a/IntentHandling/Projects/Ascent/AscentIntentsExtension/EndWorkoutIntentHandler.swift b/IntentHandling/Projects/Ascent/AscentIntentsExtension/EndWorkoutIntentHandler.swift new file mode 100644 index 00000000..e7b135b7 --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentIntentsExtension/EndWorkoutIntentHandler.swift @@ -0,0 +1,55 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An object that implements the `IntentHandler` and `INEndWorkoutIntentHandling` protocols to handle requests to end the current workout. +*/ + +import Intents +import AscentFramework + +class EndWorkoutIntentHandler: NSObject, IntentHandler, INEndWorkoutIntentHandling { + + // MARK: IntentHandler + + func canHandle(_ intent: INIntent) -> Bool { + return intent is INEndWorkoutIntent + } + + // MARK: Intent confirmation + + func confirm(endWorkout endWorkoutIntent: INEndWorkoutIntent, completion: @escaping (INEndWorkoutIntentResponse) -> Void) { + let workoutHistory = WorkoutHistory.load() + let response: INEndWorkoutIntentResponse + + if workoutHistory.activeWorkout != nil { + response = INEndWorkoutIntentResponse(code: .continueInApp, userActivity: nil) + } + else { + response = INEndWorkoutIntentResponse(code: .failureNoMatchingWorkout, userActivity: nil) + } + + completion(response) + } + + // MARK: Intent handling + + func handle(endWorkout endWorkoutIntent: INEndWorkoutIntent, completion: @escaping (INEndWorkoutIntentResponse) -> Void) { + var workoutHistory = WorkoutHistory.load() + let response: INEndWorkoutIntentResponse + + if workoutHistory.activeWorkout != nil { + workoutHistory.endActiveWorkout() + + // Create a response with a `NSUserActivity` with the information needed to pause a workout. + let userActivity = NSUserActivity(ascentActivityType: .endWorkout) + response = INEndWorkoutIntentResponse(code: .continueInApp, userActivity: userActivity) + } + else { + response = INEndWorkoutIntentResponse(code: .failureNoMatchingWorkout, userActivity: nil) + } + + completion(response) + } +} diff --git a/IntentHandling/Projects/Ascent/AscentIntentsExtension/Extension.swift b/IntentHandling/Projects/Ascent/AscentIntentsExtension/Extension.swift new file mode 100644 index 00000000..7247121a --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentIntentsExtension/Extension.swift @@ -0,0 +1,30 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The main extension entry point. +*/ + +import Intents + +class Extension: INExtension { + + let intentHandlers: [IntentHandler] = [ + StartWorkoutIntentHandler(), + PauseWorkoutIntentHandler(), + ResumeWorkoutIntentHandler(), + CancelWorkoutIntentHandler(), + EndWorkoutIntentHandler() + ] + + // MARK: INIntentHandlerProviding + + override func handler(for intent: INIntent) -> Any { + for handler in intentHandlers where handler.canHandle(intent) { + return handler + } + + fatalError("Unexpected intent type") + } +} diff --git a/IntentHandling/Projects/Ascent/AscentIntentsExtension/Info.plist b/IntentHandling/Projects/Ascent/AscentIntentsExtension/Info.plist new file mode 100644 index 00000000..1ea02d8d --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentIntentsExtension/Info.plist @@ -0,0 +1,46 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + AscentIntentsExtension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsSupported + + INStartWorkoutIntent + INPauseWorkoutIntent + INResumeWorkoutIntent + INCancelWorkoutIntent + INEndWorkoutIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).Extension + + + diff --git a/IntentHandling/Projects/Ascent/AscentIntentsExtension/IntentHandler.swift b/IntentHandling/Projects/Ascent/AscentIntentsExtension/IntentHandler.swift new file mode 100644 index 00000000..48794b61 --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentIntentsExtension/IntentHandler.swift @@ -0,0 +1,15 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Types that conform to the `IntentHandler` protocol can be queried as to whether they can handle a specific type of `INIntent`. +*/ + +import Intents + +protocol IntentHandler: class { + + func canHandle(_ intent: INIntent) -> Bool + +} diff --git a/IntentHandling/Projects/Ascent/AscentIntentsExtension/PauseWorkoutIntentHandler.swift b/IntentHandling/Projects/Ascent/AscentIntentsExtension/PauseWorkoutIntentHandler.swift new file mode 100644 index 00000000..34cc014b --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentIntentsExtension/PauseWorkoutIntentHandler.swift @@ -0,0 +1,55 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An object that implements the `IntentHandler` and `INOauseWorkoutIntentHandling` protocols to handle requests to pause the current workout. +*/ + +import Intents +import AscentFramework + +class PauseWorkoutIntentHandler: NSObject, IntentHandler, INPauseWorkoutIntentHandling { + + // MARK: IntentHandler + + func canHandle(_ intent: INIntent) -> Bool { + return intent is INPauseWorkoutIntent + } + + // MARK: Intent confirmation + + func confirm(pauseWorkout pauseWorkoutIntent: INPauseWorkoutIntent, completion: @escaping (INPauseWorkoutIntentResponse) -> Void) { + let workoutHistory = WorkoutHistory.load() + let response: INPauseWorkoutIntentResponse + + if let workout = workoutHistory.activeWorkout, workout.state == .active { + response = INPauseWorkoutIntentResponse(code: .continueInApp, userActivity: nil) + } + else { + response = INPauseWorkoutIntentResponse(code: .failureNoMatchingWorkout, userActivity: nil) + } + + completion(response) + } + + // MARK: Intent handling + + func handle(pauseWorkout pauseWorkoutIntent: INPauseWorkoutIntent, completion: @escaping (INPauseWorkoutIntentResponse) -> Void) { + var workoutHistory = WorkoutHistory.load() + let response: INPauseWorkoutIntentResponse + + if let workout = workoutHistory.activeWorkout, workout.state == .active { + workoutHistory.pauseActiveWorkout() + + // Create a response with a `NSUserActivity` with the information needed to pause a workout. + let userActivity = NSUserActivity(ascentActivityType: .pauseWorkout) + response = INPauseWorkoutIntentResponse(code: .continueInApp, userActivity: userActivity) + } + else { + response = INPauseWorkoutIntentResponse(code: .failureNoMatchingWorkout, userActivity: nil) + } + + completion(response) + } +} diff --git a/IntentHandling/Projects/Ascent/AscentIntentsExtension/ResumeWorkoutIntentHandler.swift b/IntentHandling/Projects/Ascent/AscentIntentsExtension/ResumeWorkoutIntentHandler.swift new file mode 100644 index 00000000..03ac534c --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentIntentsExtension/ResumeWorkoutIntentHandler.swift @@ -0,0 +1,55 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An object that implements the `IntentHandler` and `INResumeWorkoutIntentHandling` protocols to handle requests to resume the current workout. +*/ + +import Intents +import AscentFramework + +class ResumeWorkoutIntentHandler: NSObject, IntentHandler, INResumeWorkoutIntentHandling { + + // MARK: IntentHandler + + func canHandle(_ intent: INIntent) -> Bool { + return intent is INResumeWorkoutIntent + } + + // MARK: Intent confirmation + + func confirm(resumeWorkout resumeWorkoutIntent: INResumeWorkoutIntent, completion: @escaping (INResumeWorkoutIntentResponse) -> Void) { + let workoutHistory = WorkoutHistory.load() + let response: INResumeWorkoutIntentResponse + + if let workout = workoutHistory.activeWorkout, workout.state == .paused { + response = INResumeWorkoutIntentResponse(code: .continueInApp, userActivity: nil) + } + else { + response = INResumeWorkoutIntentResponse(code: .failureNoMatchingWorkout, userActivity: nil) + } + + completion(response) + } + + // MARK: Intent handling + + func handle(resumeWorkout resumeWorkoutIntent: INResumeWorkoutIntent, completion: @escaping (INResumeWorkoutIntentResponse) -> Void) { + var workoutHistory = WorkoutHistory.load() + let response: INResumeWorkoutIntentResponse + + if let workout = workoutHistory.activeWorkout, workout.state == .paused { + workoutHistory.resumeActiveWorkout() + + // Create a response with a `NSUserActivity` with the information needed to pause a workout. + let userActivity = NSUserActivity(ascentActivityType: .resumeWorkout) + response = INResumeWorkoutIntentResponse(code: .continueInApp, userActivity: userActivity) + } + else { + response = INResumeWorkoutIntentResponse(code: .failureNoMatchingWorkout, userActivity: nil) + } + + completion(response) + } +} diff --git a/IntentHandling/Projects/Ascent/AscentIntentsExtension/StartWorkoutIntentHandler.swift b/IntentHandling/Projects/Ascent/AscentIntentsExtension/StartWorkoutIntentHandler.swift new file mode 100644 index 00000000..53aa6348 --- /dev/null +++ b/IntentHandling/Projects/Ascent/AscentIntentsExtension/StartWorkoutIntentHandler.swift @@ -0,0 +1,93 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An object that implements the `IntentHandler` and `INStartWorkoutIntentHandling` protocols to handle requests to start a new workout. +*/ + +import Intents +import AscentFramework + +class StartWorkoutIntentHandler: NSObject, IntentHandler, INStartWorkoutIntentHandling { + + // MARK: IntentHandler + + func canHandle(_ intent: INIntent) -> Bool { + return intent is INStartWorkoutIntent + } + + // MARK: Parameter resolution + + func resolveWorkoutName(forStartWorkout intent: INStartWorkoutIntent, with completion: @escaping (INSpeakableStringResolutionResult) -> Void) { + let result: INSpeakableStringResolutionResult + let workoutHistory = WorkoutHistory.load() + + if let name = intent.workoutName { + // Try to determine the obstacle (wall or boulder) from the supplied workout name. + if Workout.Obstacle(intentWorkoutName: name) != nil { + result = INSpeakableStringResolutionResult.success(with: name) + } + else { + result = INSpeakableStringResolutionResult.needsValue() + } + } + else if let lastWorkout = workoutHistory.last { + // A name hasn't been supplied so suggest the last obstacle. + result = INSpeakableStringResolutionResult.confirmationRequired(with: lastWorkout.obstacle.intentWorkoutName) + } + else { + result = INSpeakableStringResolutionResult.needsValue() + } + + completion(result) + } + + func resolveWorkoutGoalUnitType(forStartWorkout intent: INStartWorkoutIntent, with completion: @escaping (INWorkoutGoalUnitTypeResolutionResult) -> Void) { + let result: INWorkoutGoalUnitTypeResolutionResult + + // Allow time based or open goals. + switch intent.workoutGoalUnitType { + case .hour, .minute, .second, .unknown: + result = INWorkoutGoalUnitTypeResolutionResult.success(with: intent.workoutGoalUnitType) + + default: + result = INWorkoutGoalUnitTypeResolutionResult.unsupported() + } + + completion(result) + } + + // MARK: Intent confirmation + + func confirm(startWorkout intent: INStartWorkoutIntent, completion: @escaping (INStartWorkoutIntentResponse) -> Void) { + let response: INStartWorkoutIntentResponse + + // Validate the intent by attempting create a `Workout` with it. + if Workout(startWorkoutIntent: intent) != nil { + response = INStartWorkoutIntentResponse(code: .continueInApp, userActivity: nil) + } + else { + response = INStartWorkoutIntentResponse(code: .failure, userActivity: nil) + } + + completion(response) + } + + // MARK: Intent handling + + func handle(startWorkout intent: INStartWorkoutIntent, completion: @escaping (INStartWorkoutIntentResponse) -> Void) { + let response: INStartWorkoutIntentResponse + + if let workout = Workout(startWorkoutIntent: intent) { + // Create a response with a `NSUserActivity` that contains the information needed to start a workout. + let userActivity = NSUserActivity(ascentActivityType: .start(workout)) + response = INStartWorkoutIntentResponse(code: .continueInApp, userActivity: userActivity) + } + else { + response = INStartWorkoutIntentResponse(code: .failure, userActivity: nil) + } + + completion(response) + } +} diff --git a/IntentHandling/Projects/Payments/Payments.xcodeproj/project.pbxproj b/IntentHandling/Projects/Payments/Payments.xcodeproj/project.pbxproj new file mode 100644 index 00000000..ba5d5aa8 --- /dev/null +++ b/IntentHandling/Projects/Payments/Payments.xcodeproj/project.pbxproj @@ -0,0 +1,668 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + B50A12341CD6700D00C3C6AE /* PaymentsIntentsExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = B50A122D1CD6700D00C3C6AE /* PaymentsIntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + B50A12411CD6703500C3C6AE /* IntentsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50A123B1CD6703500C3C6AE /* IntentsExtension.swift */; }; + B50A12431CD6703500C3C6AE /* SendPaymentIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50A123D1CD6703500C3C6AE /* SendPaymentIntentHandler.swift */; }; + B50A124C1CD670AC00C3C6AE /* PaymentsFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = B50A124B1CD670AC00C3C6AE /* PaymentsFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B50A12501CD670AC00C3C6AE /* PaymentsFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50A12491CD670AC00C3C6AE /* PaymentsFramework.framework */; }; + B50A12511CD670AC00C3C6AE /* PaymentsFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B50A12491CD670AC00C3C6AE /* PaymentsFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B50A12571CD670CD00C3C6AE /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50A12561CD670CD00C3C6AE /* Contact.swift */; }; + B50A12591CD670FB00C3C6AE /* INPerson+Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50A12581CD670FB00C3C6AE /* INPerson+Contact.swift */; }; + B50A125A1CD6713600C3C6AE /* PaymentsFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B50A12491CD670AC00C3C6AE /* PaymentsFramework.framework */; }; + B50A125D1CD6721800C3C6AE /* ContactLookup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50A125B1CD6721700C3C6AE /* ContactLookup.swift */; }; + B50A125E1CD6721800C3C6AE /* PaymentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50A125C1CD6721700C3C6AE /* PaymentProvider.swift */; }; + B517769E1CE5180A001C5955 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B517769D1CE5180A001C5955 /* Intents.framework */; }; + B52BA4491CE4DAA3000E6AF1 /* DictionaryRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52BA4481CE4DAA3000E6AF1 /* DictionaryRepresentable.swift */; }; + B52BA44B1CE4DAC4000E6AF1 /* Payment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52BA44A1CE4DAC4000E6AF1 /* Payment.swift */; }; + B52BA44D1CE4DAEB000E6AF1 /* Contact+DictionaryRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52BA44C1CE4DAEB000E6AF1 /* Contact+DictionaryRepresentable.swift */; }; + B52BA44F1CE4DB32000E6AF1 /* Payment+DictionaryRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52BA44E1CE4DB32000E6AF1 /* Payment+DictionaryRepresentable.swift */; }; + B5D33EAE1CC97EE500394F6A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D33EAD1CC97EE500394F6A /* AppDelegate.swift */; }; + B5D33EB01CC97EE500394F6A /* PaymentHistoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D33EAF1CC97EE500394F6A /* PaymentHistoryViewController.swift */; }; + B5D33EB31CC97EE500394F6A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B5D33EB11CC97EE500394F6A /* Main.storyboard */; }; + B5D33EB51CC97EE500394F6A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5D33EB41CC97EE500394F6A /* Assets.xcassets */; }; + B5D33EB81CC97EE500394F6A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B5D33EB61CC97EE500394F6A /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + B50A12321CD6700D00C3C6AE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B5D33EA21CC97EE500394F6A /* Project object */; + proxyType = 1; + remoteGlobalIDString = B50A122C1CD6700D00C3C6AE; + remoteInfo = PaymentsIntentsExtension; + }; + B50A124E1CD670AC00C3C6AE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B5D33EA21CC97EE500394F6A /* Project object */; + proxyType = 1; + remoteGlobalIDString = B50A12481CD670AC00C3C6AE; + remoteInfo = PaymentsFramework; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + B50A12551CD670AC00C3C6AE /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + B50A12511CD670AC00C3C6AE /* PaymentsFramework.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + B5D33EE01CC9840500394F6A /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + B50A12341CD6700D00C3C6AE /* PaymentsIntentsExtension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + B50A122D1CD6700D00C3C6AE /* PaymentsIntentsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PaymentsIntentsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + B50A12311CD6700D00C3C6AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B50A123B1CD6703500C3C6AE /* IntentsExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntentsExtension.swift; sourceTree = ""; }; + B50A123D1CD6703500C3C6AE /* SendPaymentIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendPaymentIntentHandler.swift; sourceTree = ""; }; + B50A12491CD670AC00C3C6AE /* PaymentsFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PaymentsFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B50A124B1CD670AC00C3C6AE /* PaymentsFramework.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PaymentsFramework.h; sourceTree = ""; }; + B50A124D1CD670AC00C3C6AE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B50A12561CD670CD00C3C6AE /* Contact.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; + B50A12581CD670FB00C3C6AE /* INPerson+Contact.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "INPerson+Contact.swift"; sourceTree = ""; }; + B50A125B1CD6721700C3C6AE /* ContactLookup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactLookup.swift; sourceTree = ""; }; + B50A125C1CD6721700C3C6AE /* PaymentProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentProvider.swift; sourceTree = ""; }; + B517769D1CE5180A001C5955 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; + B52BA4481CE4DAA3000E6AF1 /* DictionaryRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DictionaryRepresentable.swift; sourceTree = ""; }; + B52BA44A1CE4DAC4000E6AF1 /* Payment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Payment.swift; sourceTree = ""; }; + B52BA44C1CE4DAEB000E6AF1 /* Contact+DictionaryRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Contact+DictionaryRepresentable.swift"; sourceTree = ""; }; + B52BA44E1CE4DB32000E6AF1 /* Payment+DictionaryRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Payment+DictionaryRepresentable.swift"; sourceTree = ""; }; + B52BA4501CE4E42F000E6AF1 /* Payments.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Payments.entitlements; sourceTree = ""; }; + B52BA4511CE4E445000E6AF1 /* PaymentsIntentsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PaymentsIntentsExtension.entitlements; sourceTree = ""; }; + B574C8D61D7B81BC00F68E77 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../../README.md; sourceTree = ""; }; + B5D33EAA1CC97EE500394F6A /* Payments.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Payments.app; sourceTree = BUILT_PRODUCTS_DIR; }; + B5D33EAD1CC97EE500394F6A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + B5D33EAF1CC97EE500394F6A /* PaymentHistoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentHistoryViewController.swift; sourceTree = ""; }; + B5D33EB21CC97EE500394F6A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + B5D33EB41CC97EE500394F6A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + B5D33EB71CC97EE500394F6A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + B5D33EB91CC97EE500394F6A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B50A122A1CD6700D00C3C6AE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B517769E1CE5180A001C5955 /* Intents.framework in Frameworks */, + B50A125A1CD6713600C3C6AE /* PaymentsFramework.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B50A12451CD670AC00C3C6AE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B5D33EA71CC97EE500394F6A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B50A12501CD670AC00C3C6AE /* PaymentsFramework.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B50A122E1CD6700D00C3C6AE /* PaymentsIntentsExtension */ = { + isa = PBXGroup; + children = ( + B50A123B1CD6703500C3C6AE /* IntentsExtension.swift */, + B50A123D1CD6703500C3C6AE /* SendPaymentIntentHandler.swift */, + B50A12581CD670FB00C3C6AE /* INPerson+Contact.swift */, + B50A12311CD6700D00C3C6AE /* Info.plist */, + B52BA4511CE4E445000E6AF1 /* PaymentsIntentsExtension.entitlements */, + ); + path = PaymentsIntentsExtension; + sourceTree = ""; + }; + B50A124A1CD670AC00C3C6AE /* PaymentsFramework */ = { + isa = PBXGroup; + children = ( + B50A124B1CD670AC00C3C6AE /* PaymentsFramework.h */, + B52BA44A1CE4DAC4000E6AF1 /* Payment.swift */, + B50A125C1CD6721700C3C6AE /* PaymentProvider.swift */, + B50A12561CD670CD00C3C6AE /* Contact.swift */, + B50A125B1CD6721700C3C6AE /* ContactLookup.swift */, + B52BA4481CE4DAA3000E6AF1 /* DictionaryRepresentable.swift */, + B52BA44C1CE4DAEB000E6AF1 /* Contact+DictionaryRepresentable.swift */, + B52BA44E1CE4DB32000E6AF1 /* Payment+DictionaryRepresentable.swift */, + B50A124D1CD670AC00C3C6AE /* Info.plist */, + ); + path = PaymentsFramework; + sourceTree = ""; + }; + B517769C1CE5180A001C5955 /* System Frameworks */ = { + isa = PBXGroup; + children = ( + B517769D1CE5180A001C5955 /* Intents.framework */, + ); + name = "System Frameworks"; + sourceTree = ""; + }; + B5D33EA11CC97EE500394F6A = { + isa = PBXGroup; + children = ( + B574C8D61D7B81BC00F68E77 /* README.md */, + B5D33EAC1CC97EE500394F6A /* Payments */, + B50A122E1CD6700D00C3C6AE /* PaymentsIntentsExtension */, + B50A124A1CD670AC00C3C6AE /* PaymentsFramework */, + B5D33EAB1CC97EE500394F6A /* Products */, + B517769C1CE5180A001C5955 /* System Frameworks */, + ); + sourceTree = ""; + }; + B5D33EAB1CC97EE500394F6A /* Products */ = { + isa = PBXGroup; + children = ( + B5D33EAA1CC97EE500394F6A /* Payments.app */, + B50A122D1CD6700D00C3C6AE /* PaymentsIntentsExtension.appex */, + B50A12491CD670AC00C3C6AE /* PaymentsFramework.framework */, + ); + name = Products; + sourceTree = ""; + }; + B5D33EAC1CC97EE500394F6A /* Payments */ = { + isa = PBXGroup; + children = ( + B5D33EAD1CC97EE500394F6A /* AppDelegate.swift */, + B5D33EAF1CC97EE500394F6A /* PaymentHistoryViewController.swift */, + B5D33EB11CC97EE500394F6A /* Main.storyboard */, + B5D33EB41CC97EE500394F6A /* Assets.xcassets */, + B5D33EB61CC97EE500394F6A /* LaunchScreen.storyboard */, + B5D33EB91CC97EE500394F6A /* Info.plist */, + B52BA4501CE4E42F000E6AF1 /* Payments.entitlements */, + ); + path = Payments; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + B50A12461CD670AC00C3C6AE /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + B50A124C1CD670AC00C3C6AE /* PaymentsFramework.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + B50A122C1CD6700D00C3C6AE /* PaymentsIntentsExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = B50A12351CD6700D00C3C6AE /* Build configuration list for PBXNativeTarget "PaymentsIntentsExtension" */; + buildPhases = ( + B50A12291CD6700D00C3C6AE /* Sources */, + B50A122A1CD6700D00C3C6AE /* Frameworks */, + B50A122B1CD6700D00C3C6AE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PaymentsIntentsExtension; + productName = PaymentsIntentsExtension; + productReference = B50A122D1CD6700D00C3C6AE /* PaymentsIntentsExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + B50A12481CD670AC00C3C6AE /* PaymentsFramework */ = { + isa = PBXNativeTarget; + buildConfigurationList = B50A12521CD670AC00C3C6AE /* Build configuration list for PBXNativeTarget "PaymentsFramework" */; + buildPhases = ( + B50A12441CD670AC00C3C6AE /* Sources */, + B50A12451CD670AC00C3C6AE /* Frameworks */, + B50A12461CD670AC00C3C6AE /* Headers */, + B50A12471CD670AC00C3C6AE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PaymentsFramework; + productName = PaymentsFramework; + productReference = B50A12491CD670AC00C3C6AE /* PaymentsFramework.framework */; + productType = "com.apple.product-type.framework"; + }; + B5D33EA91CC97EE500394F6A /* Payments */ = { + isa = PBXNativeTarget; + buildConfigurationList = B5D33EBC1CC97EE500394F6A /* Build configuration list for PBXNativeTarget "Payments" */; + buildPhases = ( + B5D33EA61CC97EE500394F6A /* Sources */, + B5D33EA71CC97EE500394F6A /* Frameworks */, + B5D33EA81CC97EE500394F6A /* Resources */, + B5D33EE01CC9840500394F6A /* Embed App Extensions */, + B50A12551CD670AC00C3C6AE /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + B50A12331CD6700D00C3C6AE /* PBXTargetDependency */, + B50A124F1CD670AC00C3C6AE /* PBXTargetDependency */, + ); + name = Payments; + productName = Payments; + productReference = B5D33EAA1CC97EE500394F6A /* Payments.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B5D33EA21CC97EE500394F6A /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Apple; + TargetAttributes = { + B50A122C1CD6700D00C3C6AE = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + }; + }; + B50A12481CD670AC00C3C6AE = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + B5D33EA91CC97EE500394F6A = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.ApplicationGroups.iOS = { + enabled = 1; + }; + com.apple.Siri = { + enabled = 1; + }; + }; + }; + }; + }; + buildConfigurationList = B5D33EA51CC97EE500394F6A /* Build configuration list for PBXProject "Payments" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B5D33EA11CC97EE500394F6A; + productRefGroup = B5D33EAB1CC97EE500394F6A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B5D33EA91CC97EE500394F6A /* Payments */, + B50A122C1CD6700D00C3C6AE /* PaymentsIntentsExtension */, + B50A12481CD670AC00C3C6AE /* PaymentsFramework */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B50A122B1CD6700D00C3C6AE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B50A12471CD670AC00C3C6AE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B5D33EA81CC97EE500394F6A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B5D33EB81CC97EE500394F6A /* LaunchScreen.storyboard in Resources */, + B5D33EB51CC97EE500394F6A /* Assets.xcassets in Resources */, + B5D33EB31CC97EE500394F6A /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B50A12291CD6700D00C3C6AE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B50A12591CD670FB00C3C6AE /* INPerson+Contact.swift in Sources */, + B50A12411CD6703500C3C6AE /* IntentsExtension.swift in Sources */, + B50A12431CD6703500C3C6AE /* SendPaymentIntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B50A12441CD670AC00C3C6AE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B50A12571CD670CD00C3C6AE /* Contact.swift in Sources */, + B50A125E1CD6721800C3C6AE /* PaymentProvider.swift in Sources */, + B52BA44F1CE4DB32000E6AF1 /* Payment+DictionaryRepresentable.swift in Sources */, + B52BA4491CE4DAA3000E6AF1 /* DictionaryRepresentable.swift in Sources */, + B50A125D1CD6721800C3C6AE /* ContactLookup.swift in Sources */, + B52BA44B1CE4DAC4000E6AF1 /* Payment.swift in Sources */, + B52BA44D1CE4DAEB000E6AF1 /* Contact+DictionaryRepresentable.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B5D33EA61CC97EE500394F6A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B5D33EB01CC97EE500394F6A /* PaymentHistoryViewController.swift in Sources */, + B5D33EAE1CC97EE500394F6A /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + B50A12331CD6700D00C3C6AE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B50A122C1CD6700D00C3C6AE /* PaymentsIntentsExtension */; + targetProxy = B50A12321CD6700D00C3C6AE /* PBXContainerItemProxy */; + }; + B50A124F1CD670AC00C3C6AE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B50A12481CD670AC00C3C6AE /* PaymentsFramework */; + targetProxy = B50A124E1CD670AC00C3C6AE /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + B5D33EB11CC97EE500394F6A /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B5D33EB21CC97EE500394F6A /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + B5D33EB61CC97EE500394F6A /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + B5D33EB71CC97EE500394F6A /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + B50A12361CD6700D00C3C6AE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = PaymentsIntentsExtension/PaymentsIntentsExtension.entitlements; + INFOPLIST_FILE = PaymentsIntentsExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Payments.PaymentsIntentsExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + B50A12371CD6700D00C3C6AE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = PaymentsIntentsExtension/PaymentsIntentsExtension.entitlements; + INFOPLIST_FILE = PaymentsIntentsExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Payments.PaymentsIntentsExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + B50A12531CD670AC00C3C6AE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = PaymentsFramework/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.PaymentsFramework"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + B50A12541CD670AC00C3C6AE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = PaymentsFramework/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.PaymentsFramework"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + B5D33EBA1CC97EE500394F6A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = lossless; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + B5D33EBB1CC97EE500394F6A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = "respect-asset-catalog"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + B5D33EBD1CC97EE500394F6A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Payments/Payments.entitlements; + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + ); + INFOPLIST_FILE = Payments/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Payments"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + B5D33EBE1CC97EE500394F6A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = Payments/Payments.entitlements; + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + ); + INFOPLIST_FILE = Payments/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Payments"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B50A12351CD6700D00C3C6AE /* Build configuration list for PBXNativeTarget "PaymentsIntentsExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B50A12361CD6700D00C3C6AE /* Debug */, + B50A12371CD6700D00C3C6AE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B50A12521CD670AC00C3C6AE /* Build configuration list for PBXNativeTarget "PaymentsFramework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B50A12531CD670AC00C3C6AE /* Debug */, + B50A12541CD670AC00C3C6AE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B5D33EA51CC97EE500394F6A /* Build configuration list for PBXProject "Payments" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B5D33EBA1CC97EE500394F6A /* Debug */, + B5D33EBB1CC97EE500394F6A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B5D33EBC1CC97EE500394F6A /* Build configuration list for PBXNativeTarget "Payments" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B5D33EBD1CC97EE500394F6A /* Debug */, + B5D33EBE1CC97EE500394F6A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B5D33EA21CC97EE500394F6A /* Project object */; +} diff --git a/IntentHandling/Projects/Payments/Payments/AppDelegate.swift b/IntentHandling/Projects/Payments/Payments/AppDelegate.swift new file mode 100644 index 00000000..1103d3e9 --- /dev/null +++ b/IntentHandling/Projects/Payments/Payments/AppDelegate.swift @@ -0,0 +1,23 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application delegate. +*/ + +import UIKit +import Intents +import PaymentsFramework + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func applicationDidFinishLaunching(_ application: UIApplication) { + // Register names of contacts that may not be in the user's address book. + let contactNames = Contact.sampleContacts.map { $0.formattedName } + INVocabulary.shared().setVocabularyStrings(NSOrderedSet(array: contactNames), of: .contactName) + } +} diff --git a/IntentHandling/Projects/Payments/Payments/Assets.xcassets/AppIcon.appiconset/Contents.json b/IntentHandling/Projects/Payments/Payments/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..118c98f7 --- /dev/null +++ b/IntentHandling/Projects/Payments/Payments/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "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" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IntentHandling/Projects/Payments/Payments/Base.lproj/LaunchScreen.storyboard b/IntentHandling/Projects/Payments/Payments/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..7f01ab5c --- /dev/null +++ b/IntentHandling/Projects/Payments/Payments/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IntentHandling/Projects/Payments/Payments/Base.lproj/Main.storyboard b/IntentHandling/Projects/Payments/Payments/Base.lproj/Main.storyboard new file mode 100644 index 00000000..b46729cb --- /dev/null +++ b/IntentHandling/Projects/Payments/Payments/Base.lproj/Main.storyboard @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IntentHandling/Projects/Payments/Payments/Info.plist b/IntentHandling/Projects/Payments/Payments/Info.plist new file mode 100644 index 00000000..6905cc67 --- /dev/null +++ b/IntentHandling/Projects/Payments/Payments/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/IntentHandling/Projects/Payments/Payments/PaymentHistoryViewController.swift b/IntentHandling/Projects/Payments/Payments/PaymentHistoryViewController.swift new file mode 100644 index 00000000..1d1382d0 --- /dev/null +++ b/IntentHandling/Projects/Payments/Payments/PaymentHistoryViewController.swift @@ -0,0 +1,86 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A view controller that lists recent payments made with our app. +*/ + +import UIKit +import PaymentsFramework + +class PaymentHistoryViewController: UITableViewController { + + private let paymentProvider = PaymentProvider() + + private var payments = [Payment]() { + didSet { + // If a new array of `Payment`s has been set, reload the table view. + guard oldValue != payments && isViewLoaded else { return } + tableView.reloadData() + } + } + + /// Used to format payment amounts in table view cells. + private var amountFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + return formatter + }() + + /// Used to format payment dates in table view cells. + private var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .short + return formatter + }() + + // MARK: UIViewController + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + payments = paymentProvider.loadPaymentHistory().reversed() + } + + // MARK: UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return payments.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: PaymentTableViewCell.reuseIdentifier, for: indexPath) as? PaymentTableViewCell else { fatalError("Unable to dequeue a PaymentTableViewCell") } + let payment = payments[indexPath.row] + + // Configure the cell with the payment details. + cell.contactLabel.text = payment.contact.formattedName + + if let date = payment.date { + cell.dateLabel.text = dateFormatter.string(from: date) + } + else { + cell.dateLabel.text = "-" + } + + amountFormatter.currencyCode = payment.currencyCode + cell.amountLabel.text = amountFormatter.string(from: payment.amount) + + return cell + } +} + + + +/// Used by `PaymentHistoryViewController` to show details of a `Payment`. +class PaymentTableViewCell: UITableViewCell { + + static let reuseIdentifier = "PaymentTableViewCell" + + @IBOutlet weak var contactLabel: UILabel! + + @IBOutlet weak var dateLabel: UILabel! + + @IBOutlet weak var amountLabel: UILabel! + +} diff --git a/IntentHandling/Projects/Payments/Payments/Payments.entitlements b/IntentHandling/Projects/Payments/Payments/Payments.entitlements new file mode 100644 index 00000000..128c02d7 --- /dev/null +++ b/IntentHandling/Projects/Payments/Payments/Payments.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.developer.siri + + com.apple.security.application-groups + + group.com.example.apple-samplecode.Payments + + + diff --git a/IntentHandling/Projects/Payments/PaymentsFramework/Contact+DictionaryRepresentable.swift b/IntentHandling/Projects/Payments/PaymentsFramework/Contact+DictionaryRepresentable.swift new file mode 100644 index 00000000..ee73b578 --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsFramework/Contact+DictionaryRepresentable.swift @@ -0,0 +1,42 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extends `Contact` to allow it to be represented as and initialized with an `NSDictionary`. +*/ + +import Foundation + +extension Contact: DictionaryRepresentable { + // MARK: Types + + private struct DictionaryKeys { + static let familyName = "familyName" + static let givenName = "givenName" + static let emailAddress = "emailAddress" + } + + // MARK: DictionaryRepresentable + + var dictionaryRepresentation: [String : Any] { + var dictionary = [String: Any]() + + dictionary[DictionaryKeys.familyName] = nameComponents.familyName ?? "" + dictionary[DictionaryKeys.givenName] = nameComponents.givenName ?? "" + dictionary[DictionaryKeys.emailAddress] = emailAddress + + return dictionary + } + + init?(dictionaryRepresentation dictionary: [String: Any]) { + guard let emailAddress = dictionary[DictionaryKeys.emailAddress] as? String, !emailAddress.isEmpty else { return nil } + + var nameComponents = PersonNameComponents() + nameComponents.familyName = dictionary[DictionaryKeys.familyName] as? String + nameComponents.givenName = dictionary[DictionaryKeys.givenName] as? String + + self.nameComponents = nameComponents + self.emailAddress = emailAddress + } +} diff --git a/IntentHandling/Projects/Payments/PaymentsFramework/Contact.swift b/IntentHandling/Projects/Payments/PaymentsFramework/Contact.swift new file mode 100644 index 00000000..7c0a15cd --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsFramework/Contact.swift @@ -0,0 +1,45 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A struct that defines a contact that can receive payments from our app. +*/ + +import Intents + +public struct Contact { + + private static let nameFormatter = PersonNameComponentsFormatter() + + public let nameComponents: PersonNameComponents + + public let emailAddress: String + + public var formattedName: String { + return Contact.nameFormatter.string(from: nameComponents) + } + + public init(givenName: String?, familyName: String?, emailAddress: String) { + var nameComponents = PersonNameComponents() + nameComponents.givenName = givenName + nameComponents.familyName = familyName + + self.nameComponents = nameComponents + self.emailAddress = emailAddress + } + +} + +public extension Contact { + static let sampleContacts = [ + Contact(givenName: "Anne", familyName: "Johnson", emailAddress: "anne.johnson@example.com"), + Contact(givenName: "Maria", familyName: "Ruiz", emailAddress: "maria.ruiz@example.com"), + Contact(givenName: "Mei", familyName: "Chen", emailAddress: "mei.chen@example.com"), + Contact(givenName: "Gita", familyName: "Kumar", emailAddress: "gita.kumar@example.com"), + Contact(givenName: "Bill", familyName: "James", emailAddress: "bill.james@example.com"), + Contact(givenName: "Tom", familyName: "Clark", emailAddress: "tom.clark@example.com"), + Contact(givenName: "Juan", familyName: "Chavez", emailAddress: "juan.chavez@example.com"), + Contact(givenName: "Ravi", familyName: "Patel", emailAddress: "ravi.patel@example.com"), + ] +} diff --git a/IntentHandling/Projects/Payments/PaymentsFramework/ContactLookup.swift b/IntentHandling/Projects/Payments/PaymentsFramework/ContactLookup.swift new file mode 100644 index 00000000..1874d17d --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsFramework/ContactLookup.swift @@ -0,0 +1,51 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A class that mimics asynchronous lookups of contacts. +*/ + + +public class ContactLookup { + + public var contacts = Contact.sampleContacts + + public init() {} + + public func lookup(displayName: String, completion: (_ contacts: [Contact]) -> Void) { + /* + Here we are searching through a local array of contacts. This could + instead be an asynchronous call to a remote server. + */ + let nameFormatter = PersonNameComponentsFormatter() + + let matchingContacts = contacts.filter { contact in + nameFormatter.style = .medium + if nameFormatter.string(from: contact.nameComponents) == displayName { + return true + } + + nameFormatter.style = .short + if nameFormatter.string(from: contact.nameComponents) == displayName { + return true + } + + return false + } + + completion(matchingContacts) + } + + public func lookup(emailAddress: String, completion: (_ contact: Contact?) -> Void) { + /* + Here we are searching through a local array of contacts. This could + instead be an asynchronous call to a remote server. + */ + for contact in contacts where contact.emailAddress == emailAddress { + completion(contact) + } + + completion(nil) + } +} diff --git a/IntentHandling/Projects/Payments/PaymentsFramework/DictionaryRepresentable.swift b/IntentHandling/Projects/Payments/PaymentsFramework/DictionaryRepresentable.swift new file mode 100644 index 00000000..58a3181c --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsFramework/DictionaryRepresentable.swift @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A type that implements the `DictionaryRepresentable` can be represented as and initialized with an `NSDictionary`. +*/ + +import Foundation + +protocol DictionaryRepresentable { + + var dictionaryRepresentation: [String: Any] { get } + + init?(dictionaryRepresentation dictionary: [String: Any]) +} diff --git a/IntentHandling/Projects/Payments/PaymentsFramework/Info.plist b/IntentHandling/Projects/Payments/PaymentsFramework/Info.plist new file mode 100644 index 00000000..d3de8eef --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsFramework/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/IntentHandling/Projects/Payments/PaymentsFramework/Payment+DictionaryRepresentable.swift b/IntentHandling/Projects/Payments/PaymentsFramework/Payment+DictionaryRepresentable.swift new file mode 100644 index 00000000..74dd8cae --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsFramework/Payment+DictionaryRepresentable.swift @@ -0,0 +1,49 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extends `Payment` to allow it to be represented as and initialized with an `NSDictionary`. +*/ + +import Foundation + +extension Payment: DictionaryRepresentable { + // MARK: Types + + private struct DictionaryKeys { + static let contact = "contact" + static let amount = "amount" + static let currencyCode = "currencyCode" + static let date = "date" + } + + // MARK: DictionaryRepresentable + + var dictionaryRepresentation: [String : Any] { + var dictionary = [String: Any]() + + dictionary[DictionaryKeys.contact] = contact.dictionaryRepresentation + dictionary[DictionaryKeys.amount] = amount.doubleValue + dictionary[DictionaryKeys.currencyCode] = currencyCode + + if let date = date { + dictionary[DictionaryKeys.date] = date + } + + return dictionary + } + + init?(dictionaryRepresentation dictionary: [String: Any]) { + guard let contactDictionary = dictionary[DictionaryKeys.contact] as? [String: AnyObject], let contact = Contact(dictionaryRepresentation: contactDictionary) else { return nil } + guard let doubleAmount = dictionary[DictionaryKeys.amount] as? Double else { return nil } + guard let currencyCode = dictionary[DictionaryKeys.currencyCode] as? String else { return nil } + + let date = dictionary[DictionaryKeys.date] as? Date + + self.contact = contact + self.amount = NSDecimalNumber(value: doubleAmount) + self.currencyCode = currencyCode + self.date = date + } +} diff --git a/IntentHandling/Projects/Payments/PaymentsFramework/Payment.swift b/IntentHandling/Projects/Payments/PaymentsFramework/Payment.swift new file mode 100644 index 00000000..c35b5446 --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsFramework/Payment.swift @@ -0,0 +1,38 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A struct that defines a payment made with our app. +*/ + +import Foundation + +public struct Payment: Equatable { + + // MARK: Properties + + public let contact: Contact + + public let amount: NSDecimalNumber + + public let currencyCode: String + + public let date: Date? + + // MARK: Public initializer + + public init(contact: Contact, amount: NSDecimalNumber, currencyCode: String, date: Date? = nil) { + self.contact = contact + self.amount = amount + self.currencyCode = currencyCode + self.date = date + } +} + +public func ==(lhs: Payment, rhs: Payment) -> Bool { + return lhs.contact.emailAddress == rhs.contact.emailAddress && + lhs.amount == rhs.amount && + lhs.currencyCode == rhs.currencyCode && + lhs.date == rhs.date +} diff --git a/IntentHandling/Projects/Payments/PaymentsFramework/PaymentProvider.swift b/IntentHandling/Projects/Payments/PaymentsFramework/PaymentProvider.swift new file mode 100644 index 00000000..4d040b63 --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsFramework/PaymentProvider.swift @@ -0,0 +1,95 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A class tha mimics asynchronous calls to send payments to a server. +*/ + +import Foundation + +public class PaymentProvider { + + // MARK: Properties + + public var mostRecentPayment: Payment? { + let paymentHistory = loadPaymentHistory() + return paymentHistory.last + } + + // MARK: Intialization + + public init() {} + + // MARK: Payment methods + + public func canSend(_ payment: Payment, completion: (_ success: Bool, _ error: NSError?) -> Void) { + /* + For the purposes of this sample, we don't have a server-side component. + Instead we just accept any payment. + */ + + completion(true, nil) + } + + public func send(_ payment: Payment, completion: (_ success: Bool, _ sentPayment: Payment?, _ error: NSError?) -> Void) { + /* + For the purposes of this sample, we don't have a server-side component. + Instead we're just storing payment locally. + */ + + // Create a new `Payment` that includes the current date as the date it was made. + let datedPayment = Payment(contact: payment.contact, amount: payment.amount, currencyCode: payment.currencyCode, date: Date()) + + // Add the dated payment to the payment history and save it. + var paymentHistory = loadPaymentHistory() + paymentHistory.append(datedPayment) + save(paymentHistory) + + // Call the completion handler. + completion(true, datedPayment, nil) + } + + // MARK: Convenience + + public func validate(_ currencyCode: String) -> String? { + if currencyCode == "USD" || currencyCode == "AMBIGUOUS_DOLLAR" { + return "USD" + } + else { + return nil + } + } + + public func loadPaymentHistory() -> [Payment] { + var paymentHistory = [Payment]() + + // Parse payments from the shared `NSUserDefaults`. + let defaults = makeUserDefaults() + if let paymentsDictionaries = defaults.object(forKey: "paymentHistory") as? [[String: AnyObject]] { + paymentHistory = paymentsDictionaries.flatMap { Payment(dictionaryRepresentation: $0) } + } + + return paymentHistory + } + + private func save(_ payments: [Payment]) { + // Make sure the number of payments isn't too large + let paymentsToSave = payments.suffix(50) + + /* + Convert the payments to an array of dictionaries that can be saved in + user defaults. + */ + let paymentsDictionaries: [[String: Any]] = paymentsToSave.map { $0.dictionaryRepresentation } + + // Save the payments to shared `NSUserDefaults`. + let defaults = makeUserDefaults() + defaults.set(paymentsDictionaries as AnyObject, forKey: "paymentHistory") + } + + private func makeUserDefaults() -> UserDefaults { + guard let defaults = UserDefaults(suiteName: "group.com.example.apple-samplecode.Payments") else { fatalError("Unable to make shared NSUserDefaults object") } + return defaults + } +} diff --git a/IntentHandling/Projects/Payments/PaymentsFramework/PaymentsFramework.h b/IntentHandling/Projects/Payments/PaymentsFramework/PaymentsFramework.h new file mode 100644 index 00000000..d813cc8e --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsFramework/PaymentsFramework.h @@ -0,0 +1,15 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The main header for PaymentsFramework + */ + +#import + +//! Project version number for PaymentsFramework. +FOUNDATION_EXPORT double PaymentsFrameworkVersionNumber; + +//! Project version string for PaymentsFramework. +FOUNDATION_EXPORT const unsigned char PaymentsFrameworkVersionString[]; diff --git a/IntentHandling/Projects/Payments/PaymentsIntentsExtension/INPerson+Contact.swift b/IntentHandling/Projects/Payments/PaymentsIntentsExtension/INPerson+Contact.swift new file mode 100644 index 00000000..a714697a --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsIntentsExtension/INPerson+Contact.swift @@ -0,0 +1,17 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extends `INPerson` to add an initializer that accepts a `Contact`. +*/ + +import PaymentsFramework +import Intents + +extension INPerson { + convenience init(contact: Contact) { + let handle = INPersonHandle(value: contact.emailAddress, type: .emailAddress) + self.init(personHandle: handle, nameComponents: contact.nameComponents, displayName: contact.formattedName, image: nil, contactIdentifier: nil, customIdentifier: nil) + } +} diff --git a/IntentHandling/Projects/Payments/PaymentsIntentsExtension/Info.plist b/IntentHandling/Projects/Payments/PaymentsIntentsExtension/Info.plist new file mode 100644 index 00000000..01481b05 --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsIntentsExtension/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + PaymentsIntentsExtension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + INSendPaymentIntent + + IntentsSupported + + INSendPaymentIntent + INRequestPaymentIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentsExtension + + + diff --git a/IntentHandling/Projects/Payments/PaymentsIntentsExtension/IntentsExtension.swift b/IntentHandling/Projects/Payments/PaymentsIntentsExtension/IntentsExtension.swift new file mode 100644 index 00000000..b31999e2 --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsIntentsExtension/IntentsExtension.swift @@ -0,0 +1,24 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The main extension entry point. +*/ + +import Intents +import PaymentsFramework + +class IntentsExtension: INExtension { + + let paymentProvider = PaymentProvider() + + let contactLookup = ContactLookup() + + override func handler(for intent: INIntent) -> Any? { + // Our sample is only configured to handle the `INSendPaymentIntent`. + guard intent is INSendPaymentIntent else { fatalError("Unhandled intent type \(intent)") } + + return SendPaymentIntentHandler(paymentProvider: paymentProvider, contactLookup: contactLookup) + } +} diff --git a/IntentHandling/Projects/Payments/PaymentsIntentsExtension/PaymentsIntentsExtension.entitlements b/IntentHandling/Projects/Payments/PaymentsIntentsExtension/PaymentsIntentsExtension.entitlements new file mode 100644 index 00000000..0d1856b2 --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsIntentsExtension/PaymentsIntentsExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.example.apple-samplecode.Payments + + + diff --git a/IntentHandling/Projects/Payments/PaymentsIntentsExtension/SendPaymentIntentHandler.swift b/IntentHandling/Projects/Payments/PaymentsIntentsExtension/SendPaymentIntentHandler.swift new file mode 100644 index 00000000..5aec8da3 --- /dev/null +++ b/IntentHandling/Projects/Payments/PaymentsIntentsExtension/SendPaymentIntentHandler.swift @@ -0,0 +1,186 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A class that implements `INSendPaymentIntentHandling` to handle `INSendPaymentIntent`s +*/ + +import Intents +import PaymentsFramework + +class SendPaymentIntentHandler: NSObject, INSendPaymentIntentHandling { + // MARK: Properties + + private let paymentProvider: PaymentProvider + + private let contactLookup: ContactLookup + + // MARK: Initialization + + init(paymentProvider: PaymentProvider, contactLookup: ContactLookup) { + self.paymentProvider = paymentProvider + self.contactLookup = contactLookup + } + + // MARK: INSendPaymentIntentHandling parameter resolution + + func resolvePayee(forSendPayment intent: INSendPaymentIntent, with completion: @escaping (INPersonResolutionResult) -> Void) { + if let payee = intent.payee { + // Lookup contacts that match the payee. + contactLookup.lookup(displayName: payee.displayName) { contacts in + // Build the `INIntentResolutionResult` to pass to the `completion` closure. + let result: INPersonResolutionResult + + if let contact = contacts.first, contacts.count == 1 { + // A single match was found, the payee was successfully resolved. + let resolvedPayee = INPerson(contact: contact) + result = INPersonResolutionResult.success(with: resolvedPayee) + } + else if contacts.isEmpty { + // No matches were found. + result = INPersonResolutionResult.unsupported() + } + else { + /* + More than one match was made, the user needs to clarify which + contact they intended. + */ + let people: [INPerson] = contacts.map { contact in + return INPerson(contact: contact) + } + result = INPersonResolutionResult.disambiguation(with: people) + } + + completion(result) + } + } + else if let mostRecentPayee = paymentProvider.mostRecentPayment?.contact { + // No payee has been provided, suggest the last payee. + let result = INPersonResolutionResult.confirmationRequired(with: INPerson(contact: mostRecentPayee)) + completion(result) + } + else { + // No payee has been provided and there was no previous payee. + let result = INPersonResolutionResult.needsValue() + completion(result) + } + } + + func resolveCurrencyAmount(forSendPayment intent: INSendPaymentIntent, with completion: @escaping (INCurrencyAmountResolutionResult) -> Void) { + let result: INCurrencyAmountResolutionResult + + // Resolve the currency amount. + if let currencyAmount = intent.currencyAmount, let amount = currencyAmount.amount, let currencyCode = currencyAmount.currencyCode { + if amount.intValue <= 0 { + // The amount needs to be a positive value. + result = INCurrencyAmountResolutionResult.unsupported() + } + else if let currencyCode = paymentProvider.validate(currencyCode) { + // Make a new `INCurrencyAmount` with the resolved currency code. + let resolvedAmount = INCurrencyAmount(amount: amount, currencyCode: currencyCode) + result = INCurrencyAmountResolutionResult.success(with: resolvedAmount) + } + else { + // The currency is unsupported. + result = INCurrencyAmountResolutionResult.unsupported() + } + } + else if let mostRecentPayment = paymentProvider.mostRecentPayment { + // No amount has been provided, suggest the last amount sent. + let suggestedAmount = INCurrencyAmount(amount: mostRecentPayment.amount, currencyCode: mostRecentPayment.currencyCode) + result = INCurrencyAmountResolutionResult.confirmationRequired(with: suggestedAmount) + } + else { + // No amount has been provided and there was no previous payment. + result = INCurrencyAmountResolutionResult.needsValue() + } + + completion(result) + } + + // MARK: INSendPaymentIntentHandling intent confirmation + + func confirm(sendPayment intent: INSendPaymentIntent, completion: @escaping (INSendPaymentIntentResponse) -> Void) { + guard let payee = intent.payee, + let payeeHandle = payee.personHandle, + let currencyAmount = intent.currencyAmount, + let amount = currencyAmount.amount, + let currencyCode = currencyAmount.currencyCode + else { + completion(INSendPaymentIntentResponse(code: .failure, userActivity: nil)) + return + } + + contactLookup.lookup(emailAddress: payeeHandle.value) { contact in + guard let contact = contact else { + completion(INSendPaymentIntentResponse(code: .failure, userActivity: nil)) + return + } + + let payment = Payment(contact: contact, amount: amount, currencyCode: currencyCode) + + self.paymentProvider.canSend(payment) { success, error in + guard success else { + completion(INSendPaymentIntentResponse(code: .failure, userActivity: nil)) + return + } + + let response = INSendPaymentIntentResponse(code: .success, userActivity: nil) + response.paymentRecord = self.makePaymentRecord(for: intent) + + completion(response) + } + } + } + + // MARK: INSendPaymentIntentHandling intent handling + + func handle(sendPayment intent: INSendPaymentIntent, completion: @escaping (INSendPaymentIntentResponse) -> Void) { + guard let payee = intent.payee, + let payeeHandle = payee.personHandle, + let currencyAmount = intent.currencyAmount, + let amount = currencyAmount.amount, + let currencyCode = currencyAmount.currencyCode + else { + completion(INSendPaymentIntentResponse(code: .failure, userActivity: nil)) + return + } + + contactLookup.lookup(emailAddress: payeeHandle.value) { contact in + guard let contact = contact else { + completion(INSendPaymentIntentResponse(code: .failure, userActivity: nil)) + return + } + + let payment = Payment(contact: contact, amount: amount, currencyCode: currencyCode) + + self.paymentProvider.send(payment) { success, _, _ in + guard success else { + completion(INSendPaymentIntentResponse(code: .failure, userActivity: nil)) + return + } + + let response = INSendPaymentIntentResponse(code: .success, userActivity: nil) + response.paymentRecord = self.makePaymentRecord(for: intent) + + completion(response) + } + } + } + + // MARK: Convenience + + func makePaymentRecord(for intent: INSendPaymentIntent, status: INPaymentStatus = .completed) -> INPaymentRecord? { + let paymentMethod = INPaymentMethod(type: .unknown, name: "Payments Sample", identificationHint: nil, icon: nil) + + return INPaymentRecord( + payee: intent.payee, + payer: nil, + currencyAmount: intent.currencyAmount, + paymentMethod: paymentMethod, + note: intent.note, + status: status + ) + } +} diff --git a/IntentHandling/Projects/RideMaps/GetRideStatusIntentExtension/Info.plist b/IntentHandling/Projects/RideMaps/GetRideStatusIntentExtension/Info.plist new file mode 100644 index 00000000..dbd9fc99 --- /dev/null +++ b/IntentHandling/Projects/RideMaps/GetRideStatusIntentExtension/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + GetRideStatusIntentExtension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsSupported + + INGetRideStatusIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + + diff --git a/IntentHandling/Projects/RideMaps/GetRideStatusIntentExtension/IntentHandler.swift b/IntentHandling/Projects/RideMaps/GetRideStatusIntentExtension/IntentHandler.swift new file mode 100644 index 00000000..c3b71c8e --- /dev/null +++ b/IntentHandling/Projects/RideMaps/GetRideStatusIntentExtension/IntentHandler.swift @@ -0,0 +1,71 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An object that implements the `INGetRideStatusIntentHandling` protocol to handle ridesharing tasks. + */ + +import Intents + +// The intents whose interactions you wish to handle must be declared in the extension's Info.plist. + +class IntentHandler: INExtension, INGetRideStatusIntentHandling { + + // MARK: - INGetRideStatusIntentHandling + + /* + There are two kinds of ride status updates: + - One shot + - Live observation + + For one shot updates, -handleGetRideStatus will be called. This can be called at any time. + + For live observation, -startSendingUpdates will be called and an observer object specified. + + NOTE: You are allowed only one current ride at a time. + */ + + func handle(getRideStatus intent: INGetRideStatusIntent, completion: @escaping (INGetRideStatusIntentResponse) -> Void) { + + /* + The intent has no data on it since this method is only asking for a current ride's status. + + Query your service to see if there is a current ride in progress. If there is, return the intent response with the .success code and a valid, fully detailed rideStatus object. + A missing or blank ride option name will cause an error. + + Again, the response codes are similar to list ride / request ride and follow the same semantics. + + Sending a ride status with a completed phase is valid here, but be sure to set the completionStatus. + + If a ride is in the completed state for outstandingPaymentAmount for example, keep sending that status. + + Maps will automatically start to ignore a completed state after a set interval. + + When the user goes to get another ride, however, you can either allow that, or ask them to complete the previous ride by specifying a response code of .failureRequiringAppLaunchPreviousRideNeedsCompletion. + */ + } + + func startSendingUpdates(forGetRideStatus intent: INGetRideStatusIntent, to observer: INGetRideStatusIntentResponseObserver) { + + /* + It is time for you to start sending updates to the observer. The best thing to do here is to set up a timer to ping your service or some sort of persistent connection to your service. + + NOTE: It is completely possible for -startSendingUpdates to be called, and your extension terminated before -stopSendingUpdates is called. In this case, if your extension is restarted, -startSendingUpdates may be called again if you specify in -getRideStatus that there is a current ride. + + Store the observer in an ivar and send it the -didUpdate message whenever you have updated information about the current ride. + + Maps recommends spacing updates 1-10 seconds apart. Maps will throttle updates as it sees fit. + */ + + } + + func stopSendingUpdates(forGetRideStatus intent: INGetRideStatusIntent) { + + /* + Stop sending updates and nil out your reference to the observer. Probably stop your timer or close your connection to your service. + */ + + } +} + diff --git a/IntentHandling/Projects/RideMaps/ListRideOptionsIntentExtension/Info.plist b/IntentHandling/Projects/RideMaps/ListRideOptionsIntentExtension/Info.plist new file mode 100644 index 00000000..4a1a5c82 --- /dev/null +++ b/IntentHandling/Projects/RideMaps/ListRideOptionsIntentExtension/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ListRideOptionsIntentExtension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsSupported + + INListRideOptionsIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + + diff --git a/IntentHandling/Projects/RideMaps/ListRideOptionsIntentExtension/IntentHandler.swift b/IntentHandling/Projects/RideMaps/ListRideOptionsIntentExtension/IntentHandler.swift new file mode 100644 index 00000000..13892d72 --- /dev/null +++ b/IntentHandling/Projects/RideMaps/ListRideOptionsIntentExtension/IntentHandler.swift @@ -0,0 +1,156 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An object that implements the `INListRideOptionsIntentHandling` protocol to handle ridesharing tasks. + */ + +import Intents + +// The intents whose interactions you wish to handle must be declared in the extension's Info.plist. + +class IntentHandler: INExtension, INListRideOptionsIntentHandling { + + override func handler(for intent: INIntent) -> Any { + // This is the default implementation. If you want different objects to handle different intents, + // you can override this and return the handler you want for that particular intent. + + return self + } + + // MARK: - INListRideOptionsIntentHandling + + // For list ride options, we don't need to implement -confirmListRideOptions since it isn't used in the Maps context. + + func handle(listRideOptions intent: INListRideOptionsIntent, completion: @escaping (INListRideOptionsIntentResponse) -> Void) { + + /* + We need to do the following here: + + 1. Get the pickup and dropoff locations from the intent. + 2. Send these locations to your service. + 3. Get back a list of different ride options your service provides between these two points. + 4. Create an intent response with an appropriate response code and data. + + */ + + /* + Some helpful tips on INListRideOptionsIntentResponseCodes: + + - case unspecified + - Don't use this, it is considered a failure. + - case ready + - Don't use this, it is considered a failure. + - case inProgress + - Don't use this, it is considered a failure. + - case success + - Use this for when there are valid ride options you wish to display. + - case failure + - Use this when there is a failure. + - case failureRequiringAppLaunch + - Use this when there is a failure which can be recovered from, but only by switching to your parent app. + - case failureRequiringAppLaunchMustVerifyCredentials + - Use this when a user is not logged in or signed up for your service in your parent app. + - case failureRequiringAppLaunchNoServiceInArea + - Use this when you definitively don't offer service in the general area the user requested ride options in. + - case failureRequiringAppLaunchServiceTemporarilyUnavailable + - Use this when you temporarily don't offer service in the general area, for example if there are no vehicles available. + - case failureRequiringAppLaunchPreviousRideNeedsCompletion + - Use this when there was a previous ride in your service that the user needs to complete in your parent app. For example if the user still needs to pay for the previous ride. If there is a previous ride that needs completion, but you would still like to allow the user to book another ride, return .success. + + For the cases requiringAppLaunch, make sure to include a relevant user activity. This activity will be continued in your parent app if the user chooses to take action on the failure message in Maps. See NSUserActivity documentation here: https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSUserActivity_Class/ . + + */ + let response = INListRideOptionsIntentResponse(code: .success, userActivity: nil) + + + /* + Ride options + + Specify a ride option with the INRideOption class. You will have a chance to update the ride option object after a user books a ride, during the ride, and at the end of a ride. + + IMPORTANT: When you get a HandleRequestRideIntent below, you will not be handed back the whole ride option. Instead you will be give the following data: + - pickup location (CLPlacemark) + - dropoff location (CLPlacemark) + - ride option name (INSpeakableString) + - party size (nil if you provided none) + - payment method (INPaymentMethod) + + Therefore, you *must* make the ride option name unique so you can match the listed option with the requested option. + + */ + let smallCarOption = INRideOption(name: "Small Car", estimatedPickupDate: Date(timeIntervalSinceNow: 3 * 60)) // You must provide a name and estimated pickup date. + + smallCarOption.priceRange = INPriceRange(firstPrice: NSDecimalNumber(string: "5.60") , secondPrice: NSDecimalNumber(string: "10.78"), currencyCode: "USD") // There are different ways to define a price range and depending on which initializer you use, Maps may change the formatting of the price. + + smallCarOption.disclaimerMessage = "This is a very small car, tall passengers may not fit." // A message that is specific to this ride option. + + /* + Party size options + + If you offer different prices for different party sizes for this option, you may use this property to enumerate them. If you do not, leave this nil. + + You may have different price ranges for each party size option. If you leave the price range for the party size option nil, it will default to the ride option's price range. + + The size description is user visible text. + */ + smallCarOption.availablePartySizeOptions = [ + INRidePartySizeOption(partySizeRange: NSRange(location: 0, length: 1), sizeDescription: "One person", priceRange: nil), + INRidePartySizeOption(partySizeRange: NSRange(location: 0, length: 2), sizeDescription: "Two people", priceRange: INPriceRange(firstPrice: NSDecimalNumber(string: "6.60") , secondPrice: NSDecimalNumber(string: "11.78"), currencyCode: "USD")) + ] + smallCarOption.availablePartySizeOptionsSelectionPrompt = "Choose a party size" + + /* + Special pricing + + The special pricing string is a user facing string that describes details about the special pricing. + + The badge image is shown beside the string as a visual indicator of the special pricing. + + Setting either of these properties will result in Maps alerting the user that there is special pricing in effect. + + */ + smallCarOption.specialPricing = "High demand. 50% extra will be added to your fare." + smallCarOption.specialPricingBadgeImage = INImage(named: "specialPricingBadge") + + /* + Fare line items + + These help the user understand the breakdown of the fare for the ride option. You'll have a chance to give updated fare line items after a user books a ride, during the ride, and at the end of the ride. + */ + let base = INRideFareLineItem(title: "Base fare", price: NSDecimalNumber(string: "4.76"), currencyCode: "USD" )! + let airport = INRideFareLineItem(title: "Airport fee", price: NSDecimalNumber(string: "3.00"), currencyCode: "USD" )! + let discount = INRideFareLineItem(title: "Promo code (3fs8sdx)", price: NSDecimalNumber(string: "-4.00"), currencyCode: "USD" )! + smallCarOption.fareLineItems = [ base, airport, discount ] + + /* + User activity for booking in application + + ONLY set this if this particular ride option is not able to be booked outside of your parent application. For example if the Intents API does not support a particular feature of the ride option. + + This will cause Maps to continue the activity in the parent app rather than booking the whole ride inside Maps. + */ + smallCarOption.userActivityForBookingInApplication = NSUserActivity(activityType: "bookInApp"); + + response.rideOptions = [ smallCarOption ] + + /* + Payment methods + + Specify the payment methods that a user has registered with your service. You will be handed back the selected payment method in -handleRequestRideIntent:completion:. + */ + let paymentMethod = INPaymentMethod(type: .credit, name: "Visa Platinum", identificationHint: "•••• •••• •••• 1234", icon: INImage(named: "creditCardImage")) + let applePay = INPaymentMethod.applePay() // If you support Pay and the user has an Pay payment method set in your parent app + response.paymentMethods = [ paymentMethod, applePay ] + + + /* + Expiration date + + The date at which these ride options expire. When this date is reached, Maps may call -handleListRideOptions:completion: again. + */ + response.expirationDate = Date(timeIntervalSinceNow: 5 * 60) + } +} + diff --git a/IntentHandling/Projects/RideMaps/README.md b/IntentHandling/Projects/RideMaps/README.md new file mode 100644 index 00000000..f7deb53d --- /dev/null +++ b/IntentHandling/Projects/RideMaps/README.md @@ -0,0 +1,15 @@ +#RideMaps +##Overview + +The ridesharing domain consists of 3 individual intent handling protocols that you must conform to: +1. `INListRideOptionsIntentHandling` +2. `INRequestRideIntentHandling` +3. `INGetRideStatusIntentHandling` + +Each of the handling protocols can have resolve..., confirm..., and handle... methods. For the Maps context, we never need to implement the resolve... methods, and only need to implement the confirm method for `INRequestRideIntentHandling`. + +In this project, each protocol in the domain has been separated into its own extension. This way you can isolate your code for each protocol in different processes. + +This project includes copious comments to help you implement the ridesharing domain for intents. It does not include implementation, but will have hints about where to perform certain implementation tasks. + +*The intents you wish to handle in an extension must be declared in the extension's Info.plist.* diff --git a/IntentHandling/Projects/RideMaps/RequestRideIntentExtension/Info.plist b/IntentHandling/Projects/RideMaps/RequestRideIntentExtension/Info.plist new file mode 100644 index 00000000..0aab1e7e --- /dev/null +++ b/IntentHandling/Projects/RideMaps/RequestRideIntentExtension/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + RequestRideIntentExtension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsSupported + + INRequestRideIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + + diff --git a/IntentHandling/Projects/RideMaps/RequestRideIntentExtension/IntentHandler.swift b/IntentHandling/Projects/RideMaps/RequestRideIntentExtension/IntentHandler.swift new file mode 100644 index 00000000..614ec287 --- /dev/null +++ b/IntentHandling/Projects/RideMaps/RequestRideIntentExtension/IntentHandler.swift @@ -0,0 +1,87 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An object that implements the `INRequestRideIntentHandling` protocol to handle ridesharing tasks. + */ + +import Intents + +// The intents whose interactions you wish to handle must be declared in the extension's Info.plist. + +class IntentHandler: INExtension, INRequestRideIntentHandling { + + // MARK: - INRequestRideIntentHandling + + + func confirm(requestRide intent: INRequestRideIntent, completion: @escaping (INRequestRideIntentResponse) -> Void) { + + /* + Maps uses this method to update the pickup location for the ride. + + Confirm with your service whether this pickup -> destination route and payment info is valid. + + Use the rideOptionName on the intent to match it to a ride option given in listRideOptions. + + In addition, create and update any details on the ride option like fare, or eta for this updated pickup location. + + */ + + let rideOption = INRideOption(name: "Small car", estimatedPickupDate: Date(timeIntervalSinceNow: 5 * 60)) + + let rideStatus = INRideStatus() + rideStatus.rideOption = rideOption + rideStatus.estimatedPickupDate = Date(timeIntervalSinceNow: 5 * 60) + rideStatus.rideIdentifier = NSUUID().uuidString // This ride identifier must match the one in handleRequestRide and getRideStatus + + /* + Pickup / dropoff locations + + You can specify different pickup / dropoff locations from the ones included in the intent. + You may change both the coordinate and the name of the placemark. + + Maps will display the name that you specify, and will update the pickup location on the map to the new cooridinates. + Use this functionality to specify dedicated pickup spots or easier to spot POIs. + */ + + // set pickup and dropoff locations + + let response = INRequestRideIntentResponse(code: .success, userActivity: nil) + response.rideStatus = rideStatus + + completion(response) + } + + func handle(requestRide intent: INRequestRideIntent, completion: @escaping (INRequestRideIntentResponse) -> Void) { + + /* + Handle the actual request to book a ride here. Grab relevant information from the intent... + + - pickup location + - dropoff location + - ride option name + - party size + - payment method + + ...and make a call to your service. + + You should return a response from this method as soon as your service has acknowledged the request. + + Notice how the response codes are the same as list ride options? Use the same semantics as defined above. + + You must return a non-nil ride status with a valid ride option and ride option name, otherwise there will be an error on the Maps side. + + Most likely you will want the ride phase to be .received at this point. An .unknown ride phase here will be an error. + + IMPORTANT: Include as much information as possible on the ride status including the ride option, and estimated dates. A missing or blank ride option name will cause an error. + + The ride identifier will be consistent across this particular ride session. + + You must set the userActivityForCancelingInApplication to allow canceling of your ride. When a user selects cancel from inside Maps, this activity will be continued in your parent app to complete the cancelation. + + Also, additionalActionActivities will show up as actions the user can take which require completion inside your parent app. You can use this for things like splitting the fare, sharing ETA, contacting customer support, etc. + */ + } +} + diff --git a/IntentHandling/Projects/RideMaps/RideIntentUI/Base.lproj/MainInterface.storyboard b/IntentHandling/Projects/RideMaps/RideIntentUI/Base.lproj/MainInterface.storyboard new file mode 100644 index 00000000..831ee403 --- /dev/null +++ b/IntentHandling/Projects/RideMaps/RideIntentUI/Base.lproj/MainInterface.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IntentHandling/Projects/RideMaps/RideIntentUI/Info.plist b/IntentHandling/Projects/RideMaps/RideIntentUI/Info.plist new file mode 100644 index 00000000..ae41021e --- /dev/null +++ b/IntentHandling/Projects/RideMaps/RideIntentUI/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + RideIntentUI + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + IntentsSupported + + INListRideOptionsIntent + INRequestRideIntent + INGetRideStatusIntent + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.intents-ui-service + + + diff --git a/IntentHandling/Projects/RideMaps/RideIntentUI/IntentViewController.swift b/IntentHandling/Projects/RideMaps/RideIntentUI/IntentViewController.swift new file mode 100644 index 00000000..e9f2e2d9 --- /dev/null +++ b/IntentHandling/Projects/RideMaps/RideIntentUI/IntentViewController.swift @@ -0,0 +1,44 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An object that implements the `INRidesharingDomainHandling` protocols to handle ridesharing tasks. + */ + +import IntentsUI + +// The intents whose interactions you wish to handle must be declared in the extension's Info.plist. + +class IntentViewController: UIViewController, INUIHostedViewControlling { + + // MARK: - INUIHostedViewControlling + + // Prepare your view controller for the interaction to handle. + func configure(with interaction: INInteraction!, context: INUIHostedViewContext, completion: ((CGSize) -> Void)!) { + + /* + -configure can be called at any time. Most likely it could be called each time you send an update in get ride status live observation. + + Your non-ui extension and ui extension run in separate processes, so it is up to you how to synchronize data between the two. + + It is recommended that you configure the view controller based only on the information in the interaction object to minimize data mismatch between the two extensions. + + The interaction object contains both an intent and response. Use the information on both of these objects to correctly configure the view controller. + + IMPORTANT: Any arbitrary data can be stored in the response's user activity's user info dictionary when you send a get ride status response back to Maps. It will be handed back to you here. + + The context will let you know whether this view controller will be shown inside Maps or Siri. If it is shown inside Maps, it is not necessary nor recommended to show an MKMapView. + */ + + if let completion = completion { + completion(self.desiredSize) + } + } + + var desiredSize: CGSize { + // NOTE: Maps does not respect desired size. + return self.extensionContext!.hostedViewMaximumAllowedSize + } + +} diff --git a/IntentHandling/Projects/RideMaps/RideMaps.xcodeproj/project.pbxproj b/IntentHandling/Projects/RideMaps/RideMaps.xcodeproj/project.pbxproj new file mode 100644 index 00000000..05a166dd --- /dev/null +++ b/IntentHandling/Projects/RideMaps/RideMaps.xcodeproj/project.pbxproj @@ -0,0 +1,786 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + DC6130DA1D53BAD800601BBE /* IntentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC6130D91D53BAD800601BBE /* IntentsUI.framework */; }; + DC6130DD1D53BAD800601BBE /* IntentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6130DC1D53BAD800601BBE /* IntentViewController.swift */; }; + DC6130E01D53BAD800601BBE /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DC6130DE1D53BAD800601BBE /* MainInterface.storyboard */; }; + DC6130E41D53BAD800601BBE /* RideIntentUI.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DC6130D71D53BAD800601BBE /* RideIntentUI.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DC6130E71D53BAD800601BBE /* ListRideOptionsIntentExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DC6130CE1D53BAD800601BBE /* ListRideOptionsIntentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DC7ACBDE1D5E474B006D0EC0 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7ACBDC1D5E474B006D0EC0 /* IntentHandler.swift */; }; + DC7ACBE91D5E481B006D0EC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC7ACBE21D5E481B006D0EC0 /* AppDelegate.swift */; }; + DC7ACBEA1D5E481B006D0EC0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC7ACBE31D5E481B006D0EC0 /* Assets.xcassets */; }; + DC7ACBEB1D5E481B006D0EC0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DC7ACBE41D5E481B006D0EC0 /* LaunchScreen.storyboard */; }; + DC7ACBEC1D5E481B006D0EC0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DC7ACBE61D5E481B006D0EC0 /* Main.storyboard */; }; + DC7ACBEF1D5E49DA006D0EC0 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC7ACBEE1D5E49DA006D0EC0 /* Intents.framework */; }; + DC7ACBF01D5E49DF006D0EC0 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC7ACBEE1D5E49DA006D0EC0 /* Intents.framework */; }; + DC7ACBF11D5E49E4006D0EC0 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC7ACBEE1D5E49DA006D0EC0 /* Intents.framework */; }; + DCE9E60B1D5E428200C214BD /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE9E60A1D5E428200C214BD /* IntentHandler.swift */; }; + DCE9E60F1D5E428200C214BD /* RequestRideIntentExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DCE9E6081D5E428200C214BD /* RequestRideIntentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DCE9E61A1D5E467C00C214BD /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE9E6191D5E467C00C214BD /* IntentHandler.swift */; }; + DCE9E61E1D5E467C00C214BD /* GetRideStatusIntentExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DCE9E6171D5E467C00C214BD /* GetRideStatusIntentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + DC6130E21D53BAD800601BBE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DC6130AD1D53BAA700601BBE /* Project object */; + proxyType = 1; + remoteGlobalIDString = DC6130D61D53BAD800601BBE; + remoteInfo = RideIntentUI; + }; + DC6130E51D53BAD800601BBE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DC6130AD1D53BAA700601BBE /* Project object */; + proxyType = 1; + remoteGlobalIDString = DC6130CD1D53BAD800601BBE; + remoteInfo = RideIntent; + }; + DCE9E60D1D5E428200C214BD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DC6130AD1D53BAA700601BBE /* Project object */; + proxyType = 1; + remoteGlobalIDString = DCE9E6071D5E428200C214BD; + remoteInfo = RequestRideIntentExtension; + }; + DCE9E61C1D5E467C00C214BD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DC6130AD1D53BAA700601BBE /* Project object */; + proxyType = 1; + remoteGlobalIDString = DCE9E6161D5E467C00C214BD; + remoteInfo = GetRideStatusIntentExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + DC6130EE1D53BAD800601BBE /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + DC6130E71D53BAD800601BBE /* ListRideOptionsIntentExtension.appex in Embed App Extensions */, + DCE9E61E1D5E467C00C214BD /* GetRideStatusIntentExtension.appex in Embed App Extensions */, + DCE9E60F1D5E428200C214BD /* RequestRideIntentExtension.appex in Embed App Extensions */, + DC6130E41D53BAD800601BBE /* RideIntentUI.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + B574C8D81D7B81D700F68E77 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + DC6130B51D53BAA700601BBE /* RideMaps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RideMaps.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DC6130CE1D53BAD800601BBE /* ListRideOptionsIntentExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ListRideOptionsIntentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + DC6130D71D53BAD800601BBE /* RideIntentUI.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = RideIntentUI.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + DC6130D91D53BAD800601BBE /* IntentsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntentsUI.framework; path = System/Library/Frameworks/IntentsUI.framework; sourceTree = SDKROOT; }; + DC6130DC1D53BAD800601BBE /* IntentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentViewController.swift; sourceTree = ""; }; + DC6130DF1D53BAD800601BBE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + DC6130E11D53BAD800601BBE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DC7ACBDB1D5E474B006D0EC0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ListRideOptionsIntentExtension/Info.plist; sourceTree = SOURCE_ROOT; }; + DC7ACBDC1D5E474B006D0EC0 /* IntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = IntentHandler.swift; path = ListRideOptionsIntentExtension/IntentHandler.swift; sourceTree = SOURCE_ROOT; }; + DC7ACBE21D5E481B006D0EC0 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + DC7ACBE31D5E481B006D0EC0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + DC7ACBE51D5E481B006D0EC0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + DC7ACBE71D5E481B006D0EC0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + DC7ACBE81D5E481B006D0EC0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DC7ACBEE1D5E49DA006D0EC0 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; + DCE9E6081D5E428200C214BD /* RequestRideIntentExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = RequestRideIntentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + DCE9E60A1D5E428200C214BD /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + DCE9E60C1D5E428200C214BD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DCE9E6171D5E467C00C214BD /* GetRideStatusIntentExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = GetRideStatusIntentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + DCE9E6191D5E467C00C214BD /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + DCE9E61B1D5E467C00C214BD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + DC6130B21D53BAA700601BBE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC6130CB1D53BAD800601BBE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DC7ACBEF1D5E49DA006D0EC0 /* Intents.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC6130D41D53BAD800601BBE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DC6130DA1D53BAD800601BBE /* IntentsUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DCE9E6051D5E428200C214BD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DC7ACBF01D5E49DF006D0EC0 /* Intents.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DCE9E6141D5E467C00C214BD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DC7ACBF11D5E49E4006D0EC0 /* Intents.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + DC6130AC1D53BAA700601BBE = { + isa = PBXGroup; + children = ( + B574C8D81D7B81D700F68E77 /* README.md */, + DC7ACBE11D5E481B006D0EC0 /* RideMaps */, + DC6130CF1D53BAD800601BBE /* ListRideOptionsIntentExtension */, + DCE9E6091D5E428200C214BD /* RequestRideIntentExtension */, + DCE9E6181D5E467C00C214BD /* GetRideStatusIntentExtension */, + DC6130DB1D53BAD800601BBE /* RideIntentUI */, + DC6130D81D53BAD800601BBE /* Frameworks */, + DC6130B61D53BAA700601BBE /* Products */, + ); + sourceTree = ""; + }; + DC6130B61D53BAA700601BBE /* Products */ = { + isa = PBXGroup; + children = ( + DC6130B51D53BAA700601BBE /* RideMaps.app */, + DC6130CE1D53BAD800601BBE /* ListRideOptionsIntentExtension.appex */, + DC6130D71D53BAD800601BBE /* RideIntentUI.appex */, + DCE9E6081D5E428200C214BD /* RequestRideIntentExtension.appex */, + DCE9E6171D5E467C00C214BD /* GetRideStatusIntentExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; + DC6130CF1D53BAD800601BBE /* ListRideOptionsIntentExtension */ = { + isa = PBXGroup; + children = ( + DC7ACBDC1D5E474B006D0EC0 /* IntentHandler.swift */, + DC7ACBDB1D5E474B006D0EC0 /* Info.plist */, + ); + name = ListRideOptionsIntentExtension; + path = RideIntent; + sourceTree = ""; + }; + DC6130D81D53BAD800601BBE /* Frameworks */ = { + isa = PBXGroup; + children = ( + DC7ACBEE1D5E49DA006D0EC0 /* Intents.framework */, + DC6130D91D53BAD800601BBE /* IntentsUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + DC6130DB1D53BAD800601BBE /* RideIntentUI */ = { + isa = PBXGroup; + children = ( + DC6130DC1D53BAD800601BBE /* IntentViewController.swift */, + DC6130DE1D53BAD800601BBE /* MainInterface.storyboard */, + DC6130E11D53BAD800601BBE /* Info.plist */, + ); + path = RideIntentUI; + sourceTree = ""; + }; + DC7ACBE11D5E481B006D0EC0 /* RideMaps */ = { + isa = PBXGroup; + children = ( + DC7ACBE21D5E481B006D0EC0 /* AppDelegate.swift */, + DC7ACBE31D5E481B006D0EC0 /* Assets.xcassets */, + DC7ACBE41D5E481B006D0EC0 /* LaunchScreen.storyboard */, + DC7ACBE61D5E481B006D0EC0 /* Main.storyboard */, + DC7ACBE81D5E481B006D0EC0 /* Info.plist */, + ); + path = RideMaps; + sourceTree = ""; + }; + DCE9E6091D5E428200C214BD /* RequestRideIntentExtension */ = { + isa = PBXGroup; + children = ( + DCE9E60A1D5E428200C214BD /* IntentHandler.swift */, + DCE9E60C1D5E428200C214BD /* Info.plist */, + ); + path = RequestRideIntentExtension; + sourceTree = ""; + }; + DCE9E6181D5E467C00C214BD /* GetRideStatusIntentExtension */ = { + isa = PBXGroup; + children = ( + DCE9E6191D5E467C00C214BD /* IntentHandler.swift */, + DCE9E61B1D5E467C00C214BD /* Info.plist */, + ); + path = GetRideStatusIntentExtension; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + DC6130B41D53BAA700601BBE /* RideMaps */ = { + isa = PBXNativeTarget; + buildConfigurationList = DC6130C71D53BAA700601BBE /* Build configuration list for PBXNativeTarget "RideMaps" */; + buildPhases = ( + DC6130B11D53BAA700601BBE /* Sources */, + DC6130B21D53BAA700601BBE /* Frameworks */, + DC6130B31D53BAA700601BBE /* Resources */, + DC6130EE1D53BAD800601BBE /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + DC6130E31D53BAD800601BBE /* PBXTargetDependency */, + DC6130E61D53BAD800601BBE /* PBXTargetDependency */, + DCE9E60E1D5E428200C214BD /* PBXTargetDependency */, + DCE9E61D1D5E467C00C214BD /* PBXTargetDependency */, + ); + name = RideMaps; + productName = RideMaps; + productReference = DC6130B51D53BAA700601BBE /* RideMaps.app */; + productType = "com.apple.product-type.application"; + }; + DC6130CD1D53BAD800601BBE /* ListRideOptionsIntentExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = DC6130EB1D53BAD800601BBE /* Build configuration list for PBXNativeTarget "ListRideOptionsIntentExtension" */; + buildPhases = ( + DC6130CA1D53BAD800601BBE /* Sources */, + DC6130CB1D53BAD800601BBE /* Frameworks */, + DC6130CC1D53BAD800601BBE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ListRideOptionsIntentExtension; + productName = RideIntent; + productReference = DC6130CE1D53BAD800601BBE /* ListRideOptionsIntentExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + DC6130D61D53BAD800601BBE /* RideIntentUI */ = { + isa = PBXNativeTarget; + buildConfigurationList = DC6130E81D53BAD800601BBE /* Build configuration list for PBXNativeTarget "RideIntentUI" */; + buildPhases = ( + DC6130D31D53BAD800601BBE /* Sources */, + DC6130D41D53BAD800601BBE /* Frameworks */, + DC6130D51D53BAD800601BBE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RideIntentUI; + productName = RideIntentUI; + productReference = DC6130D71D53BAD800601BBE /* RideIntentUI.appex */; + productType = "com.apple.product-type.app-extension"; + }; + DCE9E6071D5E428200C214BD /* RequestRideIntentExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = DCE9E6101D5E428200C214BD /* Build configuration list for PBXNativeTarget "RequestRideIntentExtension" */; + buildPhases = ( + DCE9E6041D5E428200C214BD /* Sources */, + DCE9E6051D5E428200C214BD /* Frameworks */, + DCE9E6061D5E428200C214BD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RequestRideIntentExtension; + productName = RequestRideIntentExtension; + productReference = DCE9E6081D5E428200C214BD /* RequestRideIntentExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + DCE9E6161D5E467C00C214BD /* GetRideStatusIntentExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = DCE9E61F1D5E467C00C214BD /* Build configuration list for PBXNativeTarget "GetRideStatusIntentExtension" */; + buildPhases = ( + DCE9E6131D5E467C00C214BD /* Sources */, + DCE9E6141D5E467C00C214BD /* Frameworks */, + DCE9E6151D5E467C00C214BD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GetRideStatusIntentExtension; + productName = GetRideStatusIntentExtension; + productReference = DCE9E6171D5E467C00C214BD /* GetRideStatusIntentExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DC6130AD1D53BAA700601BBE /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Apple, Inc."; + TargetAttributes = { + DC6130B41D53BAA700601BBE = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + DC6130CD1D53BAD800601BBE = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + DC6130D61D53BAD800601BBE = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + DCE9E6071D5E428200C214BD = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + DCE9E6161D5E467C00C214BD = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = DC6130B01D53BAA700601BBE /* Build configuration list for PBXProject "RideMaps" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = DC6130AC1D53BAA700601BBE; + productRefGroup = DC6130B61D53BAA700601BBE /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DC6130B41D53BAA700601BBE /* RideMaps */, + DC6130CD1D53BAD800601BBE /* ListRideOptionsIntentExtension */, + DCE9E6071D5E428200C214BD /* RequestRideIntentExtension */, + DCE9E6161D5E467C00C214BD /* GetRideStatusIntentExtension */, + DC6130D61D53BAD800601BBE /* RideIntentUI */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + DC6130B31D53BAA700601BBE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC7ACBEA1D5E481B006D0EC0 /* Assets.xcassets in Resources */, + DC7ACBEC1D5E481B006D0EC0 /* Main.storyboard in Resources */, + DC7ACBEB1D5E481B006D0EC0 /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC6130CC1D53BAD800601BBE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC6130D51D53BAD800601BBE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC6130E01D53BAD800601BBE /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DCE9E6061D5E428200C214BD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DCE9E6151D5E467C00C214BD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + DC6130B11D53BAA700601BBE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC7ACBE91D5E481B006D0EC0 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC6130CA1D53BAD800601BBE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC7ACBDE1D5E474B006D0EC0 /* IntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC6130D31D53BAD800601BBE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC6130DD1D53BAD800601BBE /* IntentViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DCE9E6041D5E428200C214BD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DCE9E60B1D5E428200C214BD /* IntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DCE9E6131D5E467C00C214BD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DCE9E61A1D5E467C00C214BD /* IntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + DC6130E31D53BAD800601BBE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DC6130D61D53BAD800601BBE /* RideIntentUI */; + targetProxy = DC6130E21D53BAD800601BBE /* PBXContainerItemProxy */; + }; + DC6130E61D53BAD800601BBE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DC6130CD1D53BAD800601BBE /* ListRideOptionsIntentExtension */; + targetProxy = DC6130E51D53BAD800601BBE /* PBXContainerItemProxy */; + }; + DCE9E60E1D5E428200C214BD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DCE9E6071D5E428200C214BD /* RequestRideIntentExtension */; + targetProxy = DCE9E60D1D5E428200C214BD /* PBXContainerItemProxy */; + }; + DCE9E61D1D5E467C00C214BD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DCE9E6161D5E467C00C214BD /* GetRideStatusIntentExtension */; + targetProxy = DCE9E61C1D5E467C00C214BD /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + DC6130DE1D53BAD800601BBE /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + DC6130DF1D53BAD800601BBE /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; + DC7ACBE41D5E481B006D0EC0 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + DC7ACBE51D5E481B006D0EC0 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + DC7ACBE61D5E481B006D0EC0 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + DC7ACBE71D5E481B006D0EC0 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + DC6130C51D53BAA700601BBE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DC6130C61D53BAA700601BBE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + DC6130C81D53BAA700601BBE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = RideMaps/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.RideMaps"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + DC6130C91D53BAA700601BBE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = RideMaps/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.RideMaps"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + DC6130E91D53BAD800601BBE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = RideIntentUI/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.RideMaps.RideIntentUI"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + DC6130EA1D53BAD800601BBE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = RideIntentUI/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.RideMaps.RideIntentUI"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + DC6130EC1D53BAD800601BBE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = ListRideOptionsIntentExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.RideMaps.ListRideOptionsIntentExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + DC6130ED1D53BAD800601BBE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = ListRideOptionsIntentExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.RideMaps.ListRideOptionsIntentExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + DCE9E6111D5E428200C214BD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = RequestRideIntentExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.RideMaps.RequestRideIntentExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + DCE9E6121D5E428200C214BD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = RequestRideIntentExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.RideMaps.RequestRideIntentExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + DCE9E6201D5E467C00C214BD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = GetRideStatusIntentExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.RideMaps.GetRideStatusIntentExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + DCE9E6211D5E467C00C214BD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = GetRideStatusIntentExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.RideMaps.GetRideStatusIntentExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + DC6130B01D53BAA700601BBE /* Build configuration list for PBXProject "RideMaps" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC6130C51D53BAA700601BBE /* Debug */, + DC6130C61D53BAA700601BBE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DC6130C71D53BAA700601BBE /* Build configuration list for PBXNativeTarget "RideMaps" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC6130C81D53BAA700601BBE /* Debug */, + DC6130C91D53BAA700601BBE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DC6130E81D53BAD800601BBE /* Build configuration list for PBXNativeTarget "RideIntentUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC6130E91D53BAD800601BBE /* Debug */, + DC6130EA1D53BAD800601BBE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DC6130EB1D53BAD800601BBE /* Build configuration list for PBXNativeTarget "ListRideOptionsIntentExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC6130EC1D53BAD800601BBE /* Debug */, + DC6130ED1D53BAD800601BBE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DCE9E6101D5E428200C214BD /* Build configuration list for PBXNativeTarget "RequestRideIntentExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DCE9E6111D5E428200C214BD /* Debug */, + DCE9E6121D5E428200C214BD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DCE9E61F1D5E467C00C214BD /* Build configuration list for PBXNativeTarget "GetRideStatusIntentExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DCE9E6201D5E467C00C214BD /* Debug */, + DCE9E6211D5E467C00C214BD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = DC6130AD1D53BAA700601BBE /* Project object */; +} diff --git a/IntentHandling/Projects/RideMaps/RideMaps/AppDelegate.swift b/IntentHandling/Projects/RideMaps/RideMaps/AppDelegate.swift new file mode 100644 index 00000000..231d4e58 --- /dev/null +++ b/IntentHandling/Projects/RideMaps/RideMaps/AppDelegate.swift @@ -0,0 +1,15 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application delegate. + */ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + +} + diff --git a/IntentHandling/Projects/RideMaps/RideMaps/Assets.xcassets/AppIcon.appiconset/Contents.json b/IntentHandling/Projects/RideMaps/RideMaps/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..1d060ed2 --- /dev/null +++ b/IntentHandling/Projects/RideMaps/RideMaps/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,93 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "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" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "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" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IntentHandling/Projects/RideMaps/RideMaps/Base.lproj/LaunchScreen.storyboard b/IntentHandling/Projects/RideMaps/RideMaps/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..fdf3f97d --- /dev/null +++ b/IntentHandling/Projects/RideMaps/RideMaps/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IntentHandling/Projects/RideMaps/RideMaps/Base.lproj/Main.storyboard b/IntentHandling/Projects/RideMaps/RideMaps/Base.lproj/Main.storyboard new file mode 100644 index 00000000..6954157b --- /dev/null +++ b/IntentHandling/Projects/RideMaps/RideMaps/Base.lproj/Main.storyboard @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/IntentHandling/Projects/RideMaps/RideMaps/Info.plist b/IntentHandling/Projects/RideMaps/RideMaps/Info.plist new file mode 100644 index 00000000..d0524738 --- /dev/null +++ b/IntentHandling/Projects/RideMaps/RideMaps/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/IntentHandling/README.md b/IntentHandling/README.md new file mode 100644 index 00000000..c9d4dde5 --- /dev/null +++ b/IntentHandling/README.md @@ -0,0 +1,29 @@ +# IntentHandling: Using the Intents framework to handle custom Siri request + +The IntentHandling sample is a collection of projects showing how to use the Intents framework to handle custom Siri request in different domains. + +## Projects + +### Ascent + +The Ascent sample demonstrates how to handle Siri requests in the workout domain + +### Payments + +The Payments sample demonstrates how to handle Siri requests in the payments domain + +### RideMaps + +The Ride booking sample demonstrates how to handle Apple Maps requests in the ride booking domain + +## Requirements + +### Build + +Xcode 8.0 or later; iOS 10.0 SDK or later + +### Runtime + +iOS 10.0 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/MPSCNNHelloWorld/LICENSE.txt b/MPSCNNHelloWorld/LICENSE.txt new file mode 100644 index 00000000..0e1a0580 --- /dev/null +++ b/MPSCNNHelloWorld/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: MPSCNNHelloWorld: Simple Digit Detection Convolution Neural Networks (CNN) +Version: 1.1 + +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) 2016 Apple Inc. All Rights Reserved. diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld.xcodeproj/project.pbxproj b/MPSCNNHelloWorld/MPSCNNHelloWorld.xcodeproj/project.pbxproj new file mode 100755 index 00000000..48721c01 --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld.xcodeproj/project.pbxproj @@ -0,0 +1,447 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 2E6AB7281D47F79E00048A0B /* atomics.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E6AB7271D47F79E00048A0B /* atomics.m */; }; + 2EE557AE1D41A42B0071A3EC /* t10k-images-idx3-ubyte.data in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557AA1D41A42B0071A3EC /* t10k-images-idx3-ubyte.data */; }; + 2EE557AF1D41A42B0071A3EC /* t10k-labels-idx1-ubyte.data in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557AB1D41A42B0071A3EC /* t10k-labels-idx1-ubyte.data */; }; + 2EE557B11D41A42B0071A3EC /* train-labels-idx1-ubyte.data in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557AD1D41A42B0071A3EC /* train-labels-idx1-ubyte.data */; }; + 2EE557BA1D41A4540071A3EC /* bias_conv1.dat in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557B21D41A4540071A3EC /* bias_conv1.dat */; }; + 2EE557BB1D41A4540071A3EC /* bias_conv2.dat in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557B31D41A4540071A3EC /* bias_conv2.dat */; }; + 2EE557BC1D41A4540071A3EC /* bias_fc1.dat in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557B41D41A4540071A3EC /* bias_fc1.dat */; }; + 2EE557BD1D41A4540071A3EC /* bias_fc2.dat in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557B51D41A4540071A3EC /* bias_fc2.dat */; }; + 2EE557BE1D41A4540071A3EC /* weights_conv1.dat in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557B61D41A4540071A3EC /* weights_conv1.dat */; }; + 2EE557BF1D41A4540071A3EC /* weights_conv2.dat in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557B71D41A4540071A3EC /* weights_conv2.dat */; }; + 2EE557C01D41A4540071A3EC /* weights_fc1.dat in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557B81D41A4540071A3EC /* weights_fc1.dat */; }; + 2EE557C11D41A4540071A3EC /* weights_fc2.dat in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557B91D41A4540071A3EC /* weights_fc2.dat */; }; + 2EE557C41D41A4670071A3EC /* bias_NN.dat in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557C21D41A4670071A3EC /* bias_NN.dat */; }; + 2EE557C51D41A4670071A3EC /* weights_NN.dat in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557C31D41A4670071A3EC /* weights_NN.dat */; }; + 2EE557D11D41A5890071A3EC /* train-images-idx3-ubyte.data in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557AC1D41A42B0071A3EC /* train-images-idx3-ubyte.data */; }; + 2EE557EE1D41A6410071A3EC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE557E71D41A6410071A3EC /* AppDelegate.swift */; }; + 2EE557EF1D41A6410071A3EC /* DrawView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE557E81D41A6410071A3EC /* DrawView.swift */; }; + 2EE557F01D41A6410071A3EC /* GetMNISTData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE557E91D41A6410071A3EC /* GetMNISTData.swift */; }; + 2EE557F11D41A6410071A3EC /* MNISTDeepCNN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE557EA1D41A6410071A3EC /* MNISTDeepCNN.swift */; }; + 2EE557F21D41A6410071A3EC /* MNISTSingleLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE557EB1D41A6410071A3EC /* MNISTSingleLayer.swift */; }; + 2EE557F31D41A6410071A3EC /* SlimMPSCNN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE557EC1D41A6410071A3EC /* SlimMPSCNN.swift */; }; + 2EE557F41D41A6410071A3EC /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EE557ED1D41A6410071A3EC /* ViewController.swift */; }; + 2EE557F71D41A6690071A3EC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557F51D41A6690071A3EC /* Main.storyboard */; }; + 2EE557FA1D41A6770071A3EC /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557F81D41A6770071A3EC /* LaunchScreen.storyboard */; }; + 2EE557FC1D41A6840071A3EC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2EE557FB1D41A6840071A3EC /* Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2E0C35F31CB5B2FE0041D8E3 /* Digit Detector.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Digit Detector.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2E6AB7261D47F5F300048A0B /* atomics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = atomics.h; path = MPSCNNHelloWorld/atomics.h; sourceTree = SOURCE_ROOT; }; + 2E6AB7271D47F79E00048A0B /* atomics.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = atomics.m; path = MPSCNNHelloWorld/atomics.m; sourceTree = SOURCE_ROOT; }; + 2E6AB7291D47F9F300048A0B /* MPSCNNHelloWorld-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "MPSCNNHelloWorld-Bridging-Header.h"; path = "MPSCNNHelloWorld/MPSCNNHelloWorld-Bridging-Header.h"; sourceTree = SOURCE_ROOT; }; + 2ED4411D1D41A21900D89679 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 2EE557AA1D41A42B0071A3EC /* t10k-images-idx3-ubyte.data */ = {isa = PBXFileReference; lastKnownFileType = file; name = "t10k-images-idx3-ubyte.data"; path = "MPSCNNHelloWorld/mnistData/t10k-images-idx3-ubyte.data"; sourceTree = SOURCE_ROOT; }; + 2EE557AB1D41A42B0071A3EC /* t10k-labels-idx1-ubyte.data */ = {isa = PBXFileReference; lastKnownFileType = file; name = "t10k-labels-idx1-ubyte.data"; path = "MPSCNNHelloWorld/mnistData/t10k-labels-idx1-ubyte.data"; sourceTree = SOURCE_ROOT; }; + 2EE557AC1D41A42B0071A3EC /* train-images-idx3-ubyte.data */ = {isa = PBXFileReference; lastKnownFileType = file; name = "train-images-idx3-ubyte.data"; path = "MPSCNNHelloWorld/mnistData/train-images-idx3-ubyte.data"; sourceTree = SOURCE_ROOT; }; + 2EE557AD1D41A42B0071A3EC /* train-labels-idx1-ubyte.data */ = {isa = PBXFileReference; lastKnownFileType = file; name = "train-labels-idx1-ubyte.data"; path = "MPSCNNHelloWorld/mnistData/train-labels-idx1-ubyte.data"; sourceTree = SOURCE_ROOT; }; + 2EE557B21D41A4540071A3EC /* bias_conv1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = bias_conv1.dat; path = MPSCNNHelloWorld/deep_weights/binaries/bias_conv1.dat; sourceTree = SOURCE_ROOT; }; + 2EE557B31D41A4540071A3EC /* bias_conv2.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = bias_conv2.dat; path = MPSCNNHelloWorld/deep_weights/binaries/bias_conv2.dat; sourceTree = SOURCE_ROOT; }; + 2EE557B41D41A4540071A3EC /* bias_fc1.dat */ = {isa = PBXFileReference; lastKnownFileType = file; name = bias_fc1.dat; path = MPSCNNHelloWorld/deep_weights/binaries/bias_fc1.dat; sourceTree = SOURCE_ROOT; }; + 2EE557B51D41A4540071A3EC /* bias_fc2.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = bias_fc2.dat; path = MPSCNNHelloWorld/deep_weights/binaries/bias_fc2.dat; sourceTree = SOURCE_ROOT; }; + 2EE557B61D41A4540071A3EC /* weights_conv1.dat */ = {isa = PBXFileReference; lastKnownFileType = file; name = weights_conv1.dat; path = MPSCNNHelloWorld/deep_weights/binaries/weights_conv1.dat; sourceTree = SOURCE_ROOT; }; + 2EE557B71D41A4540071A3EC /* weights_conv2.dat */ = {isa = PBXFileReference; lastKnownFileType = file; name = weights_conv2.dat; path = MPSCNNHelloWorld/deep_weights/binaries/weights_conv2.dat; sourceTree = SOURCE_ROOT; }; + 2EE557B81D41A4540071A3EC /* weights_fc1.dat */ = {isa = PBXFileReference; lastKnownFileType = file; name = weights_fc1.dat; path = MPSCNNHelloWorld/deep_weights/binaries/weights_fc1.dat; sourceTree = SOURCE_ROOT; }; + 2EE557B91D41A4540071A3EC /* weights_fc2.dat */ = {isa = PBXFileReference; lastKnownFileType = file; name = weights_fc2.dat; path = MPSCNNHelloWorld/deep_weights/binaries/weights_fc2.dat; sourceTree = SOURCE_ROOT; }; + 2EE557C21D41A4670071A3EC /* bias_NN.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = bias_NN.dat; path = MPSCNNHelloWorld/single_layer_weights/bias_NN.dat; sourceTree = SOURCE_ROOT; }; + 2EE557C31D41A4670071A3EC /* weights_NN.dat */ = {isa = PBXFileReference; lastKnownFileType = file; name = weights_NN.dat; path = MPSCNNHelloWorld/single_layer_weights/weights_NN.dat; sourceTree = SOURCE_ROOT; }; + 2EE557E71D41A6410071A3EC /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = MPSCNNHelloWorld/AppDelegate.swift; sourceTree = SOURCE_ROOT; }; + 2EE557E81D41A6410071A3EC /* DrawView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DrawView.swift; path = MPSCNNHelloWorld/DrawView.swift; sourceTree = SOURCE_ROOT; }; + 2EE557E91D41A6410071A3EC /* GetMNISTData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GetMNISTData.swift; path = MPSCNNHelloWorld/GetMNISTData.swift; sourceTree = SOURCE_ROOT; }; + 2EE557EA1D41A6410071A3EC /* MNISTDeepCNN.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MNISTDeepCNN.swift; path = MPSCNNHelloWorld/MNISTDeepCNN.swift; sourceTree = SOURCE_ROOT; }; + 2EE557EB1D41A6410071A3EC /* MNISTSingleLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MNISTSingleLayer.swift; path = MPSCNNHelloWorld/MNISTSingleLayer.swift; sourceTree = SOURCE_ROOT; }; + 2EE557EC1D41A6410071A3EC /* SlimMPSCNN.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlimMPSCNN.swift; path = MPSCNNHelloWorld/SlimMPSCNN.swift; sourceTree = SOURCE_ROOT; }; + 2EE557ED1D41A6410071A3EC /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ViewController.swift; path = MPSCNNHelloWorld/ViewController.swift; sourceTree = SOURCE_ROOT; }; + 2EE557F61D41A6690071A3EC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = MPSCNNHelloWorld/Base.lproj/Main.storyboard; sourceTree = SOURCE_ROOT; }; + 2EE557F91D41A6770071A3EC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = MPSCNNHelloWorld/Base.lproj/LaunchScreen.storyboard; sourceTree = SOURCE_ROOT; }; + 2EE557FB1D41A6840071A3EC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = MPSCNNHelloWorld/Assets.xcassets; sourceTree = SOURCE_ROOT; }; + 2EE557FD1D41A6980071A3EC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = MPSCNNHelloWorld/Info.plist; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2E0C35F01CB5B2FE0041D8E3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2E0C35EA1CB5B2FE0041D8E3 = { + isa = PBXGroup; + children = ( + 2ED4411D1D41A21900D89679 /* README.md */, + 2E0C35F51CB5B2FE0041D8E3 /* MPSCNNHelloWorld */, + 2E0C35F41CB5B2FE0041D8E3 /* Products */, + ); + sourceTree = ""; + }; + 2E0C35F41CB5B2FE0041D8E3 /* Products */ = { + isa = PBXGroup; + children = ( + 2E0C35F31CB5B2FE0041D8E3 /* Digit Detector.app */, + ); + name = Products; + sourceTree = ""; + }; + 2E0C35F51CB5B2FE0041D8E3 /* MPSCNNHelloWorld */ = { + isa = PBXGroup; + children = ( + 2EE557ED1D41A6410071A3EC /* ViewController.swift */, + 2EE557E91D41A6410071A3EC /* GetMNISTData.swift */, + 2EE557EB1D41A6410071A3EC /* MNISTSingleLayer.swift */, + 2EE557EA1D41A6410071A3EC /* MNISTDeepCNN.swift */, + 2EE557EC1D41A6410071A3EC /* SlimMPSCNN.swift */, + 2EE557E81D41A6410071A3EC /* DrawView.swift */, + 2E6AB7291D47F9F300048A0B /* MPSCNNHelloWorld-Bridging-Header.h */, + 2E6AB7261D47F5F300048A0B /* atomics.h */, + 2E6AB7271D47F79E00048A0B /* atomics.m */, + 2EE557F51D41A6690071A3EC /* Main.storyboard */, + 2E684F051CDD596900307CBC /* mnistData */, + 2EAC52D71CDBC97700AB5026 /* Deep Model */, + 2EAC52E81CDBD63F00AB5026 /* Basic Model */, + 2E16E8241CBD6DAF008CF29A /* SupportingFiles */, + ); + name = MPSCNNHelloWorld; + path = MNIST; + sourceTree = ""; + }; + 2E16E8241CBD6DAF008CF29A /* SupportingFiles */ = { + isa = PBXGroup; + children = ( + 2EE557E71D41A6410071A3EC /* AppDelegate.swift */, + 2EE557FB1D41A6840071A3EC /* Assets.xcassets */, + 2EE557F81D41A6770071A3EC /* LaunchScreen.storyboard */, + 2EE557FD1D41A6980071A3EC /* Info.plist */, + ); + name = SupportingFiles; + sourceTree = ""; + }; + 2E684F051CDD596900307CBC /* mnistData */ = { + isa = PBXGroup; + children = ( + 2EE557AA1D41A42B0071A3EC /* t10k-images-idx3-ubyte.data */, + 2EE557AB1D41A42B0071A3EC /* t10k-labels-idx1-ubyte.data */, + 2EE557AC1D41A42B0071A3EC /* train-images-idx3-ubyte.data */, + 2EE557AD1D41A42B0071A3EC /* train-labels-idx1-ubyte.data */, + ); + name = mnistData; + sourceTree = ""; + }; + 2EAC52D71CDBC97700AB5026 /* Deep Model */ = { + isa = PBXGroup; + children = ( + 2EE557B21D41A4540071A3EC /* bias_conv1.dat */, + 2EE557B31D41A4540071A3EC /* bias_conv2.dat */, + 2EE557B41D41A4540071A3EC /* bias_fc1.dat */, + 2EE557B51D41A4540071A3EC /* bias_fc2.dat */, + 2EE557B61D41A4540071A3EC /* weights_conv1.dat */, + 2EE557B71D41A4540071A3EC /* weights_conv2.dat */, + 2EE557B81D41A4540071A3EC /* weights_fc1.dat */, + 2EE557B91D41A4540071A3EC /* weights_fc2.dat */, + ); + name = "Deep Model"; + sourceTree = ""; + }; + 2EAC52E81CDBD63F00AB5026 /* Basic Model */ = { + isa = PBXGroup; + children = ( + 2EE557C21D41A4670071A3EC /* bias_NN.dat */, + 2EE557C31D41A4670071A3EC /* weights_NN.dat */, + ); + name = "Basic Model"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2E0C35F21CB5B2FE0041D8E3 /* MPSCNNHelloWorld */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2E0C361B1CB5B2FF0041D8E3 /* Build configuration list for PBXNativeTarget "MPSCNNHelloWorld" */; + buildPhases = ( + 2E0C35EF1CB5B2FE0041D8E3 /* Sources */, + 2E0C35F01CB5B2FE0041D8E3 /* Frameworks */, + 2E0C35F11CB5B2FE0041D8E3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MPSCNNHelloWorld; + productName = MNIST; + productReference = 2E0C35F31CB5B2FE0041D8E3 /* Digit Detector.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2E0C35EB1CB5B2FE0041D8E3 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Dhruv Saksena"; + TargetAttributes = { + 2E0C35F21CB5B2FE0041D8E3 = { + CreatedOnToolsVersion = 8.0; + DevelopmentTeamName = "Apple Inc. - Core OS Plus Others"; + LastSwiftMigration = 0800; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 2E0C35EE1CB5B2FE0041D8E3 /* Build configuration list for PBXProject "MPSCNNHelloWorld" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2E0C35EA1CB5B2FE0041D8E3; + productRefGroup = 2E0C35F41CB5B2FE0041D8E3 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2E0C35F21CB5B2FE0041D8E3 /* MPSCNNHelloWorld */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2E0C35F11CB5B2FE0041D8E3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2EE557D11D41A5890071A3EC /* train-images-idx3-ubyte.data in Resources */, + 2EE557BA1D41A4540071A3EC /* bias_conv1.dat in Resources */, + 2EE557C11D41A4540071A3EC /* weights_fc2.dat in Resources */, + 2EE557AF1D41A42B0071A3EC /* t10k-labels-idx1-ubyte.data in Resources */, + 2EE557B11D41A42B0071A3EC /* train-labels-idx1-ubyte.data in Resources */, + 2EE557AE1D41A42B0071A3EC /* t10k-images-idx3-ubyte.data in Resources */, + 2EE557FC1D41A6840071A3EC /* Assets.xcassets in Resources */, + 2EE557F71D41A6690071A3EC /* Main.storyboard in Resources */, + 2EE557C01D41A4540071A3EC /* weights_fc1.dat in Resources */, + 2EE557BB1D41A4540071A3EC /* bias_conv2.dat in Resources */, + 2EE557FA1D41A6770071A3EC /* LaunchScreen.storyboard in Resources */, + 2EE557C51D41A4670071A3EC /* weights_NN.dat in Resources */, + 2EE557BD1D41A4540071A3EC /* bias_fc2.dat in Resources */, + 2EE557C41D41A4670071A3EC /* bias_NN.dat in Resources */, + 2EE557BF1D41A4540071A3EC /* weights_conv2.dat in Resources */, + 2EE557BC1D41A4540071A3EC /* bias_fc1.dat in Resources */, + 2EE557BE1D41A4540071A3EC /* weights_conv1.dat in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2E0C35EF1CB5B2FE0041D8E3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2EE557EF1D41A6410071A3EC /* DrawView.swift in Sources */, + 2EE557F21D41A6410071A3EC /* MNISTSingleLayer.swift in Sources */, + 2EE557F11D41A6410071A3EC /* MNISTDeepCNN.swift in Sources */, + 2EE557F01D41A6410071A3EC /* GetMNISTData.swift in Sources */, + 2E6AB7281D47F79E00048A0B /* atomics.m in Sources */, + 2EE557F41D41A6410071A3EC /* ViewController.swift in Sources */, + 2EE557EE1D41A6410071A3EC /* AppDelegate.swift in Sources */, + 2EE557F31D41A6410071A3EC /* SlimMPSCNN.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 2EE557F51D41A6690071A3EC /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2EE557F61D41A6690071A3EC /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 2EE557F81D41A6770071A3EC /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 2EE557F91D41A6770071A3EC /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 2E0C36191CB5B2FF0041D8E3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_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 = "iPhone Developer: dhruv_saksena (KQCTVQZQW3)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: dhruv_saksena (KQCTVQZQW3)"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2E0C361A1CB5B2FF0041D8E3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_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 = "iPhone Developer: dhruv_saksena (KQCTVQZQW3)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: dhruv_saksena (KQCTVQZQW3)"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 2E0C361C1CB5B2FF0041D8E3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + armv7s, + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = MPSCNNHelloWorld/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.MPSCNNHelloWorld"; + PRODUCT_NAME = "Digit Detector"; + PROVISIONING_PROFILE = ""; + SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "MPSCNNHelloWorld/MPSCNNHelloWorld-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 2E0C361D1CB5B2FF0041D8E3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + armv7s, + arm64, + ); + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = MPSCNNHelloWorld/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.MPSCNNHelloWorld"; + PRODUCT_NAME = "Digit Detector"; + PROVISIONING_PROFILE = ""; + SDKROOT = iphoneos; + SWIFT_OBJC_BRIDGING_HEADER = "MPSCNNHelloWorld/MPSCNNHelloWorld-Bridging-Header.h"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2E0C35EE1CB5B2FE0041D8E3 /* Build configuration list for PBXProject "MPSCNNHelloWorld" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2E0C36191CB5B2FF0041D8E3 /* Debug */, + 2E0C361A1CB5B2FF0041D8E3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2E0C361B1CB5B2FF0041D8E3 /* Build configuration list for PBXNativeTarget "MPSCNNHelloWorld" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2E0C361C1CB5B2FF0041D8E3 /* Debug */, + 2E0C361D1CB5B2FF0041D8E3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 2E0C35EB1CB5B2FE0041D8E3 /* Project object */; +} diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/AppDelegate.swift b/MPSCNNHelloWorld/MPSCNNHelloWorld/AppDelegate.swift new file mode 100755 index 00000000..d453ce6b --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/AppDelegate.swift @@ -0,0 +1,17 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate for the App +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + +} + diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/Contents.json b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 00000000..94d32ea9 --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,86 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "mnist-main-58.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "mnist-main-87.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "mnist-main-80.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "mnist-main-120.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "mnist-main-120-2.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "mnist-main-180.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "mnist-main-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "mnist-main-58-2.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "mnist-main-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "mnist-main-80-2.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "mnist-main-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "mnist-main-152.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "mnist-main-167.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-120-2.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-120-2.png new file mode 100755 index 00000000..ce0df8e0 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-120-2.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-120.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-120.png new file mode 100755 index 00000000..ce0df8e0 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-120.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-152.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-152.png new file mode 100755 index 00000000..dbea71ca Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-152.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-167.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-167.png new file mode 100755 index 00000000..31a5030c Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-167.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-180.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-180.png new file mode 100755 index 00000000..ebc76a8d Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-180.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-29.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-29.png new file mode 100755 index 00000000..f288c00a Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-29.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-40.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-40.png new file mode 100755 index 00000000..879a7bd3 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-40.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-58-2.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-58-2.png new file mode 100755 index 00000000..0e0e73b8 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-58-2.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-58.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-58.png new file mode 100755 index 00000000..0e0e73b8 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-58.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-76.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-76.png new file mode 100755 index 00000000..b20a494a Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-76.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-80-2.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-80-2.png new file mode 100755 index 00000000..b844b8b5 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-80-2.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-80.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-80.png new file mode 100755 index 00000000..b844b8b5 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-80.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-87.png b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-87.png new file mode 100755 index 00000000..5c0c5fed Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/AppIcon.appiconset/mnist-main-87.png differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/Contents.json b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/Contents.json new file mode 100755 index 00000000..da4a164c --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Base.lproj/LaunchScreen.storyboard b/MPSCNNHelloWorld/MPSCNNHelloWorld/Base.lproj/LaunchScreen.storyboard new file mode 100755 index 00000000..61c1c231 --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Base.lproj/Main.storyboard b/MPSCNNHelloWorld/MPSCNNHelloWorld/Base.lproj/Main.storyboard new file mode 100755 index 00000000..b7f65e6a --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/Base.lproj/Main.storyboard @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/DrawView.swift b/MPSCNNHelloWorld/MPSCNNHelloWorld/DrawView.swift new file mode 100755 index 00000000..a05c60fd --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/DrawView.swift @@ -0,0 +1,93 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This file has routines for drwaing and detecting user touches (input digit) +*/ + +import UIKit + +/** + This class is used to handle the drawing in the DigitView so we can get user input digit, + This class doesn't really have an MPS or Metal going in it, it is just used to get user input + */ +class DrawView: UIView { + + // some parameters of how thick a line to draw 15 seems to work + // and we have white drawings on black background just like MNIST needs its input + var linewidth = CGFloat(15) { didSet { setNeedsDisplay() } } + var color = UIColor.white { didSet { setNeedsDisplay() } } + + // we will keep touches made by user in view in these as a record so we can draw them. + var lines: [Line] = [] + var lastPoint: CGPoint! + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + lastPoint = touches.first!.location(in: self) + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + let newPoint = touches.first!.location(in: self) + // keep all lines drawn by user as touch in record so we can draw them in view + lines.append(Line(start: lastPoint, end: newPoint)) + lastPoint = newPoint + // make a draw call + setNeedsDisplay() + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + + let drawPath = UIBezierPath() + drawPath.lineCapStyle = .round + + for line in lines{ + drawPath.move(to: line.start) + drawPath.addLine(to: line.end) + } + + drawPath.lineWidth = linewidth + color.set() + drawPath.stroke() + } + + + /** + This function gets the pixel data of the view so we can put it in MTLTexture + + - Returns: + Void + */ + func getViewContext() -> CGContext? { + // our network takes in only grayscale images as input + let colorSpace:CGColorSpace = CGColorSpaceCreateDeviceGray() + + // we have 3 channels no alpha value put in the network + let bitmapInfo = CGImageAlphaInfo.none.rawValue + + // this is where our view pixel data will go in once we make the render call + let context = CGContext(data: nil, width: 28, height: 28, bitsPerComponent: 8, bytesPerRow: 28, space: colorSpace, bitmapInfo: bitmapInfo) + + // scale and translate so we have the full digit and in MNIST standard size 28x28 + context!.translateBy(x: 0 , y: 28) + context!.scaleBy(x: 28/self.frame.size.width, y: -28/self.frame.size.height) + + // put view pixel data in context + self.layer.render(in: context!) + + return context + } +} + +/** + 2 points can give a line and this class is just for that purpose, it keeps a record of a line + */ +class Line{ + var start, end: CGPoint + + init(start: CGPoint, end: CGPoint) { + self.start = start + self.end = end + } +} diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/GetMNISTData.swift b/MPSCNNHelloWorld/MPSCNNHelloWorld/GetMNISTData.swift new file mode 100755 index 00000000..cf704ff0 --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/GetMNISTData.swift @@ -0,0 +1,73 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + GetMNISTData is used to import the test set from the MNIST dataset +*/ + +import Foundation + +class GetMNISTData { + + var labels = [UInt8]() + var images = [UInt8]() + + var hdrW, hdrB: UnsafeMutableRawPointer? + var fd_b, fd_w: CInt + var sizeBias, sizeWeights: Int + + + init() { + // get the url to this layer's weights and bias + let wtPath = Bundle.main.path(forResource: "t10k-images-idx3-ubyte", ofType: "data") + let bsPath = Bundle.main.path(forResource: "t10k-labels-idx1-ubyte", ofType: "data") + + // find and open file + let URLL = Bundle.main.url(forResource: "t10k-labels-idx1-ubyte", withExtension: "data") + let dataL = NSData(contentsOf: URLL!) + + let URLI = Bundle.main.url(forResource: "t10k-images-idx3-ubyte", withExtension: "data") + let dataI = NSData(contentsOf: URLI!) + + // calculate the size of weights and bias required to be memory mapped into memory + sizeBias = dataL!.length + sizeWeights = dataI!.length + + // open file descriptors in read-only mode to parameter files + fd_w = open(wtPath!, O_RDONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) + fd_b = open(bsPath!, O_RDONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) + + assert(fd_w != -1, "Error: failed to open output file at \""+wtPath!+"\" errno = \(errno)\n") + assert(fd_b != -1, "Error: failed to open output file at \""+bsPath!+"\" errno = \(errno)\n") + + + // memory map the parameters + hdrW = mmap(nil, Int(sizeWeights), PROT_READ, MAP_FILE | MAP_SHARED, fd_w, 0); + hdrB = mmap(nil, Int(sizeBias), PROT_READ, MAP_FILE | MAP_SHARED, fd_b, 0); + + let i = UnsafePointer(hdrW!.bindMemory(to: UInt8.self, capacity: Int(sizeWeights))) + let l = UnsafePointer(hdrB!.bindMemory(to: UInt8.self, capacity: Int(sizeBias))) + + assert(i != UnsafePointer(bitPattern: -1), "mmap failed with errno = \(errno)") + assert(l != UnsafePointer(bitPattern: -1), "mmap failed with errno = \(errno)") + + + // remove first 16 bytes that contain info data from array + images = Array(UnsafeBufferPointer(start: (i + 16), count: sizeWeights - 16)) + + // remove first 8 bytes that contain file data from our labels array + labels = Array(UnsafeBufferPointer(start: (l + 8), count: sizeBias - 8)) + } + + deinit{ + // unmap files at initialization of MPSCNNFullyConnected, the weights are copied and packed internally we no longer require these + assert(munmap(hdrW, Int(sizeWeights)) == 0, "munmap failed with errno = \(errno)") + assert(munmap(hdrB, Int(sizeBias)) == 0, "munmap failed with errno = \(errno)") + + // close file descriptors + close(fd_w) + close(fd_b) + } + +} diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/Info.plist b/MPSCNNHelloWorld/MPSCNNHelloWorld/Info.plist new file mode 100755 index 00000000..40c6215d --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/MNISTDeepCNN.swift b/MPSCNNHelloWorld/MPSCNNHelloWorld/MNISTDeepCNN.swift new file mode 100755 index 00000000..b97719dd --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/MNISTDeepCNN.swift @@ -0,0 +1,147 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This is the deep layer network where we define and encode the correct layers on a command buffer as needed +*/ + +import MetalPerformanceShaders + +/** + + This class has our entire network with all layers to getting the final label + + Resources: + * [Instructions](https://www.tensorflow.org/versions/r0.8/tutorials/mnist/pros/index.html#deep-mnist-for-experts) to run this network on TensorFlow. + + */ +class MNIST_Deep_ConvNN: MNIST_Full_LayerNN{ + // MPSImageDescriptors for different layers outputs to be put in + let c1id = MPSImageDescriptor(channelFormat: MPSImageFeatureChannelFormat.float16, width: 28, height: 28, featureChannels: 32) + let p1id = MPSImageDescriptor(channelFormat: MPSImageFeatureChannelFormat.float16, width: 14, height: 14, featureChannels: 32) + let c2id = MPSImageDescriptor(channelFormat: MPSImageFeatureChannelFormat.float16, width: 14, height: 14, featureChannels: 64) + let p2id = MPSImageDescriptor(channelFormat: MPSImageFeatureChannelFormat.float16, width: 7 , height: 7 , featureChannels: 64) + let fc1id = MPSImageDescriptor(channelFormat: MPSImageFeatureChannelFormat.float16, width: 1 , height: 1 , featureChannels: 1024) + + // MPSImages and layers declared + var c1Image, c2Image, p1Image, p2Image, fc1Image: MPSImage + var conv1, conv2: MPSCNNConvolution + var fc1, fc2: MPSCNNFullyConnected + var pool: MPSCNNPoolingMax + var relu: MPSCNNNeuronReLU + + override init(withCommandQueue commandQueueIn: MTLCommandQueue!) { + // use device for a little while to initialize + let device = commandQueueIn.device + + pool = MPSCNNPoolingMax(device: device, kernelWidth: 2, kernelHeight: 2, strideInPixelsX: 2, strideInPixelsY: 2) + pool.offset = MPSOffset(x: 1, y: 1, z: 0); + pool.edgeMode = MPSImageEdgeMode.clamp + relu = MPSCNNNeuronReLU(device: device, a: 0) + + + + // Initialize MPSImage from descriptors + c1Image = MPSImage(device: device, imageDescriptor: c1id) + p1Image = MPSImage(device: device, imageDescriptor: p1id) + c2Image = MPSImage(device: device, imageDescriptor: c2id) + p2Image = MPSImage(device: device, imageDescriptor: p2id) + fc1Image = MPSImage(device: device, imageDescriptor: fc1id) + + + // setup convolution layers + conv1 = SlimMPSCNNConvolution(kernelWidth: 5, + kernelHeight: 5, + inputFeatureChannels: 1, + outputFeatureChannels: 32, + neuronFilter: relu, + device: device, + kernelParamsBinaryName: "conv1") + + conv2 = SlimMPSCNNConvolution(kernelWidth: 5, + kernelHeight: 5, + inputFeatureChannels: 32, + outputFeatureChannels: 64, + neuronFilter: relu, + device: device, + kernelParamsBinaryName: "conv2") + + + // same as a 1x1 convolution filter to produce 1x1x10 from 1x1x1024 + fc1 = SlimMPSCNNFullyConnected(kernelWidth: 7, + kernelHeight: 7, + inputFeatureChannels: 64, + outputFeatureChannels: 1024, + neuronFilter: nil, + device: device, + kernelParamsBinaryName: "fc1") + + fc2 = SlimMPSCNNFullyConnected(kernelWidth: 1, + kernelHeight: 1, + inputFeatureChannels: 1024, + outputFeatureChannels: 10, + neuronFilter: nil, + device: device, + kernelParamsBinaryName: "fc2") + + super.init(withCommandQueue: commandQueueIn) + } + + + /** + This function encodes all the layers of the network into given commandBuffer, it calls subroutines for each piece of the network + + - Parameters: + - inputImage: Image coming in on which the network will run + - imageNum: If the test set is being used we will get a value between 0 and 9999 for which of the 10,000 images is being evaluated + - correctLabel: The correct label for the inputImage while testing + + - Returns: + Guess of the network as to what the digit is as UInt + */ + override func forward(inputImage: MPSImage? = nil, imageNum: Int = 9999, correctLabel: UInt = 10) -> UInt{ + var label = UInt(99) + + // to deliver optimal performance we leave some resources used in MPSCNN to be released at next call of autoreleasepool, + // so the user can decide the appropriate time to release this + autoreleasepool{ + // Get command buffer to use in MetalPerformanceShaders. + let commandBuffer = commandQueue.makeCommandBuffer() + + // output will be stored in this image + let finalLayer = MPSImage(device: commandBuffer.device, imageDescriptor: did) + + // encode layers to metal commandBuffer + if inputImage == nil { + conv1.encode(commandBuffer: commandBuffer, sourceImage: srcImage, destinationImage: c1Image) + } + else{ + conv1.encode(commandBuffer: commandBuffer, sourceImage: inputImage!, destinationImage: c1Image) + } + + pool.encode (commandBuffer: commandBuffer, sourceImage: c1Image , destinationImage: p1Image) + conv2.encode (commandBuffer: commandBuffer, sourceImage: p1Image , destinationImage: c2Image) + pool.encode (commandBuffer: commandBuffer, sourceImage: c2Image , destinationImage: p2Image) + fc1.encode (commandBuffer: commandBuffer, sourceImage: p2Image , destinationImage: fc1Image) + fc2.encode (commandBuffer: commandBuffer, sourceImage: fc1Image , destinationImage: dstImage) + softmax.encode(commandBuffer: commandBuffer, sourceImage: dstImage , destinationImage: finalLayer) + + // add a completion handler to get the correct label the moment GPU is done and compare it to the correct output or return it + commandBuffer.addCompletedHandler { commandBuffer in + label = self.getLabel(finalLayer: finalLayer) + if(correctLabel == label){ + __atomic_increment() + } + } + + // commit commandbuffer to run on GPU and wait for completion + commandBuffer.commit() + if imageNum == 9999 { + commandBuffer.waitUntilCompleted() + } + + } + return label + } +} diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/MNISTSingleLayer.swift b/MPSCNNHelloWorld/MPSCNNHelloWorld/MNISTSingleLayer.swift new file mode 100755 index 00000000..0c1673f2 --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/MNISTSingleLayer.swift @@ -0,0 +1,157 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This is the single layer network where we define and encode the correct layers on a command buffer as needed +*/ + +import MetalPerformanceShaders +import Accelerate + +/** + + This class has our entire network with all layers to getting the final label + + Resources: + * [Instructions](https://www.tensorflow.org/versions/r0.8/tutorials/mnist/beginners/index.html#mnist-for-ml-beginners) to run this network on TensorFlow. + + */ +class MNIST_Full_LayerNN{ + + // MPSImageDescriptors for different layers outputs to be put in + let sid = MPSImageDescriptor(channelFormat: MPSImageFeatureChannelFormat.unorm8, width: 28, height: 28, featureChannels: 1) + let did = MPSImageDescriptor(channelFormat: MPSImageFeatureChannelFormat.float16, width: 1, height: 1, featureChannels: 10) + + // MPSImages and layers declared + var srcImage, dstImage : MPSImage + var layer: MPSCNNFullyConnected + var softmax : MPSCNNSoftMax + var commandQueue : MTLCommandQueue + var device : MTLDevice + + init(withCommandQueue commandQueueIn: MTLCommandQueue!){ + + // CommandQueue to be kept around + commandQueue = commandQueueIn + device = commandQueueIn.device + + // Initialize MPSImage from descriptors + srcImage = MPSImage(device: device, imageDescriptor: sid) + dstImage = MPSImage(device: device, imageDescriptor: did) + + + // setup convolution layer (which is a fully-connected layer) + // cliprect, offset is automatically set + layer = SlimMPSCNNFullyConnected(kernelWidth: 28, + kernelHeight: 28, + inputFeatureChannels : 1, + outputFeatureChannels: 10, + neuronFilter: nil, + device: device, + kernelParamsBinaryName: "NN") + + // prepare softmax layer to be applied at the end to get a clear label + softmax = MPSCNNSoftMax(device: device) + + } + + /** + This function encodes all the layers of the network into given commandBuffer, it calls subroutines for each piece of the network + + - Parameters: + - inputImage: Image coming in on which the network will run + - imageNum: If the test set is being used we will get a value between 0 and 9999 for which of the 10,000 images is being evaluated + - correctLabel: The correct label for the inputImage while testing + + - Returns: + Guess of the network as to what the digit is as UInt + */ + func forward(inputImage: MPSImage? = nil, imageNum: Int = 9999, correctLabel: UInt = 10) -> UInt { + var label = UInt(99) + + // to deliver optimal performance we leave some resources used in MPSCNN to be released at next call of autoreleasepool, + // so the user can decide the appropriate time to release this + autoreleasepool{ + // Get command buffer to use in MetalPerformanceShaders. + let commandBuffer = commandQueue.makeCommandBuffer() + + // output will be stored in this image + let finalLayer = MPSImage(device: commandBuffer.device, imageDescriptor: did) + + // encode layers to metal commandBuffer + if inputImage == nil { + layer.encode(commandBuffer: commandBuffer, sourceImage: srcImage, destinationImage: dstImage) + } + else{ + layer.encode(commandBuffer: commandBuffer, sourceImage: inputImage!, destinationImage: dstImage) + } + softmax.encode(commandBuffer: commandBuffer, sourceImage: dstImage, destinationImage: finalLayer) + + // add a completion handler to get the correct label the moment GPU is done and compare it to the correct output or return it + commandBuffer.addCompletedHandler { commandBuffer in + label = self.getLabel(finalLayer: finalLayer) + if(correctLabel == label){ + __atomic_increment() + } + } + + // commit commandbuffer to run on GPU and wait for completion + commandBuffer.commit() + if imageNum == 9999 || inputImage == nil { + commandBuffer.waitUntilCompleted() + } + + } + return label + } + + /** + This function reads the output probabilities from finalLayer to CPU, sorts them and gets the label with heighest probability + + - Parameters: + - finalLayer: output image of the network this has probabilities of each digit + + - Returns: + Guess of the network as to what the digit is as UInt + */ + func getLabel(finalLayer: MPSImage) -> UInt { + // even though we have 10 labels outputed the MTLTexture format used is RGBAFloat16 thus 3 slices will have 3*4 = 12 outputs + var result_half_array = [UInt16](repeating: 6, count: 12) + var result_float_array = [Float](repeating: 0.3, count: 10) + for i in 0...2 { + finalLayer.texture.getBytes(&(result_half_array[4*i]), + bytesPerRow: MemoryLayout.size*1*4, + bytesPerImage: MemoryLayout.size*1*1*4, + from: MTLRegion(origin: MTLOrigin(x: 0, y: 0, z: 0), + size: MTLSize(width: 1, height: 1, depth: 1)), + mipmapLevel: 0, + slice: i) + } + + // we use vImage to convert our data to float16, Metal GPUs use float16 and swift float is 32-bit + var fullResultVImagebuf = vImage_Buffer(data: &result_float_array, height: 1, width: 10, rowBytes: 10*4) + var halfResultVImagebuf = vImage_Buffer(data: &result_half_array , height: 1, width: 10, rowBytes: 10*2) + + if vImageConvert_Planar16FtoPlanarF(&halfResultVImagebuf, &fullResultVImagebuf, 0) != kvImageNoError { + print("Error in vImage") + } + + // poll all labels for probability and choose the one with max probability to return + var max:Float = 0 + var mostProbableDigit = 10 + for i in 0...9 { + if(max < result_float_array[i]){ + max = result_float_array[i] + mostProbableDigit = i + } + } + + return UInt(mostProbableDigit) + } + +} + + + + diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/MPSCNNHelloWorld-Bridging-Header.h b/MPSCNNHelloWorld/MPSCNNHelloWorld/MPSCNNHelloWorld-Bridging-Header.h new file mode 100755 index 00000000..f6988377 --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/MPSCNNHelloWorld-Bridging-Header.h @@ -0,0 +1,14 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A bridging header so our swift code can see our atomics in objC + */ + +#ifndef MPSCNNHelloWorld_Bridging_Header_h +#define MPSCNNHelloWorld_Bridging_Header_h + +#import "atomics.h" + +#endif /* MPSCNNHelloWorld_Bridging_Header_h */ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/SlimMPSCNN.swift b/MPSCNNHelloWorld/MPSCNNHelloWorld/SlimMPSCNN.swift new file mode 100755 index 00000000..643e689f --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/SlimMPSCNN.swift @@ -0,0 +1,215 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This file describes slimmer routines to create some common MPSCNNFunctions, it is useful especially to fetch network parameters from .dat files + */ + +import Foundation +import MetalPerformanceShaders + +/** + This depends on MetalPerformanceShaders.framework + + The SlimMPSCNNConvolution is a wrapper class around MPSCNNConvolution used to encapsulate: + - making an MPSCNNConvolutionDescriptor, + - adding network parameters (weights and bias binaries by memory mapping the binaries) + - getting our convolution layer + */ +class SlimMPSCNNConvolution: MPSCNNConvolution{ + /** + A property to keep info from init time whether we will pad input image or not for use during encode call + */ + private var padding = true + + /** + Initializes a fully connected kernel. + + - Parameters: + - kernelWidth: Kernel Width + - kernelHeight: Kernel Height + - inputFeatureChannels: Number feature channels in input of this layer + - outputFeatureChannels: Number feature channels from output of this layer + - neuronFilter: A neuronFilter to add at the end as activation, default is nil + - device: The MTLDevice on which this SlimMPSCNNConvolution filter will be used + - kernelParamsBinaryName: name of the layer to fetch kernelParameters by adding a prefix "weights_" or "bias_" + - padding: Bool value whether to use padding or not + - strideXY: Stride of the filter + - destinationFeatureChannelOffset: FeatureChannel no. in the destination MPSImage to start writing from, helps with concat operations + - groupNum: if grouping is used, default value is 1 meaning no groups + + - Returns: + A valid SlimMPSCNNConvolution object or nil, if failure. + */ + + + init(kernelWidth: UInt, kernelHeight: UInt, inputFeatureChannels: UInt, outputFeatureChannels: UInt, neuronFilter: MPSCNNNeuron? = nil, device: MTLDevice, kernelParamsBinaryName: String, padding willPad: Bool = true, strideXY: (UInt, UInt) = (1, 1), destinationFeatureChannelOffset: UInt = 0, groupNum: UInt = 1){ + + // calculate the size of weights and bias required to be memory mapped into memory + let sizeBias = outputFeatureChannels * UInt(MemoryLayout.size) + let sizeWeights = inputFeatureChannels * kernelHeight * kernelWidth * outputFeatureChannels * UInt(MemoryLayout.size) + + // get the url to this layer's weights and bias + let wtPath = Bundle.main.path(forResource: "weights_" + kernelParamsBinaryName, ofType: "dat") + let bsPath = Bundle.main.path(forResource: "bias_" + kernelParamsBinaryName, ofType: "dat") + + // open file descriptors in read-only mode to parameter files + let fd_w = open( wtPath!, O_RDONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) + let fd_b = open( bsPath!, O_RDONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) + + assert(fd_w != -1, "Error: failed to open output file at \""+wtPath!+"\" errno = \(errno)\n") + assert(fd_b != -1, "Error: failed to open output file at \""+bsPath!+"\" errno = \(errno)\n") + + // memory map the parameters + let hdrW = mmap(nil, Int(sizeWeights), PROT_READ, MAP_FILE | MAP_SHARED, fd_w, 0) + let hdrB = mmap(nil, Int(sizeBias), PROT_READ, MAP_FILE | MAP_SHARED, fd_b, 0) + + // cast Void pointers to Float + let w = UnsafePointer(hdrW!.bindMemory(to: Float.self, capacity: Int(sizeWeights))) + let b = UnsafePointer(hdrB!.bindMemory(to: Float.self, capacity: Int(sizeBias))) + + assert(w != UnsafePointer(bitPattern: -1), "mmap failed with errno = \(errno)") + assert(b != UnsafePointer(bitPattern: -1), "mmap failed with errno = \(errno)") + + // create appropriate convolution descriptor with appropriate stride + let convDesc = MPSCNNConvolutionDescriptor(kernelWidth: Int(kernelWidth), + kernelHeight: Int(kernelHeight), + inputFeatureChannels: Int(inputFeatureChannels), + outputFeatureChannels: Int(outputFeatureChannels), + neuronFilter: neuronFilter) + convDesc.strideInPixelsX = Int(strideXY.0) + convDesc.strideInPixelsY = Int(strideXY.1) + + assert(groupNum > 0, "Group size can't be less than 1") + convDesc.groups = Int(groupNum) + + // initialize the convolution layer by calling the parent's (MPSCNNConvlution's) initializer + super.init(device: device, + convolutionDescriptor: convDesc, + kernelWeights: w, + biasTerms: b, + flags: MPSCNNConvolutionFlags.none) + self.destinationFeatureChannelOffset = Int(destinationFeatureChannelOffset) + + // set padding for calculation of offset during encode call + padding = willPad + + // unmap files at initialization of MPSCNNConvolution, the weights are copied and packed internally we no longer require these + assert(munmap(hdrW, Int(sizeWeights)) == 0, "munmap failed with errno = \(errno)") + assert(munmap(hdrB, Int(sizeBias)) == 0, "munmap failed with errno = \(errno)") + + // close file descriptors + close(fd_w) + close(fd_b) + } + + /** + Encode a MPSCNNKernel into a command Buffer. The operation shall proceed out-of-place. + + We calculate the appropriate offset as per how TensorFlow calculates its padding using input image size and stride here. + + This [Link](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/ops/nn.py) has an explanation in header comments how tensorFlow pads its convolution input images. + + - Parameters: + - commandBuffer: A valid MTLCommandBuffer to receive the encoded filter + - sourceImage: A valid MPSImage object containing the source image. + - destinationImage: A valid MPSImage to be overwritten by result image. destinationImage may not alias sourceImage + */ + override func encode(commandBuffer: MTLCommandBuffer, sourceImage: MPSImage, destinationImage: MPSImage) { + // select offset according to padding being used or not + if padding { + let pad_along_height = ((destinationImage.height - 1) * strideInPixelsY + kernelHeight - sourceImage.height) + let pad_along_width = ((destinationImage.width - 1) * strideInPixelsX + kernelWidth - sourceImage.width) + let pad_top = Int(pad_along_height / 2) + let pad_left = Int(pad_along_width / 2) + + self.offset = MPSOffset(x: ((Int(kernelWidth)/2) - pad_left), y: (Int(kernelHeight/2) - pad_top), z: 0) + } + else{ + self.offset = MPSOffset(x: Int(kernelWidth)/2, y: Int(kernelHeight)/2, z: 0) + } + + super.encode(commandBuffer: commandBuffer, sourceImage: sourceImage, destinationImage: destinationImage) + } +} + +/** + This depends on MetalPerformanceShaders.framework + + The SlimMPSCNNFullyConnected is a wrapper class around MPSCNNFullyConnected used to encapsulate: + - making an MPSCNNConvolutionDescriptor, + - adding network parameters (weights and bias binaries by memory mapping the binaries) + - getting our fullyConnected layer + */ +class SlimMPSCNNFullyConnected: MPSCNNFullyConnected{ + /** + Initializes a fully connected kernel. + + - Parameters: + - kernelWidth: Kernel Width + - kernelHeight: Kernel Height + - inputFeatureChannels: Number feature channels in input of this layer + - outputFeatureChannels: Number feature channels from output of this layer + - neuronFilter: A neuronFilter to add at the end as activation, default is nil + - device: The MTLDevice on which this SlimMPSCNNConvolution filter will be used + - kernelParamsBinaryName: name of the layer to fetch kernelParameters by adding a prefix "weights_" or "bias_" + - destinationFeatureChannelOffset: FeatureChannel no. in the destination MPSImage to start writing from, helps with concat operations + + - Returns: + A valid SlimMPSCNNFullyConnected object or nil, if failure. + */ + + init(kernelWidth: UInt, kernelHeight: UInt, inputFeatureChannels: UInt, outputFeatureChannels: UInt, neuronFilter: MPSCNNNeuron? = nil, device: MTLDevice, kernelParamsBinaryName: String, destinationFeatureChannelOffset: UInt = 0){ + + // calculate the size of weights and bias required to be memory mapped into memory + let sizeBias = outputFeatureChannels * UInt(MemoryLayout.size) + let sizeWeights = inputFeatureChannels * kernelHeight * kernelWidth * outputFeatureChannels * UInt(MemoryLayout.size) + + // get the url to this layer's weights and bias + let wtPath = Bundle.main.path(forResource: "weights_" + kernelParamsBinaryName, ofType: "dat") + let bsPath = Bundle.main.path(forResource: "bias_" + kernelParamsBinaryName, ofType: "dat") + + // open file descriptors in read-only mode to parameter files + let fd_w = open(wtPath!, O_RDONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) + let fd_b = open(bsPath!, O_RDONLY, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH) + + assert(fd_w != -1, "Error: failed to open output file at \""+wtPath!+"\" errno = \(errno)\n") + assert(fd_b != -1, "Error: failed to open output file at \""+bsPath!+"\" errno = \(errno)\n") + + // memory map the parameters + let hdrW = mmap(nil, Int(sizeWeights), PROT_READ, MAP_FILE | MAP_SHARED, fd_w, 0) + let hdrB = mmap(nil, Int(sizeBias), PROT_READ, MAP_FILE | MAP_SHARED, fd_b, 0) + + // cast Void pointers to Float + let w = UnsafePointer(hdrW!.bindMemory(to: Float.self, capacity: Int(sizeWeights))) + let b = UnsafePointer(hdrB!.bindMemory(to: Float.self, capacity: Int(sizeBias))) + + assert(w != UnsafePointer(bitPattern: -1), "mmap failed with errno = \(errno)") + assert(b != UnsafePointer(bitPattern: -1), "mmap failed with errno = \(errno)") + + // create appropriate convolution descriptor (in fully connected, stride is always 1) + let convDesc = MPSCNNConvolutionDescriptor(kernelWidth: Int(kernelWidth), + kernelHeight: Int(kernelHeight), + inputFeatureChannels: Int(inputFeatureChannels), + outputFeatureChannels: Int(outputFeatureChannels), + neuronFilter: neuronFilter) + + // initialize the convolution layer by calling the parent's (MPSCNNFullyConnected's) initializer + super.init(device: device, + convolutionDescriptor: convDesc, + kernelWeights: w, + biasTerms: b, + flags: MPSCNNConvolutionFlags.none) + + self.destinationFeatureChannelOffset = Int(destinationFeatureChannelOffset) + + // unmap files at initialization of MPSCNNFullyConnected, the weights are copied and packed internally we no longer require these + assert(munmap(hdrW, Int(sizeWeights)) == 0, "munmap failed with errno = \(errno)") + assert(munmap(hdrB, Int(sizeBias)) == 0, "munmap failed with errno = \(errno)") + + // close file descriptors + close(fd_w) + close(fd_b) + } +} diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/ViewController.swift b/MPSCNNHelloWorld/MPSCNNHelloWorld/ViewController.swift new file mode 100755 index 00000000..7c5e9e3c --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/ViewController.swift @@ -0,0 +1,165 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View Controller for Metal Performance Shaders Sample Code. +*/ + +import UIKit +import MetalPerformanceShaders + +class ViewController: UIViewController{ + + // some properties used to control the app and store appropriate values + // we will start with the simple 1 layer + var deep = false + var commandQueue: MTLCommandQueue! + var device: MTLDevice! + + // Networks we have + var neuralNetwork: MNIST_Full_LayerNN? = nil + var neuralNetworkDeep: MNIST_Deep_ConvNN? = nil + var runningNet: MNIST_Full_LayerNN? = nil + + // loading MNIST Test Set here + let MNISTdata = GetMNISTData() + + // MNIST dataset image parameters + let mnistInputWidth = 28 + let mnistInputHeight = 28 + let mnistInputNumPixels = 784 + + // Outlets to labels and view + @IBOutlet weak var digitView: DrawView! + @IBOutlet weak var predictionLabel: UILabel! + @IBOutlet weak var accuracyLabel: UILabel! + + + override func viewDidLoad() { + super.viewDidLoad() + + // Load default device. + device = MTLCreateSystemDefaultDevice() + + // Make sure the current device supports MetalPerformanceShaders. + guard MPSSupportsMTLDevice(device) else { + print("Metal Performance Shaders not Supported on current Device") + return + } + + // Create new command queue. + commandQueue = device!.makeCommandQueue() + + // initialize the networks we shall use to detect digits + neuralNetwork = MNIST_Full_LayerNN(withCommandQueue: commandQueue) + neuralNetworkDeep = MNIST_Deep_ConvNN(withCommandQueue: commandQueue) + runningNet = neuralNetwork + } + + @IBAction func tappedDeepButton(_ sender: UIButton) { + // switch network to be used between the deep and the single layered + if deep { + sender.setTitle("Use Deep Net", for: UIControlState.normal) + runningNet = neuralNetwork + } + else{ + sender.setTitle("Use Single Layer", for: UIControlState.normal) + runningNet = neuralNetworkDeep + } + + deep = !deep + } + + @IBAction func tappedClear(_ sender: UIButton) { + // clear the digitview + digitView.lines = [] + digitView.setNeedsDisplay() + predictionLabel.isHidden = true + + } + + @IBAction func tappedTestSet(_ sender: UIButton) { + // placeholder to count number of correct detections on the test set + var correctDetections = Int32(0) + let total = Float(10000) + accuracyLabel.isHidden = false + __atomic_reset() + + // validate NeuralNetwork was initialized properly + assert(runningNet != nil) + + for i in 0.. + +static atomic_int cnt = ATOMIC_VAR_INIT(0); +void __atomic_increment(); +void __atomic_reset(); +int __get_atomic_count(); + +#endif /* atomics_h */ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/atomics.m b/MPSCNNHelloWorld/MPSCNNHelloWorld/atomics.m new file mode 100755 index 00000000..793f3ce9 --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/atomics.m @@ -0,0 +1,19 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + We define some custom atomics to be used the network so seperate threads at end of commandBuffers can safely increment. +*/ + +#import "atomics.h" + +void __atomic_increment(){ + atomic_fetch_add(&cnt, 1); +} +void __atomic_reset(){ + cnt = ATOMIC_VAR_INIT(0); +} +int __get_atomic_count(){ + return atomic_load(&cnt); +} diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_conv1.dat b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_conv1.dat new file mode 100755 index 00000000..7add15e3 --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_conv1.dat @@ -0,0 +1,2 @@ +Cs==|=£=\====޽==_=w=]=5==a=p==| +=o==/=Ħ=[=7Í=/i=!=.=s==-=oӟ=i= \ No newline at end of file diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_conv2.dat b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_conv2.dat new file mode 100755 index 00000000..7281b543 --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_conv2.dat @@ -0,0 +1,2 @@ +=j==du=`=`=*=iƢ=y;=.===ü=y=lx====Ɗ==Y=F=| +=C=jM= =j=Ȱ==*=]==Q=a=4=={=)Ч=uͤ=%==0*=ZG=Ic=Dn===<== =(==_=qr=ۮ==*=p%=2=P==G=5== \ No newline at end of file diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_fc1.dat b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_fc1.dat new file mode 100755 index 00000000..f211649a Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_fc1.dat differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_fc2.dat b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_fc2.dat new file mode 100755 index 00000000..12a2e7a2 --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/bias_fc2.dat @@ -0,0 +1 @@ +'=S==V==>=\===O= \ No newline at end of file diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_conv1.dat b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_conv1.dat new file mode 100755 index 00000000..7e38481e Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_conv1.dat differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_conv2.dat b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_conv2.dat new file mode 100755 index 00000000..f1300492 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_conv2.dat differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_fc1.dat b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_fc1.dat new file mode 100755 index 00000000..746742a5 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_fc1.dat differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_fc2.dat b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_fc2.dat new file mode 100755 index 00000000..cfaa4a33 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/deep_weights/binaries/weights_fc2.dat differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/t10k-images-idx3-ubyte.data b/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/t10k-images-idx3-ubyte.data new file mode 100755 index 00000000..1170b2ca Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/t10k-images-idx3-ubyte.data differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/t10k-labels-idx1-ubyte.data b/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/t10k-labels-idx1-ubyte.data new file mode 100755 index 00000000..d1c3a970 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/t10k-labels-idx1-ubyte.data differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/train-images-idx3-ubyte.data b/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/train-images-idx3-ubyte.data new file mode 100755 index 00000000..bbce2765 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/train-images-idx3-ubyte.data differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/train-labels-idx1-ubyte.data b/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/train-labels-idx1-ubyte.data new file mode 100755 index 00000000..d6b4c5db Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/mnistData/train-labels-idx1-ubyte.data differ diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/single_layer_weights/bias_NN.dat b/MPSCNNHelloWorld/MPSCNNHelloWorld/single_layer_weights/bias_NN.dat new file mode 100755 index 00000000..667ef435 --- /dev/null +++ b/MPSCNNHelloWorld/MPSCNNHelloWorld/single_layer_weights/bias_NN.dat @@ -0,0 +1 @@ +/j>k=.<\\?Xֽ3$?\dn \ No newline at end of file diff --git a/MPSCNNHelloWorld/MPSCNNHelloWorld/single_layer_weights/weights_NN.dat b/MPSCNNHelloWorld/MPSCNNHelloWorld/single_layer_weights/weights_NN.dat new file mode 100755 index 00000000..a186f534 Binary files /dev/null and b/MPSCNNHelloWorld/MPSCNNHelloWorld/single_layer_weights/weights_NN.dat differ diff --git a/MPSCNNHelloWorld/README.md b/MPSCNNHelloWorld/README.md new file mode 100644 index 00000000..4ae91f9b --- /dev/null +++ b/MPSCNNHelloWorld/README.md @@ -0,0 +1,30 @@ +# MPSCNNHelloWorld: Simple Digit Detection Convolution Neural Networks (CNN) + +This sample is a port of the open source library, TensorFlow trained networks trained on MNIST Dataset (http://yann.lecun.com/exdb/mnist/) via inference using Metal Performance Shaders. +The sample demonstrates how to encode different layers to the GPU and perform image recognition using trained parameters(weights and bias) that have been fetched from, pre-trained and saved network on TensorFlow. + +The Single Network can be found at: +https://www.tensorflow.org/versions/r0.8/tutorials/mnist/beginners/index.html#mnist-for-ml-beginners + +The Deep Network can be found at: +https://www.tensorflow.org/versions/r0.8/tutorials/mnist/pros/index.html#deep-mnist-for-experts + +The network parameters are stored a binary .dat files that are memory-mapped when needed. + +## Requirements + +### Build + +Xcode 8.0 or later; iOS 10.0 SDK or later + +### Runtime + +iOS 10.0 or later + +### Device Feature Set + +iOS GPU Family 2 v1 +iOS GPU Family 2 v2 +iOS GPU Family 3 v1 + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/MPSMatrixMultiplication/LICENSE.txt b/MPSMatrixMultiplication/LICENSE.txt new file mode 100644 index 00000000..49653973 --- /dev/null +++ b/MPSMatrixMultiplication/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: MPSMatrixMultiplication: Creating and Multiplying Matrices in Metal +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) 2016 Apple Inc. All Rights Reserved. diff --git a/MPSMatrixMultiplication/MPSMatrixMultiplication.xcodeproj/project.pbxproj b/MPSMatrixMultiplication/MPSMatrixMultiplication.xcodeproj/project.pbxproj new file mode 100644 index 00000000..a6d613c1 --- /dev/null +++ b/MPSMatrixMultiplication/MPSMatrixMultiplication.xcodeproj/project.pbxproj @@ -0,0 +1,307 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 94175FA11D481ADF00ED77E6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94175FA01D481ADF00ED77E6 /* AppDelegate.swift */; }; + 94175FA31D481ADF00ED77E6 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94175FA21D481ADF00ED77E6 /* ViewController.swift */; }; + 94175FA61D481ADF00ED77E6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 94175FA41D481ADF00ED77E6 /* Main.storyboard */; }; + 94175FA81D481ADF00ED77E6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 94175FA71D481ADF00ED77E6 /* Assets.xcassets */; }; + 94175FAB1D481ADF00ED77E6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 94175FA91D481ADF00ED77E6 /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 94175F9D1D481ADF00ED77E6 /* MPSMatrixMultiplication.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MPSMatrixMultiplication.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 94175FA01D481ADF00ED77E6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 94175FA21D481ADF00ED77E6 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 94175FA51D481ADF00ED77E6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 94175FA71D481ADF00ED77E6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 94175FAA1D481ADF00ED77E6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 94175FAC1D481ADF00ED77E6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B5CBACE41D48AC4600FF2CE7 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 94175F9A1D481ADF00ED77E6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 94175F941D481ADF00ED77E6 = { + isa = PBXGroup; + children = ( + B5CBACE41D48AC4600FF2CE7 /* README.md */, + 94175F9F1D481ADF00ED77E6 /* MPSMatrixMultiplication */, + 94175F9E1D481ADF00ED77E6 /* Products */, + ); + sourceTree = ""; + }; + 94175F9E1D481ADF00ED77E6 /* Products */ = { + isa = PBXGroup; + children = ( + 94175F9D1D481ADF00ED77E6 /* MPSMatrixMultiplication.app */, + ); + name = Products; + sourceTree = ""; + }; + 94175F9F1D481ADF00ED77E6 /* MPSMatrixMultiplication */ = { + isa = PBXGroup; + children = ( + 94175FA01D481ADF00ED77E6 /* AppDelegate.swift */, + 94175FA21D481ADF00ED77E6 /* ViewController.swift */, + 94175FA41D481ADF00ED77E6 /* Main.storyboard */, + 94175FA71D481ADF00ED77E6 /* Assets.xcassets */, + 94175FA91D481ADF00ED77E6 /* LaunchScreen.storyboard */, + 94175FAC1D481ADF00ED77E6 /* Info.plist */, + ); + path = MPSMatrixMultiplication; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 94175F9C1D481ADF00ED77E6 /* MPSMatrixMultiplication */ = { + isa = PBXNativeTarget; + buildConfigurationList = 94175FAF1D481ADF00ED77E6 /* Build configuration list for PBXNativeTarget "MPSMatrixMultiplication" */; + buildPhases = ( + 94175F991D481ADF00ED77E6 /* Sources */, + 94175F9A1D481ADF00ED77E6 /* Frameworks */, + 94175F9B1D481ADF00ED77E6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MPSMatrixMultiplication; + productName = MPSMatrixMultiplication; + productReference = 94175F9D1D481ADF00ED77E6 /* MPSMatrixMultiplication.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 94175F951D481ADF00ED77E6 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Apple Inc."; + TargetAttributes = { + 94175F9C1D481ADF00ED77E6 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 94175F981D481ADF00ED77E6 /* Build configuration list for PBXProject "MPSMatrixMultiplication" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 94175F941D481ADF00ED77E6; + productRefGroup = 94175F9E1D481ADF00ED77E6 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 94175F9C1D481ADF00ED77E6 /* MPSMatrixMultiplication */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 94175F9B1D481ADF00ED77E6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 94175FAB1D481ADF00ED77E6 /* LaunchScreen.storyboard in Resources */, + 94175FA81D481ADF00ED77E6 /* Assets.xcassets in Resources */, + 94175FA61D481ADF00ED77E6 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 94175F991D481ADF00ED77E6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 94175FA31D481ADF00ED77E6 /* ViewController.swift in Sources */, + 94175FA11D481ADF00ED77E6 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 94175FA41D481ADF00ED77E6 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 94175FA51D481ADF00ED77E6 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 94175FA91D481ADF00ED77E6 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 94175FAA1D481ADF00ED77E6 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 94175FAD1D481ADF00ED77E6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 94175FAE1D481ADF00ED77E6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 94175FB01D481ADF00ED77E6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = MPSMatrixMultiplication/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.MPSMatrixMultiplication"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 94175FB11D481ADF00ED77E6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = MPSMatrixMultiplication/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.MPSMatrixMultiplication"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 94175F981D481ADF00ED77E6 /* Build configuration list for PBXProject "MPSMatrixMultiplication" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 94175FAD1D481ADF00ED77E6 /* Debug */, + 94175FAE1D481ADF00ED77E6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 94175FAF1D481ADF00ED77E6 /* Build configuration list for PBXNativeTarget "MPSMatrixMultiplication" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 94175FB01D481ADF00ED77E6 /* Debug */, + 94175FB11D481ADF00ED77E6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 94175F951D481ADF00ED77E6 /* Project object */; +} diff --git a/MPSMatrixMultiplication/MPSMatrixMultiplication/AppDelegate.swift b/MPSMatrixMultiplication/MPSMatrixMultiplication/AppDelegate.swift new file mode 100644 index 00000000..47d6b637 --- /dev/null +++ b/MPSMatrixMultiplication/MPSMatrixMultiplication/AppDelegate.swift @@ -0,0 +1,15 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate for the App + */ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? +} diff --git a/MPSMatrixMultiplication/MPSMatrixMultiplication/Assets.xcassets/AppIcon.appiconset/Contents.json b/MPSMatrixMultiplication/MPSMatrixMultiplication/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..118c98f7 --- /dev/null +++ b/MPSMatrixMultiplication/MPSMatrixMultiplication/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "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" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/MPSMatrixMultiplication/MPSMatrixMultiplication/Base.lproj/LaunchScreen.storyboard b/MPSMatrixMultiplication/MPSMatrixMultiplication/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..c9054d1a --- /dev/null +++ b/MPSMatrixMultiplication/MPSMatrixMultiplication/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MPSMatrixMultiplication/MPSMatrixMultiplication/Base.lproj/Main.storyboard b/MPSMatrixMultiplication/MPSMatrixMultiplication/Base.lproj/Main.storyboard new file mode 100644 index 00000000..973a04b8 --- /dev/null +++ b/MPSMatrixMultiplication/MPSMatrixMultiplication/Base.lproj/Main.storyboard @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MPSMatrixMultiplication/MPSMatrixMultiplication/Info.plist b/MPSMatrixMultiplication/MPSMatrixMultiplication/Info.plist new file mode 100644 index 00000000..6905cc67 --- /dev/null +++ b/MPSMatrixMultiplication/MPSMatrixMultiplication/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/MPSMatrixMultiplication/MPSMatrixMultiplication/ViewController.swift b/MPSMatrixMultiplication/MPSMatrixMultiplication/ViewController.swift new file mode 100644 index 00000000..6066c9f6 --- /dev/null +++ b/MPSMatrixMultiplication/MPSMatrixMultiplication/ViewController.swift @@ -0,0 +1,125 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Sample code demonstrating matrix multiplication using the Metal Performance Shaders Framework. + */ + +import UIKit +import MetalPerformanceShaders + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + /* + Matrix multiplication parameters. This code performs the operation: + + C = A * B + + Where C is M x N, A is M x K, and B is K x N. M = N = K = 1024. + + MPSMatrixMultiplication kernels are initialized with parameters for a + generalized matrix multiplication in the same sense as the C BLAS + level 3 *gemm routines. + + This operation uses alpha = 1.0 and beta = 0.0. No matrices are + transposed. + + MPSMatrix objects use row-major ordering. + */ + let M = 1024 + let N = 1024 + let K = 1024 + let alpha = 1.0 + let beta = 0.0 + + // Use the default system device and an associated command queue. + let device = MTLCreateSystemDefaultDevice()! + let commandQueue = device.makeCommandQueue() + + /* + A MPSMatrixDescriptor object will be used to specify the matrix + properties to the MPSMatrix initialization routines. + */ + var matrixDescriptor: MPSMatrixDescriptor + + /* + All data is referenced by MTLBuffer objects. There are three MTLBuffer + objects, two for the input arrays and one for the output array. + */ + + /* + Each MTLBuffer object requires only enough storage to hold its data. + In order to achieve best performance more space may be required. This + amount of space, in bytes, may be determined by calling the + MPSMatrixDescriptor method rowBytes(fromColumns:dataType:). + */ + + // Each row of A has K values. + let ARowBytes = MPSMatrixDescriptor.rowBytes(fromColumns: K, dataType: MPSDataType.float32) + + // Each row of B has N values. + let BRowBytes = MPSMatrixDescriptor.rowBytes(fromColumns: N, dataType: MPSDataType.float32) + + // Each row of C has N values. + let CRowBytes = MPSMatrixDescriptor.rowBytes(fromColumns:N, dataType: MPSDataType.float32) + + // Create the buffers with the recommended sizes. + let ABuffer = device.makeBuffer(length: M * ARowBytes) + let BBuffer = device.makeBuffer(length: K * BRowBytes) + let CBuffer = device.makeBuffer(length: M * CRowBytes) + + /* + All buffers are encapsulated in MPSMatrix objects. Each MPSMatrix + object is created with its associated buffer and an MPSMatrixDescriptor + object which specifies dimension and type information for the matrix. + */ + + // The 'A' matrix. + matrixDescriptor = MPSMatrixDescriptor(dimensions: M, + columns: K, + rowBytes: ARowBytes, + dataType: MPSDataType.float32) + let A = MPSMatrix(buffer: ABuffer, descriptor: matrixDescriptor) + + // The 'B' matrix. + matrixDescriptor.rows = K + matrixDescriptor.columns = N + matrixDescriptor.rowBytes = BRowBytes + let B = MPSMatrix(buffer: BBuffer, descriptor: matrixDescriptor) + + // The 'C' matrix. + matrixDescriptor.rows = M + matrixDescriptor.rowBytes = CRowBytes + let C = MPSMatrix(buffer: CBuffer, descriptor: matrixDescriptor) + + /* + Create a kernel to perform generalized matrix multiplication on the + system device using the desired parameters. + */ + let sgemmKernel = MPSMatrixMultiplication(device: device, + transposeLeft: false, + transposeRight: false, + resultRows: M, + resultColumns: N, + interiorColumns: K, + alpha: alpha, + beta: beta) + + // Create a command buffer in the queue. + let commandBuffer = commandQueue.makeCommandBuffer() + + // Encode the kernel to the command buffer. + sgemmKernel.encode(commandBuffer:commandBuffer, + leftMatrix: A, + rightMatrix: B, + resultMatrix: C) + + // Commit the buffer and wait for it to complete. + commandBuffer.commit() + commandBuffer.waitUntilCompleted() + } +} diff --git a/MPSMatrixMultiplication/README.md b/MPSMatrixMultiplication/README.md new file mode 100644 index 00000000..cd97f640 --- /dev/null +++ b/MPSMatrixMultiplication/README.md @@ -0,0 +1,21 @@ +# MPSMatrixMultiplication: Creating and Multiplying Matrices in Metal + +The Metal Performance Shaders Framework provides functions for performing generalized matrix multiplication. This sample illustrates how to create MPSMatrix objects and compute their product using MPSMatrixMultiplication compute kernels. + +## Requirements + +### Build + +Xcode 8.0 or later; iOS 10.0 SDK or later + +### Runtime + +iOS 10.0 or later + +### Device Feature Set + +iOS GPU Family 2 v1 +iOS GPU Family 2 v2 +iOS GPU Family 3 v1 + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/MetalImageFilters/LICENSE.txt b/MetalImageFilters/LICENSE.txt new file mode 100644 index 00000000..a61824eb --- /dev/null +++ b/MetalImageFilters/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: Metal Image Filters: Using the image filters provided by the Metal Performance Shaders framework. +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) 2016 Apple Inc. All Rights Reserved. diff --git a/MetalImageFilters/MetalImageFilters.xcodeproj/project.pbxproj b/MetalImageFilters/MetalImageFilters.xcodeproj/project.pbxproj new file mode 100644 index 00000000..a3973991 --- /dev/null +++ b/MetalImageFilters/MetalImageFilters.xcodeproj/project.pbxproj @@ -0,0 +1,349 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 3E4369CB1D26BC66000A490E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E4369CA1D26BC66000A490E /* AppDelegate.swift */; }; + 3E4369CD1D26BC66000A490E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E4369CC1D26BC66000A490E /* ViewController.swift */; }; + 3E4369D01D26BC66000A490E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3E4369CE1D26BC66000A490E /* Main.storyboard */; }; + 3E4369D21D26BC66000A490E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3E4369D11D26BC66000A490E /* Assets.xcassets */; }; + 3E4369D51D26BC66000A490E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3E4369D31D26BC66000A490E /* LaunchScreen.storyboard */; }; + 3E6FAFC31D2A1B3800D83910 /* ImageFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E6FAFC21D2A1B3800D83910 /* ImageFilters.swift */; }; + 3E8F321C1D3CA98F000DECF0 /* VideoImageTextureProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E8F321B1D3CA98F000DECF0 /* VideoImageTextureProvider.swift */; }; + 3EABC9721D447E5500C3EDC3 /* StillImageTextureProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EABC9711D447E5500C3EDC3 /* StillImageTextureProvider.swift */; }; + 3EB84EBE1D5C7969001E545D /* final0.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 3EB84EBD1D5C7969001E545D /* final0.jpg */; }; + 63FA007C1D5E6E45009DEF93 /* final2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 63FA007A1D5E6E45009DEF93 /* final2.jpg */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 3E4369C71D26BC66000A490E /* MetalImageFilters.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MetalImageFilters.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 3E4369CA1D26BC66000A490E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 3E4369CC1D26BC66000A490E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 3E4369CF1D26BC66000A490E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 3E4369D11D26BC66000A490E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3E4369D41D26BC66000A490E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 3E4369D61D26BC66000A490E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3E6FAFC21D2A1B3800D83910 /* ImageFilters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFilters.swift; sourceTree = ""; }; + 3E8F321B1D3CA98F000DECF0 /* VideoImageTextureProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoImageTextureProvider.swift; sourceTree = ""; }; + 3EABC9711D447E5500C3EDC3 /* StillImageTextureProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StillImageTextureProvider.swift; sourceTree = ""; }; + 3EB84EBD1D5C7969001E545D /* final0.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = final0.jpg; path = Images/final0.jpg; sourceTree = ""; }; + 63FA007A1D5E6E45009DEF93 /* final2.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = final2.jpg; path = Images/final2.jpg; sourceTree = ""; }; + B5DA4CD31D88980200D4C7AA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3E4369C41D26BC66000A490E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3E4369BE1D26BC66000A490E = { + isa = PBXGroup; + children = ( + B5DA4CD31D88980200D4C7AA /* README.md */, + 3E4369C91D26BC66000A490E /* MetalImageFilters */, + 3E4369C81D26BC66000A490E /* Products */, + ); + sourceTree = ""; + }; + 3E4369C81D26BC66000A490E /* Products */ = { + isa = PBXGroup; + children = ( + 3E4369C71D26BC66000A490E /* MetalImageFilters.app */, + ); + name = Products; + sourceTree = ""; + }; + 3E4369C91D26BC66000A490E /* MetalImageFilters */ = { + isa = PBXGroup; + children = ( + 6390FBBD1D53C5D3004FB90B /* Images */, + 3E8F32181D3CA854000DECF0 /* ImageTextureProviders */, + 3E4369CA1D26BC66000A490E /* AppDelegate.swift */, + 3E4369CC1D26BC66000A490E /* ViewController.swift */, + 3E6FAFC21D2A1B3800D83910 /* ImageFilters.swift */, + 3E4369CE1D26BC66000A490E /* Main.storyboard */, + 3E4369D11D26BC66000A490E /* Assets.xcassets */, + 3E4369D31D26BC66000A490E /* LaunchScreen.storyboard */, + 3E4369D61D26BC66000A490E /* Info.plist */, + ); + path = MetalImageFilters; + sourceTree = ""; + }; + 3E8F32181D3CA854000DECF0 /* ImageTextureProviders */ = { + isa = PBXGroup; + children = ( + 3E8F321B1D3CA98F000DECF0 /* VideoImageTextureProvider.swift */, + 3EABC9711D447E5500C3EDC3 /* StillImageTextureProvider.swift */, + ); + path = ImageTextureProviders; + sourceTree = ""; + }; + 6390FBBD1D53C5D3004FB90B /* Images */ = { + isa = PBXGroup; + children = ( + 3EB84EBD1D5C7969001E545D /* final0.jpg */, + 63FA007A1D5E6E45009DEF93 /* final2.jpg */, + ); + name = Images; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3E4369C61D26BC66000A490E /* MetalImageFilters */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3E4369D91D26BC66000A490E /* Build configuration list for PBXNativeTarget "MetalImageFilters" */; + buildPhases = ( + 3E4369C31D26BC66000A490E /* Sources */, + 3E4369C41D26BC66000A490E /* Frameworks */, + 3E4369C51D26BC66000A490E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MetalImageFilters; + productName = MetaPerformanceShaderShowcase; + productReference = 3E4369C71D26BC66000A490E /* MetalImageFilters.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3E4369BF1D26BC66000A490E /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 3E4369C61D26BC66000A490E = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 3E4369C21D26BC66000A490E /* Build configuration list for PBXProject "MetalImageFilters" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3E4369BE1D26BC66000A490E; + productRefGroup = 3E4369C81D26BC66000A490E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3E4369C61D26BC66000A490E /* MetalImageFilters */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3E4369C51D26BC66000A490E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3E4369D51D26BC66000A490E /* LaunchScreen.storyboard in Resources */, + 3E4369D21D26BC66000A490E /* Assets.xcassets in Resources */, + 3E4369D01D26BC66000A490E /* Main.storyboard in Resources */, + 3EB84EBE1D5C7969001E545D /* final0.jpg in Resources */, + 63FA007C1D5E6E45009DEF93 /* final2.jpg in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3E4369C31D26BC66000A490E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3E8F321C1D3CA98F000DECF0 /* VideoImageTextureProvider.swift in Sources */, + 3E6FAFC31D2A1B3800D83910 /* ImageFilters.swift in Sources */, + 3E4369CD1D26BC66000A490E /* ViewController.swift in Sources */, + 3EABC9721D447E5500C3EDC3 /* StillImageTextureProvider.swift in Sources */, + 3E4369CB1D26BC66000A490E /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 3E4369CE1D26BC66000A490E /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 3E4369CF1D26BC66000A490E /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 3E4369D31D26BC66000A490E /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 3E4369D41D26BC66000A490E /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 3E4369D71D26BC66000A490E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = 2; + }; + name = Debug; + }; + 3E4369D81D26BC66000A490E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = 2; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3E4369DA1D26BC66000A490E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/MetalImageFilters/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.MetalImageFilters"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3E4369DB1D26BC66000A490E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/MetalImageFilters/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.MetalImageFilters"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3E4369C21D26BC66000A490E /* Build configuration list for PBXProject "MetalImageFilters" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3E4369D71D26BC66000A490E /* Debug */, + 3E4369D81D26BC66000A490E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3E4369D91D26BC66000A490E /* Build configuration list for PBXNativeTarget "MetalImageFilters" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3E4369DA1D26BC66000A490E /* Debug */, + 3E4369DB1D26BC66000A490E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3E4369BF1D26BC66000A490E /* Project object */; +} diff --git a/MetalImageFilters/MetalImageFilters/AppDelegate.swift b/MetalImageFilters/MetalImageFilters/AppDelegate.swift new file mode 100644 index 00000000..a9bc8694 --- /dev/null +++ b/MetalImageFilters/MetalImageFilters/AppDelegate.swift @@ -0,0 +1,14 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application Delegate for MetalImageFilters. + */ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? +} diff --git a/MetalImageFilters/MetalImageFilters/Assets.xcassets/AppIcon.appiconset/Contents.json b/MetalImageFilters/MetalImageFilters/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..843b8977 --- /dev/null +++ b/MetalImageFilters/MetalImageFilters/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,43 @@ +{ + "images" : [ + { + "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" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/MetalImageFilters/MetalImageFilters/Assets.xcassets/Contents.json b/MetalImageFilters/MetalImageFilters/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/MetalImageFilters/MetalImageFilters/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/MetalImageFilters/MetalImageFilters/Base.lproj/LaunchScreen.storyboard b/MetalImageFilters/MetalImageFilters/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..fdf3f97d --- /dev/null +++ b/MetalImageFilters/MetalImageFilters/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MetalImageFilters/MetalImageFilters/Base.lproj/Main.storyboard b/MetalImageFilters/MetalImageFilters/Base.lproj/Main.storyboard new file mode 100644 index 00000000..27a4451c --- /dev/null +++ b/MetalImageFilters/MetalImageFilters/Base.lproj/Main.storyboard @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MetalImageFilters/MetalImageFilters/ImageFilters.swift b/MetalImageFilters/MetalImageFilters/ImageFilters.swift new file mode 100644 index 00000000..2b6c6d29 --- /dev/null +++ b/MetalImageFilters/MetalImageFilters/ImageFilters.swift @@ -0,0 +1,483 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Image Filters for MetalImageFilters. + Each image filter is represented as a class that conforms to the CommandBufferEncodable protocol. + The protocol ensures that each image filter is initialized with a Metal device and obtains the necessary Metal objects to encode its MetalPerformanceShaders operations. + The image filters can be single-pass or multi-pass operations on Metal buffers and/or textures. + The initial source texture is always the original input image read from a file or video. + The final destination texture is always the filtered output image written to the MTKView's drawable. + */ + +import UIKit +import MetalPerformanceShaders +import MetalKit + +// MARK: Image Filters +/** Blits the source texture into the destination texture. + No image filter is applied. + */ +class PassThrough: CommandBufferEncodable { + required init(device: MTLDevice) { + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + /* destinationTexture is a 'let' constant and thus,the operation "destinationTexture = sourceTexture" is not allowed. + Instead, a blit operation is performed to copy the contents from sourceTexture to destinationTexture. + */ + let blitCommandEncoder = commandBuffer.makeBlitCommandEncoder() + blitCommandEncoder.copy(from: sourceTexture, + sourceSlice: 0, + sourceLevel: 0, + sourceOrigin: MTLOriginMake(0, 0, 0), + sourceSize: MTLSizeMake(sourceTexture.width, sourceTexture.height, 1), + to: destinationTexture, + destinationSlice: 0, + destinationLevel: 0, + destinationOrigin: MTLOriginMake(0, 0, 0)) + blitCommandEncoder.endEncoding() + } +} + +/** Applies a Gaussian blur with a sigma value of 0.5. + This is a pre-packaged convolution filter. + */ +class GaussianBlur: CommandBufferEncodable { + let gaussian: MPSImageGaussianBlur + + required init(device: MTLDevice) { + gaussian = MPSImageGaussianBlur(device: device, + sigma: 5.0) + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + gaussian.encode(commandBuffer: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: destinationTexture) + } +} + +/** Applies a median filter with a diameter value of 5. + This is a pre-packaged nonlinear filter. + */ +class Median: CommandBufferEncodable { + let median: MPSImageMedian + + required init(device: MTLDevice) { + median = MPSImageMedian(device: device, + kernelDiameter: 5) + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + median.encode(commandBuffer: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: destinationTexture) + } +} + +/** Applies a Laplacian filter with a clamped edge mode. + This is a pre-packaged convolution filter. + */ +class Laplacian: CommandBufferEncodable { + let laplacian: MPSImageLaplacian + + required init(device: MTLDevice) { + laplacian = MPSImageLaplacian(device: device) + laplacian.edgeMode = .clamp + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + laplacian.encode(commandBuffer: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: destinationTexture) + } +} + +/** Applies a Sobel filter with a clamped edge mode. + This is a pre-packaged convolution filter. + */ +class Sobel: CommandBufferEncodable { + let sobel: MPSImageSobel + + required init(device: MTLDevice) { + sobel = MPSImageSobel(device: device) + sobel.edgeMode = .clamp + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + sobel.encode(commandBuffer: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: destinationTexture) + } +} + +/** Applies a binary filter with a threshold value of 0.5 (minimum value = 0.0; maximum value = 1.0). + This is a pre-packaged nonlinear filter. + */ +class ThresholdBinary: CommandBufferEncodable { + let threshold: MPSImageThresholdBinary + + required init(device: MTLDevice) { + threshold = MPSImageThresholdBinary(device: device, + thresholdValue: 0.5, + maximumValue: 1.0, + linearGrayColorTransform: nil) + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + threshold.encode(commandBuffer: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: destinationTexture) + } +} + +/** Applies an emboss filter with a clamped edge mode. + This is a custom convolution filter. + */ +class ConvolutionEmboss: CommandBufferEncodable { + let convolution: MPSImageConvolution + + // These kernel weights create a carving effect that makes the image appear to have physical depth. + let weights: [Float] = [ + -2, 0, 0, + 0, 1, 0, + 0, 0, 2 + ] + + required init(device: MTLDevice) { + convolution = MPSImageConvolution(device: device, + kernelWidth: 3, + kernelHeight: 3, + weights: weights) + convolution.edgeMode = .clamp + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + convolution.encode(commandBuffer: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: destinationTexture) + } +} + +/** Applies a sharpen filter with a clamped edge mode. + This is a custom convolution filter. + */ +class ConvolutionSharpen: CommandBufferEncodable { + let convolution: MPSImageConvolution + + // These kernel weights accentuate image details and reduce blur. + let weights: [Float] = [ + -0.5, -1.0, -0.5, + -1.0, 7.0, -1.0, + -0.5, -1.0, -0.5 + ] + + required init(device: MTLDevice) { + convolution = MPSImageConvolution(device: device, + kernelWidth: 3, + kernelHeight: 3, + weights: weights) + convolution.edgeMode = .clamp + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + convolution.encode(commandBuffer: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: destinationTexture) + } +} + +/** Applies a dilation filter with a circular shaped kernel. + This is a pre-packaged morphology filter. + */ +class DilateBokeh: CommandBufferEncodable { + let device: MTLDevice + let bokehRadius = 7 + + /* A dilation filter requires an array of values known as the "strucuring element" or "probe", which defines which surrounding pixels to sample and their weight. + This code constructs a 2D probe which is twice the size of the bokeh radius in width and height. + Elements within the radius are set to 0 and elements outside of the radius are set to 1, which creates a circle filled with zeros. + The resulting filter gives an effect similar to a photographic "bokeh" effect, where out-of-focus bright areas dilate to circles. + */ + lazy var dilate: MPSImageDilate = { + var probe = [Float]() + let size = self.bokehRadius * 2 + 1 + let mid = Float(size) / 2 + + for i in 0 ..< size + { + for j in 0 ..< size + { + let x = abs(Float(i) - mid) + let y = abs(Float(j) - mid) + let element: Float = hypot(x, y) < Float(self.bokehRadius) ? 0.0 : 1.0 + probe.append(element) + } + } + + let dilate = MPSImageDilate( + device: self.device, + kernelWidth: size, + kernelHeight: size, + values: probe) + + return dilate + }() + + required init(device: MTLDevice) { + self.device = device + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + dilate.encode(commandBuffer: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: destinationTexture) + } +} + +/** Applies a dilation followed by an erosion. + This is a custom filter composed of two morphology filters. + */ +class MorphologyClosing: CommandBufferEncodable { + // The MPSImageAreaMax and MPSImageAreaMin filters are specialized versions of the MPSImageDilate and MPSImageErode filters, with rectangular kernels. + let max: MPSImageAreaMax + let min: MPSImageAreaMin + + required init(device: MTLDevice) { + max = MPSImageAreaMax(device: device, + kernelWidth: 9, + kernelHeight: 9) + + min = MPSImageAreaMin(device: device, + kernelWidth: 9, + kernelHeight: 9) + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + /* The MorphologyClosing filter is a two-pass operation. + The intermediate texture acts as the output for the first pass and the input for the second pass, therefore its usage is both write and read. + Its contents are only accessed by the GPU and therefore its storage mode is private. + */ + + let intermediateTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: sourceTexture.pixelFormat, + width: sourceTexture.width, + height: sourceTexture.height, + mipmapped: false) + intermediateTextureDescriptor.usage = [.shaderWrite, .shaderRead] + intermediateTextureDescriptor.storageMode = .private + let intermediateTexture = commandBuffer.device.makeTexture(descriptor: intermediateTextureDescriptor) + + // Applies the dilation to the source texture and outputs the intermediate results to the intermediate texture. + max.encode(commandBuffer: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: intermediateTexture) + + // Applies the erosion to the intermediate texture and outputs the final results to the destination texture. + min.encode(commandBuffer: commandBuffer, + sourceTexture: intermediateTexture, + destinationTexture: destinationTexture) + } +} + +/** Equalizes the histogram of an image. + This is a custom filter composed of two histogram filters. + */ +class HistogramEqualization: CommandBufferEncodable { + /* Histogram equalization flattens an images histogram and stretches it to fill the entire tonal range. + This technique is useful for revealing detail in images with close contrast values. + */ + let device: MTLDevice + let calculation: MPSImageHistogram + let equalization: MPSImageHistogramEqualization + + // Information to compute the histogram for the channels of an image. + var histogramInfo = MPSImageHistogramInfo( + numberOfHistogramEntries: 256, + histogramForAlpha: false, + minPixelValue: vector_float4(0,0,0,0), + maxPixelValue: vector_float4(1,1,1,1)) + + required init(device: MTLDevice) { + self.device = device + + /* Performing histogram equalization requires two filters: + - An MPSImageHistogram filter which calculates the image's current histogram + - An MPSImageHistogramEqualization filter which calculates and applies the equalization. + */ + calculation = MPSImageHistogram(device: device, + histogramInfo: &histogramInfo) + + equalization = MPSImageHistogramEqualization(device: device, + histogramInfo: &histogramInfo) + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + /* The length of the histogram buffer is calculated as follows: + Number of Histogram Entries * Size of 32-bit unsigned integer * Number of Image Channels + However, it is recommended that you use the histogramSize(forSourceFormat:) method to calculate the buffer length. + The buffer storage mode is private because its contents are only written by the GPU and never accessed by the CPU. + */ + let bufferLength = calculation.histogramSize(forSourceFormat: sourceTexture.pixelFormat) + let histogramInfoBuffer = device.makeBuffer(length: bufferLength, options: [.storageModePrivate]) + print("Equalization Buffer Length: \(bufferLength)") + + // Performing equalization with MPS is a three stage operation: + + // 1: The image's histogram is calculated and passed to an MPSImageHistogramInfo object. + calculation.encode(to: commandBuffer, + sourceTexture: sourceTexture, + histogram: histogramInfoBuffer, + histogramOffset: 0) + + // 2: The equalization filter's encodeTransform method creates an image transform which is used to equalize the distribution of the histogram of the source image. + equalization.encodeTransform(to: commandBuffer, + sourceTexture: sourceTexture, + histogram: histogramInfoBuffer, + histogramOffset: 0) + + // 3: The equalization filter's encode method applies the equalization transform to the source texture and and writes the output to the destination texture. + equalization.encode(commandBuffer: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: destinationTexture) + } +} + +/** Matches the histogram of an image to another. + This is a custom filter composed of two histogram filters. + */ +class HistogramSpecification: CommandBufferEncodable { + /* Histogram specification takes the histogram of one image and applies it to another. + This technique is useful for color matching two images prior to compositing them together. + */ + let device: MTLDevice + let calculation: MPSImageHistogram + let specification: MPSImageHistogramSpecification + + // Information to compute the histogram for the channels of an image. + var histogramInfo = MPSImageHistogramInfo( + numberOfHistogramEntries: 256, + histogramForAlpha: false, + minPixelValue: vector_float4(0,0,0,0), + maxPixelValue: vector_float4(1,1,1,1)) + + // The texture which will supply the source histogram for the specification operation. + lazy var imageTexture: MTLTexture = { + let image = UIImage(named: "final2.jpg")?.cgImage + let textureLoader = MTKTextureLoader(device: self.device) + // The still image is loaded directly into GPU-accessible memory that is only ever read from. + let options = [ + MTKTextureLoaderOptionTextureStorageMode: MTLStorageMode.private.rawValue, + MTKTextureLoaderOptionTextureUsage: MTLTextureUsage.shaderRead.rawValue, + MTKTextureLoaderOptionSRGB: 0 + ] + return try! textureLoader.newTexture(with: image!, options: options as [String : NSObject]?) + }() + + required init(device: MTLDevice) { + self.device = device + + /* Performing histogram specification requires two filters: + - An MPSImageHistogram filter which calculates the source and destination images' current histograms. + - An MPSImageHistogramSpecification filter which calculates and applies the specification. + */ + calculation = MPSImageHistogram(device: device, + histogramInfo: &histogramInfo) + + specification = MPSImageHistogramSpecification(device: device, + histogramInfo: &histogramInfo) + } + + func encode(to commandBuffer: MTLCommandBuffer, sourceTexture: MTLTexture, destinationTexture: MTLTexture) { + /* The length of the histogram buffer is calculated as follows: + Number of Histogram Entries * Size of 32-bit unsigned integer * Number of Image Channels + However, it is recommended that you use the histogramSize(forSourceFormat:) method to calculate the buffer length. + The buffer storage mode is private because its contents are only written by the GPU and never accessed by the CPU. + */ + let bufferLength = calculation.histogramSize(forSourceFormat: sourceTexture.pixelFormat) + let sourceHistogramInfoBuffer = device.makeBuffer(length: bufferLength, options: [.storageModePrivate]) + let desiredHistogramInfoBuffer = device.makeBuffer(length: bufferLength, options: [.storageModePrivate]) + print("Specification Buffer Length: \(bufferLength)") + + // Performing equalization with MPS is a four stage operation: + + // 1: The histogram of the image to transform is calculated and passed to an MPSImageHistogramInfo object. + calculation.encode(to: commandBuffer, + sourceTexture: sourceTexture, + histogram: sourceHistogramInfoBuffer, + histogramOffset: 0) + + // 2: The histogram of the image to specify from is calculated and passed to an MPSImageHistogramInfo object. + calculation.encode(to: commandBuffer, + sourceTexture: imageTexture, + histogram: desiredHistogramInfoBuffer, + histogramOffset: 0) + + // 3: The specification filter's encodeTransform method creates an image transform which is used to specify the histogram. + specification.encodeTransform(to: commandBuffer, + sourceTexture: sourceTexture, + sourceHistogram: sourceHistogramInfoBuffer, + sourceHistogramOffset: 0, + desiredHistogram: desiredHistogramInfoBuffer, + desiredHistogramOffset: 0) + + // 4: The specification filter's encode method applies the specification transform to the source texture and and writes it to the destination texture + specification.encode(commandBuffer: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: destinationTexture) + } +} + +// MARK: CommandBufferEncodable Protocol +/** A protocol which ensures that each image filter is initialized with a Metal device and can encode its operations with the necessary Metal objects. + */ +protocol CommandBufferEncodable { + init(device: MTLDevice) + + func encode(to commandBuffer: MTLCommandBuffer, + sourceTexture: MTLTexture, + destinationTexture: MTLTexture) +} + +// MARK: SupportedImageFilter Enum +enum SupportedImageFilter: String { + case PassThrough + case GaussianBlur + case Median + case Laplacian + case Sobel + case ThresholdBinary + case ConvolutionEmboss + case ConvolutionSharpen + case DilateBokeh + case MorphologyClosing + case HistogramEqualization + case HistogramSpecification + + static var supportedImageFilterNames: [String] { + let imageFilters: [SupportedImageFilter] = [ + .PassThrough, + .GaussianBlur, + .Median, + .Laplacian, + .Sobel, + .ThresholdBinary, + .ConvolutionEmboss, + .ConvolutionSharpen, + .DilateBokeh, + .MorphologyClosing, + .HistogramEqualization, + .HistogramSpecification + ] + return imageFilters.map{$0.rawValue} + } + + static func imageFilterOfIndex(_ index: Int) -> SupportedImageFilter? { + return imageFilterOfName(supportedImageFilterNames[index]) + } + + static func imageFilterOfName(_ name: String?) -> SupportedImageFilter? { + return SupportedImageFilter(rawValue: name ?? "") + } +} diff --git a/MetalImageFilters/MetalImageFilters/ImageTextureProviders/StillImageTextureProvider.swift b/MetalImageFilters/MetalImageFilters/ImageTextureProviders/StillImageTextureProvider.swift new file mode 100644 index 00000000..a5c7f0fd --- /dev/null +++ b/MetalImageFilters/MetalImageFilters/ImageTextureProviders/StillImageTextureProvider.swift @@ -0,0 +1,37 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Still Image Texture Provider for MetalImageFilters. + Uses the MetalKit texture loader to load an still image into a Metal texture. + */ + +import MetalKit + +/// Uses the MetalKit texture loader to load a still image into a Metal texture. +class StillImageTextureProvider: NSObject { + /// The source texture for image filter operations. + var texture: MTLTexture? + + /// Returns an initialized StillImageTextureProvider object with a source texture, or nil in case of failure. + required init?(device: MTLDevice, imageName: String) { + super.init() + + let loader = MTKTextureLoader(device: device) + let image = UIImage(named: imageName)?.cgImage + // The still image is loaded directly into GPU-accessible memory that is only ever read from. + let options = [ + MTKTextureLoaderOptionTextureStorageMode: MTLStorageMode.private.rawValue, + MTKTextureLoaderOptionTextureUsage: MTLTextureUsage.shaderRead.rawValue, + MTKTextureLoaderOptionSRGB: 0 + ] + do { + let fileTexture = try loader.newTexture(with: image!, options: options as [String : NSObject]?) + texture = fileTexture + } catch let error as NSError { + print("Error loading still image texture: \(error)") + return nil + } + } +} diff --git a/MetalImageFilters/MetalImageFilters/ImageTextureProviders/VideoImageTextureProvider.swift b/MetalImageFilters/MetalImageFilters/ImageTextureProviders/VideoImageTextureProvider.swift new file mode 100644 index 00000000..2f321bbc --- /dev/null +++ b/MetalImageFilters/MetalImageFilters/ImageTextureProviders/VideoImageTextureProvider.swift @@ -0,0 +1,147 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Video Image Texture Provider for MetalImageFilters. + Uses CoreVideo buffers to load a stream of AVFoundation video images into a Metal texture. + The Metal textures are delivered to the View Controller via a protocol delegate. + */ + +import UIKit +import AVFoundation + +// MARK: VideoImageTextureProvider + +/// Provides an interface for sending/receiving a stream of Metal textures. +protocol VideoImageTextureProviderDelegate: class { + func videoImageTextureProvider(_: VideoImageTextureProvider, didProvideTexture texture: MTLTexture) +} + +/// Initializes an AVFoundation capture session for streaming real-time video. +class VideoImageTextureProvider: NSObject { + var textureCache : CVMetalTextureCache? + let captureSession = AVCaptureSession() + let sampleBufferCallbackQueue = DispatchQueue(label: "MetalImageFiltersQueue") + weak var delegate: VideoImageTextureProviderDelegate! + + // MARK: Initialization + + /// Returns an initialized VideoImageTextureProvider object with an associated Metal device and delegate, or nil in case of failure. + required init?(device: MTLDevice, delegate: VideoImageTextureProviderDelegate) { + super.init() + + CVMetalTextureCacheCreate(kCFAllocatorDefault, + nil, + device, + nil, + &textureCache) + self.delegate = delegate + + // Class initialization fails if the capture session could not be initialized. + if(!didInitializeCaptureSession()) { + return nil + } + } + + /// Attempts to initialize a capture session. + func didInitializeCaptureSession() -> Bool { + + /* The capture session preset is fixed at a 960x540 pixel resolution that matches the MTKView pixel resolution. + This ensures screen size compatibility with all target iOS devices, without having to downsample or transform the video image. + */ + captureSession.sessionPreset = AVCaptureSessionPresetiFrame960x540 + + // Use a guard to ensure the method can access a video capture device with a given camera position + guard let camera = AVCaptureDevice.defaultDevice(withDeviceType: .builtInWideAngleCamera, + mediaType: AVMediaTypeVideo, + position: .back) + else { + print("Unable to access camera.") + return false + } + + do { + let input = try AVCaptureDeviceInput(device: camera) + if(captureSession.canAddInput(input)) { + captureSession.addInput(input) + } + else { + print("Unable to add camera input.") + return false + } + } + catch let error as NSError { + print("Error accessing camera input: \(error)") + return false + } + + /* Creates a video data output object with a 32-bit BGRA pixel format. + Setting self to the output object's sample buffer delegate allows this class to respond to every + frame update. + */ + let videoOutput = AVCaptureVideoDataOutput() + videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as AnyHashable: Int(kCVPixelFormatType_32BGRA)] + videoOutput.setSampleBufferDelegate(self, queue: sampleBufferCallbackQueue) + + if captureSession.canAddOutput(videoOutput) { + captureSession.addOutput(videoOutput) + } + else { + print("Unable to add camera input.") + return false + } + + return true + } + + // MARK: Capture Session Controls + func startRunning() { + sampleBufferCallbackQueue.async { + self.captureSession.startRunning() + } + } + + func stopRunning() { + captureSession.stopRunning() + } +} + +// MARK: AVCaptureVideoDataOutputSampleBufferDelegate + +/** Having the VideoImageTextureProvider class conform to the AVCaptureVideoDataOutputSampleBufferDelegate protocol and be the video output object's sample buffer delegate allows it to respond to every video capture frame update. + */ +extension VideoImageTextureProvider: AVCaptureVideoDataOutputSampleBufferDelegate +{ + func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) { + + connection.videoOrientation = AVCaptureVideoOrientation(rawValue: UIApplication.shared.statusBarOrientation.rawValue)! + + guard + let cameraTextureCache = textureCache, + let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return + } + + /** Given a pixel buffer, the following code populates a Metal texture with the contents of the captured video frame. + */ + var cameraTexture: CVMetalTexture? + let cameraTextureWidth = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0) + let cameraTextureHeight = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) + CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, + cameraTextureCache, + pixelBuffer, + nil, + MTLPixelFormat.bgra8Unorm, + cameraTextureWidth, + cameraTextureHeight, + 0, + &cameraTexture) + + if let cameraTexture = cameraTexture, + let metalTexture = CVMetalTextureGetTexture(cameraTexture) { + // Call the delegate method whenever a new video frame has been converted into a Metal texture + delegate.videoImageTextureProvider(self, didProvideTexture: metalTexture) + } + } +} diff --git a/MetalImageFilters/MetalImageFilters/Images/final0.jpg b/MetalImageFilters/MetalImageFilters/Images/final0.jpg new file mode 100644 index 00000000..0e12775e Binary files /dev/null and b/MetalImageFilters/MetalImageFilters/Images/final0.jpg differ diff --git a/MetalImageFilters/MetalImageFilters/Images/final2.jpg b/MetalImageFilters/MetalImageFilters/Images/final2.jpg new file mode 100644 index 00000000..c26e18a0 Binary files /dev/null and b/MetalImageFilters/MetalImageFilters/Images/final2.jpg differ diff --git a/MetalImageFilters/MetalImageFilters/Info.plist b/MetalImageFilters/MetalImageFilters/Info.plist new file mode 100644 index 00000000..ed883b84 --- /dev/null +++ b/MetalImageFilters/MetalImageFilters/Info.plist @@ -0,0 +1,52 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSCameraUsageDescription + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIRequiresFullScreen + + UIStatusBarHidden + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/MetalImageFilters/MetalImageFilters/ViewController.swift b/MetalImageFilters/MetalImageFilters/ViewController.swift new file mode 100644 index 00000000..d0e3658e --- /dev/null +++ b/MetalImageFilters/MetalImageFilters/ViewController.swift @@ -0,0 +1,244 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View Controller for MetalImageFilters. + The filtered images are displayed within a MetalKit view. + The view's UI controls are gesture-driven as follows: + - Swipe left or right to change the current filter. + - Swipe down to display the video image. + - Swipe up to display the still image. + The MetalKit view's draw loop is called manually whenever: + - The still image is swapped into the view (draws once). + - A new video frame is provided (draws at 30 FPS). + */ + +import UIKit +import MetalPerformanceShaders +import MetalKit + +class ViewController: UIViewController { + // MARK: IB Outlets + @IBOutlet weak var mtkView: MTKView! + @IBOutlet weak var filterLabel: UILabel! + + // MARK: Metal Properties + let device = MTLCreateSystemDefaultDevice()! + var commandQueue: MTLCommandQueue! + var sourceTexture: MTLTexture? + + // MARK: Image Texture Providers + lazy var stillImageTextureProvider: StillImageTextureProvider? = { + /* The image file is fixed at a 960x540 pixel resolution that matches the MTKView pixel resolution. + This ensures screen size compatibility with all target iOS devices, without having to downsample or transform the image file. + */ + let provider = StillImageTextureProvider(device: self.device, imageName: "final0.jpg") + return provider + }() + + lazy var videoImageTextureProvider: VideoImageTextureProvider? = { + let provider = VideoImageTextureProvider(device: self.device, delegate: self) + return provider + }() + + // MARK: Image Filters + // Lazily initialized variables for each of the supported filters + lazy var passThrough: PassThrough = { + return PassThrough(device: self.device) + }() + + lazy var gaussianBlur: GaussianBlur = { + return GaussianBlur(device: self.device) + }() + + lazy var median: Median = { + return Median(device: self.device) + }() + + lazy var laplacian: Laplacian = { + return Laplacian(device: self.device) + }() + + lazy var sobel: Sobel = { + return Sobel(device: self.device) + }() + + lazy var thresholdBinary: ThresholdBinary = { + return ThresholdBinary(device: self.device) + }() + + lazy var convolutionEmboss: ConvolutionEmboss = { + return ConvolutionEmboss(device: self.device) + }() + + lazy var convolutionSharpen: ConvolutionSharpen = { + return ConvolutionSharpen(device: self.device) + }() + + lazy var dilateBokeh: DilateBokeh = { + return DilateBokeh(device: self.device) + }() + + lazy var morphologyClosing: MorphologyClosing = { + return MorphologyClosing(device: self.device) + }() + + lazy var histogramEqualization: HistogramEqualization = { + return HistogramEqualization(device: self.device) + }() + + lazy var histogramSpecification: HistogramSpecification = { + return HistogramSpecification(device: self.device) + }() + + // MARK: Selection properties + /// This property cycles through the supported image filters and updates the UI accordingly. + var imageFilterIndex = 0 { + didSet { + if imageFilterIndex < 0 { + imageFilterIndex = SupportedImageFilter.supportedImageFilterNames.count - 1 + } + else { + imageFilterIndex = imageFilterIndex % SupportedImageFilter.supportedImageFilterNames.count + } + filterLabel.text = (SupportedImageFilter.imageFilterOfIndex(imageFilterIndex)?.rawValue)! + " Filter" + } + } + + /** This property toggles between the video and still image. + When the video is running, the delegate method calls the MTKView's draw() method. + When the video is not running, the else clause calls the MTKView's draw() method. + */ + var videoIsRunning = false { + didSet { + if videoIsRunning == true { + videoImageTextureProvider?.startRunning() + } + else { + videoImageTextureProvider?.stopRunning() + sourceTexture = stillImageTextureProvider?.texture + mtkView.draw() + } + } + } + + // MARK: View Controller Life Cycle + override func viewDidLoad() { + super.viewDidLoad() + + imageFilterIndex = 0 + setupMetal() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + /** The content is rendered *after* the view has appeared. + This allows the MTKView to set up properly and get the current drawable. + The MTKView's draw() method is called once after the still image has been loaded. + */ + sourceTexture = stillImageTextureProvider?.texture + mtkView.draw() + } + + // MARK: Metal Setup + private func setupMetal() { + commandQueue = device.makeCommandQueue() + + /** MetalPerformanceShaders is a compute-based framework. + This means that the drawable's texture is *written* to, not *rendered* to. + The destination texture for all image filter operations is not a traditional framebuffer. + */ + mtkView.framebufferOnly = false + + /** This sample manages the MTKView's draw loop manually (i.e. the draw() method is called explicitly). + For the still image, the content only needs to be filtered once. + For the video image, the content only needs to be filtered whenever the camera provides a new video frame. + */ + mtkView.isPaused = true + + mtkView.delegate = self + mtkView.device = device + mtkView.colorPixelFormat = .bgra8Unorm + } + + // MARK: IB Actions + @IBAction func didSwipeLeft(sender: UISwipeGestureRecognizer) { + imageFilterIndex += 1 + if(!videoIsRunning) { + mtkView.draw() + } + } + + @IBAction func didSwipeRight(sender: UISwipeGestureRecognizer) { + imageFilterIndex -= 1 + if(!videoIsRunning) { + mtkView.draw() + } + } + + @IBAction func didSwipeUpOrDown(sender: UISwipeGestureRecognizer) { + videoIsRunning = !videoIsRunning + } +} + +// MARK: MTKViewDelegate +extension ViewController: MTKViewDelegate { + func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + } + + func draw(in view: MTKView) { + // Use a guard to ensure the method has a valid current drawable, a source texture, and an image filter. + guard + let currentDrawable = mtkView.currentDrawable, + let sourceTexture = sourceTexture, + let supportedImageFilter = SupportedImageFilter.imageFilterOfIndex(imageFilterIndex) else { + return + } + + let commandBuffer = commandQueue.makeCommandBuffer(); + + let imageFilter: CommandBufferEncodable + switch supportedImageFilter { + case .PassThrough: imageFilter = passThrough + case .GaussianBlur: imageFilter = gaussianBlur + case .Median: imageFilter = median + case .Laplacian: imageFilter = laplacian + case .Sobel: imageFilter = sobel + case .ThresholdBinary: imageFilter = thresholdBinary + case .ConvolutionEmboss: imageFilter = convolutionEmboss + case .ConvolutionSharpen: imageFilter = convolutionSharpen + case .DilateBokeh: imageFilter = dilateBokeh + case .MorphologyClosing: imageFilter = morphologyClosing + case .HistogramEqualization: imageFilter = histogramEqualization + case .HistogramSpecification: imageFilter = histogramSpecification + } + + /** Obtain the current drawable. + The final destination texture is always the filtered output image written to the MTKView's drawable. + */ + let destinationTexture = currentDrawable.texture + + // Encode the image filter operation. + imageFilter.encode(to: commandBuffer, + sourceTexture: sourceTexture, + destinationTexture: destinationTexture) + + // Schedule a presentation. + commandBuffer.present(currentDrawable) + + // Commit the command buffer to the GPU. + commandBuffer.commit() + } +} + +// MARK: AVCaptureVideoDataOutputSampleBufferDelegate +extension ViewController: VideoImageTextureProviderDelegate +{ + func videoImageTextureProvider(_: VideoImageTextureProvider, didProvideTexture texture: MTLTexture) { + // Replace the source tetxure and call the MTKView's draw() method whenever the camera provides a new video frame. + sourceTexture = texture + mtkView.draw() + } +} diff --git a/MetalImageFilters/README.md b/MetalImageFilters/README.md new file mode 100644 index 00000000..20f16b38 --- /dev/null +++ b/MetalImageFilters/README.md @@ -0,0 +1,19 @@ +# Metal Image Filters: Using the image filters provided by the Metal Performance Shaders framework. + +This sample demonstrates how to use a selection of MPSKernel classes to filter an image on the GPU. The image is processed as a MTLTexture object, obtained from either a bundled file or a real-time video stream. The filters demonstrated range from a single-pass Sobel filter to a multi-pass histogram specification. + +## Requirements + +### Build + +Xcode 8.0 or later; iOS 10.0 SDK or later + +### Runtime + +iOS 10.0 or later + +### Device Feature Set + +iOS GPU Family 2 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/Pathfinder/LICENSE.txt b/Pathfinder/LICENSE.txt new file mode 100644 index 00000000..ee9ef21f --- /dev/null +++ b/Pathfinder/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: Pathfinder: GameplayKit Pathfinding Basics +Version: 1.1 + +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) 2016 Apple Inc. All Rights Reserved. diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ee4c1c --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,166 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "iOS-29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "iOS-29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "iOS-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "iOS-40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "iPhone-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "iPhone-60@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "iOS-29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "iOS-29@2x-1.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "iOS-40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "iOS-40@2x-1.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "iPad-76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "iPad-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "iOS_pro_pathfinding_2x.png", + "scale" : "2x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "Mac-16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "Mac-32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "Mac-33.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "Mac-64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "Mac-128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "Mac-256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "Mac-257.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "Mac-512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "Mac-513.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "Mac-1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-1024.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-1024.png new file mode 100644 index 00000000..6783659b Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-1024.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-128.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-128.png new file mode 100644 index 00000000..7d73cbe0 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-128.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-16.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-16.png new file mode 100644 index 00000000..c5497fc5 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-16.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-256.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-256.png new file mode 100644 index 00000000..8df28d0d Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-256.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-257.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-257.png new file mode 100644 index 00000000..8df28d0d Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-257.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-32.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-32.png new file mode 100644 index 00000000..60524e6a Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-32.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-33.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-33.png new file mode 100644 index 00000000..60524e6a Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-33.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-512.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-512.png new file mode 100644 index 00000000..8f69e9af Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-512.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-513.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-513.png new file mode 100644 index 00000000..8f69e9af Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-513.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-64.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-64.png new file mode 100644 index 00000000..3b310b03 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/Mac-64.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@1x.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@1x.png new file mode 100644 index 00000000..6b49b87a Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@1x.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@2x-1.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@2x-1.png new file mode 100644 index 00000000..cfa5d7e6 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@2x-1.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@2x.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@2x.png new file mode 100644 index 00000000..cfa5d7e6 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@2x.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@3x.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@3x.png new file mode 100644 index 00000000..b4efb676 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-29@3x.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@1x.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@1x.png new file mode 100644 index 00000000..122f21e2 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@1x.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@2x-1.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@2x-1.png new file mode 100644 index 00000000..58ae3211 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@2x-1.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@2x.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@2x.png new file mode 100644 index 00000000..58ae3211 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@2x.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@3x.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@3x.png new file mode 100644 index 00000000..0be8481a Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS-40@3x.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS_pro_pathfinding_2x.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS_pro_pathfinding_2x.png new file mode 100644 index 00000000..e183229f Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iOS_pro_pathfinding_2x.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPad-76@1x.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPad-76@1x.png new file mode 100644 index 00000000..a420ffb1 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPad-76@1x.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPad-76@2x.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPad-76@2x.png new file mode 100644 index 00000000..a13e8b30 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPad-76@2x.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPhone-60@2x.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPhone-60@2x.png new file mode 100644 index 00000000..0be8481a Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPhone-60@2x.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPhone-60@3x.png b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPhone-60@3x.png new file mode 100644 index 00000000..7b83b786 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/AppIcon.appiconset/iPhone-60@3x.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS Launch Image.launchimage/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS Launch Image.launchimage/Contents.json new file mode 100644 index 00000000..b6fb74e7 --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS Launch Image.launchimage/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "orientation" : "landscape", + "idiom" : "tv", + "filename" : "pathfinder.png", + "extent" : "full-screen", + "minimum-system-version" : "9.0", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS Launch Image.launchimage/pathfinder.png b/Pathfinder/PathFinder/Assets.xcassets/tvOS Launch Image.launchimage/pathfinder.png new file mode 100644 index 00000000..b88140cb Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/tvOS Launch Image.launchimage/pathfinder.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Contents.json new file mode 100644 index 00000000..d29f024e --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Arrows.imagestacklayer/Content.imageset/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Arrows.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..f5ebd61d --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Arrows.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "layer3.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Arrows.imagestacklayer/Content.imageset/layer3.png b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Arrows.imagestacklayer/Content.imageset/layer3.png new file mode 100644 index 00000000..4bdb0e5c Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Arrows.imagestacklayer/Content.imageset/layer3.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Arrows.imagestacklayer/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Arrows.imagestacklayer/Contents.json new file mode 100644 index 00000000..27ac3d21 --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Arrows.imagestacklayer/Contents.json @@ -0,0 +1,16 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "frame-size" : { + "width" : 144, + "height" : 144 + }, + "frame-center" : { + "x" : 208, + "y" : 127 + } + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..dc2b0338 --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "gradient-fill-1.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/gradient-fill-1.png b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/gradient-fill-1.png new file mode 100644 index 00000000..e8196e05 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/gradient-fill-1.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 00000000..176551ed --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,16 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "frame-size" : { + "width" : 400, + "height" : 240 + }, + "frame-center" : { + "x" : 200, + "y" : 120 + } + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Circle.imagestacklayer/Content.imageset/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Circle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..caa570ee --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Circle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "layer4.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Circle.imagestacklayer/Content.imageset/layer4.png b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Circle.imagestacklayer/Content.imageset/layer4.png new file mode 100644 index 00000000..43b72f68 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Circle.imagestacklayer/Content.imageset/layer4.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Circle.imagestacklayer/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Circle.imagestacklayer/Contents.json new file mode 100644 index 00000000..eabf910d --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Circle.imagestacklayer/Contents.json @@ -0,0 +1,16 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "frame-size" : { + "width" : 70, + "height" : 70 + }, + "frame-center" : { + "x" : 142, + "y" : 60 + } + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Contents.json new file mode 100644 index 00000000..dc3d7957 --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Contents.json @@ -0,0 +1,23 @@ +{ + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Circle.imagestacklayer" + }, + { + "filename" : "Arrows.imagestacklayer" + }, + { + "filename" : "Slash.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/-copy2.png b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/-copy2.png new file mode 100644 index 00000000..208df396 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/-copy2.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..7561a97f --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "-copy2.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 00000000..ec5b7c7a --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,16 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "frame-size" : { + "width" : 64, + "height" : 64 + }, + "frame-center" : { + "x" : 265, + "y" : 183 + } + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Slash.imagestacklayer/Content.imageset/-copy.png b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Slash.imagestacklayer/Content.imageset/-copy.png new file mode 100644 index 00000000..7773e1f5 Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Slash.imagestacklayer/Content.imageset/-copy.png differ diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Slash.imagestacklayer/Content.imageset/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Slash.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000..f7b571cf --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Slash.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "-copy.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Slash.imagestacklayer/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Slash.imagestacklayer/Contents.json new file mode 100644 index 00000000..efbdb9cc --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/App Icon - Small.imagestack/Slash.imagestacklayer/Contents.json @@ -0,0 +1,16 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "frame-size" : { + "width" : 63, + "height" : 62 + }, + "frame-center" : { + "x" : 201.5, + "y" : 120 + } + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Contents.json new file mode 100644 index 00000000..dea6e49f --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "assets" : [ + { + "size" : "1280x768", + "idiom" : "tv", + "filename" : "App Icon - Large.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "400x240", + "idiom" : "tv", + "filename" : "App Icon - Small.imagestack", + "role" : "primary-app-icon" + }, + { + "size" : "2320x720", + "idiom" : "tv", + "filename" : "Top Shelf Image Wide.imageset", + "role" : "top-shelf-image-wide" + }, + { + "size" : "1920x720", + "idiom" : "tv", + "filename" : "Top Shelf Image.imageset", + "role" : "top-shelf-image" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Top Shelf Image Wide.imageset/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 00000000..0564959f --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Top Shelf Image.imageset/Contents.json b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 00000000..7101666b --- /dev/null +++ b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "tv", + "filename" : "topshelf.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Top Shelf Image.imageset/topshelf.png b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Top Shelf Image.imageset/topshelf.png new file mode 100644 index 00000000..b567b8bd Binary files /dev/null and b/Pathfinder/PathFinder/Assets.xcassets/tvOS.brandassets/Top Shelf Image.imageset/topshelf.png differ diff --git a/Pathfinder/PathFinder/GameScene.sks b/Pathfinder/PathFinder/GameScene.sks new file mode 100644 index 00000000..e9bfbf38 Binary files /dev/null and b/Pathfinder/PathFinder/GameScene.sks differ diff --git a/Pathfinder/PathFinder/GameScene.swift b/Pathfinder/PathFinder/GameScene.swift new file mode 100644 index 00000000..a11d0d5a --- /dev/null +++ b/Pathfinder/PathFinder/GameScene.swift @@ -0,0 +1,189 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An `SKScene` subclass that handles logic and visuals. +*/ + +import SpriteKit +import GameplayKit + +class GameScene: SKScene { + // MARK: Properties + + /// Holds information about the maze. + var maze = Maze() + + /// Whether the solution is currently displayed or not. + var hasSolutionDisplayed = false + + /** + Contains optional sprite nodes that are used to visualize the maze + graph. The nodes are arranged in a 2D array (an array with rows and + columns) so that the array index of a sprite node in this array + corresponds to the coordinates of the node in the maze graph. A node at + an index exists if the corresponding maze node exists; otherwise, the + sprite node is nil. + */ + @nonobjc var spriteNodes = [[SKSpriteNode?]]() + + // MARK: Methods + + /// Creates a new maze, or solves the newly created maze. + func createOrSolveMaze() { + if hasSolutionDisplayed { + createMaze() + } + else { + solveMaze() + } + } + + /** + Creates a maze object, and creates a visual representation of that maze + using sprites. + */ + func createMaze() { + maze = Maze() + generateMazeNodes() + hasSolutionDisplayed = false + } + + /** + Uses GameplayKit's pathfinding to find a solution to the maze, then + solves it. + */ + func solveMaze() { + guard let solution = maze.solutionPath else { + assertionFailure("Solution not retrievable from maze.") + return + } + + animateSolution(solution) + hasSolutionDisplayed = true + } + + // MARK: SpriteKit Methods + + /// Generates a maze when the game starts. + override func didMove(to _: SKView) { + createMaze() + } + + /// Generates sprite nodes that comprise the maze's visual representation. + func generateMazeNodes() { + // Initialize the an array of sprites for the maze. + spriteNodes += [[SKSpriteNode?]](repeating: [SKSpriteNode?](repeating: nil, count: (Maze.dimensions * 2) - 1), count: Maze.dimensions + ) + + /* + Grab the maze's parent node from the scene and use it to + calculate the size of the maze's cell sprites. + */ + let mazeParentNode = childNode(withName: "maze") as! SKSpriteNode + let cellDimension = mazeParentNode.size.height / CGFloat(Maze.dimensions) + + // Remove existing maze cell sprites from the previous maze. + mazeParentNode.removeAllChildren() + + // For each maze node in the maze graph, create a corresponding sprite. + let graphNodes = maze.graph.nodes as? [GKGridGraphNode] + for node in graphNodes! { + // Get the position of the maze node. + let x = Int(node.gridPosition.x) + let y = Int(node.gridPosition.y) + + /* + Create a maze sprite node and place the sprite at the correct + location relative to the maze's parent node. + */ + let mazeNode = SKSpriteNode( + color: SKColor.darkGray, + size: CGSize(width: cellDimension, height: cellDimension) + ) + mazeNode.anchorPoint = CGPoint(x: 0, y: 0) + mazeNode.position = CGPoint(x: CGFloat(x) * cellDimension, y: CGFloat(y) * cellDimension) + + // Add the maze sprite node to the maze's parent node. + mazeParentNode.addChild(mazeNode) + + /* + Add the maze sprite node to the 2D array of sprite nodes so we + can reference it later. + */ + spriteNodes[x][y] = mazeNode + } + + // Grab the coordinates of the start and end maze sprite nodes. + let startNodeX = Int(maze.startNode.gridPosition.x) + let startNodeY = Int(maze.startNode.gridPosition.y) + let endNodeX = Int(maze.endNode.gridPosition.x) + let endNodeY = Int(maze.endNode.gridPosition.y) + + // Color the start and end nodes green and red, respectively. + spriteNodes[startNodeX][startNodeY]?.color = SKColor.green + spriteNodes[endNodeX][endNodeY]?.color = SKColor.red + } + + /// Animates a solution to the maze. + func animateSolution(_ solution: [GKGridGraphNode]) { + /* + The animation works by animating sprites with different start delays. + actionDelay represents this delay, which increases by + an interval of actionInterval with each iteration of the loop. + */ + var actionDelay: TimeInterval = 0 + let actionInterval = 0.005 + + /* + Light up each sprite in the solution sequence, except for the + start and end nodes. + */ + for i in 1...(solution.count - 2) { + // Grab the position of the maze graph node. + let x = Int(solution[i].gridPosition.x) + let y = Int(solution[i].gridPosition.y) + + /* + Increment the action delay so this sprite is highlighted + after the previous one. + */ + actionDelay += actionInterval + + // Run the animation action on the maze sprite node. + if let mazeNode = spriteNodes[x][y] { + mazeNode.run( + SKAction.sequence( + [SKAction.colorize(with: SKColor.gray, colorBlendFactor: 1, duration: 0.2), + SKAction.wait(forDuration: actionDelay), + SKAction.colorize(with: SKColor.white, colorBlendFactor: 1, duration: 0), + SKAction.colorize(with: SKColor.lightGray, colorBlendFactor: 1, duration: 0.3)] + ) + ) + } + } + } +} + +// MARK: OS X Input Handling + +#if os(OSX) + extension GameScene { + /** + Advances the game by creating a new maze or solving the existing maze if + a key press is detected. + */ + override func keyDown(with _: NSEvent) { + createOrSolveMaze() + } + + /** + Advances the game by creating a new maze or solving the existing maze if + a click is detected. + */ + override func mouseDown(with _: NSEvent) { + createOrSolveMaze() + } + } +#endif diff --git a/Pathfinder/PathFinder/Maze.swift b/Pathfinder/PathFinder/Maze.swift new file mode 100644 index 00000000..8230a03d --- /dev/null +++ b/Pathfinder/PathFinder/Maze.swift @@ -0,0 +1,77 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Handles data for a maze. +*/ + +import GameplayKit +import SpriteKit + +class Maze { + // MARK: Properties + + /** + Defines the width (and height) of the maze. This is the actual number of rows (and columns) of the maze graph. Since the maze contains walls between traversable areas, these walls must be represented in the navigability graph. + + - Note: This value must be odd. + */ + static let dimensions = 25 + + /// A grid-based graph representing the navigability space of the maze. + var graph: GKGridGraph + + /// A node in a grid-based graph representing the starting point of the maze. + var startNode: GKGridGraphNode + + /// A node in a grid-based graph representing the ending point of the maze. + var endNode: GKGridGraphNode + + /** + Computes a solution to the maze by using GameplayKit's pathfinding + on the maze's GKGridGraph. + */ + var solutionPath: [GKGridGraphNode]? { + // Calculate a solution path to the maze. + let solution = graph.findPath(from: startNode, to: endNode) as! [GKGridGraphNode] + + /* + If the solution path is not empty, return the path. Otherwise, + throw an error. + */ + if solution.isEmpty { + assertionFailure("No path exists between startNode and endNode.") + return nil + } + else { + return solution + } + } + + // MARK: Initialization + + init() { + // Initialize the maze graph. At this point, the graph has no walls. + graph = GKGridGraph(fromGridStartingAt: int2(0, 0), width: Int32(Maze.dimensions), height: Int32(Maze.dimensions), diagonalsAllowed: false) + + /* + Define the maze's start and end nodes. + + - Note: These nodes must have both an even x and an even y + coordinate, otherwise they may not remain on the maze graph after + the maze walls are removed. + */ + startNode = graph.node(atGridPosition: int2(0, Int32(Maze.dimensions - 1)))! + endNode = graph.node(atGridPosition: int2(Int32(Maze.dimensions - 1), 0))! + + /* + Create a MazeBuilder to generate a random set of walls, then remove + them from the maze graph. By removing these nodes, you prevent them + from being traversable, so they serve as impassible walls. + */ + let mazeBuilder = MazeBuilder(maze: self) + let mazeWalls = mazeBuilder.mazeWallsForRemoval() + graph.remove(mazeWalls) + } +} diff --git a/Pathfinder/PathFinder/MazeBuilder.swift b/Pathfinder/PathFinder/MazeBuilder.swift new file mode 100644 index 00000000..edb63e03 --- /dev/null +++ b/Pathfinder/PathFinder/MazeBuilder.swift @@ -0,0 +1,240 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Handles random generation for a Maze object. +*/ + +import GameplayKit + +class MazeBuilder { + // MARK: Types + + /** + An enum for the cardinal directions. This enables you to randomly + generate a direction with a random number generator. + */ + enum Direction: Int { + case left = 0, down, right, up + + /// Generates a random direction. + static func random() -> Direction { + /* + Generate a random number from 0-3, and return a corresponing + Direction enum. + */ + let randomInt = GKRandomSource.sharedRandom().nextInt(upperBound: 4) + + return Direction(rawValue: randomInt)! + } + + // The offset value for the x-axis associated with a direction. + var dx: Int { + switch self { + case .up, .down: return 0 + case .left: return -2 + case .right: return 2 + } + } + + // The offset value for the y-axis associated with a direction. + var dy: Int { + switch self { + case .left, .right: return 0 + case .up: return 2 + case .down: return -2 + } + } + } + + // MARK: Properties + + /// A reference to the maze that the maze builder is building for. + let maze: Maze + + /// Holds graph nodes designated as walls. + var wallNodes = [GKGridGraphNode]() + + /// Used as a stack to search the maze during during maze generation. + var searchStack = [GKGridGraphNode]() + + /// Used to keep track of visited nodes during maze generation. + var visitedNodes = [GKGridGraphNode]() + + /** + Returns every potential wall in the maze graph to the walls array. Due + to the way the maze is constructed, a node is a potential wall if it has + an odd x or y coordinate. + */ + var potentialWalls: [GKGridGraphNode] { + // Grab the graph nodes from the maze graph. + let graphNodes = maze.graph.nodes as! [GKGridGraphNode] + + // Filter in the nodes that could potentially be walls. + let potentialWalls = graphNodes.filter { node in + // Grab the coordinates of the maze node. + let x = Int(node.gridPosition.x) + let y = Int(node.gridPosition.y) + + // If the maze node has an odd coordinate, filter it into the array. + return x % 2 == 1 || y % 2 == 1 + } + + return potentialWalls + } + + // MARK: Initialization + + init(maze: Maze) { + self.maze = maze + } + + // MARK: Methods + + /** + Returns an array of maze graph nodes representing walls in the maze. + These nodes are to be removed from the pathfinding graph, since walls + are impassible. + + This maze generation algorithm uses a depth-first search (DFS). + It uses a stack to track its progress through the maze, and an array + to check how much of the maze it has visited. It works like this: + the starting node is added to the stack and array. The algorithm + selects a node neighboring the top node of the stack (the starting + node, in this case). It then removes the wall separating those two + nodes, and adds the neighboring node to the stack and array. This + process continues until the top node of the stack has no unvisited + neighbors. When what happens, the algorithm removes nodes from the + stack until the top node has an unvisited neighbor, and the process + continues. Eventually the entire maze will have been visited, the + stack will be empty, and the maze is created. + + Instead of removing walls directly, this method keeps track of + which walls need to be removed, and returns those nodes. + */ + func mazeWallsForRemoval() -> [GKGridGraphNode] { + // First, add all of the potential walls to the array of walls. + wallNodes += potentialWalls + + // Initialize both the stack and array with the starting maze graph node. + searchStack.append(maze.startNode) + visitedNodes.append(maze.startNode) + + // Until the stack is empty, process the maze graph. + while let topNode = searchStack.last { + /* + First, check if the top node of the stack has any unvisited + neighbors. If so, select a random unvisited neighbor to visit. + Otherwise, remove the top node. + */ + guard hasUnvisitedNeighborNode(topNode) else { + // Remove the top node. + searchStack.removeLast() + + // Skip to the next iteration of the while loop. + continue + } + + /* + Check random neighboring directions until a neighboring node + is found. Then visit that node. + */ + exploreUnvisitedNodes: while true { + // Generate a random direction. + let randomDirection = Direction.random() + + /* + If a direction should be explored by the algorithm, explore + the node in that direction and exit the while loop. + */ + if shouldExploreInDirectionFromNode(topNode, inDirection: randomDirection) { + exploreNodeInDirectionFromNode(topNode, inDirection: randomDirection) + break exploreUnvisitedNodes + } + } + } + + // Return a set of walls that can be removed to form a maze. + return wallNodes + } + + /** + Tests whether a node in a direction from a given node is unvisited. If + so, it is explorable by the maze generation algorithm. + */ + func shouldExploreInDirectionFromNode(_ node: GKGridGraphNode, inDirection direction: Direction) -> Bool { + // Get the direction of the offset. + let dx = direction.dx + let dy = direction.dy + + // Get the location of the current node. + let x = node.gridPosition.x + let y = node.gridPosition.y + + // Return whether the node is unvisited or not. + return nodeIsUnvisitedAtCoordinates(x: x + dx, y: y + dy) + } + + /** + Explores a direction in the maze generation algorithm, removing the wall + between the given node and a node in the given direction. + */ + func exploreNodeInDirectionFromNode(_ node: GKGridGraphNode, inDirection direction: Direction) { + // Get the direction of the offset. + let dx = direction.dx + let dy = direction.dy + + // Get the location of the current node. + let x = node.gridPosition.x + let y = node.gridPosition.y + + // Get the location of node in the given direction. + let nodeInDirectionPosition = int2(x + dx, y + dy) + let nodeInDirection = maze.graph.node(atGridPosition: nodeInDirectionPosition)! + + // Add the node in the direction to the stack, and mark it as visited. + searchStack.append(nodeInDirection) + visitedNodes.append(nodeInDirection) + + // Remove the wall between this node and the current node. + let wallNodePosition = int2(x + dx / 2, y + dy / 2) + let wallNode = maze.graph.node(atGridPosition: wallNodePosition)! + let wallNodeIndex = wallNodes.index(of: wallNode)! + wallNodes.remove(at: wallNodeIndex) + } + + /// Checks if the given maze graph node has any unvisited neighbor nodes. + func hasUnvisitedNeighborNode(_ currentNode: GKGridGraphNode) -> Bool { + // Grab the position of the given maze graph node. + let x = currentNode.gridPosition.x + let y = currentNode.gridPosition.y + + // Check whether the left, right, top, or bottom nodes are unvisited. + let leftNodeIsUnvisited = nodeIsUnvisitedAtCoordinates(x: x - 2, y: y) + let rightNodeIsUnvisited = nodeIsUnvisitedAtCoordinates(x: x + 2, y: y) + let topNodeIsUnvisited = nodeIsUnvisitedAtCoordinates(x: x, y: y + 2) + let bottomNodeIsUnvisited = nodeIsUnvisitedAtCoordinates(x: x, y: y - 2) + + /* + If any of the neighboring nodes are unvisited, return that the node + has at least one unvisited neighbor node. Otherwise, return that it + doesn't. + */ + let hasUnvisitedNeighborNode = leftNodeIsUnvisited || rightNodeIsUnvisited || topNodeIsUnvisited || bottomNodeIsUnvisited + return hasUnvisitedNeighborNode + } + + /// This method checks if a node is unvisited. + func nodeIsUnvisitedAtCoordinates(x: Int32, y: Int32) -> Bool { + // Check if a node with the given position exists. + let nodePosition = int2(x, y) + guard let node = maze.graph.node(atGridPosition: nodePosition) else { + return false + } + + // Return if the node is unvisited. + let nodeIsUnvisited = !visitedNodes.contains(node) + return nodeIsUnvisited + } +} diff --git a/Pathfinder/Pathfinder (OS X)/AppDelegate.swift b/Pathfinder/Pathfinder (OS X)/AppDelegate.swift new file mode 100644 index 00000000..7d99e7ad --- /dev/null +++ b/Pathfinder/Pathfinder (OS X)/AppDelegate.swift @@ -0,0 +1,19 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate for the OS X version of Pathfinder. +*/ + +import Cocoa +import SpriteKit + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + // MARK: Methods + + func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { + return true + } +} \ No newline at end of file diff --git a/Pathfinder/Pathfinder (OS X)/Base.lproj/MainMenu.xib b/Pathfinder/Pathfinder (OS X)/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..c05f1405 --- /dev/null +++ b/Pathfinder/Pathfinder (OS X)/Base.lproj/MainMenu.xib @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pathfinder/Pathfinder (OS X)/GameViewController.swift b/Pathfinder/Pathfinder (OS X)/GameViewController.swift new file mode 100644 index 00000000..cac82348 --- /dev/null +++ b/Pathfinder/Pathfinder (OS X)/GameViewController.swift @@ -0,0 +1,31 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An `NSViewController` subclass that stores references to game-wide input sources and managers. +*/ + +import SpriteKit + +class GameViewController: NSViewController { + // MARK: Properties + + let scene = GameScene(fileNamed: "GameScene")! + + // MARK: Methods + + override func viewDidLoad() { + super.viewDidLoad() + + let skView = view as! SKView + + // Set the scale mode to scale to fit the window. + scene.scaleMode = .aspectFit + + skView.presentScene(scene) + + // SpriteKit applies additional optimizations to improve rendering performance. + skView.ignoresSiblingOrder = true + } +} diff --git a/Pathfinder/Pathfinder (OS X)/Info.plist b/Pathfinder/Pathfinder (OS X)/Info.plist new file mode 100644 index 00000000..b763ec42 --- /dev/null +++ b/Pathfinder/Pathfinder (OS X)/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2016 Apple. All rights reserved. + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/Pathfinder/Pathfinder (iOS)/AppDelegate.swift b/Pathfinder/Pathfinder (iOS)/AppDelegate.swift new file mode 100644 index 00000000..8228d97e --- /dev/null +++ b/Pathfinder/Pathfinder (iOS)/AppDelegate.swift @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Application delegate for the iOS version of Pathfinder. +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + // MARK: Properties + + var window: UIWindow? +} \ No newline at end of file diff --git a/Pathfinder/Pathfinder (iOS)/Base.lproj/LaunchScreen.storyboard b/Pathfinder/Pathfinder (iOS)/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..2e721e18 --- /dev/null +++ b/Pathfinder/Pathfinder (iOS)/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pathfinder/Pathfinder (iOS)/Base.lproj/Main.storyboard b/Pathfinder/Pathfinder (iOS)/Base.lproj/Main.storyboard new file mode 100644 index 00000000..ed29710f --- /dev/null +++ b/Pathfinder/Pathfinder (iOS)/Base.lproj/Main.storyboard @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pathfinder/Pathfinder (iOS)/GameViewController.swift b/Pathfinder/Pathfinder (iOS)/GameViewController.swift new file mode 100644 index 00000000..b6e05f3e --- /dev/null +++ b/Pathfinder/Pathfinder (iOS)/GameViewController.swift @@ -0,0 +1,40 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A `UIViewController` subclass that stores references to game-wide input sources and managers. +*/ + +import UIKit +import SpriteKit + +class GameViewController: UIViewController { + // MARK: Properties + + let scene = GameScene(fileNamed: "GameScene")! + + // MARK: Methods + + override func viewDidLoad() { + super.viewDidLoad() + + let skView = view as! SKView + + // Set the scale mode to scale to fit the window. + scene.scaleMode = .aspectFit + + skView.presentScene(scene) + + // SpriteKit applies additional optimizations to improve rendering performance. + skView.ignoresSiblingOrder = true + } + + /** + Detects taps. If a tap is detected, the the game + advances by creating a new maze or solving the existing maze. + */ + @IBAction func handleTap(_: AnyObject) { + scene.createOrSolveMaze() + } +} diff --git a/Pathfinder/Pathfinder (iOS)/Info.plist b/Pathfinder/Pathfinder (iOS)/Info.plist new file mode 100644 index 00000000..44468d2e --- /dev/null +++ b/Pathfinder/Pathfinder (iOS)/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Pathfinder/Pathfinder (tvOS)/Base.lproj/Main.storyboard b/Pathfinder/Pathfinder (tvOS)/Base.lproj/Main.storyboard new file mode 100644 index 00000000..d8ad5104 --- /dev/null +++ b/Pathfinder/Pathfinder (tvOS)/Base.lproj/Main.storyboard @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pathfinder/Pathfinder (tvOS)/Info.plist b/Pathfinder/Pathfinder (tvOS)/Info.plist new file mode 100644 index 00000000..4f338601 --- /dev/null +++ b/Pathfinder/Pathfinder (tvOS)/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + + diff --git a/Pathfinder/Pathfinder.xcodeproj/project.pbxproj b/Pathfinder/Pathfinder.xcodeproj/project.pbxproj new file mode 100644 index 00000000..c61833ec --- /dev/null +++ b/Pathfinder/Pathfinder.xcodeproj/project.pbxproj @@ -0,0 +1,589 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 112FE97E1C5999130064E15F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 112FE97C1C5999130064E15F /* Main.storyboard */; }; + 112FE9851C5999600064E15F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4AA55301B6ACA85008D3323 /* AppDelegate.swift */; }; + 112FE9861C5999600064E15F /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42DBDC61B7947F90032B883 /* GameViewController.swift */; }; + 112FE9871C5999650064E15F /* Maze.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40581251B7C08C100E0EC65 /* Maze.swift */; }; + 112FE9881C5999650064E15F /* GameScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A432D8E41B7D58F30039067A /* GameScene.swift */; }; + 112FE9891C5999650064E15F /* GameScene.sks in Resources */ = {isa = PBXBuildFile; fileRef = A4774EAB1B434B2B007C1EB1 /* GameScene.sks */; }; + 112FE98A1C5999650064E15F /* MazeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C714661B86970B00D5F10C /* MazeBuilder.swift */; }; + 112FE98B1C5999650064E15F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A4774EAD1B434B2B007C1EB1 /* Assets.xcassets */; }; + A40581261B7C08C100E0EC65 /* Maze.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40581251B7C08C100E0EC65 /* Maze.swift */; }; + A40581271B7C08C100E0EC65 /* Maze.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40581251B7C08C100E0EC65 /* Maze.swift */; }; + A42047381B7AC58900619C99 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4AA55301B6ACA85008D3323 /* AppDelegate.swift */; }; + A42DBDC51B79464A0032B883 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A4774EAD1B434B2B007C1EB1 /* Assets.xcassets */; }; + A42DBDC81B7948850032B883 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42DBDC61B7947F90032B883 /* GameViewController.swift */; }; + A432D8E51B7D58F30039067A /* GameScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A432D8E41B7D58F30039067A /* GameScene.swift */; }; + A432D8E61B7D58F30039067A /* GameScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A432D8E41B7D58F30039067A /* GameScene.swift */; }; + A4514AF61B7981A200D57806 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A4774EAD1B434B2B007C1EB1 /* Assets.xcassets */; }; + A4774EA81B434B2B007C1EB1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4774EA71B434B2B007C1EB1 /* AppDelegate.swift */; }; + A4774EAC1B434B2B007C1EB1 /* GameScene.sks in Resources */ = {isa = PBXBuildFile; fileRef = A4774EAB1B434B2B007C1EB1 /* GameScene.sks */; }; + A4774EB11B434B2B007C1EB1 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = A4774EAF1B434B2B007C1EB1 /* MainMenu.xib */; }; + A4AA553A1B6ACA85008D3323 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A4AA55381B6ACA85008D3323 /* Main.storyboard */; }; + A4AA553F1B6ACA85008D3323 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A4AA553D1B6ACA85008D3323 /* LaunchScreen.storyboard */; }; + A4AA55461B6ACAD4008D3323 /* GameScene.sks in Resources */ = {isa = PBXBuildFile; fileRef = A4774EAB1B434B2B007C1EB1 /* GameScene.sks */; }; + A4C714671B86970B00D5F10C /* MazeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C714661B86970B00D5F10C /* MazeBuilder.swift */; }; + A4C714681B86970B00D5F10C /* MazeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4C714661B86970B00D5F10C /* MazeBuilder.swift */; }; + A4CD0B751B870DC100FF533D /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4CD0B741B870DC100FF533D /* GameViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 112FE9701C5999130064E15F /* Pathfinder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pathfinder.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 112FE97D1C5999130064E15F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 112FE9811C5999130064E15F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A40581251B7C08C100E0EC65 /* Maze.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Maze.swift; sourceTree = ""; }; + A42DBDC61B7947F90032B883 /* GameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameViewController.swift; path = "Pathfinder (iOS)/GameViewController.swift"; sourceTree = SOURCE_ROOT; }; + A432D8E41B7D58F30039067A /* GameScene.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameScene.swift; path = Pathfinder/GameScene.swift; sourceTree = SOURCE_ROOT; }; + A4774EA41B434B2B007C1EB1 /* Pathfinder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pathfinder.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A4774EA71B434B2B007C1EB1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = "../PathFinder (OS X)/AppDelegate.swift"; sourceTree = ""; }; + A4774EAB1B434B2B007C1EB1 /* GameScene.sks */ = {isa = PBXFileReference; lastKnownFileType = file.sks; path = GameScene.sks; sourceTree = ""; }; + A4774EAD1B434B2B007C1EB1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A4774EB01B434B2B007C1EB1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + A4774EB21B434B2B007C1EB1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = "../PathFinder (OS X)/Info.plist"; sourceTree = ""; }; + A4AA552E1B6ACA85008D3323 /* Pathfinder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pathfinder.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A4AA55301B6ACA85008D3323 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + A4AA55391B6ACA85008D3323 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + A4AA553E1B6ACA85008D3323 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + A4AA55401B6ACA85008D3323 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A4C714661B86970B00D5F10C /* MazeBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MazeBuilder.swift; sourceTree = ""; }; + A4CD0B741B870DC100FF533D /* GameViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameViewController.swift; path = "Pathfinder (OS X)/GameViewController.swift"; sourceTree = SOURCE_ROOT; }; + A4DD8ECD1B85AA0C00A026D3 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 112FE96D1C5999130064E15F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A4774EA11B434B2B007C1EB1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A4AA552B1B6ACA85008D3323 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 112FE9711C5999130064E15F /* Pathfinder (tvOS) */ = { + isa = PBXGroup; + children = ( + 112FE97C1C5999130064E15F /* Main.storyboard */, + 112FE9811C5999130064E15F /* Info.plist */, + ); + path = "Pathfinder (tvOS)"; + sourceTree = ""; + }; + A4774E9B1B434B2B007C1EB1 = { + isa = PBXGroup; + children = ( + A4DD8ECD1B85AA0C00A026D3 /* README.md */, + A4AA55441B6ACAA5008D3323 /* Pathfinder */, + A4774EA61B434B2B007C1EB1 /* Pathfinder (OS X) */, + A4AA552F1B6ACA85008D3323 /* Pathfinder (iOS) */, + 112FE9711C5999130064E15F /* Pathfinder (tvOS) */, + A4774EA51B434B2B007C1EB1 /* Products */, + ); + sourceTree = ""; + }; + A4774EA51B434B2B007C1EB1 /* Products */ = { + isa = PBXGroup; + children = ( + A4774EA41B434B2B007C1EB1 /* Pathfinder.app */, + A4AA552E1B6ACA85008D3323 /* Pathfinder.app */, + 112FE9701C5999130064E15F /* Pathfinder.app */, + ); + name = Products; + sourceTree = ""; + }; + A4774EA61B434B2B007C1EB1 /* Pathfinder (OS X) */ = { + isa = PBXGroup; + children = ( + A4774EA71B434B2B007C1EB1 /* AppDelegate.swift */, + A4CD0B741B870DC100FF533D /* GameViewController.swift */, + A4774EAF1B434B2B007C1EB1 /* MainMenu.xib */, + A4774EB21B434B2B007C1EB1 /* Info.plist */, + ); + name = "Pathfinder (OS X)"; + path = PathFinder; + sourceTree = ""; + }; + A4AA552F1B6ACA85008D3323 /* Pathfinder (iOS) */ = { + isa = PBXGroup; + children = ( + A4AA55301B6ACA85008D3323 /* AppDelegate.swift */, + A42DBDC61B7947F90032B883 /* GameViewController.swift */, + A4AA55381B6ACA85008D3323 /* Main.storyboard */, + A4AA553D1B6ACA85008D3323 /* LaunchScreen.storyboard */, + A4AA55401B6ACA85008D3323 /* Info.plist */, + ); + path = "Pathfinder (iOS)"; + sourceTree = ""; + }; + A4AA55441B6ACAA5008D3323 /* Pathfinder */ = { + isa = PBXGroup; + children = ( + A40581251B7C08C100E0EC65 /* Maze.swift */, + A432D8E41B7D58F30039067A /* GameScene.swift */, + A4774EAB1B434B2B007C1EB1 /* GameScene.sks */, + A4C714661B86970B00D5F10C /* MazeBuilder.swift */, + A4774EAD1B434B2B007C1EB1 /* Assets.xcassets */, + ); + name = Pathfinder; + path = PathFinder; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 112FE96F1C5999130064E15F /* Pathfinder (tvOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 112FE9841C5999130064E15F /* Build configuration list for PBXNativeTarget "Pathfinder (tvOS)" */; + buildPhases = ( + 112FE96C1C5999130064E15F /* Sources */, + 112FE96D1C5999130064E15F /* Frameworks */, + 112FE96E1C5999130064E15F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Pathfinder (tvOS)"; + productName = PathfinderTV; + productReference = 112FE9701C5999130064E15F /* Pathfinder.app */; + productType = "com.apple.product-type.application"; + }; + A4774EA31B434B2B007C1EB1 /* Pathfinder (OS X) */ = { + isa = PBXNativeTarget; + buildConfigurationList = A4774ECB1B434B2B007C1EB1 /* Build configuration list for PBXNativeTarget "Pathfinder (OS X)" */; + buildPhases = ( + A4774EA01B434B2B007C1EB1 /* Sources */, + A4774EA11B434B2B007C1EB1 /* Frameworks */, + A4774EA21B434B2B007C1EB1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Pathfinder (OS X)"; + productName = PathFinder; + productReference = A4774EA41B434B2B007C1EB1 /* Pathfinder.app */; + productType = "com.apple.product-type.application"; + }; + A4AA552D1B6ACA85008D3323 /* Pathfinder (iOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = A4AA55411B6ACA85008D3323 /* Build configuration list for PBXNativeTarget "Pathfinder (iOS)" */; + buildPhases = ( + A4AA552A1B6ACA85008D3323 /* Sources */, + A4AA552B1B6ACA85008D3323 /* Frameworks */, + A4AA552C1B6ACA85008D3323 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Pathfinder (iOS)"; + productName = "Pathfinder – iOS"; + productReference = A4AA552E1B6ACA85008D3323 /* Pathfinder.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A4774E9C1B434B2B007C1EB1 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0730; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Apple; + TargetAttributes = { + 112FE96F1C5999130064E15F = { + CreatedOnToolsVersion = 7.3; + LastSwiftMigration = 0800; + }; + A4774EA31B434B2B007C1EB1 = { + CreatedOnToolsVersion = 7.0; + LastSwiftMigration = 0800; + }; + A4AA552D1B6ACA85008D3323 = { + CreatedOnToolsVersion = 7.0; + LastSwiftMigration = 0800; + }; + }; + }; + buildConfigurationList = A4774E9F1B434B2B007C1EB1 /* Build configuration list for PBXProject "Pathfinder" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A4774E9B1B434B2B007C1EB1; + productRefGroup = A4774EA51B434B2B007C1EB1 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A4AA552D1B6ACA85008D3323 /* Pathfinder (iOS) */, + A4774EA31B434B2B007C1EB1 /* Pathfinder (OS X) */, + 112FE96F1C5999130064E15F /* Pathfinder (tvOS) */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 112FE96E1C5999130064E15F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 112FE9891C5999650064E15F /* GameScene.sks in Resources */, + 112FE98B1C5999650064E15F /* Assets.xcassets in Resources */, + 112FE97E1C5999130064E15F /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A4774EA21B434B2B007C1EB1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A4774EAC1B434B2B007C1EB1 /* GameScene.sks in Resources */, + A4514AF61B7981A200D57806 /* Assets.xcassets in Resources */, + A4774EB11B434B2B007C1EB1 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A4AA552C1B6ACA85008D3323 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A4AA55461B6ACAD4008D3323 /* GameScene.sks in Resources */, + A4AA553F1B6ACA85008D3323 /* LaunchScreen.storyboard in Resources */, + A4AA553A1B6ACA85008D3323 /* Main.storyboard in Resources */, + A42DBDC51B79464A0032B883 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 112FE96C1C5999130064E15F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 112FE9861C5999600064E15F /* GameViewController.swift in Sources */, + 112FE9851C5999600064E15F /* AppDelegate.swift in Sources */, + 112FE9881C5999650064E15F /* GameScene.swift in Sources */, + 112FE9871C5999650064E15F /* Maze.swift in Sources */, + 112FE98A1C5999650064E15F /* MazeBuilder.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A4774EA01B434B2B007C1EB1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A432D8E61B7D58F30039067A /* GameScene.swift in Sources */, + A40581271B7C08C100E0EC65 /* Maze.swift in Sources */, + A4774EA81B434B2B007C1EB1 /* AppDelegate.swift in Sources */, + A4CD0B751B870DC100FF533D /* GameViewController.swift in Sources */, + A4C714681B86970B00D5F10C /* MazeBuilder.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A4AA552A1B6ACA85008D3323 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A4C714671B86970B00D5F10C /* MazeBuilder.swift in Sources */, + A432D8E51B7D58F30039067A /* GameScene.swift in Sources */, + A42047381B7AC58900619C99 /* AppDelegate.swift in Sources */, + A42DBDC81B7948850032B883 /* GameViewController.swift in Sources */, + A40581261B7C08C100E0EC65 /* Maze.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 112FE97C1C5999130064E15F /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 112FE97D1C5999130064E15F /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + A4774EAF1B434B2B007C1EB1 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + A4774EB01B434B2B007C1EB1 /* Base */, + ); + name = MainMenu.xib; + path = "../PathFinder (OS X)"; + sourceTree = ""; + }; + A4AA55381B6ACA85008D3323 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + A4AA55391B6ACA85008D3323 /* Base */, + ); + name = Main.storyboard; + path = "../Pathfinder (iOS)"; + sourceTree = ""; + }; + A4AA553D1B6ACA85008D3323 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + A4AA553E1B6ACA85008D3323 /* Base */, + ); + name = LaunchScreen.storyboard; + path = "../Pathfinder (iOS)"; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 112FE9821C5999130064E15F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = tvOS; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "tvOS Launch Image"; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + INFOPLIST_FILE = "Pathfinder (tvOS)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Pathfinder"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SDKROOT = appletvos; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 9.2; + }; + name = Debug; + }; + 112FE9831C5999130064E15F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = tvOS; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "tvOS Launch Image"; + INFOPLIST_FILE = "Pathfinder (tvOS)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Pathfinder"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SDKROOT = appletvos; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 9.2; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A4774EC91B434B2B007C1EB1 /* 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_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A4774ECA1B434B2B007C1EB1 /* 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_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + }; + name = Release; + }; + A4774ECC1B434B2B007C1EB1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "$(SRCROOT)/Pathfinder (OS X)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Pathfinder"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + A4774ECD1B434B2B007C1EB1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "$(SRCROOT)/Pathfinder (OS X)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Pathfinder"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + A4AA55421B6ACA85008D3323 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/Pathfinder (iOS)/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Pathfinder"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + PROVISIONING_PROFILE = ""; + SDKROOT = iphoneos; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A4AA55431B6ACA85008D3323 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = "$(SRCROOT)/Pathfinder (iOS)/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Pathfinder"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + PROVISIONING_PROFILE = ""; + SDKROOT = iphoneos; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 112FE9841C5999130064E15F /* Build configuration list for PBXNativeTarget "Pathfinder (tvOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 112FE9821C5999130064E15F /* Debug */, + 112FE9831C5999130064E15F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A4774E9F1B434B2B007C1EB1 /* Build configuration list for PBXProject "Pathfinder" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A4774EC91B434B2B007C1EB1 /* Debug */, + A4774ECA1B434B2B007C1EB1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A4774ECB1B434B2B007C1EB1 /* Build configuration list for PBXNativeTarget "Pathfinder (OS X)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A4774ECC1B434B2B007C1EB1 /* Debug */, + A4774ECD1B434B2B007C1EB1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A4AA55411B6ACA85008D3323 /* Build configuration list for PBXNativeTarget "Pathfinder (iOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A4AA55421B6ACA85008D3323 /* Debug */, + A4AA55431B6ACA85008D3323 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A4774E9C1B434B2B007C1EB1 /* Project object */; +} diff --git a/Pathfinder/Pathfinder.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Pathfinder/Pathfinder.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/Pathfinder/Pathfinder.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Pathfinder/Pathfinder.xcodeproj/project.xcworkspace/xcuserdata/marie_liu.xcuserdatad/UserInterfaceState.xcuserstate b/Pathfinder/Pathfinder.xcodeproj/project.xcworkspace/xcuserdata/marie_liu.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 00000000..48e89c5e Binary files /dev/null and b/Pathfinder/Pathfinder.xcodeproj/project.xcworkspace/xcuserdata/marie_liu.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 00000000..fe2b4541 --- /dev/null +++ b/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,5 @@ + + + diff --git a/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcschemes/PathFinder (OS X).xcscheme b/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcschemes/PathFinder (OS X).xcscheme new file mode 100644 index 00000000..d2b4af34 --- /dev/null +++ b/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcschemes/PathFinder (OS X).xcscheme @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcschemes/Pathfinder (iOS).xcscheme b/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcschemes/Pathfinder (iOS).xcscheme new file mode 100644 index 00000000..e7d6943f --- /dev/null +++ b/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcschemes/Pathfinder (iOS).xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcschemes/xcschememanagement.plist b/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..912668da --- /dev/null +++ b/Pathfinder/Pathfinder.xcodeproj/xcuserdata/marie_liu.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,42 @@ + + + + + SchemeUserState + + PathFinder (OS X).xcscheme + + orderHint + 1 + + Pathfinder (iOS).xcscheme + + orderHint + 0 + + + SuppressBuildableAutocreation + + A4774EA31B434B2B007C1EB1 + + primary + + + A4774EB61B434B2B007C1EB1 + + primary + + + A4774EC11B434B2B007C1EB1 + + primary + + + A4AA552D1B6ACA85008D3323 + + primary + + + + + diff --git a/Pathfinder/README.md b/Pathfinder/README.md new file mode 100644 index 00000000..152c22cc --- /dev/null +++ b/Pathfinder/README.md @@ -0,0 +1,27 @@ +# Pathfinder: Pathfinding Basics + +This sample demonstrates how to use GameplayKit’s pathfinding features to map out a game world and find paths through it. + +## Playing the game + +Tap anywhere (iOS), press any key (OS X), or click the Siri Remote touch surface (tvOS) to show the solution for the displayed maze. Tap/click again to generate a new maze. + +## Structure + +The `MazeBuilder` class implements a general algorithm for random maze generation, creating 2D mazes expressed through `GKGridGraph` objects. + +The `Maze` class represents a generated maze, and its `solution` property getter uses `GKGraph.findPathFromNode(_:toNode:)` to obtain a path through the maze. + +The `GameScene` class generates a visual representation of each `Maze` object, animates the display of maze solutions, and handles events to display/solve new mazes. + +## Requirements + +### Build + +Xcode 7 with OS X 10.11, iOS 9.0, or tvOS 9.0 SDK + +### Runtime + +OS X 10.11, iOS 9.0, or tvOS 9.0 + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/PrintPhoto/LICENSE.txt b/PrintPhoto/LICENSE.txt new file mode 100644 index 00000000..126ba15f --- /dev/null +++ b/PrintPhoto/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: PrintPhoto +Version: 3.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. diff --git a/PrintPhoto/PrintPhoto.xcodeproj/project.pbxproj b/PrintPhoto/PrintPhoto.xcodeproj/project.pbxproj new file mode 100644 index 00000000..2edfc1ef --- /dev/null +++ b/PrintPhoto/PrintPhoto.xcodeproj/project.pbxproj @@ -0,0 +1,319 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 3E14C3D21B867219003F3591 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E14C3D11B867219003F3591 /* AppDelegate.swift */; }; + 3E14C3D41B867219003F3591 /* CustomAssetPrintViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E14C3D31B867219003F3591 /* CustomAssetPrintViewController.swift */; }; + 3E14C3D71B867219003F3591 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3E14C3D51B867219003F3591 /* Main.storyboard */; }; + 3E14C3D91B867219003F3591 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3E14C3D81B867219003F3591 /* Assets.xcassets */; }; + 3E14C3DC1B867219003F3591 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3E14C3DA1B867219003F3591 /* LaunchScreen.storyboard */; }; + 3E14C3E51B868126003F3591 /* CustomAssetPrintPageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E14C3E41B868126003F3591 /* CustomAssetPrintPageRenderer.swift */; settings = {ASSET_TAGS = (); }; }; + 3E1AC9721B8B75A2006FC729 /* StandardAssetPrintViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1AC9711B8B75A2006FC729 /* StandardAssetPrintViewController.swift */; settings = {ASSET_TAGS = (); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 3E14C3CE1B867219003F3591 /* PrintPhoto.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PrintPhoto.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 3E14C3D11B867219003F3591 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 3E14C3D31B867219003F3591 /* CustomAssetPrintViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAssetPrintViewController.swift; sourceTree = ""; }; + 3E14C3D61B867219003F3591 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 3E14C3D81B867219003F3591 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3E14C3DB1B867219003F3591 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 3E14C3DD1B867219003F3591 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3E14C3E31B86723E003F3591 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 3E14C3E41B868126003F3591 /* CustomAssetPrintPageRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CustomAssetPrintPageRenderer.swift; path = Assets.xcassets/CustomAssetPrintPageRenderer.swift; sourceTree = ""; }; + 3E1AC9711B8B75A2006FC729 /* StandardAssetPrintViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandardAssetPrintViewController.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3E14C3CB1B867219003F3591 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3E14C3C51B867219003F3591 = { + isa = PBXGroup; + children = ( + 3E14C3E31B86723E003F3591 /* README.md */, + 3E14C3D01B867219003F3591 /* PrintPhoto */, + 3E14C3CF1B867219003F3591 /* Products */, + ); + sourceTree = ""; + }; + 3E14C3CF1B867219003F3591 /* Products */ = { + isa = PBXGroup; + children = ( + 3E14C3CE1B867219003F3591 /* PrintPhoto.app */, + ); + name = Products; + sourceTree = ""; + }; + 3E14C3D01B867219003F3591 /* PrintPhoto */ = { + isa = PBXGroup; + children = ( + 3E14C3D11B867219003F3591 /* AppDelegate.swift */, + 3E1AC9701B8B745E006FC729 /* Standard Printing */, + 3E1AC96F1B8B7458006FC729 /* Custom Asset Print Rendering */, + 3E14C3D51B867219003F3591 /* Main.storyboard */, + 3E14C3D81B867219003F3591 /* Assets.xcassets */, + 3E14C3DA1B867219003F3591 /* LaunchScreen.storyboard */, + 3E14C3DD1B867219003F3591 /* Info.plist */, + ); + path = PrintPhoto; + sourceTree = ""; + }; + 3E1AC96F1B8B7458006FC729 /* Custom Asset Print Rendering */ = { + isa = PBXGroup; + children = ( + 3E14C3D31B867219003F3591 /* CustomAssetPrintViewController.swift */, + 3E14C3E41B868126003F3591 /* CustomAssetPrintPageRenderer.swift */, + ); + name = "Custom Asset Print Rendering"; + sourceTree = ""; + }; + 3E1AC9701B8B745E006FC729 /* Standard Printing */ = { + isa = PBXGroup; + children = ( + 3E1AC9711B8B75A2006FC729 /* StandardAssetPrintViewController.swift */, + ); + name = "Standard Printing"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3E14C3CD1B867219003F3591 /* PrintPhoto */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3E14C3E01B867219003F3591 /* Build configuration list for PBXNativeTarget "PrintPhoto" */; + buildPhases = ( + 3E14C3CA1B867219003F3591 /* Sources */, + 3E14C3CB1B867219003F3591 /* Frameworks */, + 3E14C3CC1B867219003F3591 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PrintPhoto; + productName = PrintPhoto; + productReference = 3E14C3CE1B867219003F3591 /* PrintPhoto.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3E14C3C61B867219003F3591 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = "Apple Inc"; + TargetAttributes = { + 3E14C3CD1B867219003F3591 = { + CreatedOnToolsVersion = 7.0; + }; + }; + }; + buildConfigurationList = 3E14C3C91B867219003F3591 /* Build configuration list for PBXProject "PrintPhoto" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3E14C3C51B867219003F3591; + productRefGroup = 3E14C3CF1B867219003F3591 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3E14C3CD1B867219003F3591 /* PrintPhoto */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3E14C3CC1B867219003F3591 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3E14C3DC1B867219003F3591 /* LaunchScreen.storyboard in Resources */, + 3E14C3D91B867219003F3591 /* Assets.xcassets in Resources */, + 3E14C3D71B867219003F3591 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3E14C3CA1B867219003F3591 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3E14C3D41B867219003F3591 /* CustomAssetPrintViewController.swift in Sources */, + 3E14C3D21B867219003F3591 /* AppDelegate.swift in Sources */, + 3E1AC9721B8B75A2006FC729 /* StandardAssetPrintViewController.swift in Sources */, + 3E14C3E51B868126003F3591 /* CustomAssetPrintPageRenderer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 3E14C3D51B867219003F3591 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 3E14C3D61B867219003F3591 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 3E14C3DA1B867219003F3591 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 3E14C3DB1B867219003F3591 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 3E14C3DE1B867219003F3591 /* 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; + 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; + }; + 3E14C3DF1B867219003F3591 /* 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; + }; + 3E14C3E11B867219003F3591 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = PrintPhoto/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.PrintPhoto"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 3E14C3E21B867219003F3591 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = PrintPhoto/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.PrintPhoto"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3E14C3C91B867219003F3591 /* Build configuration list for PBXProject "PrintPhoto" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3E14C3DE1B867219003F3591 /* Debug */, + 3E14C3DF1B867219003F3591 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3E14C3E01B867219003F3591 /* Build configuration list for PBXNativeTarget "PrintPhoto" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3E14C3E11B867219003F3591 /* Debug */, + 3E14C3E21B867219003F3591 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3E14C3C61B867219003F3591 /* Project object */; +} diff --git a/PrintPhoto/PrintPhoto.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/PrintPhoto/PrintPhoto.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..9e43d9c3 --- /dev/null +++ b/PrintPhoto/PrintPhoto.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/PrintPhoto/PrintPhoto.xcodeproj/xcuserdata/amigicovsky.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/PrintPhoto/PrintPhoto.xcodeproj/xcuserdata/amigicovsky.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 00000000..fe2b4541 --- /dev/null +++ b/PrintPhoto/PrintPhoto.xcodeproj/xcuserdata/amigicovsky.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,5 @@ + + + diff --git a/PrintPhoto/PrintPhoto.xcodeproj/xcuserdata/amigicovsky.xcuserdatad/xcschemes/PrintPhoto.xcscheme b/PrintPhoto/PrintPhoto.xcodeproj/xcuserdata/amigicovsky.xcuserdatad/xcschemes/PrintPhoto.xcscheme new file mode 100644 index 00000000..030cb7ff --- /dev/null +++ b/PrintPhoto/PrintPhoto.xcodeproj/xcuserdata/amigicovsky.xcuserdatad/xcschemes/PrintPhoto.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PrintPhoto/PrintPhoto.xcodeproj/xcuserdata/amigicovsky.xcuserdatad/xcschemes/xcschememanagement.plist b/PrintPhoto/PrintPhoto.xcodeproj/xcuserdata/amigicovsky.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..629ef4f2 --- /dev/null +++ b/PrintPhoto/PrintPhoto.xcodeproj/xcuserdata/amigicovsky.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,22 @@ + + + + + SchemeUserState + + PrintPhoto.xcscheme + + orderHint + 0 + + + SuppressBuildableAutocreation + + 3E14C3CD1B867219003F3591 + + primary + + + + + diff --git a/PrintPhoto/PrintPhoto/AppDelegate.swift b/PrintPhoto/PrintPhoto/AppDelegate.swift new file mode 100644 index 00000000..34a685ae --- /dev/null +++ b/PrintPhoto/PrintPhoto/AppDelegate.swift @@ -0,0 +1,16 @@ +/* + Copyright (C) 2015 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Main application entry point. +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + // MARK: Properties + + var window: UIWindow? +} \ No newline at end of file diff --git a/PrintPhoto/PrintPhoto/Assets.xcassets/AppIcon.appiconset/Contents.json b/PrintPhoto/PrintPhoto/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/PrintPhoto/PrintPhoto/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PrintPhoto/PrintPhoto/Assets.xcassets/Contents.json b/PrintPhoto/PrintPhoto/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/PrintPhoto/PrintPhoto/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PrintPhoto/PrintPhoto/Assets.xcassets/CustomAssetPrintPageRenderer.swift b/PrintPhoto/PrintPhoto/Assets.xcassets/CustomAssetPrintPageRenderer.swift new file mode 100644 index 00000000..ff10cbc7 --- /dev/null +++ b/PrintPhoto/PrintPhoto/Assets.xcassets/CustomAssetPrintPageRenderer.swift @@ -0,0 +1,98 @@ +/* + Copyright (C) 2015 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + `UIPrintPageRenderer` subclass for drawing an image for print. +*/ + +import UIKit + +/// A `UIPrintPageRenderer` subclass to print an image. +class CustomAssetPrintPageRenderer: UIPrintPageRenderer { + // MARK: Properties + + var image: UIImage + + // MARK: Initilization + + init(image: UIImage) { + self.image = image + } + + // MARK: UIPrintPageRenderer Overrides + + override func numberOfPages() -> Int { + return 1 + } + + override func drawPageAtIndex(pageIndex: Int, inRect printableRect: CGRect) { + /* + When `drawPageAtIndex(_:inRect:)` is invoked, `paperRect` reflects the + size of the paper we are printing on and `printableRect` reflects the + rectangle describing the imageable area of the page, that is the portion + of the page that the printer can mark without clipping. + */ + let paperSize = paperRect.size + let imageableAreaSize = printableRect.size + + /* + If `paperRect` and `printableRect` are the same size, the sheet is + borderless and we will use the fill algorithm. Otherwise we will uniformly + scale the image to fit the imageable area as close as is possible without + clipping. + */ + let fillsSheet = paperSize == imageableAreaSize + + let imageSize = image.size + + let destinationRect: CGRect + if fillsSheet { + destinationRect = CGRect(origin: .zero, size: paperSize) + } + else { + destinationRect = printableRect + } + + /* + Calculate the ratios of the destination rectangle width and height to + the image width and height. + */ + let widthScale = destinationRect.width / imageSize.width + let heightScale = destinationRect.height / imageSize.height + + // Scale the image to have some padding within the page. + let scale: CGFloat + + if fillsSheet { + // Produce a fill to the entire sheet and clips content. + scale = (widthScale > heightScale ? widthScale : heightScale) + } + else { + // Show all the content at the expense of additional white space. + scale = (widthScale < heightScale ? widthScale : heightScale) + } + + /* + Compute the coordinates for `centeredDestinationRect` so that the scaled + image is centered on the sheet. + */ + let printOriginX = (paperSize.width - imageSize.width * scale) / 2 + let printOriginY = (paperSize.height - imageSize.height * scale) / 2 + let printWidth = imageSize.width * scale + let printHeight = imageSize.height * scale + + let printRect = CGRect(x: printOriginX, y: printOriginY, width: printWidth, height: printHeight) + + // Inset the printed image by 10% of the size of the image. + let inset = max(printRect.width, printRect.height) * 0.1 + let insettedPrintRect = printRect.insetBy(dx: inset, dy: inset) + + // Create the vignette clipping. + let context = UIGraphicsGetCurrentContext()! + CGContextAddEllipseInRect(context, insettedPrintRect) + CGContextClip(context) + + image.drawInRect(insettedPrintRect) + } +} diff --git a/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/Contents.json b/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/Contents.json new file mode 100644 index 00000000..3c2faf70 --- /dev/null +++ b/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "FirstImage.jpg", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "FirstImage-1.jpg", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "FirstImage-2.jpg", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/FirstImage-1.jpg b/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/FirstImage-1.jpg new file mode 100644 index 00000000..beafcfd6 Binary files /dev/null and b/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/FirstImage-1.jpg differ diff --git a/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/FirstImage-2.jpg b/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/FirstImage-2.jpg new file mode 100644 index 00000000..beafcfd6 Binary files /dev/null and b/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/FirstImage-2.jpg differ diff --git a/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/FirstImage.jpg b/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/FirstImage.jpg new file mode 100644 index 00000000..beafcfd6 Binary files /dev/null and b/PrintPhoto/PrintPhoto/Assets.xcassets/Horse.imageset/FirstImage.jpg differ diff --git a/PrintPhoto/PrintPhoto/Base.lproj/LaunchScreen.storyboard b/PrintPhoto/PrintPhoto/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..a967f71c --- /dev/null +++ b/PrintPhoto/PrintPhoto/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PrintPhoto/PrintPhoto/Base.lproj/Main.storyboard b/PrintPhoto/PrintPhoto/Base.lproj/Main.storyboard new file mode 100644 index 00000000..04242fcf --- /dev/null +++ b/PrintPhoto/PrintPhoto/Base.lproj/Main.storyboard @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/PrintPhoto/PrintPhoto/CustomAssetPrintViewController.swift b/PrintPhoto/PrintPhoto/CustomAssetPrintViewController.swift new file mode 100644 index 00000000..2e64ab61 --- /dev/null +++ b/PrintPhoto/PrintPhoto/CustomAssetPrintViewController.swift @@ -0,0 +1,53 @@ +/* + Copyright (C) 2015 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A `UIViewController` subclass to handle picking, viewing, and printing of a photo. +*/ + +import MobileCoreServices +import UIKit + +class CustomAssetPrintViewController: UIViewController, UINavigationControllerDelegate { + // MARK: Properties + + @IBOutlet weak var imageView: UIImageView! + + // MARK: Target / Action Methods + + /// Invoked when the user chooses the action icon for printing. + @IBAction func shareImage() { + guard let image = imageView.image else { + fatalError("\(__FUNCTION__) expects image to not be nil.") + } + + let printPageRenderer = CustomAssetPrintPageRenderer(image: image) + + // Create a print info object for the activity. + let printInfo = UIPrintInfo.printInfo() + + /* + This application prints photos. UIKit will pick a paper size and print + quality appropriate for this content type. + */ + printInfo.outputType = .Photo + + // Use the name from the image metadata we've set. + printInfo.jobName = "Horse" + + // Give the print info and page renderer to UIKit. + let printActivityItems: [AnyObject] = [ + printInfo, + printPageRenderer + ] + + /* + Let the `UIActivityViewController` class handle presenting an action + sheet that will let the user print the image. + */ + let activityViewController = UIActivityViewController(activityItems: printActivityItems, applicationActivities: nil) + + presentViewController(activityViewController, animated: true, completion: nil) + } +} \ No newline at end of file diff --git a/PrintPhoto/PrintPhoto/Info.plist b/PrintPhoto/PrintPhoto/Info.plist new file mode 100644 index 00000000..40c6215d --- /dev/null +++ b/PrintPhoto/PrintPhoto/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/PrintPhoto/PrintPhoto/StandardAssetPrintViewController.swift b/PrintPhoto/PrintPhoto/StandardAssetPrintViewController.swift new file mode 100644 index 00000000..29631ed0 --- /dev/null +++ b/PrintPhoto/PrintPhoto/StandardAssetPrintViewController.swift @@ -0,0 +1,49 @@ +/* + Copyright (C) 2015 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A `UIViewController` subclass to handle picking, viewing, and printing of a photo. +*/ + +import MobileCoreServices +import UIKit + +class StandardAssetPrintViewController: UIViewController { + // MARK: Properties + + @IBOutlet weak var imageView: UIImageView! + + // MARK: Target / Action Methods + + /// Invoked when the user chooses the action icon for printing. + @IBAction func shareImage() { + // The image should never be nil (it's set in Interface Builder). + let image = imageView.image! + + // Create a print info object for the activity. + let printInfo = UIPrintInfo.printInfo() + + /* + This application prints photos. UIKit will pick a paper size and print + quality appropriate for this content type. + */ + printInfo.outputType = .Photo + + // Use the name from the image metadata we've set. + printInfo.jobName = "Horse" + + let printActivityItems: [AnyObject] = [ + printInfo, + image + ] + + /* + Let the `UIActivityViewController` class handle presenting an action + sheet that will let the user print the image. + */ + let activityViewController = UIActivityViewController(activityItems: printActivityItems, applicationActivities: nil) + + presentViewController(activityViewController, animated: true, completion: nil) + } +} \ No newline at end of file diff --git a/PrintPhoto/README.md b/PrintPhoto/README.md new file mode 100644 index 00000000..a46ec48d --- /dev/null +++ b/PrintPhoto/README.md @@ -0,0 +1,25 @@ +# PrintPhoto + +Demonstrates how to print an image via share sheets with the UIActivityViewController class. The sample also shows how to print simple images and custom drawings. + +## Requirements + +### Build + +Xcode 7.1 and iOS 9.0 SDK or later + +### Runtime + +iOS 9.0 or later + +## Architecture + +This sample can be run on a device or on the simulator. + +PrintPhoto demonstrates how to enable users to print an image using a share sheet. To do this you need to present a customized UIActivityViewController with the data you want to print. This sample shows two ways of doing this: + +1) Providing a UIImage instance to the UIActivityViewController. Using this approach allows UIKit to pick the optimal way to print an image. You can see how this works in the StandardAssetPrintViewController. + +2) Providing a UIPrintPageRenderer subclass that renders an image with customized UI. In this example we show how to draw the image as a vignette. However, the purpose of this sample _is not_ to show how to print a vignette but to show you that you can render any custom graphics you may have in your application. You can do this using CoreGraphics. For more information on how to print pages with custom graphics, see the CustomAssetPrintPageRenderer class. To see how you set up a UIActivityViewController with activity items that will allow you to use a custom UIPrintPageRenderer to print, see the CustomAssetPrintViewController class. Note that this class is very similar to the StandardAssetPrintViewController class: the only difference is that the standard view controller provides an image and the custom view controller passes a UIPrintPageRenderer subclass. + +Copyright (C) 2015 Apple Inc. All rights reserved. diff --git a/README.md b/README.md index 2ac6953c..20138418 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,77 @@ # [Swift学习资源](http://blog.liulantao.com/SwiftBeginnersGuide/) -![Swift](https://devimages.apple.com.edgekey.net/home/images/home-hero-swift-hero.png) +![Swift](http://images.apple.com/v/swift/b/images/overview/icon_swift_hero_large_2x.png) -[最新内容请查看Wiki](https://github.com/Lax/iOS-Swift-Demos/wiki) +> Swift,一种强大的开源编程语言, +> 让大家都能开发出众的 App。 +> Swift 是一种强劲而直观的编程语言,它由 Apple 创造,可用来为 iOS、Mac、Apple TV 和 Apple Watch 开发 app。它旨在为开发者提供充分的自由。Swift 易用并且开源,只要有想法,谁都可以创造非凡。 + +> Swift is a high-performance system programming language. It has a clean and modern syntax, offers seamless access to existing C and Objective-C code and frameworks, and is memory safe by default. + +> On December 3, 2015, the Swift language, supporting libraries, debugger, and package manager were published under the Apache 2.0 license with a Runtime Library Exception, and Swift.org was created to host the project. 苹果公司在WWDC 2014上宣布了他们将会推出一款新的编程语言,面向iOS和OS X系统的开发人员,这个新的语言被命名为Swift。 -Swift在iOS 8发布的时候推向市场,用来取代现有的Objective-C语言,对于这个巨大的决定,苹果公司的解释是Swift速度更快,使用起来更加容易。在Swift推出之后,苹果公司应该也不会停止对Objective-C的支持,开发工具会同时支持两种语言。 +Swift在iOS 8发布的时候推向市场,用来取代 Objective-C 语言。 +对于这个巨大的决定,苹果公司的解释是Swift速度更快,使用起来更加容易。 +在Swift推出之后,苹果公司应该不会停止对 Objective-C 的支持,开发工具会同时支持两种语言。 +在 Swift 开源后,开发者社区活跃。目前 CocoaPods 中有大量的第三方开发库已经支持 Swift。 -## 官方文档及示例 +### 贡献 -### 官方文档 +- 提交 PR - [iOS-Swift-Demos](https://github.com/Lax/iOS-Swift-Demos) +- 加入 QQ群 32958950 -* 《The Swift Programming Language》 - * [苹果官方版本](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/) - * iBooks版本([官方](https://itunes.apple.com/book/the-swift-programming-language/id881256329)) - * EPUB版本([官方](https://swift.org/documentation/TheSwiftProgrammingLanguage(Swift3).epub)) +### 官方资源 -* [《API Reference》](https://developer.apple.com/reference?language=swift) +- [Swift 开发者社区 - Swift.org](https://swift.org) -* Swift介绍 [Introducing Swift](https://developer.apple.com/swift/) +- [Swift 代码库](https://github.com/apple/swift) + +- [Apple developer 的 Swift 首页](https://developer.apple.com/swift/) + - [Swift Blog](https://developer.apple.com/swift/playgrounds/) + - [Swift Resources](https://developer.apple.com/swift/resources/) + - [Swift Playgrounds](https://developer.apple.com/swift/playgrounds/) Learn Swift on iPad + +- [苹果公司官网的 Swift 页面](http://www.apple.com/swift/) + - [Xcode](https://itunes.apple.com/app/xcode/id497799835) + +### 权威文档 + +* 《The Swift Programming Language》- The Definitive Book + - [Web](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/index.html) + - ePub([官方](https://swift.org/documentation/TheSwiftProgrammingLanguage(Swift3.0.1).epub) + - [iBooks Store](https://itunes.apple.com/us/book-series/swift-programming-series/id888896989?mt=11) + +* 《App Development with Swift》 + - [iBooks Store](https://itunes.apple.com/us/book/app-development-with-swift/id1118575552?mt=11) + +* 《Using Swift with Cocoa and Objective-C》 + - [iBooks Store](https://itunes.apple.com/us/book/using-swift-cocoa-objective/id888894773?mt=11) + - [Web](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/index.html) + +* 《[Swift Standard Library API Reference](https://developer.apple.com/reference/swift)》 + +* 《[Start Developing iOS Apps](https://developer.apple.com/library/content/referencelibrary/GettingStarted/DevelopiOSAppsSwift/index.html)》 + +* 《[API Design Guidelines](https://swift.org/documentation/api-design-guidelines/)》 + +* 《[Swift Programming Language Evolution](http://apple.github.io/swift-evolution/)》 + +* [WWDC2014 Videos](https://developer.apple.com/videos/wwdc/2014/) + +### 社区文档 + +- 《The Swift Programming Language》 [社区译中文版](https://github.com/numbbbbb/the-swift-programming-language-in-chinese) + +### 课程 + +- [Stanford University: Developing iOS 9 Apps with Swift](https://itunes.com/StanfordSwift) + +- [Plymouth University: iOS Development in Swift](https://itunes.com/PlymouthSwift) -* [WWDC2014 Videos](https://developer.apple.com/videos/wwdc/2014/) ### 非官方文档与社区(英文) @@ -77,19 +125,13 @@ Swift在iOS 8发布的时候推向市场,用来取代现有的Objective-C语 * [SPACESHIP OPERATOR IN SWIFT](http://vperi.com/2014/06/05/spaceship-operator-in-swift/) 和 [REGULAR EXPRESSIONS IN SWITCH STATEMENTS](http://vperi.com/2014/06/08/regular-expressions-in-switch-statements/) by Venkat Peri -### 博客与翻译(中文) - -有网友第一时间开始了官方文档的翻译工作,相信近期将由更多文档和教程出现。 - - #### 社区 * https://www.v2ex.com/go/swift * http://swift.sh * http://swift-china.org * CocoaChina的[Swift讨论区](http://www.cocoachina.com/bbs/thread.php?fid=57) -* QQ群 - * iOS开发者-开始Swift,群号:32958950,申请时请说明身份。 +* QQ群 iOS开发者-开始Swift,群号:32958950,申请时请说明身份。 #### 翻译 @@ -108,8 +150,7 @@ Swift在iOS 8发布的时候推向市场,用来取代现有的Objective-C语 #### 课程 -*    《[Swift Education](http://swifteducation.github.io/teaching_app_development_with_swift/)》 -*   《[SwiftV课堂](http://www.swiftv.cn/)》 免费Swift学习视频 +* 《[SwiftV课堂](http://www.swiftv.cn/)》 免费Swift学习视频 * 《[Apple Swift语言基础教程](http://www.jikexueyuan.com/course/92.html)》极客学院 @@ -184,18 +225,18 @@ Apple同时发布了3个示例程序,用于初窥Swift开发的项目。 ## 其它 -* [iOS Developer Library](https://developer.apple.com/library/prerelease/ios/navigation/) +* [iOS Developer Library](https://developer.apple.com/library/ios/navigation/) * [Chris Lattner](http://nondot.org/sabre/) Swift设计者 * [iOS 7.1 to iOS 8.0 API Differences](https://developer.apple.com/library/prerelease/ios/releasenotes/General/iOS80APIDiffs/index.html) * [App Extensions Increase Your Impact](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/index.html) -* [Swift学习资源](http://blog.liulantao.com/SwiftBeginnersGuide/) 新手的Swift学习资料汇总,比较详细总结了常用的资源。 +* [Swift学习资源](http://blog.liulantao.com/SwiftBeginnersGuide/) Swift 学习资料汇总,比较详细总结了常用的资源。 -### 因为重名躺枪的Swift +### 因为重名躺枪的 Swift * [Swift Lang](http://swift-lang.org) 一门很专业的并行编程语言,有苹果在Swift页面的链接,肯定带过去很多访问量。 * [OpenStack Swift](https://github.com/openstack/swift) OpenStack Object Storage (Swift)。 -* [Swift聊天工具](http://swift.im) 基于XMPP的聊天工具及服务端SDK。 +* [Swift 聊天工具](http://swift.im) 基于XMPP的聊天工具及服务端SDK。 * [Taylor Swift](http://en.wikipedia.org/wiki/Taylor_Swift) 美国乡村音乐女創作歌手、吉他歌手、演员。这位1989年出生的美女获得过数不清的格莱美奖及其它排行榜大奖。2014/05/30刚举办了泰勒•斯威夫特“红”巡演上海演唱会。WWDC2014之后三天,她从Google搜索结果首页被挤出,很受伤,歌迷们也很受伤。去[脸盆网](https://www.facebook.com/TaylorSwift)关注她,去音悦台[听她的歌](http://www.yinyuetai.com/fanclub/122)。 diff --git a/SamplePhotoEditingExtension/App (OS X)/AppDelegate.swift b/SamplePhotoEditingExtension/App (OS X)/AppDelegate.swift new file mode 100644 index 00000000..d52c148d --- /dev/null +++ b/SamplePhotoEditingExtension/App (OS X)/AppDelegate.swift @@ -0,0 +1,17 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application delegate. + */ + +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + + // No functionality in the host app, so nothing needed here. + +} + diff --git a/SamplePhotoEditingExtension/App (OS X)/Assets.xcassets/AppIcon.appiconset/Contents.json b/SamplePhotoEditingExtension/App (OS X)/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..2db2b1c7 --- /dev/null +++ b/SamplePhotoEditingExtension/App (OS X)/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SamplePhotoEditingExtension/App (OS X)/Base.lproj/Main.storyboard b/SamplePhotoEditingExtension/App (OS X)/Base.lproj/Main.storyboard new file mode 100644 index 00000000..d843e001 --- /dev/null +++ b/SamplePhotoEditingExtension/App (OS X)/Base.lproj/Main.storyboard @@ -0,0 +1,703 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This app serves only to host the example photo editing extension. + +To test the extension, edit a photo in the Photos app and choose the Photo Edit extension from the "..." menu. + + + + + + + + + + + + + + + + + + + diff --git a/SamplePhotoEditingExtension/App (OS X)/Info.plist b/SamplePhotoEditingExtension/App (OS X)/Info.plist new file mode 100644 index 00000000..d3d37546 --- /dev/null +++ b/SamplePhotoEditingExtension/App (OS X)/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2016 Apple. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + + diff --git a/SamplePhotoEditingExtension/App (iOS)/AppDelegate.swift b/SamplePhotoEditingExtension/App (iOS)/AppDelegate.swift new file mode 100644 index 00000000..0b0122de --- /dev/null +++ b/SamplePhotoEditingExtension/App (iOS)/AppDelegate.swift @@ -0,0 +1,19 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application delegate. + */ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + // No functionality in the host app, so nothing needed here. + +} + diff --git a/SamplePhotoEditingExtension/App (iOS)/Assets.xcassets/AppIcon.appiconset/Contents.json b/SamplePhotoEditingExtension/App (iOS)/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/SamplePhotoEditingExtension/App (iOS)/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SamplePhotoEditingExtension/App (iOS)/Base.lproj/LaunchScreen.storyboard b/SamplePhotoEditingExtension/App (iOS)/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..2e721e18 --- /dev/null +++ b/SamplePhotoEditingExtension/App (iOS)/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SamplePhotoEditingExtension/App (iOS)/Base.lproj/Main.storyboard b/SamplePhotoEditingExtension/App (iOS)/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f66104d6 --- /dev/null +++ b/SamplePhotoEditingExtension/App (iOS)/Base.lproj/Main.storyboard @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SamplePhotoEditingExtension/App (iOS)/Info.plist b/SamplePhotoEditingExtension/App (iOS)/Info.plist new file mode 100644 index 00000000..40c6215d --- /dev/null +++ b/SamplePhotoEditingExtension/App (iOS)/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/SamplePhotoEditingExtension/Extension (OS X)/Base.lproj/PhotoEditingViewController.xib b/SamplePhotoEditingExtension/Extension (OS X)/Base.lproj/PhotoEditingViewController.xib new file mode 100644 index 00000000..a67dcc06 --- /dev/null +++ b/SamplePhotoEditingExtension/Extension (OS X)/Base.lproj/PhotoEditingViewController.xib @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SamplePhotoEditingExtension/Extension (OS X)/Base.lproj/PhotoFilterItem.xib b/SamplePhotoEditingExtension/Extension (OS X)/Base.lproj/PhotoFilterItem.xib new file mode 100644 index 00000000..af1f28f4 --- /dev/null +++ b/SamplePhotoEditingExtension/Extension (OS X)/Base.lproj/PhotoFilterItem.xib @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SamplePhotoEditingExtension/Extension (OS X)/Info.plist b/SamplePhotoEditingExtension/Extension (OS X)/Info.plist new file mode 100644 index 00000000..5d91a817 --- /dev/null +++ b/SamplePhotoEditingExtension/Extension (OS X)/Info.plist @@ -0,0 +1,46 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Photo Edit + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSExtension + + NSExtensionAttributes + + PHSupportedMediaTypes + + Image + LivePhoto + Video + + + NSExtensionPointIdentifier + com.apple.photo-editing + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).PhotoEditingViewController + + NSHumanReadableCopyright + Copyright © 2016 Apple. All rights reserved. + + diff --git a/SamplePhotoEditingExtension/Extension (OS X)/PhotoEditingViewController.swift b/SamplePhotoEditingExtension/Extension (OS X)/PhotoEditingViewController.swift new file mode 100644 index 00000000..309d858d --- /dev/null +++ b/SamplePhotoEditingExtension/Extension (OS X)/PhotoEditingViewController.swift @@ -0,0 +1,190 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This view controller provides the UI for the photo edting extension in OS X. + */ + +import Cocoa +import Photos +import PhotosUI + +class PhotoEditingViewController: NSViewController, ContentEditingDelegate { + + @IBOutlet weak var collectionView: NSCollectionView! + @IBOutlet weak var previewImageView: NSImageView! + + // Shared object to handle editing in both iOS and OS X + let editController = ContentEditingController() + + // ContentEditingDelegate callbacks to UI + var preselectedFilterIndex: Int? + var previewImage: CIImage? + + // Hide stored properties in computed properties to allow use of @available. + private var livePhoto: AnyObject? + @available(OSXApplicationExtension 10.12, *) + var previewLivePhoto: PHLivePhoto? { + set { + livePhoto = newValue + + if previewLivePhotoView == nil { + previewLivePhotoView = PHLivePhotoView(frame: previewImageView.bounds) + previewLivePhotoView!.topAnchor.constraint(equalTo: previewImageView.topAnchor).isActive = true + previewLivePhotoView!.bottomAnchor.constraint(equalTo: previewImageView.bottomAnchor).isActive = true + previewLivePhotoView!.leftAnchor.constraint(equalTo: previewImageView.leftAnchor).isActive = true + previewLivePhotoView!.rightAnchor.constraint(equalTo: previewImageView.rightAnchor).isActive = true + previewImageView.addSubview(previewLivePhotoView!) + } + previewLivePhotoView!.livePhoto = previewLivePhoto + } + get { + return livePhoto as! PHLivePhoto? + } + } + private var livePhotoView: NSView? + @available(OSXApplicationExtension 10.12, *) + var previewLivePhotoView: PHLivePhotoView? { + set { livePhotoView = newValue } + get { return livePhotoView as! PHLivePhotoView? } + } + + override func viewDidLoad() { + super.viewDidLoad() + editController.delegate = self + } + + override func viewWillAppear() { + super.viewWillAppear() + + if let index = preselectedFilterIndex { + let indexPath = IndexPath(item: index, section: 0) + collectionView!.selectItems(at: [indexPath], scrollPosition: .centeredVertically) + updateSelection(for: collectionView.item(at: indexPath)!) + } + } + +} + +// MARK: PHContentEditingController +extension PhotoEditingViewController: PHContentEditingController { + + // Forward all methods to shared implementation for both platforms. + + func canHandle(_ adjustmentData: PHAdjustmentData) -> Bool { + return editController.canHandle(adjustmentData) + } + + func startContentEditing(with contentEditingInput: PHContentEditingInput, placeholderImage: NSImage) { + + previewImageView.image = placeholderImage + collectionView.reloadData() + + editController.startContentEditing(with: contentEditingInput) + } + + func finishContentEditing(completionHandler: @escaping ((PHContentEditingOutput?) -> Void)) { + // Update UI to reflect that editing has finished and output is being rendered. + + editController.finishContentEditing(completionHandler: completionHandler) + } + + var shouldShowCancelConfirmation: Bool { + return editController.shouldShowCancelConfirmation + } + + func cancelContentEditing() { + editController.cancelContentEditing() + } + +} + +extension PhotoEditingViewController: NSCollectionViewDataSource { + + func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int { + return editController.filterNames.count + } + + func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem { + + let item = collectionView.makeItem(withIdentifier: "PhotoFilterItem", for: indexPath) + + let filterName = editController.filterNames[indexPath.item!] + if filterName == editController.wwdcFilter { + item.textField!.stringValue = editController.wwdcFilter + } else { + // Query Core Image for filter's display name. + let filter = CIFilter(name: filterName)! + let filterDisplayName = filter.attributes[kCIAttributeFilterDisplayName]! as! String + item.textField!.stringValue = filterDisplayName + } + + // Show the preview image defined by the editing controller. + if let images = editController.previewImages { + previewImage = images[indexPath.item] + item.imageView!.image = NSImage(ciImage: previewImage!) + } + return item + } + +} + +extension PhotoEditingViewController: NSCollectionViewDelegate { + + func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set) { + guard let indexPath = indexPaths.first else { return } + updateSelection(for: collectionView.item(at: indexPath)!) + + let filterName = editController.filterNames[indexPath.item] + editController.selectedFilterName = filterName + + // Edit controller has already defined preview images for all filters, + // so just switch the big preview to the right one. + if let images = editController.previewImages { + previewImage = images[indexPath.item] + previewImageView.image = NSImage(ciImage: previewImage!) + } + + if #available(OSXApplicationExtension 10.12, *) { + editController.updateLivePhotoIfNeeded() // applies filter, sets previewLivePhoto on completion + } + + } + + func collectionView(_ collectionView: NSCollectionView, didDeselectItemsAt indexPaths: Set) { + guard let indexPath = indexPaths.first else { return } + updateSelection(for: collectionView.item(at: indexPath)!) + } + + func updateSelection(for item: NSCollectionViewItem) { + + let selectionColor = NSColor.alternateSelectedControlColor + item.imageView!.layer!.borderColor = selectionColor.cgColor + item.imageView!.layer!.borderWidth = item.isSelected ? 2 : 0 + + item.textField!.textColor = item.isSelected ? selectionColor : NSColor.alternateSelectedControlTextColor + } + +} + +// Convenience extension for creating a CIImageRep-backed NSImage. +private extension NSImage { + convenience init(ciImage: CIImage) { + self.init(size: ciImage.extent.size) + self.addRepresentation(NSCIImageRep(ciImage: ciImage)) + } +} + +// IndexPath.init(item:section) and IndexPath.item are missing from OS X in the WWDC seed. +// Use this extension as a temporary workaround. +private extension IndexPath { + init(item: Int, section: Int) { + self.init(indexes: [section, item]) + } + var item: Int! { + return (self as NSIndexPath).item + } +} + + diff --git a/SamplePhotoEditingExtension/Extension (OS X)/Photo_Edit.entitlements b/SamplePhotoEditingExtension/Extension (OS X)/Photo_Edit.entitlements new file mode 100644 index 00000000..f2ef3ae0 --- /dev/null +++ b/SamplePhotoEditingExtension/Extension (OS X)/Photo_Edit.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/SamplePhotoEditingExtension/Extension (Shared)/ContentEditingController.swift b/SamplePhotoEditingExtension/Extension (Shared)/ContentEditingController.swift new file mode 100644 index 00000000..0674e578 --- /dev/null +++ b/SamplePhotoEditingExtension/Extension (Shared)/ContentEditingController.swift @@ -0,0 +1,375 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This class provides the core photo editing functionality for both OS X and iOS extensions. + */ + +import Photos +import PhotosUI +import AVFoundation +#if os(iOS) +import MobileCoreServices +#endif + +/// Protocol for communication back to the view controller that owns the ContentEditingController. +protocol ContentEditingDelegate { + var preselectedFilterIndex: Int? { get set } + var previewImage: CIImage? { get set } + @available(OSXApplicationExtension 10.12, *) + var previewLivePhoto: PHLivePhoto? { get set } +} + +/// Provides photo editing functions for both OS X and iOS photo extension view controllers. +class ContentEditingController: NSObject { + + var input: PHContentEditingInput! + var delegate: ContentEditingDelegate! + + // Wrap in a lazy var so it can be hidden from earlier OS with @available. + @available(OSXApplicationExtension 10.12, iOSApplicationExtension 10.0, *) + lazy var livePhotoContext: PHLivePhotoEditingContext = { + return PHLivePhotoEditingContext(livePhotoEditingInput: self.input)! + }() + + static let wwdcLogo: CIImage = { + guard let url = Bundle(for: ContentEditingController.self).url(forResource: "Logo_WWDC2016", withExtension: "png") + else { fatalError("missing watermark image") } + guard let image = CIImage(contentsOf: url) + else { fatalError("can't load watermark image") } + return image + }() + + lazy var formatIdentifier = Bundle(for: ContentEditingController.self).bundleIdentifier! + let formatVersion = "1.0" + + var selectedFilterName: String? + let wwdcFilter = "WWDC16" + let filterNames = ["WWDC16", "CISepiaTone", "CIPhotoEffectChrome", "CIPhotoEffectInstant", "CIColorInvert", "CIColorPosterize"] + var previewImages: [CIImage]? + + // MARK: PHContentEditingController + + func canHandle(_ adjustmentData: PHAdjustmentData) -> Bool { + // Check the adjustment's identifier and version to allow resuming prior edits. + return adjustmentData.formatIdentifier == formatIdentifier && adjustmentData.formatVersion == formatVersion + } + + func startContentEditing(with contentEditingInput: PHContentEditingInput) { + input = contentEditingInput + + // Create preview images for all filters. + // If adjustment data is compatbile, these start from the last edit's pre-filter image. + updateImagePreviews() + + // Read adjustment data to choose (again) the last chosen filter. + if let adjustmentData = input.adjustmentData { + do { + selectedFilterName = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(adjustmentData.data as NSData) as? String + } catch { + print("can't unarchive adjustment data, reverting to default filter") + } + } + + // Show filter previews for the input image in the UI. + if let filterName = selectedFilterName, let index = filterNames.index(of: filterName) { + delegate.preselectedFilterIndex = index + delegate.previewImage = previewImages![index] + } + // ...including for Live Photo, if editing that kind of asset. + if #available(OSXApplicationExtension 10.12, iOSApplicationExtension 10.0, *) { + updateLivePhotoIfNeeded() + } + } + + func finishContentEditing(completionHandler: @escaping (PHContentEditingOutput?) -> Void) { + // Update UI to reflect that editing has finished and output is being rendered. + + let output = PHContentEditingOutput(contentEditingInput: input) + + // All this extension needs for resuming edits is a filter name, so that's the adjustment data. + output.adjustmentData = PHAdjustmentData( + formatIdentifier: formatIdentifier, formatVersion: formatVersion, + data: NSKeyedArchiver.archivedData(withRootObject: (selectedFilterName ?? "") as NSString) + ) + + if #available(OSXApplicationExtension 10.12, iOSApplicationExtension 10.0, *), input.livePhoto != nil { + // PHLivePhotoEditingContext already uses a background queue, so no dispatch + livePhotoContext.saveLivePhoto(to: output) { success, error in + if success { + completionHandler(output) + } else { + NSLog("can't output live photo") + completionHandler(nil) + } + } + return + } + + // Render and provide output on a background queue. + DispatchQueue.global(qos: .userInitiated).async { + switch self.input.mediaType { + case .image: + self.processImage(to: output, completionHandler: completionHandler) + case .video: + self.processVideo(to: output, completionHandler: completionHandler) + default: + NSLog("can't handle media type \(self.input.mediaType)") + completionHandler(nil) + } + } + } + + var shouldShowCancelConfirmation: Bool { + /* + This extension doesn't involve any major editing, just picking a filter, + so there's no need to confirm cancellation -- all you lose when canceling + is your choice of filter, which you can restore with one click. + + If your extension UI involves lots of adjusting parameters, or "painting" + edits onto the image like brush strokes, the user doesn't want to lose those + with a stray tap/click of the Cancel button, so return true if your state + reflects such invested user effort. + */ + return false + } + + func cancelContentEditing() { + // Nothing to clean up in this extension. If your extension creates temporary + // files, etc, destroy them here. + } + + // MARK: Media processing + + func updateImagePreviews() { + previewImages = filterNames.map { filterName in + + // Load preview-size image to process from input. + let inputImage: CIImage + if input.mediaType == .video { + guard let avAsset = input.audiovisualAsset + else { fatalError("can't get input AV asset") } + inputImage = avAsset.thumbnailImage + } else { // mediaType == .photo + guard let image = input.displaySizeImage + else { fatalError("missing input image") } + guard let ciImage = CIImage(image: image) + else { fatalError("can't load input image to apply edit") } + inputImage = ciImage + } + + // Define output image with Core Image edits. + if filterName == wwdcFilter { + return inputImage.applyingWWDCDemoEffect(shouldWatermark: false) + } else { + return inputImage.applyingFilter(filterName, withInputParameters: nil) + } + } + } + + @available(OSXApplicationExtension 10.12, iOSApplicationExtension 10.0, *) + func updateLivePhotoIfNeeded() { + if input.livePhoto != nil { + switch self.selectedFilterName { + case .some(wwdcFilter): + setupWWDCDemoProcessor() + case .some(let filterName): + livePhotoContext.frameProcessor = { frame, _ in + return frame.image.applyingFilter(filterName, withInputParameters: nil) + } + default: + // Passthru to preview the unedited Live Photo at display size. + livePhotoContext.frameProcessor = { frame, _ in + return frame.image + } + } + let size = input.displaySizeImage!.size + livePhotoContext.prepareLivePhotoForPlayback(withTargetSize: size, options: nil, completionHandler: { livePhoto, error in + self.delegate.previewLivePhoto = livePhoto + }) + } + } + + /// Advanced Live Photo processing shown at WWDC16 session 505 + @available(OSXApplicationExtension 10.12, iOSApplicationExtension 10.0, *) + func setupWWDCDemoProcessor() { + + /** + Simple linear ramp to convert frame times + from the range 0 ... photoTime ... duration) + to the range -1 ... 0 ... +1 + */ + let photoTime = CMTimeGetSeconds(livePhotoContext.photoTime) + let duration = CMTimeGetSeconds(livePhotoContext.duration) + func convertTime(_ time: Float64) -> CGFloat { + if time < photoTime { + return CGFloat((time - photoTime) / photoTime) + } else { + return CGFloat((time - photoTime) / (duration - photoTime)) + } + } + + livePhotoContext.frameProcessor = { frame, _ in + return frame.image.applyingWWDCDemoEffect( + // Normalized frame time for animating the effect: + time: convertTime(CMTimeGetSeconds(frame.time)), + // Scale factor for pixel-size-dependent effects: + scale: frame.renderScale, + // Add watermark only to the still photo frame in the Live Photo: + shouldWatermark: frame.type == .photo) + } + } + + func processImage(to output: PHContentEditingOutput, completionHandler: ((PHContentEditingOutput?) -> Void)) { + + // Load full-size image to process from input. + guard let url = input.fullSizeImageURL + else { fatalError("missing input image url") } + guard let inputImage = CIImage(contentsOf: url) + else { fatalError("can't load input image to apply edit") } + + // Define output image with Core Image edits. + let orientedImage = inputImage.applyingOrientation(input.fullSizeImageOrientation) + let outputImage: CIImage + switch selectedFilterName { + case .some(wwdcFilter): + outputImage = orientedImage.applyingWWDCDemoEffect() + case .some(let filterName): + outputImage = orientedImage.applyingFilter(filterName, withInputParameters: nil) + default: + outputImage = orientedImage + } + + // Usually you want to create a CIContext early and reuse it, but + // this extension uses one (explicitly) only on exit. + let context = CIContext() + // Render the filtered image to the expected output URL. + if #available(OSXApplicationExtension 10.12, iOSApplicationExtension 10.0, *) { + // Use Core Image convenience method to write JPEG where supported. + do { + try context.writeJPEGRepresentation(of: outputImage, to: output.renderedContentURL, colorSpace: inputImage.colorSpace!) + completionHandler(output) + } catch let error { + NSLog("can't write image: \(error)") + completionHandler(nil) + } + } else { + // Use CGImageDestination to write JPEG in older OS. + guard let cgImage = context.createCGImage(outputImage, from: outputImage.extent) + else { fatalError("can't create CGImage") } + guard let destination = CGImageDestinationCreateWithURL(output.renderedContentURL as CFURL, kUTTypeJPEG, 1, nil) + else { fatalError("can't create CGImageDestination") } + CGImageDestinationAddImage(destination, cgImage, nil) + let success = CGImageDestinationFinalize(destination) + if success { + completionHandler(output) + } else { + completionHandler(nil) + } + } + + } + + func processVideo(to output: PHContentEditingOutput, completionHandler: @escaping ((PHContentEditingOutput?) -> Void)) { + + // Load AVAsset to process from input. + guard let avAsset = input.audiovisualAsset + else { fatalError("can't get input AV asset") } + let duration = CMTimeGetSeconds(avAsset.duration) + + // Set up a video composition to apply the filter. + let composition = AVVideoComposition( + asset: avAsset, + applyingCIFiltersWithHandler: { request in + let filtered: CIImage + switch self.selectedFilterName { + case .some(self.wwdcFilter): + let frameTime = CGFloat(CMTimeGetSeconds(request.compositionTime) / duration) + filtered = request.sourceImage.applyingWWDCDemoEffect(time: frameTime) + case .some(let filterName): + filtered = request.sourceImage.applyingFilter(filterName, withInputParameters: nil) + default: + filtered = request.sourceImage // Passthru if no filter is selected + } + request.finish(with: filtered, context: nil) + }) + + // Write the processed asset to the output URL. + guard let export = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetHighestQuality) + else { fatalError("can't set up AV export session") } + + export.outputFileType = AVFileTypeQuickTimeMovie + export.outputURL = output.renderedContentURL + export.videoComposition = composition + export.exportAsynchronously { + completionHandler(output) + } + } + +} + +private extension CIImage { + + func applyingWWDCDemoEffect(time: CGFloat = 0, scale: CGFloat = 1, shouldWatermark: Bool = true) -> CIImage { + + // Demo step 1: Crop to square, animating crop position. + let length = min(extent.width, extent.height) + let cropOrigin = CGPoint(x: (1 + time) * (extent.width - length) / 2, + y: (1 + time) * (extent.height - length) / 2) + let cropRect = CGRect(origin: cropOrigin, + size: CGSize(width: length, height: length)) + let cropped = self.cropping(to: cropRect) + + // Demo step 2: Add vignette effect. + let vignetted = cropped.applyingFilter("CIVignetteEffect", withInputParameters: + [ kCIInputCenterKey: CIVector(x: cropped.extent.midX, y: cropped.extent.midY), + kCIInputRadiusKey: length * CGFloat(M_SQRT1_2), + ]) + + // Demo step 3: Add line screen effect. + let screen = vignetted.applyingFilter("CILineScreen", withInputParameters: + [ kCIInputAngleKey : CGFloat.pi * 3/4, + kCIInputCenterKey : CIVector(x: vignetted.extent.midX, y: vignetted.extent.midY), + kCIInputWidthKey : 50 * scale + ]) + let screened = screen.applyingFilter("CIMultiplyCompositing", withInputParameters: [kCIInputBackgroundImageKey: self]) + + // Demo step 5: Add watermark if desired. + if shouldWatermark { + // Scale logo to rendering resolution and position it for compositing. + let logoWidth = ContentEditingController.wwdcLogo.extent.width + let logoScale = screened.extent.width * 0.7 / logoWidth + let scaledLogo = ContentEditingController.wwdcLogo + .applying(CGAffineTransform(scaleX: logoScale, y: logoScale)) + let logo = scaledLogo + .applying(CGAffineTransform(translationX: screened.extent.minX + (screened.extent.width - scaledLogo.extent.width) / 2, y: screened.extent.minY + scaledLogo.extent.height)) + // Composite logo over the main image. + return logo.applyingFilter("CILinearDodgeBlendMode", withInputParameters: [kCIInputBackgroundImageKey: screened]) + } else { + return screened + } + } + +#if os(OSX) + // CIImage.init(NSImage) is missing from Swift in the WWDC seed. + // Use this extension as a temporary workaround. + convenience init?(image: NSImage) { + guard let imageRep = image.representations.first as? NSBitmapImageRep + else { return nil } + guard let cgImage = imageRep.cgImage + else { return nil } + self.init(cgImage: cgImage) + } +#endif +} + +private extension AVAsset { + var thumbnailImage: CIImage { + let imageGenerator = AVAssetImageGenerator(asset: self) + imageGenerator.appliesPreferredTrackTransform = true + + let cgImage = try! imageGenerator.copyCGImage(at: CMTime(seconds: 0, preferredTimescale: 30), actualTime: nil) + return CIImage(cgImage: cgImage) + } +} diff --git a/SamplePhotoEditingExtension/Extension (Shared)/Logo_WWDC2016.png b/SamplePhotoEditingExtension/Extension (Shared)/Logo_WWDC2016.png new file mode 100755 index 00000000..7d69540e Binary files /dev/null and b/SamplePhotoEditingExtension/Extension (Shared)/Logo_WWDC2016.png differ diff --git a/SamplePhotoEditingExtension/Extension (iOS)/Base.lproj/MainInterface.storyboard b/SamplePhotoEditingExtension/Extension (iOS)/Base.lproj/MainInterface.storyboard new file mode 100644 index 00000000..2660ee16 --- /dev/null +++ b/SamplePhotoEditingExtension/Extension (iOS)/Base.lproj/MainInterface.storyboard @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SamplePhotoEditingExtension/Extension (iOS)/Info.plist b/SamplePhotoEditingExtension/Extension (iOS)/Info.plist new file mode 100644 index 00000000..f3d92e81 --- /dev/null +++ b/SamplePhotoEditingExtension/Extension (iOS)/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Photo Edit + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + PHSupportedMediaTypes + + Image + LivePhoto + Video + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.photo-editing + + + diff --git a/SamplePhotoEditingExtension/Extension (iOS)/PhotoEditingViewController.swift b/SamplePhotoEditingExtension/Extension (iOS)/PhotoEditingViewController.swift new file mode 100644 index 00000000..ecf9a94e --- /dev/null +++ b/SamplePhotoEditingExtension/Extension (iOS)/PhotoEditingViewController.swift @@ -0,0 +1,151 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + This view controller provides the UI for the photo edting extension in iOS. + */ + +import UIKit +import Photos +import PhotosUI + +class PhotoEditingViewController: UIViewController, ContentEditingDelegate { + + @IBOutlet weak var previewImageView: UIImageView! + @IBOutlet weak var backgroundImageView: UIImageView! + @IBOutlet weak var collectionView: UICollectionView! + @IBOutlet weak var livePhotoView: PHLivePhotoView! + + // Shared object to handle editing in both iOS and OS X + lazy var editController = ContentEditingController() + + // ContentEditingDelegate callbacks to UI + var preselectedFilterIndex: Int? + var previewImage: CIImage? + var previewLivePhoto: PHLivePhoto? { + didSet { + livePhotoView.livePhoto = previewLivePhoto + livePhotoView.contentMode = .scaleAspectFit + } + } + + override func viewDidLoad() { + super.viewDidLoad() + editController.delegate = self + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let index = preselectedFilterIndex { + let indexPath = IndexPath(row: index, section: 0) + collectionView!.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally) + updateSelection(for: collectionView.cellForItem(at: indexPath)!) + } + } +} + +// MARK: PHContentEditingController +extension PhotoEditingViewController: PHContentEditingController { + + // Forward all methods to shared implementation for both platforms. + + func canHandle(_ adjustmentData: PHAdjustmentData) -> Bool { + return editController.canHandle(adjustmentData) + } + + func startContentEditing(with contentEditingInput: PHContentEditingInput, placeholderImage: UIImage) { + + // Except here: show the image preview before forwarding. + previewImageView.image = placeholderImage + backgroundImageView.image = placeholderImage + collectionView.reloadData() + + editController.startContentEditing(with: contentEditingInput) + } + + func finishContentEditing(completionHandler: @escaping (PHContentEditingOutput?) -> Swift.Void) { + editController.finishContentEditing(completionHandler: completionHandler) + } + + var shouldShowCancelConfirmation: Bool { + return editController.shouldShowCancelConfirmation + } + + func cancelContentEditing() { + editController.cancelContentEditing() + } + +} + +// Cell class for collection view +class PhotoFilterCell: UICollectionViewCell { + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var filterNameLabel: UILabel! +} + +// MARK: UICollectionViewDataSource +extension PhotoEditingViewController: UICollectionViewDataSource { + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return editController.filterNames.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: PhotoFilterCell.self), for: indexPath) as? PhotoFilterCell + else { fatalError("unexpected cell in storyboard") } + + let filterName = editController.filterNames[indexPath.item] + if filterName == editController.wwdcFilter { + cell.filterNameLabel.text = editController.wwdcFilter + } else { + // Query Core Image for filter's display name. + let filter = CIFilter(name: filterName)! + let filterDisplayName = filter.attributes[kCIAttributeFilterDisplayName]! as! String + cell.filterNameLabel.text = filterDisplayName + } + // Show the preview image defined by the editing controller. + if let images = editController.previewImages { + cell.imageView.image = UIImage(ciImage: images[indexPath.item]) + } + + return cell + } +} + +// MARK: UICollectionViewDelegate +extension PhotoEditingViewController: UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + updateSelection(for: collectionView.cellForItem(at: indexPath)!) + + let filterName = editController.filterNames[indexPath.item] + editController.selectedFilterName = filterName + + // Edit controller has already defined preview images for all filters, + // so just switch the big preview to the right one. + if let images = editController.previewImages { + previewImage = images[indexPath.item] + previewImageView.image = UIImage(ciImage: previewImage!) + } + + if #available(iOSApplicationExtension 10.0, *) { + editController.updateLivePhotoIfNeeded() // applies filter, sets previewLivePhoto on completion + } + } + + func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { + updateSelection(for: collectionView.cellForItem(at: indexPath)!) + } + + func updateSelection(for cell: UICollectionViewCell) { + guard let cell = cell as? PhotoFilterCell else { fatalError("unexpected cell") } + + cell.imageView.layer.borderColor = view.tintColor.cgColor + cell.imageView.layer.borderWidth = cell.isSelected ? 2 : 0 + + cell.filterNameLabel.textColor = cell.isSelected ? view.tintColor : .white + } +} diff --git a/SamplePhotoEditingExtension/LICENSE.txt b/SamplePhotoEditingExtension/LICENSE.txt new file mode 100644 index 00000000..48be7056 --- /dev/null +++ b/SamplePhotoEditingExtension/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: Sample Photo Editing Extension +Version: 2.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) 2016 Apple Inc. All Rights Reserved. diff --git a/SamplePhotoEditingExtension/Photo Edit.xcodeproj/project.pbxproj b/SamplePhotoEditingExtension/Photo Edit.xcodeproj/project.pbxproj new file mode 100644 index 00000000..2915d056 --- /dev/null +++ b/SamplePhotoEditingExtension/Photo Edit.xcodeproj/project.pbxproj @@ -0,0 +1,785 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 116898691CEB9E9E00AD073E /* PhotoFilterItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = 116898671CEB9E9E00AD073E /* PhotoFilterItem.xib */; }; + 1194F4F11CE64AEC00F4EDF0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1194F4F01CE64AEC00F4EDF0 /* AppDelegate.swift */; }; + 1194F4F61CE64AEC00F4EDF0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1194F4F41CE64AEC00F4EDF0 /* Main.storyboard */; }; + 1194F4F81CE64AEC00F4EDF0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1194F4F71CE64AEC00F4EDF0 /* Assets.xcassets */; }; + 1194F4FB1CE64AEC00F4EDF0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1194F4F91CE64AEC00F4EDF0 /* LaunchScreen.storyboard */; }; + 1194F5091CE658DF00F4EDF0 /* PhotosUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1194F5081CE658DF00F4EDF0 /* PhotosUI.framework */; }; + 1194F50B1CE658DF00F4EDF0 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1194F50A1CE658DF00F4EDF0 /* Photos.framework */; }; + 1194F50E1CE658DF00F4EDF0 /* PhotoEditingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1194F50D1CE658DF00F4EDF0 /* PhotoEditingViewController.swift */; }; + 1194F5111CE658DF00F4EDF0 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1194F50F1CE658DF00F4EDF0 /* MainInterface.storyboard */; }; + 1194F5151CE658DF00F4EDF0 /* Photo Edit.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 1194F5061CE658DF00F4EDF0 /* Photo Edit.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 1194F5211CE681A100F4EDF0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1194F5201CE681A100F4EDF0 /* AppDelegate.swift */; }; + 1194F5251CE681A100F4EDF0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1194F5241CE681A100F4EDF0 /* Assets.xcassets */; }; + 1194F5281CE681A100F4EDF0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1194F5261CE681A100F4EDF0 /* Main.storyboard */; }; + 1194F5321CE682BE00F4EDF0 /* PhotosUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1194F5081CE658DF00F4EDF0 /* PhotosUI.framework */; }; + 1194F5331CE682BE00F4EDF0 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1194F50A1CE658DF00F4EDF0 /* Photos.framework */; }; + 1194F5381CE682BE00F4EDF0 /* PhotoEditingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1194F5371CE682BE00F4EDF0 /* PhotoEditingViewController.swift */; }; + 1194F53B1CE682BE00F4EDF0 /* PhotoEditingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1194F5391CE682BE00F4EDF0 /* PhotoEditingViewController.xib */; }; + 1194F53F1CE682BE00F4EDF0 /* Photo Edit.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 1194F5311CE682BE00F4EDF0 /* Photo Edit.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 1194F5481CE6873C00F4EDF0 /* ContentEditingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1194F5471CE6873C00F4EDF0 /* ContentEditingController.swift */; }; + 1194F5491CE6873C00F4EDF0 /* ContentEditingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1194F5471CE6873C00F4EDF0 /* ContentEditingController.swift */; }; + 11B4A8FD1D025F1700F2E5EE /* Logo_WWDC2016.png in Resources */ = {isa = PBXBuildFile; fileRef = 11B4A8FC1D025F1700F2E5EE /* Logo_WWDC2016.png */; }; + 11B4A8FE1D025F1700F2E5EE /* Logo_WWDC2016.png in Resources */ = {isa = PBXBuildFile; fileRef = 11B4A8FC1D025F1700F2E5EE /* Logo_WWDC2016.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 1194F5131CE658DF00F4EDF0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1194F4E51CE64AEC00F4EDF0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1194F5051CE658DF00F4EDF0; + remoteInfo = "Photo Edit"; + }; + 1194F53D1CE682BE00F4EDF0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1194F4E51CE64AEC00F4EDF0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1194F5301CE682BE00F4EDF0; + remoteInfo = "Photo Edit"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 1194F5191CE658DF00F4EDF0 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 1194F5151CE658DF00F4EDF0 /* Photo Edit.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1194F5431CE682BE00F4EDF0 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 1194F53F1CE682BE00F4EDF0 /* Photo Edit.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1112F6AB1CEA9ED700D27706 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 116898681CEB9E9E00AD073E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PhotoFilterItem.xib; sourceTree = ""; }; + 1194F4ED1CE64AEC00F4EDF0 /* Photo Edit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Photo Edit.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1194F4F01CE64AEC00F4EDF0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 1194F4F51CE64AEC00F4EDF0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 1194F4F71CE64AEC00F4EDF0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1194F4FA1CE64AEC00F4EDF0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 1194F4FC1CE64AEC00F4EDF0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1194F5061CE658DF00F4EDF0 /* Photo Edit.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Photo Edit.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1194F5081CE658DF00F4EDF0 /* PhotosUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PhotosUI.framework; path = System/Library/Frameworks/PhotosUI.framework; sourceTree = SDKROOT; }; + 1194F50A1CE658DF00F4EDF0 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; + 1194F50D1CE658DF00F4EDF0 /* PhotoEditingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoEditingViewController.swift; sourceTree = ""; }; + 1194F5101CE658DF00F4EDF0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 1194F5121CE658DF00F4EDF0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1194F51E1CE681A100F4EDF0 /* Photo Edit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Photo Edit.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1194F5201CE681A100F4EDF0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 1194F5241CE681A100F4EDF0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1194F5271CE681A100F4EDF0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 1194F5291CE681A100F4EDF0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1194F5311CE682BE00F4EDF0 /* Photo Edit.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Photo Edit.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1194F5361CE682BE00F4EDF0 /* Photo_Edit.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Photo_Edit.entitlements; sourceTree = ""; }; + 1194F5371CE682BE00F4EDF0 /* PhotoEditingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoEditingViewController.swift; sourceTree = ""; }; + 1194F53A1CE682BE00F4EDF0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PhotoEditingViewController.xib; sourceTree = ""; }; + 1194F53C1CE682BE00F4EDF0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1194F5471CE6873C00F4EDF0 /* ContentEditingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentEditingController.swift; sourceTree = ""; }; + 11B4A8FC1D025F1700F2E5EE /* Logo_WWDC2016.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Logo_WWDC2016.png; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1194F4EA1CE64AEC00F4EDF0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1194F5031CE658DF00F4EDF0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1194F5091CE658DF00F4EDF0 /* PhotosUI.framework in Frameworks */, + 1194F50B1CE658DF00F4EDF0 /* Photos.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1194F51B1CE681A100F4EDF0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1194F52E1CE682BE00F4EDF0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1194F5321CE682BE00F4EDF0 /* PhotosUI.framework in Frameworks */, + 1194F5331CE682BE00F4EDF0 /* Photos.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1194F4E41CE64AEC00F4EDF0 = { + isa = PBXGroup; + children = ( + 1112F6AB1CEA9ED700D27706 /* README.md */, + 1194F5451CE6837B00F4EDF0 /* Photo Editing Extension */, + 1194F5461CE6838E00F4EDF0 /* Host App */, + 1194F5071CE658DF00F4EDF0 /* Frameworks */, + 1194F4EE1CE64AEC00F4EDF0 /* Products */, + ); + sourceTree = ""; + }; + 1194F4EE1CE64AEC00F4EDF0 /* Products */ = { + isa = PBXGroup; + children = ( + 1194F4ED1CE64AEC00F4EDF0 /* Photo Edit.app */, + 1194F5061CE658DF00F4EDF0 /* Photo Edit.appex */, + 1194F51E1CE681A100F4EDF0 /* Photo Edit.app */, + 1194F5311CE682BE00F4EDF0 /* Photo Edit.appex */, + ); + name = Products; + sourceTree = ""; + }; + 1194F4EF1CE64AEC00F4EDF0 /* iOS */ = { + isa = PBXGroup; + children = ( + 1194F4F01CE64AEC00F4EDF0 /* AppDelegate.swift */, + 1194F4F41CE64AEC00F4EDF0 /* Main.storyboard */, + 1194F4F71CE64AEC00F4EDF0 /* Assets.xcassets */, + 1194F4F91CE64AEC00F4EDF0 /* LaunchScreen.storyboard */, + 1194F4FC1CE64AEC00F4EDF0 /* Info.plist */, + ); + name = iOS; + path = "App (iOS)"; + sourceTree = ""; + }; + 1194F5071CE658DF00F4EDF0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1194F5081CE658DF00F4EDF0 /* PhotosUI.framework */, + 1194F50A1CE658DF00F4EDF0 /* Photos.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 1194F50C1CE658DF00F4EDF0 /* iOS */ = { + isa = PBXGroup; + children = ( + 1194F50D1CE658DF00F4EDF0 /* PhotoEditingViewController.swift */, + 1194F50F1CE658DF00F4EDF0 /* MainInterface.storyboard */, + 1194F5121CE658DF00F4EDF0 /* Info.plist */, + ); + name = iOS; + path = "Extension (iOS)"; + sourceTree = ""; + }; + 1194F51F1CE681A100F4EDF0 /* OS X */ = { + isa = PBXGroup; + children = ( + 1194F5201CE681A100F4EDF0 /* AppDelegate.swift */, + 1194F5241CE681A100F4EDF0 /* Assets.xcassets */, + 1194F5261CE681A100F4EDF0 /* Main.storyboard */, + 1194F5291CE681A100F4EDF0 /* Info.plist */, + ); + name = "OS X"; + path = "App (OS X)"; + sourceTree = ""; + }; + 1194F5341CE682BE00F4EDF0 /* OS X */ = { + isa = PBXGroup; + children = ( + 1194F5371CE682BE00F4EDF0 /* PhotoEditingViewController.swift */, + 1194F5391CE682BE00F4EDF0 /* PhotoEditingViewController.xib */, + 116898671CEB9E9E00AD073E /* PhotoFilterItem.xib */, + 1194F53C1CE682BE00F4EDF0 /* Info.plist */, + 1194F5351CE682BE00F4EDF0 /* Supporting Files */, + ); + name = "OS X"; + path = "Extension (OS X)"; + sourceTree = ""; + }; + 1194F5351CE682BE00F4EDF0 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 1194F5361CE682BE00F4EDF0 /* Photo_Edit.entitlements */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1194F5441CE682F500F4EDF0 /* Shared */ = { + isa = PBXGroup; + children = ( + 1194F5471CE6873C00F4EDF0 /* ContentEditingController.swift */, + 11B4A8FC1D025F1700F2E5EE /* Logo_WWDC2016.png */, + ); + name = Shared; + path = "Extension (Shared)"; + sourceTree = ""; + }; + 1194F5451CE6837B00F4EDF0 /* Photo Editing Extension */ = { + isa = PBXGroup; + children = ( + 1194F5441CE682F500F4EDF0 /* Shared */, + 1194F50C1CE658DF00F4EDF0 /* iOS */, + 1194F5341CE682BE00F4EDF0 /* OS X */, + ); + name = "Photo Editing Extension"; + sourceTree = ""; + }; + 1194F5461CE6838E00F4EDF0 /* Host App */ = { + isa = PBXGroup; + children = ( + 1194F4EF1CE64AEC00F4EDF0 /* iOS */, + 1194F51F1CE681A100F4EDF0 /* OS X */, + ); + name = "Host App"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1194F4EC1CE64AEC00F4EDF0 /* Photo Edit App iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1194F4FF1CE64AEC00F4EDF0 /* Build configuration list for PBXNativeTarget "Photo Edit App iOS" */; + buildPhases = ( + 1194F4E91CE64AEC00F4EDF0 /* Sources */, + 1194F4EA1CE64AEC00F4EDF0 /* Frameworks */, + 1194F4EB1CE64AEC00F4EDF0 /* Resources */, + 1194F5191CE658DF00F4EDF0 /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 1194F5141CE658DF00F4EDF0 /* PBXTargetDependency */, + ); + name = "Photo Edit App iOS"; + productName = "Photo Edit"; + productReference = 1194F4ED1CE64AEC00F4EDF0 /* Photo Edit.app */; + productType = "com.apple.product-type.application"; + }; + 1194F5051CE658DF00F4EDF0 /* Photo Edit Extension iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1194F5161CE658DF00F4EDF0 /* Build configuration list for PBXNativeTarget "Photo Edit Extension iOS" */; + buildPhases = ( + 1194F5021CE658DF00F4EDF0 /* Sources */, + 1194F5031CE658DF00F4EDF0 /* Frameworks */, + 1194F5041CE658DF00F4EDF0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Photo Edit Extension iOS"; + productName = "Photo Edit"; + productReference = 1194F5061CE658DF00F4EDF0 /* Photo Edit.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 1194F51D1CE681A100F4EDF0 /* Photo Edit App OS X */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1194F52A1CE681A100F4EDF0 /* Build configuration list for PBXNativeTarget "Photo Edit App OS X" */; + buildPhases = ( + 1194F51A1CE681A100F4EDF0 /* Sources */, + 1194F51B1CE681A100F4EDF0 /* Frameworks */, + 1194F51C1CE681A100F4EDF0 /* Resources */, + 1194F5431CE682BE00F4EDF0 /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 1194F53E1CE682BE00F4EDF0 /* PBXTargetDependency */, + ); + name = "Photo Edit App OS X"; + productName = "Photo Edit"; + productReference = 1194F51E1CE681A100F4EDF0 /* Photo Edit.app */; + productType = "com.apple.product-type.application"; + }; + 1194F5301CE682BE00F4EDF0 /* Photo Edit Extension OS X */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1194F5401CE682BE00F4EDF0 /* Build configuration list for PBXNativeTarget "Photo Edit Extension OS X" */; + buildPhases = ( + 1194F52D1CE682BE00F4EDF0 /* Sources */, + 1194F52E1CE682BE00F4EDF0 /* Frameworks */, + 1194F52F1CE682BE00F4EDF0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Photo Edit Extension OS X"; + productName = "Photo Edit"; + productReference = 1194F5311CE682BE00F4EDF0 /* Photo Edit.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1194F4E51CE64AEC00F4EDF0 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Apple; + TargetAttributes = { + 1194F4EC1CE64AEC00F4EDF0 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + 1194F5051CE658DF00F4EDF0 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + 1194F51D1CE681A100F4EDF0 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + 1194F5301CE682BE00F4EDF0 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 1194F4E81CE64AEC00F4EDF0 /* Build configuration list for PBXProject "Photo Edit" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1194F4E41CE64AEC00F4EDF0; + productRefGroup = 1194F4EE1CE64AEC00F4EDF0 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1194F4EC1CE64AEC00F4EDF0 /* Photo Edit App iOS */, + 1194F5051CE658DF00F4EDF0 /* Photo Edit Extension iOS */, + 1194F51D1CE681A100F4EDF0 /* Photo Edit App OS X */, + 1194F5301CE682BE00F4EDF0 /* Photo Edit Extension OS X */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1194F4EB1CE64AEC00F4EDF0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1194F4FB1CE64AEC00F4EDF0 /* LaunchScreen.storyboard in Resources */, + 1194F4F81CE64AEC00F4EDF0 /* Assets.xcassets in Resources */, + 1194F4F61CE64AEC00F4EDF0 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1194F5041CE658DF00F4EDF0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 11B4A8FD1D025F1700F2E5EE /* Logo_WWDC2016.png in Resources */, + 1194F5111CE658DF00F4EDF0 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1194F51C1CE681A100F4EDF0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1194F5251CE681A100F4EDF0 /* Assets.xcassets in Resources */, + 1194F5281CE681A100F4EDF0 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1194F52F1CE682BE00F4EDF0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 11B4A8FE1D025F1700F2E5EE /* Logo_WWDC2016.png in Resources */, + 1194F53B1CE682BE00F4EDF0 /* PhotoEditingViewController.xib in Resources */, + 116898691CEB9E9E00AD073E /* PhotoFilterItem.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1194F4E91CE64AEC00F4EDF0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1194F4F11CE64AEC00F4EDF0 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1194F5021CE658DF00F4EDF0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1194F5481CE6873C00F4EDF0 /* ContentEditingController.swift in Sources */, + 1194F50E1CE658DF00F4EDF0 /* PhotoEditingViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1194F51A1CE681A100F4EDF0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1194F5211CE681A100F4EDF0 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1194F52D1CE682BE00F4EDF0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1194F5491CE6873C00F4EDF0 /* ContentEditingController.swift in Sources */, + 1194F5381CE682BE00F4EDF0 /* PhotoEditingViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 1194F5141CE658DF00F4EDF0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1194F5051CE658DF00F4EDF0 /* Photo Edit Extension iOS */; + targetProxy = 1194F5131CE658DF00F4EDF0 /* PBXContainerItemProxy */; + }; + 1194F53E1CE682BE00F4EDF0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1194F5301CE682BE00F4EDF0 /* Photo Edit Extension OS X */; + targetProxy = 1194F53D1CE682BE00F4EDF0 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 116898671CEB9E9E00AD073E /* PhotoFilterItem.xib */ = { + isa = PBXVariantGroup; + children = ( + 116898681CEB9E9E00AD073E /* Base */, + ); + name = PhotoFilterItem.xib; + sourceTree = ""; + }; + 1194F4F41CE64AEC00F4EDF0 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1194F4F51CE64AEC00F4EDF0 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 1194F4F91CE64AEC00F4EDF0 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1194F4FA1CE64AEC00F4EDF0 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 1194F50F1CE658DF00F4EDF0 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1194F5101CE658DF00F4EDF0 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; + 1194F5261CE681A100F4EDF0 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1194F5271CE681A100F4EDF0 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 1194F5391CE682BE00F4EDF0 /* PhotoEditingViewController.xib */ = { + isa = PBXVariantGroup; + children = ( + 1194F53A1CE682BE00F4EDF0 /* Base */, + ); + name = PhotoEditingViewController.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 1194F4FD1CE64AEC00F4EDF0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = lossless; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 1194F4FE1CE64AEC00F4EDF0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = "respect-asset-catalog"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.3; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1194F5001CE64AEC00F4EDF0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "App (iOS)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Photo-Edit"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 1194F5011CE64AEC00F4EDF0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "App (iOS)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Photo-Edit"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + 1194F5171CE658DF00F4EDF0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = "Extension (iOS)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Photo-Edit.extension"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 1194F5181CE658DF00F4EDF0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = "Extension (iOS)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Photo-Edit.extension"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + 1194F52B1CE681A100F4EDF0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "-"; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "App (OS X)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Photo-Edit"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 1194F52C1CE681A100F4EDF0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "-"; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "App (OS X)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Photo-Edit"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + 1194F5411CE682BE00F4EDF0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "Extension (OS X)/Photo_Edit.entitlements"; + CODE_SIGN_IDENTITY = "-"; + INFOPLIST_FILE = "Extension (OS X)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Photo-Edit.extension"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 1194F5421CE682BE00F4EDF0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = "Extension (OS X)/Photo_Edit.entitlements"; + CODE_SIGN_IDENTITY = "-"; + INFOPLIST_FILE = "Extension (OS X)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Photo-Edit.extension"; + PRODUCT_NAME = "$(PROJECT_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1194F4E81CE64AEC00F4EDF0 /* Build configuration list for PBXProject "Photo Edit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1194F4FD1CE64AEC00F4EDF0 /* Debug */, + 1194F4FE1CE64AEC00F4EDF0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1194F4FF1CE64AEC00F4EDF0 /* Build configuration list for PBXNativeTarget "Photo Edit App iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1194F5001CE64AEC00F4EDF0 /* Debug */, + 1194F5011CE64AEC00F4EDF0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1194F5161CE658DF00F4EDF0 /* Build configuration list for PBXNativeTarget "Photo Edit Extension iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1194F5171CE658DF00F4EDF0 /* Debug */, + 1194F5181CE658DF00F4EDF0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1194F52A1CE681A100F4EDF0 /* Build configuration list for PBXNativeTarget "Photo Edit App OS X" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1194F52B1CE681A100F4EDF0 /* Debug */, + 1194F52C1CE681A100F4EDF0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1194F5401CE682BE00F4EDF0 /* Build configuration list for PBXNativeTarget "Photo Edit Extension OS X" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1194F5411CE682BE00F4EDF0 /* Debug */, + 1194F5421CE682BE00F4EDF0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 1194F4E51CE64AEC00F4EDF0 /* Project object */; +} diff --git a/SamplePhotoEditingExtension/README.md b/SamplePhotoEditingExtension/README.md new file mode 100644 index 00000000..2e1ace83 --- /dev/null +++ b/SamplePhotoEditingExtension/README.md @@ -0,0 +1,25 @@ +# Photo Edit + +This sample code shows how to implement a Photo Editing extension. + +The extension allows the user to select a filter effect to apply to the photo or video selected in Photos (iOS or OS X) or Camera (iOS only). To use the sample extension, edit a photo or video using the Photos app, and tap the extension icon. + +In both iOS and OS X, a PhotoEditingViewController class presents the extension's UI and, through the PHContentEditingController, responds to messages from Photos that define the photo editing process. Each platform's PhotoEditingViewController class forwards those messages to the ContentEditingController class, which provides the common functionality for processing photos, videos, and Live Photos on both platforms. + +Note that the app in this sample only serves as a host for the extension -- it has no UI or functionality of its own. + +## Setup Instructions + +Run the Photo Edit app to install it. To enable the extension, edit a photo or video using the Photos app, tap the extension icon (three dots in a circle), tap More, and switch Photo Filter on. + +## Requirements + +### Build + +Xcode 8.0 (iOS 10.0 / OS X 10.12 SDK) + +### Runtime + +iOS 9.1, OS X 10.11 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/Speakerbox/Assets/Speakerbox-1024.png b/Speakerbox/Assets/Speakerbox-1024.png new file mode 100644 index 00000000..d30121ea Binary files /dev/null and b/Speakerbox/Assets/Speakerbox-1024.png differ diff --git a/Speakerbox/Assets/Speakerbox-256.png b/Speakerbox/Assets/Speakerbox-256.png new file mode 100644 index 00000000..c80bcc1e Binary files /dev/null and b/Speakerbox/Assets/Speakerbox-256.png differ diff --git a/Speakerbox/Assets/Speakerbox-512.png b/Speakerbox/Assets/Speakerbox-512.png new file mode 100644 index 00000000..2162ea4f Binary files /dev/null and b/Speakerbox/Assets/Speakerbox-512.png differ diff --git a/Speakerbox/IntentsExtension/Info.plist b/Speakerbox/IntentsExtension/Info.plist new file mode 100644 index 00000000..9ab64af5 --- /dev/null +++ b/Speakerbox/IntentsExtension/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + IntentsExtension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + IntentsSupported + + INStartAudioCallIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + + diff --git a/Speakerbox/IntentsExtension/IntentHandler.swift b/Speakerbox/IntentsExtension/IntentHandler.swift new file mode 100644 index 00000000..a581ea87 --- /dev/null +++ b/Speakerbox/IntentsExtension/IntentHandler.swift @@ -0,0 +1,30 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Intents handler principal class +*/ + +import Intents + +class IntentHandler: INExtension, INStartAudioCallIntentHandling { + + func handle(startAudioCall intent: INStartAudioCallIntent, completion: @escaping (INStartAudioCallIntentResponse) -> Void) { + let response: INStartAudioCallIntentResponse + defer { + completion(response) + } + + // Ensure there is a person handle + guard intent.contacts?.first?.personHandle != nil else { + response = INStartAudioCallIntentResponse(code: .failure, userActivity: nil) + return + } + + let userActivity = NSUserActivity(activityType: String(describing: INStartAudioCallIntent.self)) + + response = INStartAudioCallIntentResponse(code: .continueInApp, userActivity: userActivity) + } + +} diff --git a/Speakerbox/LICENSE.txt b/Speakerbox/LICENSE.txt new file mode 100644 index 00000000..71da5c37 --- /dev/null +++ b/Speakerbox/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: Speakerbox: Using CallKit to create a VoIP app +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) 2016 Apple Inc. All Rights Reserved. diff --git a/Speakerbox/README.md b/Speakerbox/README.md new file mode 100644 index 00000000..6662f1b3 --- /dev/null +++ b/Speakerbox/README.md @@ -0,0 +1,26 @@ +# Speakerbox: Using CallKit to create a VoIP app + +This sample app demonstrates how to use CallKit.framework in a VoIP app to allow it to integrate natively into the system. + +## Requirements + +### Build + +Xcode 8.0 or later; iOS 10.0 SDK or later + +### Runtime + +iOS 10.0 or later + +## About Speakerbox + +Speakerbox is an example VoIP app which uses CallKit.framework to make and receive calls. It demonstrates several key areas: + +- Creating a CXProvider and setting its delegate, in order to perform call actions in response to delegate callbacks. For example, the app's ProviderDelegate implements `-provider:performAnswerCallAction:` to handle answering an incoming call. +- Calling CXProvider API to provide updates to call metadata (using the CXCallUpdate class) and call lifecycle (using the various `report…` methods). For example, the app calls `CXProvider.reportOutgoingCall(with:connectedAtDate:)` to notify the system when an outgoing call has connected. +- Creating a CXCallController in order to request transactions in response to user interactions in the app. For example, to start an outgoing call, the app calls `CXCallController.request(_:)` to request a CXTransaction containing a CXStartCallAction. +- Registering an app's CXProviderConfiguration, in order to configure certain behaviors of the app. For example, the app sets `maximumCallsPerCallGroup` to 1 to indicate that calls may not be grouped together. +- Handling call audio, including the correct points to configure the app's AVAudioSession versus starting call audio media. See `configureAudioSession()`, `startAudio()`, and `stopAudio()`. +- Starting an outgoing call in response to an INStartAudioCallIntent, including introspecting the INInteraction and NSUserActivity which contain the intent. + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/Speakerbox/Speakerbox.xcodeproj/project.pbxproj b/Speakerbox/Speakerbox.xcodeproj/project.pbxproj new file mode 100644 index 00000000..b246eed1 --- /dev/null +++ b/Speakerbox/Speakerbox.xcodeproj/project.pbxproj @@ -0,0 +1,655 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1717FC631C7EE70C00DE25F9 /* DialOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1717FC621C7EE70C00DE25F9 /* DialOptionsViewController.swift */; }; + 17275C671C7E7E2B000B48A0 /* CallsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17275C661C7E7E2B000B48A0 /* CallsViewController.swift */; }; + 17275C6A1C7E815C000B48A0 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 17275C6C1C7E815C000B48A0 /* Localizable.strings */; }; + 1768B0781C7D2BEB004D401D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1768B0771C7D2BEB004D401D /* AppDelegate.swift */; }; + 1768B07D1C7D2BEB004D401D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1768B07B1C7D2BEB004D401D /* Main.storyboard */; }; + 1768B07F1C7D2BEB004D401D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1768B07E1C7D2BEB004D401D /* Assets.xcassets */; }; + 1768B0821C7D2BEB004D401D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1768B0801C7D2BEB004D401D /* LaunchScreen.storyboard */; }; + 1768B0901C7D2D87004D401D /* AudioController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1768B08F1C7D2D87004D401D /* AudioController.mm */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 1768B0961C7D2E9B004D401D /* CAXException.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1768B0951C7D2E9B004D401D /* CAXException.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 1768B0991C7D2EAA004D401D /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1768B0981C7D2EAA004D401D /* CADebugMacros.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 1768B09D1C7D2F87004D401D /* CAStreamBasicDescription.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1768B09C1C7D2F87004D401D /* CAStreamBasicDescription.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 1780D7451D0A1B5500BE1A8C /* Ringtone.caf in Resources */ = {isa = PBXBuildFile; fileRef = 1780D7441D0A1B5100BE1A8C /* Ringtone.caf */; }; + 17995F861C7EF01D001FDD06 /* CallSummaryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17995F851C7EF01D001FDD06 /* CallSummaryTableViewCell.swift */; }; + 17995F891C7EF22A001FDD06 /* CallDurationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17995F881C7EF22A001FDD06 /* CallDurationFormatter.swift */; }; + 17995F8C1C7EF547001FDD06 /* UIFont+Speakerbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17995F8B1C7EF547001FDD06 /* UIFont+Speakerbox.swift */; }; + 179ADCCC1CE5252000874792 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179ADCCB1CE5252000874792 /* IntentHandler.swift */; }; + 179ADCD01CE5252000874792 /* IntentsExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 179ADCC91CE5252000874792 /* IntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 17AE708E1CF5875600D27E55 /* SimulateIncomingCallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AE708D1CF5875600D27E55 /* SimulateIncomingCallViewController.swift */; }; + 17AE70901CF60D6E00D27E55 /* CallAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AE708F1CF60D6E00D27E55 /* CallAudio.swift */; }; + 17B013AC1CF57EBD00987901 /* Array+Speakerbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B013AB1CF57EBD00987901 /* Array+Speakerbox.swift */; }; + 17CB213A1C7D3CD200D2883E /* ProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CB21391C7D3CD200D2883E /* ProviderDelegate.swift */; }; + 17CB213E1C7D421500D2883E /* SpeakerboxCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CB213D1C7D421500D2883E /* SpeakerboxCall.swift */; }; + 17CB21431C7E259D00D2883E /* StartCallConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CB21421C7E259D00D2883E /* StartCallConvertible.swift */; }; + 17CB21451C7E25EC00D2883E /* URL+StartCallConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CB21441C7E25EC00D2883E /* URL+StartCallConvertible.swift */; }; + 17CB21471C7E460000D2883E /* NSUserActivity+StartCallConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CB21461C7E460000D2883E /* NSUserActivity+StartCallConvertible.swift */; }; + 7CF2D82A1C90F48400114D4B /* SpeakerboxCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CF2D8291C90F48400114D4B /* SpeakerboxCallManager.swift */; }; + B51360291D03854400A54075 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B51360281D03854400A54075 /* Accelerate.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 179ADCCE1CE5252000874792 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1768B06C1C7D2BEB004D401D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 179ADCC81CE5252000874792; + remoteInfo = IntentsExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 179ADCD41CE5252000874792 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 179ADCD01CE5252000874792 /* IntentsExtension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 170FC0F91CF4E608002ACB41 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 1717FC621C7EE70C00DE25F9 /* DialOptionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DialOptionsViewController.swift; sourceTree = ""; }; + 17275C661C7E7E2B000B48A0 /* CallsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallsViewController.swift; sourceTree = ""; }; + 17275C6B1C7E815C000B48A0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; + 1768B0741C7D2BEB004D401D /* Speakerbox.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Speakerbox.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1768B0771C7D2BEB004D401D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 1768B07C1C7D2BEB004D401D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 1768B07E1C7D2BEB004D401D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1768B0811C7D2BEB004D401D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 1768B0831C7D2BEB004D401D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1768B08A1C7D2D5D004D401D /* Speakerbox-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Speakerbox-Bridging-Header.h"; sourceTree = ""; }; + 1768B08E1C7D2D87004D401D /* AudioController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioController.h; sourceTree = ""; }; + 1768B08F1C7D2D87004D401D /* AudioController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AudioController.mm; sourceTree = ""; }; + 1768B0941C7D2E9B004D401D /* CAXException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CAXException.h; sourceTree = ""; }; + 1768B0951C7D2E9B004D401D /* CAXException.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CAXException.cpp; sourceTree = ""; }; + 1768B0971C7D2EAA004D401D /* CADebugMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CADebugMacros.h; sourceTree = ""; }; + 1768B0981C7D2EAA004D401D /* CADebugMacros.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CADebugMacros.cpp; sourceTree = ""; }; + 1768B09B1C7D2F87004D401D /* CAStreamBasicDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CAStreamBasicDescription.h; sourceTree = ""; }; + 1768B09C1C7D2F87004D401D /* CAStreamBasicDescription.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CAStreamBasicDescription.cpp; sourceTree = ""; }; + 1768B09E1C7D2FBA004D401D /* CAMath.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CAMath.h; sourceTree = ""; }; + 1780D7441D0A1B5100BE1A8C /* Ringtone.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Ringtone.caf; sourceTree = ""; }; + 17995F851C7EF01D001FDD06 /* CallSummaryTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallSummaryTableViewCell.swift; sourceTree = ""; }; + 17995F881C7EF22A001FDD06 /* CallDurationFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallDurationFormatter.swift; sourceTree = ""; }; + 17995F8B1C7EF547001FDD06 /* UIFont+Speakerbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIFont+Speakerbox.swift"; sourceTree = ""; }; + 179ADCC91CE5252000874792 /* IntentsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IntentsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 179ADCCB1CE5252000874792 /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + 179ADCCD1CE5252000874792 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 17AE708D1CF5875600D27E55 /* SimulateIncomingCallViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimulateIncomingCallViewController.swift; sourceTree = ""; }; + 17AE708F1CF60D6E00D27E55 /* CallAudio.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallAudio.swift; path = Speakerbox/CallAudio.swift; sourceTree = SOURCE_ROOT; }; + 17B013AB1CF57EBD00987901 /* Array+Speakerbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Speakerbox.swift"; sourceTree = ""; }; + 17CB21391C7D3CD200D2883E /* ProviderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProviderDelegate.swift; sourceTree = ""; }; + 17CB213D1C7D421500D2883E /* SpeakerboxCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeakerboxCall.swift; sourceTree = ""; }; + 17CB21421C7E259D00D2883E /* StartCallConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartCallConvertible.swift; sourceTree = ""; }; + 17CB21441C7E25EC00D2883E /* URL+StartCallConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+StartCallConvertible.swift"; sourceTree = ""; }; + 17CB21461C7E460000D2883E /* NSUserActivity+StartCallConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+StartCallConvertible.swift"; sourceTree = ""; }; + 7CF2D8291C90F48400114D4B /* SpeakerboxCallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeakerboxCallManager.swift; sourceTree = ""; }; + B51360281D03854400A54075 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1768B0711C7D2BEB004D401D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B51360291D03854400A54075 /* Accelerate.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 179ADCC61CE5252000874792 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1721B1C51CEC5F81009A1961 /* UI */ = { + isa = PBXGroup; + children = ( + 1768B07B1C7D2BEB004D401D /* Main.storyboard */, + 1768B0801C7D2BEB004D401D /* LaunchScreen.storyboard */, + 17275C651C7E7E1C000B48A0 /* View Controllers */, + 17995F841C7EF00B001FDD06 /* Views */, + 1768B07E1C7D2BEB004D401D /* Assets.xcassets */, + 1780D7431D0A1B4300BE1A8C /* Sounds */, + ); + name = UI; + sourceTree = ""; + }; + 1721B1C71CEC5FB1009A1961 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 1768B08A1C7D2D5D004D401D /* Speakerbox-Bridging-Header.h */, + 1768B0831C7D2BEB004D401D /* Info.plist */, + 17275C6C1C7E815C000B48A0 /* Localizable.strings */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 17275C651C7E7E1C000B48A0 /* View Controllers */ = { + isa = PBXGroup; + children = ( + 17275C661C7E7E2B000B48A0 /* CallsViewController.swift */, + 1717FC621C7EE70C00DE25F9 /* DialOptionsViewController.swift */, + 17AE708D1CF5875600D27E55 /* SimulateIncomingCallViewController.swift */, + ); + name = "View Controllers"; + sourceTree = ""; + }; + 1768B06B1C7D2BEB004D401D = { + isa = PBXGroup; + children = ( + 170FC0F91CF4E608002ACB41 /* README.md */, + 1768B0761C7D2BEB004D401D /* Speakerbox */, + 179ADCCA1CE5252000874792 /* IntentsExtension */, + 1768B0751C7D2BEB004D401D /* Products */, + B51360271D03854400A54075 /* Frameworks */, + ); + sourceTree = ""; + }; + 1768B0751C7D2BEB004D401D /* Products */ = { + isa = PBXGroup; + children = ( + 1768B0741C7D2BEB004D401D /* Speakerbox.app */, + 179ADCC91CE5252000874792 /* IntentsExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 1768B0761C7D2BEB004D401D /* Speakerbox */ = { + isa = PBXGroup; + children = ( + 17FA238D1CEE315700A06A15 /* App */, + 1721B1C51CEC5F81009A1961 /* UI */, + 17CB21381C7D3B4B00D2883E /* Call Management */, + 17CB21411C7E255B00D2883E /* Start Call Handling */, + 1768B0891C7D2C5D004D401D /* Audio */, + 17995F871C7EF20F001FDD06 /* Data Formatters */, + 17995F8A1C7EF531001FDD06 /* Extensions */, + 1721B1C71CEC5FB1009A1961 /* Supporting Files */, + ); + path = Speakerbox; + sourceTree = ""; + }; + 1768B0891C7D2C5D004D401D /* Audio */ = { + isa = PBXGroup; + children = ( + 17AE708F1CF60D6E00D27E55 /* CallAudio.swift */, + 1768B08E1C7D2D87004D401D /* AudioController.h */, + 1768B08F1C7D2D87004D401D /* AudioController.mm */, + 1768B09A1C7D2EAF004D401D /* Utility */, + ); + path = Audio; + sourceTree = ""; + }; + 1768B09A1C7D2EAF004D401D /* Utility */ = { + isa = PBXGroup; + children = ( + 1768B0971C7D2EAA004D401D /* CADebugMacros.h */, + 1768B0981C7D2EAA004D401D /* CADebugMacros.cpp */, + 1768B0941C7D2E9B004D401D /* CAXException.h */, + 1768B0951C7D2E9B004D401D /* CAXException.cpp */, + 1768B09E1C7D2FBA004D401D /* CAMath.h */, + 1768B09B1C7D2F87004D401D /* CAStreamBasicDescription.h */, + 1768B09C1C7D2F87004D401D /* CAStreamBasicDescription.cpp */, + ); + name = Utility; + sourceTree = ""; + }; + 1780D7431D0A1B4300BE1A8C /* Sounds */ = { + isa = PBXGroup; + children = ( + 1780D7441D0A1B5100BE1A8C /* Ringtone.caf */, + ); + name = Sounds; + sourceTree = ""; + }; + 17995F841C7EF00B001FDD06 /* Views */ = { + isa = PBXGroup; + children = ( + 17995F851C7EF01D001FDD06 /* CallSummaryTableViewCell.swift */, + ); + name = Views; + sourceTree = ""; + }; + 17995F871C7EF20F001FDD06 /* Data Formatters */ = { + isa = PBXGroup; + children = ( + 17995F881C7EF22A001FDD06 /* CallDurationFormatter.swift */, + ); + name = "Data Formatters"; + sourceTree = ""; + }; + 17995F8A1C7EF531001FDD06 /* Extensions */ = { + isa = PBXGroup; + children = ( + 17995F8B1C7EF547001FDD06 /* UIFont+Speakerbox.swift */, + 17B013AB1CF57EBD00987901 /* Array+Speakerbox.swift */, + ); + name = Extensions; + sourceTree = ""; + }; + 179ADCCA1CE5252000874792 /* IntentsExtension */ = { + isa = PBXGroup; + children = ( + 179ADCCB1CE5252000874792 /* IntentHandler.swift */, + 179ADCCD1CE5252000874792 /* Info.plist */, + ); + path = IntentsExtension; + sourceTree = ""; + }; + 17CB21381C7D3B4B00D2883E /* Call Management */ = { + isa = PBXGroup; + children = ( + 7CF2D8291C90F48400114D4B /* SpeakerboxCallManager.swift */, + 17CB213D1C7D421500D2883E /* SpeakerboxCall.swift */, + ); + name = "Call Management"; + sourceTree = ""; + }; + 17CB21411C7E255B00D2883E /* Start Call Handling */ = { + isa = PBXGroup; + children = ( + 17CB21421C7E259D00D2883E /* StartCallConvertible.swift */, + 17CB21441C7E25EC00D2883E /* URL+StartCallConvertible.swift */, + 17CB21461C7E460000D2883E /* NSUserActivity+StartCallConvertible.swift */, + ); + name = "Start Call Handling"; + sourceTree = ""; + }; + 17FA238D1CEE315700A06A15 /* App */ = { + isa = PBXGroup; + children = ( + 1768B0771C7D2BEB004D401D /* AppDelegate.swift */, + 17CB21391C7D3CD200D2883E /* ProviderDelegate.swift */, + ); + name = App; + sourceTree = ""; + }; + B51360271D03854400A54075 /* Frameworks */ = { + isa = PBXGroup; + children = ( + B51360281D03854400A54075 /* Accelerate.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1768B0731C7D2BEB004D401D /* Speakerbox */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1768B0861C7D2BEB004D401D /* Build configuration list for PBXNativeTarget "Speakerbox" */; + buildPhases = ( + 1768B0701C7D2BEB004D401D /* Sources */, + 1768B0711C7D2BEB004D401D /* Frameworks */, + 1768B0721C7D2BEB004D401D /* Resources */, + 179ADCD41CE5252000874792 /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 179ADCCF1CE5252000874792 /* PBXTargetDependency */, + ); + name = Speakerbox; + productName = Speakerbox; + productReference = 1768B0741C7D2BEB004D401D /* Speakerbox.app */; + productType = "com.apple.product-type.application"; + }; + 179ADCC81CE5252000874792 /* IntentsExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 179ADCD31CE5252000874792 /* Build configuration list for PBXNativeTarget "IntentsExtension" */; + buildPhases = ( + 179ADCC51CE5252000874792 /* Sources */, + 179ADCC61CE5252000874792 /* Frameworks */, + 179ADCC71CE5252000874792 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = IntentsExtension; + productName = IntentsExtension; + productReference = 179ADCC91CE5252000874792 /* IntentsExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1768B06C1C7D2BEB004D401D /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Apple; + TargetAttributes = { + 1768B0731C7D2BEB004D401D = { + CreatedOnToolsVersion = 7.3; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; + }; + }; + 179ADCC81CE5252000874792 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 1768B06F1C7D2BEB004D401D /* Build configuration list for PBXProject "Speakerbox" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1768B06B1C7D2BEB004D401D; + productRefGroup = 1768B0751C7D2BEB004D401D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1768B0731C7D2BEB004D401D /* Speakerbox */, + 179ADCC81CE5252000874792 /* IntentsExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1768B0721C7D2BEB004D401D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1780D7451D0A1B5500BE1A8C /* Ringtone.caf in Resources */, + 17275C6A1C7E815C000B48A0 /* Localizable.strings in Resources */, + 1768B0821C7D2BEB004D401D /* LaunchScreen.storyboard in Resources */, + 1768B07F1C7D2BEB004D401D /* Assets.xcassets in Resources */, + 1768B07D1C7D2BEB004D401D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 179ADCC71CE5252000874792 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1768B0701C7D2BEB004D401D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 17995F8C1C7EF547001FDD06 /* UIFont+Speakerbox.swift in Sources */, + 17CB21451C7E25EC00D2883E /* URL+StartCallConvertible.swift in Sources */, + 1768B0781C7D2BEB004D401D /* AppDelegate.swift in Sources */, + 17995F891C7EF22A001FDD06 /* CallDurationFormatter.swift in Sources */, + 1717FC631C7EE70C00DE25F9 /* DialOptionsViewController.swift in Sources */, + 1768B0901C7D2D87004D401D /* AudioController.mm in Sources */, + 17275C671C7E7E2B000B48A0 /* CallsViewController.swift in Sources */, + 17995F861C7EF01D001FDD06 /* CallSummaryTableViewCell.swift in Sources */, + 1768B0991C7D2EAA004D401D /* CADebugMacros.cpp in Sources */, + 7CF2D82A1C90F48400114D4B /* SpeakerboxCallManager.swift in Sources */, + 17B013AC1CF57EBD00987901 /* Array+Speakerbox.swift in Sources */, + 17AE70901CF60D6E00D27E55 /* CallAudio.swift in Sources */, + 17CB21431C7E259D00D2883E /* StartCallConvertible.swift in Sources */, + 17CB21471C7E460000D2883E /* NSUserActivity+StartCallConvertible.swift in Sources */, + 17CB213E1C7D421500D2883E /* SpeakerboxCall.swift in Sources */, + 17AE708E1CF5875600D27E55 /* SimulateIncomingCallViewController.swift in Sources */, + 1768B0961C7D2E9B004D401D /* CAXException.cpp in Sources */, + 1768B09D1C7D2F87004D401D /* CAStreamBasicDescription.cpp in Sources */, + 17CB213A1C7D3CD200D2883E /* ProviderDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 179ADCC51CE5252000874792 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 179ADCCC1CE5252000874792 /* IntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 179ADCCF1CE5252000874792 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 179ADCC81CE5252000874792 /* IntentsExtension */; + targetProxy = 179ADCCE1CE5252000874792 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 17275C6C1C7E815C000B48A0 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 17275C6B1C7E815C000B48A0 /* Base */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + 1768B07B1C7D2BEB004D401D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1768B07C1C7D2BEB004D401D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 1768B0801C7D2BEB004D401D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1768B0811C7D2BEB004D401D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 1768B0841C7D2BEB004D401D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = lossless; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_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; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 1768B0851C7D2BEB004D401D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = "respect-asset-catalog"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_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; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1768B0871C7D2BEB004D401D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + ); + INFOPLIST_FILE = Speakerbox/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Speakerbox"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Speakerbox/Speakerbox-Bridging-Header.h"; + }; + name = Debug; + }; + 1768B0881C7D2BEB004D401D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + ); + INFOPLIST_FILE = Speakerbox/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Speakerbox"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Speakerbox/Speakerbox-Bridging-Header.h"; + }; + name = Release; + }; + 179ADCD11CE5252000874792 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + INFOPLIST_FILE = IntentsExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Speakerbox.IntentsExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + }; + name = Debug; + }; + 179ADCD21CE5252000874792 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + INFOPLIST_FILE = IntentsExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Speakerbox.IntentsExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1768B06F1C7D2BEB004D401D /* Build configuration list for PBXProject "Speakerbox" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1768B0841C7D2BEB004D401D /* Debug */, + 1768B0851C7D2BEB004D401D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1768B0861C7D2BEB004D401D /* Build configuration list for PBXNativeTarget "Speakerbox" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1768B0871C7D2BEB004D401D /* Debug */, + 1768B0881C7D2BEB004D401D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 179ADCD31CE5252000874792 /* Build configuration list for PBXNativeTarget "IntentsExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 179ADCD11CE5252000874792 /* Debug */, + 179ADCD21CE5252000874792 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 1768B06C1C7D2BEB004D401D /* Project object */; +} diff --git a/Speakerbox/Speakerbox/AppDelegate.swift b/Speakerbox/Speakerbox/AppDelegate.swift new file mode 100644 index 00000000..7034be46 --- /dev/null +++ b/Speakerbox/Speakerbox/AppDelegate.swift @@ -0,0 +1,88 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application delegate. +*/ + +import UIKit +import PushKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate { + + class var shared: AppDelegate { + return UIApplication.shared.delegate as! AppDelegate + } + + var window: UIWindow? + let pushRegistry = PKPushRegistry(queue: DispatchQueue.main) + let callManager = SpeakerboxCallManager() + var providerDelegate: ProviderDelegate? + + // MARK: UIApplicationDelegate + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { + print("Finished launching with options: \(launchOptions)") + + pushRegistry.delegate = self + pushRegistry.desiredPushTypes = [.voIP] + + providerDelegate = ProviderDelegate(callManager: callManager) + + return true + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { + guard let handle = url.startCallHandle else { + print("Could not determine start call handle from URL: \(url)") + return false + } + + callManager.startCall(handle: handle) + return true + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { + guard let handle = userActivity.startCallHandle else { + print("Could not determine start call handle from user activity: \(userActivity)") + return false + } + + guard let video = userActivity.video else { + print("Could not determine video from user activity: \(userActivity)") + return false + } + + callManager.startCall(handle: handle, video: video) + return true + } + + // MARK: PKPushRegistryDelegate + + func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, forType type: PKPushType) { + /* + Store push credentials on server for the active user. + For sample app purposes, do nothing since everything is being done locally. + */ + } + + func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, forType type: PKPushType) { + guard type == .voIP else { return } + + if let uuidString = payload.dictionaryPayload["UUID"] as? String, + let handle = payload.dictionaryPayload["handle"] as? String, + let hasVideo = payload.dictionaryPayload["hasVideo"] as? Bool, + let uuid = UUID(uuidString: uuidString) + { + displayIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo) + } + } + + /// Display the incoming call to the user + func displayIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)? = nil) { + providerDelegate?.reportIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo, completion: completion) + } + +} diff --git a/Speakerbox/Speakerbox/Array+Speakerbox.swift b/Speakerbox/Speakerbox/Array+Speakerbox.swift new file mode 100644 index 00000000..6b0b6d7c --- /dev/null +++ b/Speakerbox/Speakerbox/Array+Speakerbox.swift @@ -0,0 +1,19 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extension of Array for utility API +*/ + +extension Array { + + mutating func removeFirst(where predicate: (Element) throws -> Bool) rethrows { + guard let index = try index(where: predicate) else { + return + } + + remove(at: index) + } + +} diff --git a/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Contents.json b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..cf0a63d2 --- /dev/null +++ b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,86 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Speakerbox-58.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Speakerbox-87.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Speakerbox-80.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Speakerbox-120.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Speakerbox-120.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Speakerbox-180.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Speakerbox-29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Speakerbox-58.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Speakerbox-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Speakerbox-80.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Speakerbox-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Speakerbox-152.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Speakerbox-167.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-120.png b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-120.png new file mode 100644 index 00000000..9b2e71af Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-120.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-152.png b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-152.png new file mode 100644 index 00000000..3eea1dd8 Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-152.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-167.png b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-167.png new file mode 100644 index 00000000..a28356ce Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-167.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-180.png b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-180.png new file mode 100644 index 00000000..a1d5ff62 Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-180.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-29.png b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-29.png new file mode 100644 index 00000000..b8330548 Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-29.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-40.png b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-40.png new file mode 100644 index 00000000..ff122769 Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-40.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-58.png b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-58.png new file mode 100644 index 00000000..594372a3 Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-58.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-76.png b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-76.png new file mode 100644 index 00000000..da1a2f0b Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-76.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-80.png b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-80.png new file mode 100644 index 00000000..13c8bf93 Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-80.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-87.png b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-87.png new file mode 100644 index 00000000..b2ce6ff9 Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/AppIcon.appiconset/Speakerbox-87.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/Contents.json b/Speakerbox/Speakerbox/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/Speakerbox/Speakerbox/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/Contents.json b/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/Contents.json new file mode 100644 index 00000000..f9721523 --- /dev/null +++ b/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "IconMask-40.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "IconMask-80.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "IconMask-120.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/IconMask-120.png b/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/IconMask-120.png new file mode 100644 index 00000000..e1f2001e Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/IconMask-120.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/IconMask-40.png b/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/IconMask-40.png new file mode 100644 index 00000000..13cd129d Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/IconMask-40.png differ diff --git a/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/IconMask-80.png b/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/IconMask-80.png new file mode 100644 index 00000000..37f29424 Binary files /dev/null and b/Speakerbox/Speakerbox/Assets.xcassets/IconMask.imageset/IconMask-80.png differ diff --git a/Speakerbox/Speakerbox/Audio/AudioController.h b/Speakerbox/Speakerbox/Audio/AudioController.h new file mode 100644 index 00000000..200cb8ec --- /dev/null +++ b/Speakerbox/Speakerbox/Audio/AudioController.h @@ -0,0 +1,20 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + (Borrowed from aurioTouch sample code) This class demonstrates the audio APIs used to capture audio data from the microphone and play it out to the speaker. It also demonstrates how to play system sounds +*/ + +#import +#import + +@interface AudioController : NSObject + +@property (nonatomic, assign) BOOL muteAudio; +@property (nonatomic, assign, readonly) BOOL audioChainIsBeingReconstructed; + +- (OSStatus)startIOUnit; +- (OSStatus)stopIOUnit; + +@end diff --git a/Speakerbox/Speakerbox/Audio/AudioController.mm b/Speakerbox/Speakerbox/Audio/AudioController.mm new file mode 100644 index 00000000..d28fbeaf --- /dev/null +++ b/Speakerbox/Speakerbox/Audio/AudioController.mm @@ -0,0 +1,302 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + (Borrowed from aurioTouch sample code) This class demonstrates the audio APIs used to capture audio data from the microphone and play it out to the speaker. It also demonstrates how to play system sounds +*/ + +#import "AudioController.h" + +// Framework includes +#import + +// Utility file includes +#import "CAXException.h" +#import "CAStreamBasicDescription.h" + + +struct CallbackData { + AudioUnit rioUnit; + BOOL* muteAudio; + BOOL* audioChainIsBeingReconstructed; + + CallbackData(): rioUnit(NULL), muteAudio(NULL), audioChainIsBeingReconstructed(NULL) {} +} cd; + +// Render callback function +static OSStatus performRender (void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames, + AudioBufferList *ioData) +{ + OSStatus err = noErr; + if (*cd.audioChainIsBeingReconstructed == NO) + { + // we are calling AudioUnitRender on the input bus of Apple Voice Processing IO + // this will store the audio data captured by the microphone in ioData + err = AudioUnitRender(cd.rioUnit, ioActionFlags, inTimeStamp, 1, inNumberFrames, ioData); + + // mute audio if needed + if (*cd.muteAudio) + { + for (UInt32 i=0; imNumberBuffers; ++i) + memset(ioData->mBuffers[i].mData, 0, ioData->mBuffers[i].mDataByteSize); + } + } + + return err; +} + + +@interface AudioController () { + AudioUnit _rioUnit; + BOOL _audioChainIsBeingReconstructed; +} + +- (void)setupAudioSession; +- (void)setupIOUnit; +- (void)setupAudioChain; + +@end + +@implementation AudioController + +@synthesize muteAudio = _muteAudio; + +- (id)init +{ + if (self = [super init]) { + _muteAudio = YES; + [self setupAudioChain]; + } + return self; +} + + +- (void)handleInterruption:(NSNotification *)notification +{ + try { + UInt8 theInterruptionType = [[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] intValue]; + NSLog(@"Session interrupted > --- %s ---\n", theInterruptionType == AVAudioSessionInterruptionTypeBegan ? "Begin Interruption" : "End Interruption"); + + if (theInterruptionType == AVAudioSessionInterruptionTypeBegan) { + [self stopIOUnit]; + } + + if (theInterruptionType == AVAudioSessionInterruptionTypeEnded) { + // make sure to activate the session + NSError *error = nil; + [[AVAudioSession sharedInstance] setActive:YES error:&error]; + if (nil != error) NSLog(@"AVAudioSession set active failed with error: %@", error); + + [self startIOUnit]; + } + } catch (CAXException e) { + char buf[256]; + fprintf(stderr, "Error: %s (%s)\n", e.mOperation, e.FormatError(buf)); + } +} + + +- (void)handleRouteChange:(NSNotification *)notification +{ + UInt8 reasonValue = [[notification.userInfo valueForKey:AVAudioSessionRouteChangeReasonKey] intValue]; + AVAudioSessionRouteDescription *routeDescription = [notification.userInfo valueForKey:AVAudioSessionRouteChangePreviousRouteKey]; + + NSLog(@"Route change:"); + switch (reasonValue) { + case AVAudioSessionRouteChangeReasonNewDeviceAvailable: + NSLog(@" NewDeviceAvailable"); + break; + case AVAudioSessionRouteChangeReasonOldDeviceUnavailable: + NSLog(@" OldDeviceUnavailable"); + break; + case AVAudioSessionRouteChangeReasonCategoryChange: + NSLog(@" CategoryChange"); + NSLog(@" New Category: %@", [[AVAudioSession sharedInstance] category]); + break; + case AVAudioSessionRouteChangeReasonOverride: + NSLog(@" Override"); + break; + case AVAudioSessionRouteChangeReasonWakeFromSleep: + NSLog(@" WakeFromSleep"); + break; + case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory: + NSLog(@" NoSuitableRouteForCategory"); + break; + default: + NSLog(@" ReasonUnknown"); + } + + NSLog(@"Previous route:\n"); + NSLog(@"%@", routeDescription); +} + +- (void)handleMediaServerReset:(NSNotification *)notification +{ + NSLog(@"Media server has reset"); + _audioChainIsBeingReconstructed = YES; + + usleep(25000); //wait here for some time to ensure that we don't delete these objects while they are being accessed elsewhere + + // rebuild the audio chain + [self setupAudioChain]; + [self startIOUnit]; + + _audioChainIsBeingReconstructed = NO; +} + +- (void)setupAudioSession +{ + try { + // Configure the audio session + AVAudioSession *sessionInstance = [AVAudioSession sharedInstance]; + + // we are going to play and record so we pick that category + NSError *error = nil; + [sessionInstance setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]; + XThrowIfError((OSStatus)error.code, "couldn't set session's audio category"); + + // set the mode to voice chat + [sessionInstance setMode:AVAudioSessionModeVoiceChat error:&error]; + XThrowIfError((OSStatus)error.code, "couldn't set session's audio mode"); + + // set the buffer duration to 5 ms + NSTimeInterval bufferDuration = .005; + [sessionInstance setPreferredIOBufferDuration:bufferDuration error:&error]; + XThrowIfError((OSStatus)error.code, "couldn't set session's I/O buffer duration"); + + // set the session's sample rate + [sessionInstance setPreferredSampleRate:44100 error:&error]; + XThrowIfError((OSStatus)error.code, "couldn't set session's preferred sample rate"); + + // add interruption handler + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleInterruption:) + name:AVAudioSessionInterruptionNotification + object:sessionInstance]; + + // we don't do anything special in the route change notification + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleRouteChange:) + name:AVAudioSessionRouteChangeNotification + object:sessionInstance]; + + // if media services are reset, we need to rebuild our audio chain + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(handleMediaServerReset:) + name: AVAudioSessionMediaServicesWereResetNotification + object: sessionInstance]; + } + + catch (CAXException &e) { + NSLog(@"Error returned from setupAudioSession: %d: %s", (int)e.mError, e.mOperation); + } + catch (...) { + NSLog(@"Unknown error returned from setupAudioSession"); + } + + return; +} + + +- (void)setupIOUnit +{ + try { + // Create a new instance of Apple Voice Processing IO + + AudioComponentDescription desc; + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_VoiceProcessingIO; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + + AudioComponent comp = AudioComponentFindNext(NULL, &desc); + XThrowIfError(AudioComponentInstanceNew(comp, &_rioUnit), "couldn't create a new instance of Apple Voice Processing IO"); + + // Enable input and output on Apple Voice Processing IO + // Input is enabled on the input scope of the input element + // Output is enabled on the output scope of the output element + + UInt32 one = 1; + XThrowIfError(AudioUnitSetProperty(_rioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &one, sizeof(one)), "could not enable input on Apple Voice Processing IO"); + XThrowIfError(AudioUnitSetProperty(_rioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, 0, &one, sizeof(one)), "could not enable output on Apple Voice Processing IO"); + + // Explicitly set the input and output client formats + // sample rate = 44100, num channels = 1, format = 32 bit floating point + + CAStreamBasicDescription ioFormat = CAStreamBasicDescription(44100, 1, CAStreamBasicDescription::kPCMFormatFloat32, false); + XThrowIfError(AudioUnitSetProperty(_rioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &ioFormat, sizeof(ioFormat)), "couldn't set the input client format on Apple Voice Processing IO"); + XThrowIfError(AudioUnitSetProperty(_rioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &ioFormat, sizeof(ioFormat)), "couldn't set the output client format on Apple Voice Processing IO"); + + // Set the MaximumFramesPerSlice property. This property is used to describe to an audio unit the maximum number + // of samples it will be asked to produce on any single given call to AudioUnitRender + UInt32 maxFramesPerSlice = 4096; + XThrowIfError(AudioUnitSetProperty(_rioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maxFramesPerSlice, sizeof(UInt32)), "couldn't set max frames per slice on Apple Voice Processing IO"); + + // Get the property value back from Apple Voice Processing IO. We are going to use this value to allocate buffers accordingly + UInt32 propSize = sizeof(UInt32); + XThrowIfError(AudioUnitGetProperty(_rioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maxFramesPerSlice, &propSize), "couldn't get max frames per slice on Apple Voice Processing IO"); + + // We need references to certain data in the render callback + // This simple struct is used to hold that information + + cd.rioUnit = _rioUnit; + cd.muteAudio = &_muteAudio; + cd.audioChainIsBeingReconstructed = &_audioChainIsBeingReconstructed; + + // Set the render callback on Apple Voice Processing IO + AURenderCallbackStruct renderCallback; + renderCallback.inputProc = performRender; + renderCallback.inputProcRefCon = NULL; + XThrowIfError(AudioUnitSetProperty(_rioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &renderCallback, sizeof(renderCallback)), "couldn't set render callback on Apple Voice Processing IO"); + + // Initialize the Apple Voice Processing IO instance + XThrowIfError(AudioUnitInitialize(_rioUnit), "couldn't initialize Apple Voice Processing IO instance"); + } + + catch (CAXException &e) { + NSLog(@"Error returned from setupIOUnit: %d: %s", (int)e.mError, e.mOperation); + } + catch (...) { + NSLog(@"Unknown error returned from setupIOUnit"); + } + + return; +} + +- (void)setupAudioChain +{ + [self setupAudioSession]; + [self setupIOUnit]; +} + +- (OSStatus)startIOUnit +{ + OSStatus err = AudioOutputUnitStart(_rioUnit); + if (err) NSLog(@"couldn't start Apple Voice Processing IO: %d", (int)err); + return err; +} + +- (OSStatus)stopIOUnit +{ + OSStatus err = AudioOutputUnitStop(_rioUnit); + if (err) NSLog(@"couldn't stop Apple Voice Processing IO: %d", (int)err); + return err; +} + +- (BOOL)audioChainIsBeingReconstructed +{ + return _audioChainIsBeingReconstructed; +} + +- (void)dealloc +{ + [super dealloc]; +} + +@end diff --git a/Speakerbox/Speakerbox/Audio/CADebugMacros.cpp b/Speakerbox/Speakerbox/Audio/CADebugMacros.cpp new file mode 100644 index 00000000..45cf84f6 --- /dev/null +++ b/Speakerbox/Speakerbox/Audio/CADebugMacros.cpp @@ -0,0 +1,50 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + (Borrowed from aurioTouch sample code) Part of CoreAudio Utility Classes +*/ + +#include "CADebugMacros.h" +#include +#include +#if TARGET_API_MAC_OSX + #include +#endif + +#if DEBUG +#include + +void DebugPrint(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); +} +#endif // DEBUG + +#if TARGET_API_MAC_OSX +void LogError(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); +#if DEBUG + vprintf(fmt, args); +#endif + vsyslog(LOG_ERR, fmt, args); + va_end(args); +} + +void LogWarning(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); +#if DEBUG + vprintf(fmt, args); +#endif + vsyslog(LOG_WARNING, fmt, args); + va_end(args); +} +#endif diff --git a/Speakerbox/Speakerbox/Audio/CADebugMacros.h b/Speakerbox/Speakerbox/Audio/CADebugMacros.h new file mode 100644 index 00000000..35df4cd2 --- /dev/null +++ b/Speakerbox/Speakerbox/Audio/CADebugMacros.h @@ -0,0 +1,542 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + (Borrowed from aurioTouch sample code) Part of CoreAudio Utility Classes +*/ + +#if !defined(__CADebugMacros_h__) +#define __CADebugMacros_h__ + +//============================================================================= +// Includes +//============================================================================= + +#if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) + #include +#else + #include "CoreAudioTypes.h" +#endif + +//============================================================================= +// CADebugMacros +//============================================================================= + +//#define CoreAudio_StopOnFailure 1 +//#define CoreAudio_TimeStampMessages 1 +//#define CoreAudio_ThreadStampMessages 1 +//#define CoreAudio_FlushDebugMessages 1 + +#if TARGET_RT_BIG_ENDIAN + #define CA4CCToCString(the4CC) { ((char*)&the4CC)[0], ((char*)&the4CC)[1], ((char*)&the4CC)[2], ((char*)&the4CC)[3], 0 } + #define CACopy4CCToCString(theCString, the4CC) { theCString[0] = ((char*)&the4CC)[0]; theCString[1] = ((char*)&the4CC)[1]; theCString[2] = ((char*)&the4CC)[2]; theCString[3] = ((char*)&the4CC)[3]; theCString[4] = 0; } +#else + #define CA4CCToCString(the4CC) { ((char*)&the4CC)[3], ((char*)&the4CC)[2], ((char*)&the4CC)[1], ((char*)&the4CC)[0], 0 } + #define CACopy4CCToCString(theCString, the4CC) { theCString[0] = ((char*)&the4CC)[3]; theCString[1] = ((char*)&the4CC)[2]; theCString[2] = ((char*)&the4CC)[1]; theCString[3] = ((char*)&the4CC)[0]; theCString[4] = 0; } +#endif + +// This is a macro that does a sizeof and casts the result to a UInt32. This is useful for all the +// places where -wshorten64-32 catches assigning a sizeof expression to a UInt32. +// For want of a better place to park this, we'll park it here. +#define SizeOf32(X) ((UInt32)sizeof(X)) + +// This is a macro that does a offsetof and casts the result to a UInt32. This is useful for all the +// places where -wshorten64-32 catches assigning an offsetof expression to a UInt32. +// For want of a better place to park this, we'll park it here. +#define OffsetOf32(X, Y) ((UInt32)offsetof(X, Y)) + +// This macro casts the expression to a UInt32. It is called out specially to allow us to track casts +// that have been added purely to avert -wshorten64-32 warnings on 64 bit platforms. +// For want of a better place to park this, we'll park it here. +#define ToUInt32(X) ((UInt32)(X)) + +#pragma mark Basic Definitions + +#if 0 // DEBUG || CoreAudio_Debug + // can be used to break into debugger immediately, also see CADebugger + #define BusError() { long* p=NULL; *p=0; } + + // basic debugging print routines + #if TARGET_OS_MAC && !TARGET_API_MAC_CARBON + extern void DebugStr(const unsigned char* debuggerMsg); + #define DebugMessage(msg) DebugStr("\p"msg) + #define DebugMessageN1(msg, N1) + #define DebugMessageN2(msg, N1, N2) + #define DebugMessageN3(msg, N1, N2, N3) + #else + #include "CADebugPrintf.h" + + #if (CoreAudio_FlushDebugMessages && !CoreAudio_UseSysLog) || defined(CoreAudio_UseSideFile) + #define FlushRtn ,fflush(DebugPrintfFile) + #else + #define FlushRtn + #endif + + #if CoreAudio_ThreadStampMessages + #include + #include "CAHostTimeBase.h" + #if TARGET_RT_64_BIT + #define DebugPrintfThreadIDFormat "%16p" + #else + #define DebugPrintfThreadIDFormat "%8p" + #endif + #define DebugMsg(inFormat, ...) DebugPrintf("%17qd: " DebugPrintfThreadIDFormat " " inFormat, CAHostTimeBase::GetCurrentTimeInNanos(), pthread_self(), ## __VA_ARGS__) FlushRtn + #elif CoreAudio_TimeStampMessages + #include "CAHostTimeBase.h" + #define DebugMsg(inFormat, ...) DebugPrintf("%17qd: " inFormat, CAHostTimeBase::GetCurrentTimeInNanos(), ## __VA_ARGS__) FlushRtn + #else + #define DebugMsg(inFormat, ...) DebugPrintf(inFormat, ## __VA_ARGS__) FlushRtn + #endif + #endif + void DebugPrint(const char *fmt, ...); // can be used like printf + #ifndef DEBUGPRINT + #define DEBUGPRINT(msg) DebugPrint msg // have to double-parenthesize arglist (see Debugging.h) + #endif + #if VERBOSE + #define vprint(msg) DEBUGPRINT(msg) + #else + #define vprint(msg) + #endif + + // Original macro keeps its function of turning on and off use of CADebuggerStop() for both asserts and throws. + // For backwards compat, it overrides any setting of the two sub-macros. + #if CoreAudio_StopOnFailure + #include "CADebugger.h" + #undef CoreAudio_StopOnAssert + #define CoreAudio_StopOnAssert 1 + #undef CoreAudio_StopOnThrow + #define CoreAudio_StopOnThrow 1 + #define STOP CADebuggerStop() + #else + #define STOP + #endif + + #if CoreAudio_StopOnAssert + #if !CoreAudio_StopOnFailure + #include "CADebugger.h" + #define STOP + #endif + #define __ASSERT_STOP CADebuggerStop() + #else + #define __ASSERT_STOP + #endif + + #if CoreAudio_StopOnThrow + #if !CoreAudio_StopOnFailure + #include "CADebugger.h" + #define STOP + #endif + #define __THROW_STOP CADebuggerStop() + #else + #define __THROW_STOP + #endif + +#else + #define DebugMsg(inFormat, ...) + #ifndef DEBUGPRINT + #define DEBUGPRINT(msg) + #endif + #define vprint(msg) + #define STOP + #define __ASSERT_STOP + #define __THROW_STOP +#endif + +// Old-style numbered DebugMessage calls are implemented in terms of DebugMsg() now +#define DebugMessage(msg) DebugMsg(msg) +#define DebugMessageN1(msg, N1) DebugMsg(msg, N1) +#define DebugMessageN2(msg, N1, N2) DebugMsg(msg, N1, N2) +#define DebugMessageN3(msg, N1, N2, N3) DebugMsg(msg, N1, N2, N3) +#define DebugMessageN4(msg, N1, N2, N3, N4) DebugMsg(msg, N1, N2, N3, N4) +#define DebugMessageN5(msg, N1, N2, N3, N4, N5) DebugMsg(msg, N1, N2, N3, N4, N5) +#define DebugMessageN6(msg, N1, N2, N3, N4, N5, N6) DebugMsg(msg, N1, N2, N3, N4, N5, N6) +#define DebugMessageN7(msg, N1, N2, N3, N4, N5, N6, N7) DebugMsg(msg, N1, N2, N3, N4, N5, N6, N7) +#define DebugMessageN8(msg, N1, N2, N3, N4, N5, N6, N7, N8) DebugMsg(msg, N1, N2, N3, N4, N5, N6, N7, N8) +#define DebugMessageN9(msg, N1, N2, N3, N4, N5, N6, N7, N8, N9) DebugMsg(msg, N1, N2, N3, N4, N5, N6, N7, N8, N9) + +void LogError(const char *fmt, ...); // writes to syslog (and stderr if debugging) +void LogWarning(const char *fmt, ...); // writes to syslog (and stderr if debugging) + +#define NO_ACTION (void)0 + +#if DEBUG || CoreAudio_Debug + +#pragma mark Debug Macros + +#define Assert(inCondition, inMessage) \ + if(!(inCondition)) \ + { \ + DebugMessage(inMessage); \ + __ASSERT_STOP; \ + } + +#define AssertFileLine(inCondition, inMessage) \ + if(!(inCondition)) \ + { \ + DebugMessageN3("%s, line %d: %s", __FILE__, __LINE__, inMessage); \ + __ASSERT_STOP; \ + } + +#define AssertNoError(inError, inMessage) \ + { \ + SInt32 __Err = (inError); \ + if(__Err != 0) \ + { \ + char __4CC[5] = CA4CCToCString(__Err); \ + DebugMessageN2(inMessage ", Error: %d (%s)", (int)__Err, __4CC); \ + __ASSERT_STOP; \ + } \ + } + +#define AssertNoKernelError(inError, inMessage) \ + { \ + unsigned int __Err = (unsigned int)(inError); \ + if(__Err != 0) \ + { \ + DebugMessageN1(inMessage ", Error: 0x%X", __Err); \ + __ASSERT_STOP; \ + } \ + } + +#define AssertNotNULL(inPtr, inMessage) \ + { \ + if((inPtr) == NULL) \ + { \ + DebugMessage(inMessage); \ + __ASSERT_STOP; \ + } \ + } + +#define FailIf(inCondition, inHandler, inMessage) \ + if(inCondition) \ + { \ + DebugMessage(inMessage); \ + STOP; \ + goto inHandler; \ + } + +#define FailWithAction(inCondition, inAction, inHandler, inMessage) \ + if(inCondition) \ + { \ + DebugMessage(inMessage); \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } + +#define FailIfNULL(inPointer, inAction, inHandler, inMessage) \ + if((inPointer) == NULL) \ + { \ + DebugMessage(inMessage); \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } + +#define FailIfKernelError(inKernelError, inAction, inHandler, inMessage) \ + { \ + unsigned int __Err = (inKernelError); \ + if(__Err != 0) \ + { \ + DebugMessageN1(inMessage ", Error: 0x%X", __Err); \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } \ + } + +#define FailIfError(inError, inAction, inHandler, inMessage) \ + { \ + SInt32 __Err = (inError); \ + if(__Err != 0) \ + { \ + char __4CC[5] = CA4CCToCString(__Err); \ + DebugMessageN2(inMessage ", Error: %ld (%s)", (long int)__Err, __4CC); \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } \ + } + +#define FailIfNoMessage(inCondition, inHandler, inMessage) \ + if(inCondition) \ + { \ + STOP; \ + goto inHandler; \ + } + +#define FailWithActionNoMessage(inCondition, inAction, inHandler, inMessage) \ + if(inCondition) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } + +#define FailIfNULLNoMessage(inPointer, inAction, inHandler, inMessage) \ + if((inPointer) == NULL) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } + +#define FailIfKernelErrorNoMessage(inKernelError, inAction, inHandler, inMessage) \ + { \ + unsigned int __Err = (inKernelError); \ + if(__Err != 0) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } \ + } + +#define FailIfErrorNoMessage(inError, inAction, inHandler, inMessage) \ + { \ + SInt32 __Err = (inError); \ + if(__Err != 0) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } \ + } + +#if defined(__cplusplus) + +#define Throw(inException) __THROW_STOP; throw (inException) + +#define ThrowIf(inCondition, inException, inMessage) \ + if(inCondition) \ + { \ + DebugMessage(inMessage); \ + Throw(inException); \ + } + +#define ThrowIfNULL(inPointer, inException, inMessage) \ + if((inPointer) == NULL) \ + { \ + DebugMessage(inMessage); \ + Throw(inException); \ + } + +#define ThrowIfKernelError(inKernelError, inException, inMessage) \ + { \ + unsigned int __Err = (inKernelError); \ + if(__Err != 0) \ + { \ + DebugMessageN1(inMessage ", Error: 0x%X", __Err); \ + Throw(inException); \ + } \ + } + +#define ThrowIfError(inError, inException, inMessage) \ + { \ + SInt32 __Err = (inError); \ + if(__Err != 0) \ + { \ + char __4CC[5] = CA4CCToCString(__Err); \ + DebugMessageN2(inMessage ", Error: %d (%s)", (int)__Err, __4CC); \ + Throw(inException); \ + } \ + } + +#if TARGET_OS_WIN32 +#define ThrowIfWinError(inError, inException, inMessage) \ + { \ + HRESULT __Err = (inError); \ + if(FAILED(__Err)) \ + { \ + DebugMessageN2(inMessage ", Code: %d, Facility: 0x%X", HRESULT_CODE(__Err), HRESULT_FACILITY(__Err)); \ + Throw(inException); \ + } \ + } +#endif + +#define SubclassResponsibility(inMethodName, inException) \ + { \ + DebugMessage(inMethodName": Subclasses must implement this method"); \ + Throw(inException); \ + } + +#endif // defined(__cplusplus) + +#else + +#pragma mark Release Macros + +#define Assert(inCondition, inMessage) \ + if(!(inCondition)) \ + { \ + __ASSERT_STOP; \ + } + +#define AssertFileLine(inCondition, inMessage) Assert(inCondition, inMessage) + +#define AssertNoError(inError, inMessage) \ + { \ + SInt32 __Err = (inError); \ + if(__Err != 0) \ + { \ + __ASSERT_STOP; \ + } \ + } + +#define AssertNoKernelError(inError, inMessage) \ + { \ + unsigned int __Err = (unsigned int)(inError); \ + if(__Err != 0) \ + { \ + __ASSERT_STOP; \ + } \ + } + +#define AssertNotNULL(inPtr, inMessage) \ + { \ + if((inPtr) == NULL) \ + { \ + __ASSERT_STOP; \ + } \ + } + +#define FailIf(inCondition, inHandler, inMessage) \ + if(inCondition) \ + { \ + STOP; \ + goto inHandler; \ + } + +#define FailWithAction(inCondition, inAction, inHandler, inMessage) \ + if(inCondition) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } + +#define FailIfNULL(inPointer, inAction, inHandler, inMessage) \ + if((inPointer) == NULL) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } + +#define FailIfKernelError(inKernelError, inAction, inHandler, inMessage) \ + if((inKernelError) != 0) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } + +#define FailIfError(inError, inAction, inHandler, inMessage) \ + if((inError) != 0) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } + +#define FailIfNoMessage(inCondition, inHandler, inMessage) \ + if(inCondition) \ + { \ + STOP; \ + goto inHandler; \ + } + +#define FailWithActionNoMessage(inCondition, inAction, inHandler, inMessage) \ + if(inCondition) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } + +#define FailIfNULLNoMessage(inPointer, inAction, inHandler, inMessage) \ + if((inPointer) == NULL) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } + +#define FailIfKernelErrorNoMessage(inKernelError, inAction, inHandler, inMessage) \ + { \ + unsigned int __Err = (inKernelError); \ + if(__Err != 0) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } \ + } + +#define FailIfErrorNoMessage(inError, inAction, inHandler, inMessage) \ + { \ + SInt32 __Err = (inError); \ + if(__Err != 0) \ + { \ + STOP; \ + { inAction; } \ + goto inHandler; \ + } \ + } + +#if defined(__cplusplus) + +#define Throw(inException) __THROW_STOP; throw (inException) + +#define ThrowIf(inCondition, inException, inMessage) \ + if(inCondition) \ + { \ + Throw(inException); \ + } + +#define ThrowIfNULL(inPointer, inException, inMessage) \ + if((inPointer) == NULL) \ + { \ + Throw(inException); \ + } + +#define ThrowIfKernelError(inKernelError, inException, inMessage) \ + { \ + unsigned int __Err = (inKernelError); \ + if(__Err != 0) \ + { \ + Throw(inException); \ + } \ + } + +#define ThrowIfError(inError, inException, inMessage) \ + { \ + SInt32 __Err = (inError); \ + if(__Err != 0) \ + { \ + Throw(inException); \ + } \ + } + +#if TARGET_OS_WIN32 +#define ThrowIfWinError(inError, inException, inMessage) \ + { \ + HRESULT __Err = (inError); \ + if(FAILED(__Err)) \ + { \ + Throw(inException); \ + } \ + } +#endif + +#define SubclassResponsibility(inMethodName, inException) \ + { \ + Throw(inException); \ + } + +#endif // defined(__cplusplus) + +#endif // DEBUG || CoreAudio_Debug + +#endif diff --git a/Speakerbox/Speakerbox/Audio/CAMath.h b/Speakerbox/Speakerbox/Audio/CAMath.h new file mode 100644 index 00000000..46da47d7 --- /dev/null +++ b/Speakerbox/Speakerbox/Audio/CAMath.h @@ -0,0 +1,30 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + (Borrowed from aurioTouch sample code) Part of CoreAudio Utility Classes +*/ + +#ifndef __CAMath_h__ +#define __CAMath_h__ + +#if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) + #include +#else + #include +#endif + +inline bool fiszero(Float64 f) { return (f == 0.); } +inline bool fiszero(Float32 f) { return (f == 0.f); } + +inline bool fnonzero(Float64 f) { return !fiszero(f); } +inline bool fnonzero(Float32 f) { return !fiszero(f); } + +inline bool fequal(const Float64 &a, const Float64 &b) { return a == b; } +inline bool fequal(const Float32 &a, const Float32 &b) { return a == b; } + +inline bool fnotequal(const Float64 &a, const Float64 &b) { return !fequal(a, b); } +inline bool fnotequal(const Float32 &a, const Float32 &b) { return !fequal(a, b); } + +#endif // __CAMath_h__ diff --git a/Speakerbox/Speakerbox/Audio/CAStreamBasicDescription.cpp b/Speakerbox/Speakerbox/Audio/CAStreamBasicDescription.cpp new file mode 100644 index 00000000..61f67a94 --- /dev/null +++ b/Speakerbox/Speakerbox/Audio/CAStreamBasicDescription.cpp @@ -0,0 +1,887 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + (Borrowed from aurioTouch sample code) Part of CoreAudio Utility Classes +*/ + +#include "CAStreamBasicDescription.h" +#include "CAMath.h" + +#if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) + #include +#else + #include +#endif + +#pragma mark This file needs to compile on earlier versions of the OS, so please keep that in mind when editing it + +char *CAStringForOSType (OSType t, char *writeLocation, size_t bufsize) +{ + if (bufsize > 0) { + char *p = writeLocation, *pend = writeLocation + bufsize; + union { UInt32 i; unsigned char str[4]; } u; + unsigned char *q = u.str; + u.i = CFSwapInt32HostToBig(t); + + bool hasNonPrint = false; + for (int i = 0; i < 4; ++i) { + if (!(isprint(*q) && *q != '\\')) { + hasNonPrint = true; + break; + } + q++; + } + q = u.str; + + if (hasNonPrint) + p += snprintf (p, pend - p, "0x"); + else if (p < pend) + *p++ = '\''; + + for (int i = 0; i < 4 && p < pend; ++i) { + if (hasNonPrint) { + p += snprintf(p, pend - p, "%02X", *q++); + } else { + *p++ = *q++; + } + } + if (!hasNonPrint && p < pend) + *p++ = '\''; + if (p >= pend) p -= 1; + *p = '\0'; + } + return writeLocation; +} + + +const AudioStreamBasicDescription CAStreamBasicDescription::sEmpty = { 0.0, 0, 0, 0, 0, 0, 0, 0, 0 }; + +CAStreamBasicDescription::CAStreamBasicDescription() +{ + memset (this, 0, sizeof(AudioStreamBasicDescription)); +} + +CAStreamBasicDescription::CAStreamBasicDescription(const AudioStreamBasicDescription &desc) +{ + SetFrom(desc); +} + + +CAStreamBasicDescription::CAStreamBasicDescription(double inSampleRate, UInt32 inFormatID, + UInt32 inBytesPerPacket, UInt32 inFramesPerPacket, + UInt32 inBytesPerFrame, UInt32 inChannelsPerFrame, + UInt32 inBitsPerChannel, UInt32 inFormatFlags) +{ + mSampleRate = inSampleRate; + mFormatID = inFormatID; + mBytesPerPacket = inBytesPerPacket; + mFramesPerPacket = inFramesPerPacket; + mBytesPerFrame = inBytesPerFrame; + mChannelsPerFrame = inChannelsPerFrame; + mBitsPerChannel = inBitsPerChannel; + mFormatFlags = inFormatFlags; + mReserved = 0; +} + +char *CAStreamBasicDescription::AsString(char *buf, size_t _bufsize, bool brief /*=false*/) const +{ + int bufsize = (int)_bufsize; // must be signed to protect against overflow + char *theBuffer = buf; + int nc; + char formatID[24]; + CAStringForOSType(mFormatID, formatID, sizeof(formatID)); + if (brief) { + CommonPCMFormat com; + bool interleaved; + if (IdentifyCommonPCMFormat(com, &interleaved) && com != kPCMFormatOther) { + const char *desc; + switch (com) { + case kPCMFormatInt16: + desc = "Int16"; + break; + case kPCMFormatInt32: + desc = "Int32"; + break; + case kPCMFormatFixed824: + desc = "Int8.24"; + break; + case kPCMFormatFloat32: + desc = "Float32"; + break; + case kPCMFormatFloat64: + desc = "Float64"; + break; + default: + desc = NULL; + break; + } + if (desc) { + const char *inter =""; + if (mChannelsPerFrame > 1) + inter = !interleaved ? ", non-inter" : ", inter"; + snprintf(buf, static_cast(bufsize), "%2d ch, %6.0f Hz, %s%s", (int)mChannelsPerFrame, mSampleRate, desc, inter); + return theBuffer; + } + } + if (mChannelsPerFrame == 0 && mSampleRate == 0.0 && mFormatID == 0) { + snprintf(buf, static_cast(bufsize), "%2d ch, %6.0f Hz", (int)mChannelsPerFrame, mSampleRate); + return theBuffer; + } + } + + nc = snprintf(buf, static_cast(bufsize), "%2d ch, %6.0f Hz, %s (0x%08X) ", (int)NumberChannels(), mSampleRate, formatID, (int)mFormatFlags); + buf += nc; if ((bufsize -= nc) <= 0) goto exit; + if (mFormatID == kAudioFormatLinearPCM) { + bool isInt = !(mFormatFlags & kLinearPCMFormatFlagIsFloat); + int wordSize = static_cast(SampleWordSize()); + const char *endian = (wordSize > 1) ? + ((mFormatFlags & kLinearPCMFormatFlagIsBigEndian) ? " big-endian" : " little-endian" ) : ""; + const char *sign = isInt ? + ((mFormatFlags & kLinearPCMFormatFlagIsSignedInteger) ? " signed" : " unsigned") : ""; + const char *floatInt = isInt ? "integer" : "float"; + char packed[32]; + if (wordSize > 0 && PackednessIsSignificant()) { + if (mFormatFlags & kLinearPCMFormatFlagIsPacked) + snprintf(packed, sizeof(packed), "packed in %d bytes", wordSize); + else + snprintf(packed, sizeof(packed), "unpacked in %d bytes", wordSize); + } else + packed[0] = '\0'; + const char *align = (wordSize > 0 && AlignmentIsSignificant()) ? + ((mFormatFlags & kLinearPCMFormatFlagIsAlignedHigh) ? " high-aligned" : " low-aligned") : ""; + const char *deinter = (mFormatFlags & kAudioFormatFlagIsNonInterleaved) ? ", deinterleaved" : ""; + const char *commaSpace = (packed[0]!='\0') || (align[0]!='\0') ? ", " : ""; + char bitdepth[20]; + + int fracbits = (mFormatFlags & kLinearPCMFormatFlagsSampleFractionMask) >> kLinearPCMFormatFlagsSampleFractionShift; + if (fracbits > 0) + snprintf(bitdepth, sizeof(bitdepth), "%d.%d", (int)mBitsPerChannel - fracbits, fracbits); + else + snprintf(bitdepth, sizeof(bitdepth), "%d", (int)mBitsPerChannel); + + /*nc =*/ snprintf(buf, static_cast(bufsize), "%s-bit%s%s %s%s%s%s%s", + bitdepth, endian, sign, floatInt, + commaSpace, packed, align, deinter); + // buf += nc; if ((bufsize -= nc) <= 0) goto exit; + } else if (mFormatID == kAudioFormatAppleLossless) { + int sourceBits = 0; + switch (mFormatFlags) + { + case 1: // kAppleLosslessFormatFlag_16BitSourceData + sourceBits = 16; + break; + case 2: // kAppleLosslessFormatFlag_20BitSourceData + sourceBits = 20; + break; + case 3: // kAppleLosslessFormatFlag_24BitSourceData + sourceBits = 24; + break; + case 4: // kAppleLosslessFormatFlag_32BitSourceData + sourceBits = 32; + break; + } + if (sourceBits) + nc = snprintf(buf, static_cast(bufsize), "from %d-bit source, ", sourceBits); + else + nc = snprintf(buf, static_cast(bufsize), "from UNKNOWN source bit depth, "); + buf += nc; if ((bufsize -= nc) <= 0) goto exit; + /*nc =*/ snprintf(buf, static_cast(bufsize), "%d frames/packet", (int)mFramesPerPacket); + // buf += nc; if ((bufsize -= nc) <= 0) goto exit; + } + else + /*nc =*/ snprintf(buf, static_cast(bufsize), "%d bits/channel, %d bytes/packet, %d frames/packet, %d bytes/frame", + (int)mBitsPerChannel, (int)mBytesPerPacket, (int)mFramesPerPacket, (int)mBytesPerFrame); +exit: + return theBuffer; +} + +void CAStreamBasicDescription::NormalizeLinearPCMFormat(AudioStreamBasicDescription& ioDescription) +{ + // the only thing that changes is to make mixable linear PCM into the canonical linear PCM format + if((ioDescription.mFormatID == kAudioFormatLinearPCM) && ((ioDescription.mFormatFlags & kIsNonMixableFlag) == 0)) + { + // the canonical linear PCM format + ioDescription.mFormatFlags = kAudioFormatFlagsCanonical; + ioDescription.mBytesPerPacket = SizeOf32(AudioSampleType) * ioDescription.mChannelsPerFrame; + ioDescription.mFramesPerPacket = 1; + ioDescription.mBytesPerFrame = SizeOf32(AudioSampleType) * ioDescription.mChannelsPerFrame; + ioDescription.mBitsPerChannel = 8 * SizeOf32(AudioSampleType); + } +} + +void CAStreamBasicDescription::NormalizeLinearPCMFormat(bool inNativeEndian, AudioStreamBasicDescription& ioDescription) +{ + // the only thing that changes is to make mixable linear PCM into the canonical linear PCM format + if((ioDescription.mFormatID == kAudioFormatLinearPCM) && ((ioDescription.mFormatFlags & kIsNonMixableFlag) == 0)) + { + // the canonical linear PCM format + ioDescription.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked; + if(inNativeEndian) + { +#if TARGET_RT_BIG_ENDIAN + ioDescription.mFormatFlags |= kAudioFormatFlagIsBigEndian; +#endif + } + else + { +#if TARGET_RT_LITTLE_ENDIAN + ioDescription.mFormatFlags |= kAudioFormatFlagIsBigEndian; +#endif + } + ioDescription.mBytesPerPacket = SizeOf32(AudioSampleType) * ioDescription.mChannelsPerFrame; + ioDescription.mFramesPerPacket = 1; + ioDescription.mBytesPerFrame = SizeOf32(AudioSampleType) * ioDescription.mChannelsPerFrame; + ioDescription.mBitsPerChannel = 8 * SizeOf32(AudioSampleType); + } +} + +void CAStreamBasicDescription::VirtualizeLinearPCMFormat(AudioStreamBasicDescription& ioDescription) +{ + // the only thing that changes is to make mixable linear PCM into the HAL's virtual linear PCM format, which is Float32 currently + if((ioDescription.mFormatID == kAudioFormatLinearPCM) && ((ioDescription.mFormatFlags & kIsNonMixableFlag) == 0)) + { + // the virtual linear PCM format + ioDescription.mFormatFlags = kAudioFormatFlagsNativeFloatPacked; + ioDescription.mBytesPerPacket = SizeOf32(Float32) * ioDescription.mChannelsPerFrame; + ioDescription.mFramesPerPacket = 1; + ioDescription.mBytesPerFrame = SizeOf32(Float32) * ioDescription.mChannelsPerFrame; + ioDescription.mBitsPerChannel = 8 * SizeOf32(Float32); + } +} + +void CAStreamBasicDescription::VirtualizeLinearPCMFormat(bool inNativeEndian, AudioStreamBasicDescription& ioDescription) +{ + // the only thing that changes is to make mixable linear PCM into the HAL's virtual linear PCM format, which is Float32 currently + if((ioDescription.mFormatID == kAudioFormatLinearPCM) && ((ioDescription.mFormatFlags & kIsNonMixableFlag) == 0)) + { + // the virtual linear PCM format + ioDescription.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked; + if(inNativeEndian) + { +#if TARGET_RT_BIG_ENDIAN + ioDescription.mFormatFlags |= kAudioFormatFlagIsBigEndian; +#endif + } + else + { +#if TARGET_RT_LITTLE_ENDIAN + ioDescription.mFormatFlags |= kAudioFormatFlagIsBigEndian; +#endif + } + ioDescription.mBytesPerPacket = SizeOf32(Float32) * ioDescription.mChannelsPerFrame; + ioDescription.mFramesPerPacket = 1; + ioDescription.mBytesPerFrame = SizeOf32(Float32) * ioDescription.mChannelsPerFrame; + ioDescription.mBitsPerChannel = 8 * SizeOf32(Float32); + } +} + +void CAStreamBasicDescription::ResetFormat(AudioStreamBasicDescription& ioDescription) +{ + ioDescription.mSampleRate = 0; + ioDescription.mFormatID = 0; + ioDescription.mBytesPerPacket = 0; + ioDescription.mFramesPerPacket = 0; + ioDescription.mBytesPerFrame = 0; + ioDescription.mChannelsPerFrame = 0; + ioDescription.mBitsPerChannel = 0; + ioDescription.mFormatFlags = 0; +} + +void CAStreamBasicDescription::FillOutFormat(AudioStreamBasicDescription& ioDescription, const AudioStreamBasicDescription& inTemplateDescription) +{ + if(fiszero(ioDescription.mSampleRate)) + { + ioDescription.mSampleRate = inTemplateDescription.mSampleRate; + } + if(ioDescription.mFormatID == 0) + { + ioDescription.mFormatID = inTemplateDescription.mFormatID; + } + if(ioDescription.mFormatFlags == 0) + { + ioDescription.mFormatFlags = inTemplateDescription.mFormatFlags; + } + if(ioDescription.mBytesPerPacket == 0) + { + ioDescription.mBytesPerPacket = inTemplateDescription.mBytesPerPacket; + } + if(ioDescription.mFramesPerPacket == 0) + { + ioDescription.mFramesPerPacket = inTemplateDescription.mFramesPerPacket; + } + if(ioDescription.mBytesPerFrame == 0) + { + ioDescription.mBytesPerFrame = inTemplateDescription.mBytesPerFrame; + } + if(ioDescription.mChannelsPerFrame == 0) + { + ioDescription.mChannelsPerFrame = inTemplateDescription.mChannelsPerFrame; + } + if(ioDescription.mBitsPerChannel == 0) + { + ioDescription.mBitsPerChannel = inTemplateDescription.mBitsPerChannel; + } +} + +void CAStreamBasicDescription::GetSimpleName(const AudioStreamBasicDescription& inDescription, char* outName, UInt32 inMaxNameLength, bool inAbbreviate, bool inIncludeSampleRate) +{ + if(inIncludeSampleRate) + { + int theCharactersWritten = snprintf(outName, inMaxNameLength, "%.0f ", inDescription.mSampleRate); + outName += theCharactersWritten; + inMaxNameLength -= static_cast(theCharactersWritten); + } + + switch(inDescription.mFormatID) + { + case kAudioFormatLinearPCM: + { + const char* theEndianString = NULL; + if((inDescription.mFormatFlags & kAudioFormatFlagIsBigEndian) != 0) + { + #if TARGET_RT_LITTLE_ENDIAN + theEndianString = "Big Endian"; + #endif + } + else + { + #if TARGET_RT_BIG_ENDIAN + theEndianString = "Little Endian"; + #endif + } + + const char* theKindString = NULL; + if((inDescription.mFormatFlags & kAudioFormatFlagIsFloat) != 0) + { + theKindString = (inAbbreviate ? "Float" : "Floating Point"); + } + else if((inDescription.mFormatFlags & kAudioFormatFlagIsSignedInteger) != 0) + { + theKindString = (inAbbreviate ? "SInt" : "Signed Integer"); + } + else + { + theKindString = (inAbbreviate ? "UInt" : "Unsigned Integer"); + } + + const char* thePackingString = NULL; + if((inDescription.mFormatFlags & kAudioFormatFlagIsPacked) == 0) + { + if((inDescription.mFormatFlags & kAudioFormatFlagIsAlignedHigh) != 0) + { + thePackingString = "High"; + } + else + { + thePackingString = "Low"; + } + } + + const char* theMixabilityString = NULL; + if((inDescription.mFormatFlags & kIsNonMixableFlag) == 0) + { + theMixabilityString = "Mixable"; + } + else + { + theMixabilityString = "Unmixable"; + } + + if(inAbbreviate) + { + if(theEndianString != NULL) + { + if(thePackingString != NULL) + { + snprintf(outName, inMaxNameLength, "%s %d Ch %s %s %s%d/%s%d", theMixabilityString, (int)inDescription.mChannelsPerFrame, theEndianString, thePackingString, theKindString, (int)inDescription.mBitsPerChannel, theKindString, (int)(inDescription.mBytesPerFrame / inDescription.mChannelsPerFrame) * 8); + } + else + { + snprintf(outName, inMaxNameLength, "%s %d Ch %s %s%d", theMixabilityString, (int)inDescription.mChannelsPerFrame, theEndianString, theKindString, (int)inDescription.mBitsPerChannel); + } + } + else + { + if(thePackingString != NULL) + { + snprintf(outName, inMaxNameLength, "%s %d Ch %s %s%d/%s%d", theMixabilityString, (int)inDescription.mChannelsPerFrame, thePackingString, theKindString, (int)inDescription.mBitsPerChannel, theKindString, (int)((inDescription.mBytesPerFrame / inDescription.mChannelsPerFrame) * 8)); + } + else + { + snprintf(outName, inMaxNameLength, "%s %d Ch %s%d", theMixabilityString, (int)inDescription.mChannelsPerFrame, theKindString, (int)inDescription.mBitsPerChannel); + } + } + } + else + { + if(theEndianString != NULL) + { + if(thePackingString != NULL) + { + snprintf(outName, inMaxNameLength, "%s %d Channel %d Bit %s %s Aligned %s in %d Bits", theMixabilityString, (int)inDescription.mChannelsPerFrame, (int)inDescription.mBitsPerChannel, theEndianString, theKindString, thePackingString, (int)(inDescription.mBytesPerFrame / inDescription.mChannelsPerFrame) * 8); + } + else + { + snprintf(outName, inMaxNameLength, "%s %d Channel %d Bit %s %s", theMixabilityString, (int)inDescription.mChannelsPerFrame, (int)inDescription.mBitsPerChannel, theEndianString, theKindString); + } + } + else + { + if(thePackingString != NULL) + { + snprintf(outName, inMaxNameLength, "%s %d Channel %d Bit %s Aligned %s in %d Bits", theMixabilityString, (int)inDescription.mChannelsPerFrame, (int)inDescription.mBitsPerChannel, theKindString, thePackingString, (int)(inDescription.mBytesPerFrame / inDescription.mChannelsPerFrame) * 8); + } + else + { + snprintf(outName, inMaxNameLength, "%s %d Channel %d Bit %s", theMixabilityString, (int)inDescription.mChannelsPerFrame, (int)inDescription.mBitsPerChannel, theKindString); + } + } + } + } + break; + + case kAudioFormatAC3: + strlcpy(outName, "AC-3", sizeof(outName)); + break; + + case kAudioFormat60958AC3: + strlcpy(outName, "AC-3 for SPDIF", sizeof(outName)); + break; + + default: + CACopy4CCToCString(outName, inDescription.mFormatID); + break; + }; +} + +#if CoreAudio_Debug +#include "CALogMacros.h" + +void CAStreamBasicDescription::PrintToLog(const AudioStreamBasicDescription& inDesc) +{ + PrintFloat (" Sample Rate: ", inDesc.mSampleRate); + Print4CharCode (" Format ID: ", inDesc.mFormatID); + PrintHex (" Format Flags: ", inDesc.mFormatFlags); + PrintInt (" Bytes per Packet: ", inDesc.mBytesPerPacket); + PrintInt (" Frames per Packet: ", inDesc.mFramesPerPacket); + PrintInt (" Bytes per Frame: ", inDesc.mBytesPerFrame); + PrintInt (" Channels per Frame: ", inDesc.mChannelsPerFrame); + PrintInt (" Bits per Channel: ", inDesc.mBitsPerChannel); +} +#endif + +bool operator<(const AudioStreamBasicDescription& x, const AudioStreamBasicDescription& y) +{ + bool theAnswer = false; + bool isDone = false; + + // note that if either side is 0, that field is skipped + + // format ID is the first order sort + if((!isDone) && ((x.mFormatID != 0) && (y.mFormatID != 0))) + { + if(x.mFormatID != y.mFormatID) + { + // formats are sorted numerically except that linear + // PCM is always first + if(x.mFormatID == kAudioFormatLinearPCM) + { + theAnswer = true; + } + else if(y.mFormatID == kAudioFormatLinearPCM) + { + theAnswer = false; + } + else + { + theAnswer = x.mFormatID < y.mFormatID; + } + isDone = true; + } + } + + + // mixable is always better than non-mixable for linear PCM and should be the second order sort item + if((!isDone) && ((x.mFormatID == kAudioFormatLinearPCM) && (y.mFormatID == kAudioFormatLinearPCM))) + { + if(((x.mFormatFlags & kIsNonMixableFlag) == 0) && ((y.mFormatFlags & kIsNonMixableFlag) != 0)) + { + theAnswer = true; + isDone = true; + } + else if(((x.mFormatFlags & kIsNonMixableFlag) != 0) && ((y.mFormatFlags & kIsNonMixableFlag) == 0)) + { + theAnswer = false; + isDone = true; + } + } + + // floating point vs integer for linear PCM only + if((!isDone) && ((x.mFormatID == kAudioFormatLinearPCM) && (y.mFormatID == kAudioFormatLinearPCM))) + { + if((x.mFormatFlags & kAudioFormatFlagIsFloat) != (y.mFormatFlags & kAudioFormatFlagIsFloat)) + { + // floating point is better than integer + theAnswer = y.mFormatFlags & kAudioFormatFlagIsFloat; + isDone = true; + } + } + + // bit depth + if((!isDone) && ((x.mBitsPerChannel != 0) && (y.mBitsPerChannel != 0))) + { + if(x.mBitsPerChannel != y.mBitsPerChannel) + { + // deeper bit depths are higher quality + theAnswer = x.mBitsPerChannel < y.mBitsPerChannel; + isDone = true; + } + } + + // sample rate + if((!isDone) && fnonzero(x.mSampleRate) && fnonzero(y.mSampleRate)) + { + if(fnotequal(x.mSampleRate, y.mSampleRate)) + { + // higher sample rates are higher quality + theAnswer = x.mSampleRate < y.mSampleRate; + isDone = true; + } + } + + // number of channels + if((!isDone) && ((x.mChannelsPerFrame != 0) && (y.mChannelsPerFrame != 0))) + { + if(x.mChannelsPerFrame != y.mChannelsPerFrame) + { + // more channels is higher quality + theAnswer = x.mChannelsPerFrame < y.mChannelsPerFrame; + //isDone = true; + } + } + + return theAnswer; +} + +UInt32 CAStreamBasicDescription::GetRegularizedFormatFlags(bool forHardware) const +{ + UInt32 result = mFormatFlags; + + if (IsPCM()) { + // First, if there are bits other than AllClear set, clear it because it's lying. + if (result & ~kAudioFormatFlagsAreAllClear) + result &= ~kAudioFormatFlagsAreAllClear; + + // If not forHardware, remove the mixability flag. + if (!forHardware) + result &= ~kLinearPCMFormatFlagIsNonMixable; + + // If the format has no extra bits, then it is packed. + if (!PackednessIsSignificant()) + result |= kLinearPCMFormatFlagIsPacked; + + // Remove the high-aligned flag if alignment is irrelevant. + if (!AlignmentIsSignificant()) + result &= ~kLinearPCMFormatFlagIsAlignedHigh; + + // Remove the signed integer bit if it's float + if (result & kLinearPCMFormatFlagIsFloat) + result &= ~kLinearPCMFormatFlagIsSignedInteger; + + // If the bit depth is 8 bits or less and the format is packed, we don't care about endianness + if (mBitsPerChannel <= 8 && (result & kLinearPCMFormatFlagIsPacked)) + result &= kAudioFormatFlagIsBigEndian; + + // If there is 1 channel, we don't care about non-interleavedness. + if (mChannelsPerFrame == 1) + result &= ~kLinearPCMFormatFlagIsNonInterleaved; + + // Finally, if the bits really are all 0, set the AllClear flag. + if (result == 0) + result = kAudioFormatFlagsAreAllClear; + } + return result; +} + +// private +bool CAStreamBasicDescription::EquivalentFormatFlags(const AudioStreamBasicDescription &x, const AudioStreamBasicDescription &y, bool forHardware, bool usingWildcards) +{ + if (usingWildcards) + { + // if either of the formats is a wildcard, we don't care about the flags + // if either of the flags is a wildcard, we have matched + if (x.mFormatID == 0 || y.mFormatID == 0 || x.mFormatFlags == 0 || y.mFormatFlags == 0) + { + return true; + } + } + + if (x.mFormatID != kAudioFormatLinearPCM) // we already know the formatID's match and have taken wildcards out of the picture. + return x.mFormatFlags == y.mFormatFlags; + + // It is safe to down-cast from AudioStreamBasicDescription to its C++ wrapper. + // The cast could be avoided with a copy, but here, efficiency matters. + const CAStreamBasicDescription &a = *static_cast(&x); + const CAStreamBasicDescription &b = *static_cast(&y); + + return a.GetRegularizedFormatFlags(forHardware) == b.GetRegularizedFormatFlags(forHardware); +} + +bool CAStreamBasicDescription::IsExactlyEqual(const AudioStreamBasicDescription &x, const AudioStreamBasicDescription &y) +{ + // mReserved didn't exist in early versions of OS X; we want to ignore differences there. + // The structure is properly packed up until that point, so the shortcut of using memcmp() + // instead of individual field comparisons is safe. + return memcmp(&x, &y, offsetof(AudioStreamBasicDescription, mReserved)) == 0; +} + +#define MATCH_WITH_WILDCARD(name) ((x.name) == 0 || (y.name) == 0 || (x.name) == (y.name)) + +bool CAStreamBasicDescription::IsEquivalent(const AudioStreamBasicDescription &x, const AudioStreamBasicDescription &y, ComparisonOptions options) +{ + if (options & kCompareUsingWildcards) { + return + // check the sample rate + (fiszero(x.mSampleRate) || fiszero(y.mSampleRate) || fequal(x.mSampleRate, y.mSampleRate)) + + // check the format ids + && MATCH_WITH_WILDCARD(mFormatID) + + // check the bytes per packet + && MATCH_WITH_WILDCARD(mBytesPerPacket) + + // check the frames per packet + && MATCH_WITH_WILDCARD(mFramesPerPacket) + + // check the bytes per frame + && MATCH_WITH_WILDCARD(mBytesPerFrame) + + // check the channels per frame + && MATCH_WITH_WILDCARD(mChannelsPerFrame) + + // check the bits per channel + && MATCH_WITH_WILDCARD(mBitsPerChannel) + + // Only if we get this far, do the work of matching the format flags + && EquivalentFormatFlags(x, y, options & kCompareForHardware, /*usingWildcards=*/true); + } else { + return x.mSampleRate == y.mSampleRate + && x.mFormatID == y.mFormatID + && x.mBytesPerPacket == y.mBytesPerPacket + && x.mFramesPerPacket == y.mFramesPerPacket + && x.mChannelsPerFrame == y.mChannelsPerFrame + && x.mBitsPerChannel == y.mBitsPerChannel + && EquivalentFormatFlags(x, y, options & kCompareForHardware, /*usingWildcards=*/false); + } +} + +// DEPRECATED. +bool operator==(const AudioStreamBasicDescription& x, const AudioStreamBasicDescription& y) +{ + return CAStreamBasicDescription::IsEquivalent(x, y, CAStreamBasicDescription::kCompareUsingWildcards | CAStreamBasicDescription::kCompareForHardware); +} + +// To be deprecated. +bool CAStreamBasicDescription::IsEqual(const AudioStreamBasicDescription &other, bool interpretingWildcards) const +{ + if (interpretingWildcards) + return CAStreamBasicDescription::IsEquivalent(*this, other, CAStreamBasicDescription::kCompareUsingWildcards | CAStreamBasicDescription::kCompareForHardware); + return IsExactlyEqual(*this, other); +} + +// DEPRECATED. +bool CAStreamBasicDescription::IsEqual(const AudioStreamBasicDescription &other) const +{ + return CAStreamBasicDescription::IsEquivalent(*this, other, CAStreamBasicDescription::kCompareUsingWildcards | CAStreamBasicDescription::kCompareForHardware); +} + +bool SanityCheck(const AudioStreamBasicDescription& x) +{ + // This function returns false if there are sufficiently insane values in any field. + // It is very conservative so even some very unlikely values will pass. + // This is just meant to catch the case where the data from a file is corrupted. + + return + (x.mSampleRate >= 0.) + && (x.mSampleRate < 3e6) // SACD sample rate is 2.8224 MHz + && (x.mBytesPerPacket < 1000000) + && (x.mFramesPerPacket < 1000000) + && (x.mBytesPerFrame < 1000000) + && (x.mChannelsPerFrame > 0) + && (x.mChannelsPerFrame <= 1024) + && (x.mBitsPerChannel <= 1024) + && (x.mFormatID != 0) + && !(x.mFormatID == kAudioFormatLinearPCM && (x.mFramesPerPacket != 1 || x.mBytesPerPacket != x.mBytesPerFrame)); +} + +bool CAStreamBasicDescription::FromText(const char *inTextDesc, AudioStreamBasicDescription &fmt) +{ + const char *p = inTextDesc; + + memset(&fmt, 0, sizeof(fmt)); + + bool isPCM = true; // until proven otherwise + UInt32 pcmFlags = kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger; + + if (p[0] == '-') // previously we required a leading dash on PCM formats + ++p; + + if (p[0] == 'B' && p[1] == 'E') { + pcmFlags |= kLinearPCMFormatFlagIsBigEndian; + p += 2; + } else if (p[0] == 'L' && p[1] == 'E') { + p += 2; + } else { + // default is native-endian +#if TARGET_RT_BIG_ENDIAN + pcmFlags |= kLinearPCMFormatFlagIsBigEndian; +#endif + } + if (p[0] == 'F') { + pcmFlags = (pcmFlags & ~static_cast(kAudioFormatFlagIsSignedInteger)) | kAudioFormatFlagIsFloat; + ++p; + } else { + if (p[0] == 'U') { + pcmFlags &= ~static_cast(kAudioFormatFlagIsSignedInteger); + ++p; + } + if (p[0] == 'I') + ++p; + else { + // it's not PCM; presumably some other format (NOT VALIDATED; use AudioFormat for that) + isPCM = false; + p = inTextDesc; // go back to the beginning + char buf[4] = { ' ',' ',' ',' ' }; + for (int i = 0; i < 4; ++i) { + if (*p != '\\') { + if ((buf[i] = *p++) == '\0') { + // special-case for 'aac' + if (i != 3) return false; + --p; // keep pointing at the terminating null + buf[i] = ' '; + break; + } + } else { + // "\xNN" is a hex byte + if (*++p != 'x') return false; + int x; + if (sscanf(++p, "%02X", &x) != 1) return false; + buf[i] = static_cast(x); + p += 2; + } + } + + if (strchr("-@/#", buf[3])) { + // further special-casing for 'aac' + buf[3] = ' '; + --p; + } + + memcpy(&fmt.mFormatID, buf, 4); + fmt.mFormatID = CFSwapInt32BigToHost(fmt.mFormatID); + } + } + + if (isPCM) { + fmt.mFormatID = kAudioFormatLinearPCM; + fmt.mFormatFlags = pcmFlags; + fmt.mFramesPerPacket = 1; + fmt.mChannelsPerFrame = 1; + UInt32 bitdepth = 0, fracbits = 0; + while (isdigit(*p)) + bitdepth = 10 * bitdepth + static_cast(*p++ - '0'); + if (*p == '.') { + ++p; + if (!isdigit(*p)) { + fprintf(stderr, "Expected fractional bits following '.'\n"); + goto Bail; + } + while (isdigit(*p)) + fracbits = 10 * fracbits + static_cast(*p++ - '0'); + bitdepth += fracbits; + fmt.mFormatFlags |= (fracbits << kLinearPCMFormatFlagsSampleFractionShift); + } + fmt.mBitsPerChannel = bitdepth; + fmt.mBytesPerPacket = fmt.mBytesPerFrame = (bitdepth + 7) / 8; + if (bitdepth & 7) { + // assume unpacked. (packed odd bit depths are describable but not supported in AudioConverter.) + fmt.mFormatFlags &= ~static_cast(kLinearPCMFormatFlagIsPacked); + // alignment matters; default to high-aligned. use ':L_' for low. + fmt.mFormatFlags |= kLinearPCMFormatFlagIsAlignedHigh; + } + } + if (*p == '@') { + ++p; + while (isdigit(*p)) + fmt.mSampleRate = 10 * fmt.mSampleRate + (*p++ - '0'); + } + if (*p == '/') { + UInt32 flags = 0; + while (true) { + char c = *++p; + if (c >= '0' && c <= '9') + flags = (flags << 4) | static_cast(c - '0'); + else if (c >= 'A' && c <= 'F') + flags = (flags << 4) | static_cast(c - 'A' + 10); + else if (c >= 'a' && c <= 'f') + flags = (flags << 4) | static_cast(c - 'a' + 10); + else break; + } + fmt.mFormatFlags = flags; + } + if (*p == '#') { + ++p; + while (isdigit(*p)) + fmt.mFramesPerPacket = 10 * fmt.mFramesPerPacket + static_cast(*p++ - '0'); + } + if (*p == ':') { + ++p; + fmt.mFormatFlags &= ~static_cast(kLinearPCMFormatFlagIsPacked); + if (*p == 'L') + fmt.mFormatFlags &= ~static_cast(kLinearPCMFormatFlagIsAlignedHigh); + else if (*p == 'H') + fmt.mFormatFlags |= kLinearPCMFormatFlagIsAlignedHigh; + else + goto Bail; + ++p; + UInt32 bytesPerFrame = 0; + while (isdigit(*p)) + bytesPerFrame = 10 * bytesPerFrame + static_cast(*p++ - '0'); + fmt.mBytesPerFrame = fmt.mBytesPerPacket = bytesPerFrame; + } + if (*p == ',') { + ++p; + int ch = 0; + while (isdigit(*p)) + ch = 10 * ch + (*p++ - '0'); + fmt.mChannelsPerFrame = static_cast(ch); + if (*p == 'D') { + ++p; + if (fmt.mFormatID != kAudioFormatLinearPCM) { + fprintf(stderr, "non-interleaved flag invalid for non-PCM formats\n"); + goto Bail; + } + fmt.mFormatFlags |= kAudioFormatFlagIsNonInterleaved; + } else { + if (*p == 'I') ++p; // default + if (fmt.mFormatID == kAudioFormatLinearPCM) + fmt.mBytesPerPacket = fmt.mBytesPerFrame *= static_cast(ch); + } + } + if (*p != '\0') { + fprintf(stderr, "extra characters at end of format string: %s\n", p); + goto Bail; + } + return true; + +Bail: + fprintf(stderr, "Invalid format string: %s\n", inTextDesc); + fprintf(stderr, "Syntax of format strings is: \n"); + return false; +} + +const char *CAStreamBasicDescription::sTextParsingUsageString = + "format[@sample_rate_hz][/format_flags][#frames_per_packet][:LHbytesPerFrame][,channelsDI].\n" + "Format for PCM is [-][BE|LE]{F|I|UI}{bitdepth}; else a 4-char format code (e.g. aac, alac).\n"; diff --git a/Speakerbox/Speakerbox/Audio/CAStreamBasicDescription.h b/Speakerbox/Speakerbox/Audio/CAStreamBasicDescription.h new file mode 100644 index 00000000..d33f4c28 --- /dev/null +++ b/Speakerbox/Speakerbox/Audio/CAStreamBasicDescription.h @@ -0,0 +1,448 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + (Borrowed from aurioTouch sample code) Part of CoreAudio Utility Classes +*/ + +#ifndef __CAStreamBasicDescription_h__ +#define __CAStreamBasicDescription_h__ + +#if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) + #include + #include +#else + #include "CoreAudioTypes.h" + #include "CoreFoundation.h" +#endif + +#include "CADebugMacros.h" +#include // for memset, memcpy +#include // for FILE * + +#pragma mark This file needs to compile on more earlier versions of the OS, so please keep that in mind when editing it + +#ifndef ASBD_STRICT_EQUALITY + #define ASBD_STRICT_EQUALITY 0 +#endif + +#if __GNUC__ && ASBD_STRICT_EQUALITY + // not turning on the deprecation just yet + #define ASBD_EQUALITY_DEPRECATED __attribute__((deprecated("This method uses a possibly surprising wildcard comparison (i.e. 0 channels == 1 channel)"))) +#else + #define ASBD_EQUALITY_DEPRECATED +#endif + +#ifndef CA_CANONICAL_DEPRECATED + #define CA_CANONICAL_DEPRECATED +#endif + +extern char *CAStringForOSType (OSType t, char *writeLocation, size_t bufsize); + +// define Leopard specific symbols for backward compatibility if applicable +#if COREAUDIOTYPES_VERSION < 1050 +typedef Float32 AudioSampleType; +enum { kAudioFormatFlagsCanonical = kAudioFormatFlagIsFloat | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked }; +#endif +#if COREAUDIOTYPES_VERSION < 1051 +typedef Float32 AudioUnitSampleType; +enum { + kLinearPCMFormatFlagsSampleFractionShift = 7, + kLinearPCMFormatFlagsSampleFractionMask = (0x3F << kLinearPCMFormatFlagsSampleFractionShift), +}; +#endif + +// define the IsMixable format flag for all versions of the system +#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_3) + enum { kIsNonMixableFlag = kAudioFormatFlagIsNonMixable }; +#else + enum { kIsNonMixableFlag = (1L << 6) }; +#endif + +//============================================================================= +// CAStreamBasicDescription +// +// This is a wrapper class for the AudioStreamBasicDescription struct. +// It adds a number of convenience routines, but otherwise adds nothing +// to the footprint of the original struct. +//============================================================================= +class CAStreamBasicDescription : + public AudioStreamBasicDescription +{ + +// Constants +public: + static const AudioStreamBasicDescription sEmpty; + + enum CommonPCMFormat { + kPCMFormatOther = 0, + kPCMFormatFloat32 = 1, + kPCMFormatInt16 = 2, + kPCMFormatFixed824 = 3, + kPCMFormatFloat64 = 4, + kPCMFormatInt32 = 5 + }; + + // options for IsEquivalent + enum { + kCompareDefault = 0, + kCompareUsingWildcards = 1 << 0, // treats fields with values of 0 as wildcards. + // too liberal if you need to represent 0 channels. + kCompareForHardware = 1 << 1, // formats are hardware formats (IsNonMixable flag is significant). + + kCompareForHardwareUsingWildcards = kCompareForHardware + kCompareUsingWildcards // for convenience + }; + typedef UInt32 ComparisonOptions; + +// Construction/Destruction +public: + CAStreamBasicDescription(); + + CAStreamBasicDescription(const AudioStreamBasicDescription &desc); + + CAStreamBasicDescription( double inSampleRate, UInt32 inFormatID, + UInt32 inBytesPerPacket, UInt32 inFramesPerPacket, + UInt32 inBytesPerFrame, UInt32 inChannelsPerFrame, + UInt32 inBitsPerChannel, UInt32 inFormatFlags); + + CAStreamBasicDescription( double inSampleRate, UInt32 inNumChannels, CommonPCMFormat pcmf, bool inIsInterleaved) { + unsigned wordsize; + + mSampleRate = inSampleRate; + mFormatID = kAudioFormatLinearPCM; + mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; + mFramesPerPacket = 1; + mChannelsPerFrame = inNumChannels; + mBytesPerFrame = mBytesPerPacket = 0; + mReserved = 0; + + switch (pcmf) { + default: + return; + case kPCMFormatFloat32: + wordsize = 4; + mFormatFlags |= kAudioFormatFlagIsFloat; + break; + case kPCMFormatFloat64: + wordsize = 8; + mFormatFlags |= kAudioFormatFlagIsFloat; + break; + case kPCMFormatInt16: + wordsize = 2; + mFormatFlags |= kAudioFormatFlagIsSignedInteger; + break; + case kPCMFormatInt32: + wordsize = 4; + mFormatFlags |= kAudioFormatFlagIsSignedInteger; + break; + case kPCMFormatFixed824: + wordsize = 4; + mFormatFlags |= kAudioFormatFlagIsSignedInteger | (24 << kLinearPCMFormatFlagsSampleFractionShift); + break; + } + mBitsPerChannel = wordsize * 8; + if (inIsInterleaved) + mBytesPerFrame = mBytesPerPacket = wordsize * inNumChannels; + else { + mFormatFlags |= kAudioFormatFlagIsNonInterleaved; + mBytesPerFrame = mBytesPerPacket = wordsize; + } + } + +// Assignment + CAStreamBasicDescription& operator=(const AudioStreamBasicDescription& v) { SetFrom(v); return *this; } + + void SetFrom(const AudioStreamBasicDescription &desc) + { + memcpy(this, &desc, sizeof(AudioStreamBasicDescription)); + } + + bool FromText(const char *inTextDesc) { return FromText(inTextDesc, *this); } + static bool FromText(const char *inTextDesc, AudioStreamBasicDescription &outDesc); + // return true if parsing was successful + + static const char *sTextParsingUsageString; + + // _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + // + // interrogation + + bool IsPCM() const { return mFormatID == kAudioFormatLinearPCM; } + + bool PackednessIsSignificant() const + { + Assert(IsPCM(), "PackednessIsSignificant only applies for PCM"); + return (SampleWordSize() << 3) != mBitsPerChannel; + } + + bool AlignmentIsSignificant() const + { + return PackednessIsSignificant() || (mBitsPerChannel & 7) != 0; + } + + bool IsInterleaved() const + { + return !(mFormatFlags & kAudioFormatFlagIsNonInterleaved); + } + + bool IsSignedInteger() const + { + return IsPCM() && (mFormatFlags & kAudioFormatFlagIsSignedInteger); + } + + bool IsFloat() const + { + return IsPCM() && (mFormatFlags & kAudioFormatFlagIsFloat); + } + + bool IsNativeEndian() const + { + return (mFormatFlags & kAudioFormatFlagIsBigEndian) == kAudioFormatFlagsNativeEndian; + } + + // for sanity with interleaved/deinterleaved possibilities, never access mChannelsPerFrame, use these: + UInt32 NumberInterleavedChannels() const { return IsInterleaved() ? mChannelsPerFrame : 1; } + UInt32 NumberChannelStreams() const { return IsInterleaved() ? 1 : mChannelsPerFrame; } + UInt32 NumberChannels() const { return mChannelsPerFrame; } + UInt32 SampleWordSize() const { + return (mBytesPerFrame > 0 && NumberInterleavedChannels()) ? mBytesPerFrame / NumberInterleavedChannels() : 0; + } + + UInt32 FramesToBytes(UInt32 nframes) const { return nframes * mBytesPerFrame; } + UInt32 BytesToFrames(UInt32 nbytes) const { + Assert(mBytesPerFrame > 0, "bytesPerFrame must be > 0 in BytesToFrames"); + return nbytes / mBytesPerFrame; + } + + bool SameChannelsAndInterleaving(const CAStreamBasicDescription &a) const + { + return this->NumberChannels() == a.NumberChannels() && this->IsInterleaved() == a.IsInterleaved(); + } + + bool IdentifyCommonPCMFormat(CommonPCMFormat &outFormat, bool *outIsInterleaved=NULL) const + { // return true if it's a valid PCM format. + + outFormat = kPCMFormatOther; + // trap out patently invalid formats. + if (mFormatID != kAudioFormatLinearPCM || mFramesPerPacket != 1 || mBytesPerFrame != mBytesPerPacket || mBitsPerChannel/8 > mBytesPerFrame || mChannelsPerFrame == 0) + return false; + bool interleaved = (mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0; + if (outIsInterleaved != NULL) *outIsInterleaved = interleaved; + unsigned wordsize = mBytesPerFrame; + if (interleaved) { + if (wordsize % mChannelsPerFrame != 0) return false; + wordsize /= mChannelsPerFrame; + } + + if ((mFormatFlags & kAudioFormatFlagIsBigEndian) == kAudioFormatFlagsNativeEndian + && wordsize * 8 == mBitsPerChannel) { + // packed and native endian, good + if (mFormatFlags & kLinearPCMFormatFlagIsFloat) { + // float: reject nonsense bits + if (mFormatFlags & (kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagsSampleFractionMask)) + return false; + if (wordsize == 4) + outFormat = kPCMFormatFloat32; + if (wordsize == 8) + outFormat = kPCMFormatFloat64; + } else if (mFormatFlags & kLinearPCMFormatFlagIsSignedInteger) { + // signed int + unsigned fracbits = (mFormatFlags & kLinearPCMFormatFlagsSampleFractionMask) >> kLinearPCMFormatFlagsSampleFractionShift; + if (wordsize == 4 && fracbits == 24) + outFormat = kPCMFormatFixed824; + else if (wordsize == 4 && fracbits == 0) + outFormat = kPCMFormatInt32; + else if (wordsize == 2 && fracbits == 0) + outFormat = kPCMFormatInt16; + } + } + return true; + } + + bool IsCommonFloat32(bool *outIsInterleaved=NULL) const { + CommonPCMFormat fmt; + return IdentifyCommonPCMFormat(fmt, outIsInterleaved) && fmt == kPCMFormatFloat32; + } + bool IsCommonFloat64(bool *outIsInterleaved=NULL) const { + CommonPCMFormat fmt; + return IdentifyCommonPCMFormat(fmt, outIsInterleaved) && fmt == kPCMFormatFloat64; + } + bool IsCommonFixed824(bool *outIsInterleaved=NULL) const { + CommonPCMFormat fmt; + return IdentifyCommonPCMFormat(fmt, outIsInterleaved) && fmt == kPCMFormatFixed824; + } + bool IsCommonInt16(bool *outIsInterleaved=NULL) const { + CommonPCMFormat fmt; + return IdentifyCommonPCMFormat(fmt, outIsInterleaved) && fmt == kPCMFormatInt16; + } + + // _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + // + // manipulation + + CA_CANONICAL_DEPRECATED + void SetCanonical(UInt32 nChannels, bool interleaved) + // note: leaves sample rate untouched + { + mFormatID = kAudioFormatLinearPCM; + UInt32 sampleSize = SizeOf32(AudioSampleType); + mFormatFlags = kAudioFormatFlagsCanonical; + mBitsPerChannel = 8 * sampleSize; + mChannelsPerFrame = nChannels; + mFramesPerPacket = 1; + if (interleaved) + mBytesPerPacket = mBytesPerFrame = nChannels * sampleSize; + else { + mBytesPerPacket = mBytesPerFrame = sampleSize; + mFormatFlags |= kAudioFormatFlagIsNonInterleaved; + } + } + + CA_CANONICAL_DEPRECATED + bool IsCanonical() const + { + if (mFormatID != kAudioFormatLinearPCM) return false; + UInt32 reqFormatFlags; + UInt32 flagsMask = (kLinearPCMFormatFlagIsFloat | kLinearPCMFormatFlagIsBigEndian | kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked | kLinearPCMFormatFlagsSampleFractionMask); + bool interleaved = (mFormatFlags & kAudioFormatFlagIsNonInterleaved) == 0; + unsigned sampleSize = SizeOf32(AudioSampleType); + reqFormatFlags = kAudioFormatFlagsCanonical; + UInt32 reqFrameSize = interleaved ? (mChannelsPerFrame * sampleSize) : sampleSize; + + return ((mFormatFlags & flagsMask) == reqFormatFlags + && mBitsPerChannel == 8 * sampleSize + && mFramesPerPacket == 1 + && mBytesPerFrame == reqFrameSize + && mBytesPerPacket == reqFrameSize); + } + + CA_CANONICAL_DEPRECATED + void SetAUCanonical(UInt32 nChannels, bool interleaved) + { + mFormatID = kAudioFormatLinearPCM; +#if CA_PREFER_FIXED_POINT + mFormatFlags = kAudioFormatFlagsCanonical | (kAudioUnitSampleFractionBits << kLinearPCMFormatFlagsSampleFractionShift); +#else + mFormatFlags = kAudioFormatFlagsCanonical; +#endif + mChannelsPerFrame = nChannels; + mFramesPerPacket = 1; + mBitsPerChannel = 8 * SizeOf32(AudioUnitSampleType); + if (interleaved) + mBytesPerPacket = mBytesPerFrame = nChannels * SizeOf32(AudioUnitSampleType); + else { + mBytesPerPacket = mBytesPerFrame = SizeOf32(AudioUnitSampleType); + mFormatFlags |= kAudioFormatFlagIsNonInterleaved; + } + } + + void ChangeNumberChannels(UInt32 nChannels, bool interleaved) + // alter an existing format + { + Assert(IsPCM(), "ChangeNumberChannels only works for PCM formats"); + UInt32 wordSize = SampleWordSize(); // get this before changing ANYTHING + if (wordSize == 0) + wordSize = (mBitsPerChannel + 7) / 8; + mChannelsPerFrame = nChannels; + mFramesPerPacket = 1; + if (interleaved) { + mBytesPerPacket = mBytesPerFrame = nChannels * wordSize; + mFormatFlags &= ~static_cast(kAudioFormatFlagIsNonInterleaved); + } else { + mBytesPerPacket = mBytesPerFrame = wordSize; + mFormatFlags |= kAudioFormatFlagIsNonInterleaved; + } + } + + // _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + // + // other + + // IsEqual: Deprecated because of widespread errors due to the default wildcarding behavior. + ASBD_EQUALITY_DEPRECATED + bool IsEqual(const AudioStreamBasicDescription &other) const; + bool IsEqual(const AudioStreamBasicDescription &other, bool interpretingWildcards) const; + + // IsExactlyEqual: bit-for-bit. usually unnecessarily strict. + static bool IsExactlyEqual(const AudioStreamBasicDescription &x, const AudioStreamBasicDescription &y); + + // IsEquivalent: Returns whether the two formats are functionally the same, i.e. if one could + // be correctly passed as the other without an AudioConverter. + static bool IsEquivalent(const AudioStreamBasicDescription &x, const AudioStreamBasicDescription &y) { return IsEquivalent(x, y, kCompareDefault); } + static bool IsEquivalent(const AudioStreamBasicDescription &x, const AudioStreamBasicDescription &y, ComparisonOptions comparisonOptions); + + // Member versions of IsExactlyEqual and IsEquivalent. + bool IsExactlyEqual(const AudioStreamBasicDescription &other) const { return IsExactlyEqual(*this, other); } + bool IsEquivalent(const AudioStreamBasicDescription &other) const { return IsEquivalent(*this, other); } + bool IsEquivalent(const AudioStreamBasicDescription &other, ComparisonOptions comparisonOptions) const { return IsEquivalent(*this, other, comparisonOptions); } + + void Print() const { + Print (stdout); + } + + void Print(FILE* file) const { + PrintFormat (file, "", "AudioStreamBasicDescription:"); + } + + void PrintFormat(FILE *f, const char *indent, const char *name) const { + char buf[256]; + fprintf(f, "%s%s %s\n", indent, name, AsString(buf, sizeof(buf))); + } + + void PrintFormat2(FILE *f, const char *indent, const char *name) const { // no trailing newline + char buf[256]; + fprintf(f, "%s%s %s", indent, name, AsString(buf, sizeof(buf))); + } + + char * AsString(char *buf, size_t bufsize, bool brief=false) const; + + static void Print (const AudioStreamBasicDescription &inDesc) + { + CAStreamBasicDescription desc(inDesc); + desc.Print (); + } + + OSStatus Save(CFPropertyListRef *outData) const; + + OSStatus Restore(CFPropertyListRef &inData); + +// Operations + static bool IsMixable(const AudioStreamBasicDescription& inDescription) { return (inDescription.mFormatID == kAudioFormatLinearPCM) && ((inDescription.mFormatFlags & kIsNonMixableFlag) == 0); } + CA_CANONICAL_DEPRECATED + static void NormalizeLinearPCMFormat(AudioStreamBasicDescription& ioDescription); + CA_CANONICAL_DEPRECATED + static void NormalizeLinearPCMFormat(bool inNativeEndian, AudioStreamBasicDescription& ioDescription); + static void VirtualizeLinearPCMFormat(AudioStreamBasicDescription& ioDescription); + static void VirtualizeLinearPCMFormat(bool inNativeEndian, AudioStreamBasicDescription& ioDescription); + static void ResetFormat(AudioStreamBasicDescription& ioDescription); + static void FillOutFormat(AudioStreamBasicDescription& ioDescription, const AudioStreamBasicDescription& inTemplateDescription); + static void GetSimpleName(const AudioStreamBasicDescription& inDescription, char* outName, UInt32 inMaxNameLength, bool inAbbreviate, bool inIncludeSampleRate = false); + +#if CoreAudio_Debug + static void PrintToLog(const AudioStreamBasicDescription& inDesc); +#endif + + UInt32 GetRegularizedFormatFlags(bool forHardware) const; + +private: + static bool EquivalentFormatFlags(const AudioStreamBasicDescription &x, const AudioStreamBasicDescription &y, bool forHardware, bool usingWildcards); +}; + +#define CAStreamBasicDescription_EmptyInit 0.0, 0, 0, 0, 0, 0, 0, 0, 0 +#define CAStreamBasicDescription_Empty { CAStreamBasicDescription_EmptyInit } + +// operator== is deprecated because it uses the deprecated IsEqual(other, true). +bool operator<(const AudioStreamBasicDescription& x, const AudioStreamBasicDescription& y); +ASBD_EQUALITY_DEPRECATED bool operator==(const AudioStreamBasicDescription& x, const AudioStreamBasicDescription& y); +#if TARGET_OS_MAC || (TARGET_OS_WIN32 && (_MSC_VER > 600)) +ASBD_EQUALITY_DEPRECATED inline bool operator!=(const AudioStreamBasicDescription& x, const AudioStreamBasicDescription& y) { return !(x == y); } +ASBD_EQUALITY_DEPRECATED inline bool operator<=(const AudioStreamBasicDescription& x, const AudioStreamBasicDescription& y) { return (x < y) || (x == y); } +ASBD_EQUALITY_DEPRECATED inline bool operator>=(const AudioStreamBasicDescription& x, const AudioStreamBasicDescription& y) { return !(x < y); } +ASBD_EQUALITY_DEPRECATED inline bool operator>(const AudioStreamBasicDescription& x, const AudioStreamBasicDescription& y) { return !((x < y) || (x == y)); } +#endif + +bool SanityCheck(const AudioStreamBasicDescription& x); + + +#endif // __CAStreamBasicDescription_h__ diff --git a/Speakerbox/Speakerbox/Audio/CAXException.cpp b/Speakerbox/Speakerbox/Audio/CAXException.cpp new file mode 100644 index 00000000..992c2e09 --- /dev/null +++ b/Speakerbox/Speakerbox/Audio/CAXException.cpp @@ -0,0 +1,11 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + (Borrowed from aurioTouch sample code) Part of CoreAudio Utility Classes +*/ + +#include "CAXException.h" + +CAXException::WarningHandler CAXException::sWarningHandler = NULL; diff --git a/Speakerbox/Speakerbox/Audio/CAXException.h b/Speakerbox/Speakerbox/Audio/CAXException.h new file mode 100644 index 00000000..f81da043 --- /dev/null +++ b/Speakerbox/Speakerbox/Audio/CAXException.h @@ -0,0 +1,300 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + (Borrowed from aurioTouch sample code) Part of CoreAudio Utility Classes +*/ + +#ifndef __CAXException_h__ +#define __CAXException_h__ + +#if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) + #include +#else + #include + #include +#endif +#include "CADebugMacros.h" +#include +//#include +#include + + +class CAX4CCString { +public: + CAX4CCString(OSStatus error) { + // see if it appears to be a 4-char-code + char *str = mStr; + *(UInt32 *)(str + 1) = CFSwapInt32HostToBig(error); + if (isprint(str[1]) && isprint(str[2]) && isprint(str[3]) && isprint(str[4])) { + str[0] = str[5] = '\''; + str[6] = '\0'; + } else if (error > -200000 && error < 200000) + // no, format it as an integer + sprintf(str, "%d", (int)error); + else + sprintf(str, "0x%x", (int)error); + } + const char *get() const { return mStr; } + operator const char *() const { return mStr; } +private: + char mStr[16]; +}; + +// An extended exception class that includes the name of the failed operation +class CAXException { +public: + CAXException(const char *operation, OSStatus err) : + mError(err) + { + if (operation == NULL) + mOperation[0] = '\0'; + else if (strlen(operation) >= sizeof(mOperation)) { + memcpy(mOperation, operation, sizeof(mOperation) - 1); + mOperation[sizeof(mOperation) - 1] = '\0'; + } else + + strlcpy(mOperation, operation, sizeof(mOperation)); + } + + char *FormatError(char *str) const + { + return FormatError(str, mError); + } + + char mOperation[256]; + const OSStatus mError; + + // ------------------------------------------------- + + typedef void (*WarningHandler)(const char *msg, OSStatus err); + + static char *FormatError(char *str, OSStatus error) + { + strcpy(str, CAX4CCString(error)); + return str; + } + + static void Warning(const char *s, OSStatus error) + { + if (sWarningHandler) + (*sWarningHandler)(s, error); + } + + static void SetWarningHandler(WarningHandler f) { sWarningHandler = f; } +private: + static WarningHandler sWarningHandler; +}; + +#if DEBUG || CoreAudio_Debug + #define XThrowIfError(error, operation) \ + do { \ + OSStatus __err = error; \ + if (__err) { \ + DebugMessageN2("about to throw %s: %s", CAX4CCString(__err).get(), operation);\ + __THROW_STOP; \ + throw CAXException(operation, __err); \ + } \ + } while (0) + + #define XThrowIf(condition, error, operation) \ + do { \ + if (condition) { \ + OSStatus __err = error; \ + DebugMessageN2("about to throw %s: %s", CAX4CCString(__err).get(), operation);\ + __THROW_STOP; \ + throw CAXException(operation, __err); \ + } \ + } while (0) + + #define XRequireNoError(error, label) \ + do { \ + OSStatus __err = error; \ + if (__err) { \ + DebugMessageN2("about to throw %s: %s", CAX4CCString(__err).get(), #error);\ + STOP; \ + goto label; \ + } \ + } while (0) + + #define XAssert(assertion) \ + do { \ + if (!(assertion)) { \ + DebugMessageN3("[%s, %d] error: failed assertion: %s", __FILE__, __LINE__, #assertion); \ + __ASSERT_STOP; \ + } \ + } while (0) + + #define XAssertNoError(error) \ + do { \ + OSStatus __err = error; \ + if (__err) { \ + DebugMessageN2("error %s: %s", CAX4CCString(__err).get(), #error);\ + STOP; \ + } \ + } while (0) + + #define ca_require_noerr(errorCode, exceptionLabel) \ + do \ + { \ + int evalOnceErrorCode = (errorCode); \ + if ( __builtin_expect(0 != evalOnceErrorCode, 0) ) \ + { \ + DebugMessageN5("ca_require_noerr: [%s, %d] (goto %s;) %s:%d", \ + #errorCode, evalOnceErrorCode, \ + #exceptionLabel, \ + __FILE__, \ + __LINE__); \ + goto exceptionLabel; \ + } \ + } while ( 0 ) + + #define ca_verify_noerr(errorCode) \ + do \ + { \ + int evalOnceErrorCode = (errorCode); \ + if ( __builtin_expect(0 != evalOnceErrorCode, 0) ) \ + { \ + DebugMessageN4("ca_verify_noerr: [%s, %d] %s:%d", \ + #errorCode, evalOnceErrorCode, \ + __FILE__, \ + __LINE__); \ + } \ + } while ( 0 ) + + #define ca_debug_string(message) \ + do \ + { \ + DebugMessageN3("ca_debug_string: %s %s:%d", \ + message, \ + __FILE__, \ + __LINE__); \ + } while ( 0 ) + + + #define ca_verify(assertion) \ + do \ + { \ + if ( __builtin_expect(!(assertion), 0) ) \ + { \ + DebugMessageN3("ca_verify: %s %s:%d", \ + #assertion, \ + __FILE__, \ + __LINE__); \ + } \ + } while ( 0 ) + + #define ca_require(assertion, exceptionLabel) \ + do \ + { \ + if ( __builtin_expect(!(assertion), 0) ) \ + { \ + DebugMessageN4("ca_require: %s %s %s:%d", \ + #assertion, \ + #exceptionLabel, \ + __FILE__, \ + __LINE__); \ + goto exceptionLabel; \ + } \ + } while ( 0 ) + + #define ca_check(assertion) \ + do \ + { \ + if ( __builtin_expect(!(assertion), 0) ) \ + { \ + DebugMessageN3("ca_check: %s %s:%d", \ + #assertion, \ + __FILE__, \ + __LINE__); \ + } \ + } while ( 0 ) + +#else + #define XThrowIfError(error, operation) \ + do { \ + OSStatus __err = error; \ + if (__err) { \ + throw CAXException(operation, __err); \ + } \ + } while (0) + + #define XThrowIf(condition, error, operation) \ + do { \ + if (condition) { \ + OSStatus __err = error; \ + throw CAXException(operation, __err); \ + } \ + } while (0) + + #define XRequireNoError(error, label) \ + do { \ + OSStatus __err = error; \ + if (__err) { \ + goto label; \ + } \ + } while (0) + + #define XAssert(assertion) \ + do { \ + if (!(assertion)) { \ + } \ + } while (0) + + #define XAssertNoError(error) \ + do { \ + /*OSStatus __err =*/ error; \ + } while (0) + + #define ca_require_noerr(errorCode, exceptionLabel) \ + do \ + { \ + if ( __builtin_expect(0 != (errorCode), 0) ) \ + { \ + goto exceptionLabel; \ + } \ + } while ( 0 ) + + #define ca_verify_noerr(errorCode) \ + do \ + { \ + if ( 0 != (errorCode) ) \ + { \ + } \ + } while ( 0 ) + + #define ca_debug_string(message) + + #define ca_verify(assertion) \ + do \ + { \ + if ( !(assertion) ) \ + { \ + } \ + } while ( 0 ) + + #define ca_require(assertion, exceptionLabel) \ + do \ + { \ + if ( __builtin_expect(!(assertion), 0) ) \ + { \ + goto exceptionLabel; \ + } \ + } while ( 0 ) + + #define ca_check(assertion) \ + do \ + { \ + if ( !(assertion) ) \ + { \ + } \ + } while ( 0 ) + + +#endif + +#define XThrow(error, operation) XThrowIf(true, error, operation) +#define XThrowIfErr(error) XThrowIfError(error, #error) + +#endif // __CAXException_h__ diff --git a/Speakerbox/Speakerbox/Base.lproj/LaunchScreen.storyboard b/Speakerbox/Speakerbox/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..2e721e18 --- /dev/null +++ b/Speakerbox/Speakerbox/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Speakerbox/Speakerbox/Base.lproj/Localizable.strings b/Speakerbox/Speakerbox/Base.lproj/Localizable.strings new file mode 100644 index 00000000..75242211 --- /dev/null +++ b/Speakerbox/Speakerbox/Base.lproj/Localizable.strings @@ -0,0 +1,24 @@ +/* + Localizable.strings + Speakerbox + + Copyright © 2016 Apple. All rights reserved. +*/ + +"APPLICATION_NAME" = "Speakerbox"; + +"CALL_STATUS_SENDING" = "Dialing…"; +"CALL_STATUS_RINGING" = "Ringing"; +"CALL_STATUS_CONNECTING" = "Connecting…"; +"CALL_STATUS_HELD" = "On Hold"; +"CALL_STATUS_ACTIVE" = "Active"; + +"TABLE_CELL_EDIT_ACTION_END" = "End"; + +"DIAL_OPTIONS_NAVIGATION_PROMPT" = "Specify a destination"; + +"SIMULATE_INCOMING_CALL_NAVIGATION_PROMPT" = "Specify a destination"; + +"CALL_VIDEO_SWITCH_LABEL" = "Video Call"; +"CALL_DELAY_STEPPER_LABEL" = "Delay Call %.0lf second(s)"; +"DELAY_EXPLANATION_LABEL" = "Delay the call and lock your device to experience an incoming call on the lock screen."; diff --git a/Speakerbox/Speakerbox/Base.lproj/Main.storyboard b/Speakerbox/Speakerbox/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f5916ca8 --- /dev/null +++ b/Speakerbox/Speakerbox/Base.lproj/Main.storyboard @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Speakerbox/Speakerbox/CallAudio.swift b/Speakerbox/Speakerbox/CallAudio.swift new file mode 100644 index 00000000..886f7187 --- /dev/null +++ b/Speakerbox/Speakerbox/CallAudio.swift @@ -0,0 +1,37 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + High-level call audio management functions +*/ + +import Foundation + +private var audioController: AudioController? + +func configureAudioSession() { + print("Configuring audio session") + + if audioController == nil { + audioController = AudioController() + } +} + +func startAudio() { + print("Starting audio") + + if audioController?.startIOUnit() == kAudioServicesNoError { + audioController?.muteAudio = false + } else { + // handle error + } +} + +func stopAudio() { + print("Stopping audio") + + if audioController?.stopIOUnit() != kAudioServicesNoError { + // handle error + } +} diff --git a/Speakerbox/Speakerbox/CallDurationFormatter.swift b/Speakerbox/Speakerbox/CallDurationFormatter.swift new file mode 100644 index 00000000..6cd8a81b --- /dev/null +++ b/Speakerbox/Speakerbox/CallDurationFormatter.swift @@ -0,0 +1,32 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Utility wrapper of NSDateComponentsFormatter for formatting call durations into strings +*/ + +import Foundation + +final class CallDurationFormatter { + + private let dateFormatter: DateComponentsFormatter + + init() { + dateFormatter = DateComponentsFormatter() + dateFormatter.unitsStyle = .positional + dateFormatter.allowedUnits = [.minute, .second] + dateFormatter.zeroFormattingBehavior = .pad + } + + // MARK: API + + func format(dateComponents: DateComponents) -> String? { + return dateFormatter.string(from: dateComponents) + } + + func format(timeInterval: TimeInterval) -> String? { + return dateFormatter.string(from: timeInterval) + } + +} diff --git a/Speakerbox/Speakerbox/CallSummaryTableViewCell.swift b/Speakerbox/Speakerbox/CallSummaryTableViewCell.swift new file mode 100644 index 00000000..a4ad160a --- /dev/null +++ b/Speakerbox/Speakerbox/CallSummaryTableViewCell.swift @@ -0,0 +1,17 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Table view cell representing a single call +*/ + +import UIKit + +final class CallSummaryTableViewCell: UITableViewCell { + + @IBOutlet weak var handleLabel: UILabel! + @IBOutlet weak var callStatusTextLabel: UILabel! + @IBOutlet weak var durationLabel: UILabel! + +} diff --git a/Speakerbox/Speakerbox/CallsViewController.swift b/Speakerbox/Speakerbox/CallsViewController.swift new file mode 100644 index 00000000..98a66b46 --- /dev/null +++ b/Speakerbox/Speakerbox/CallsViewController.swift @@ -0,0 +1,192 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller which displays list of calls +*/ + +import UIKit + +final class CallsViewController: UITableViewController { + + var callManager: SpeakerboxCallManager? + + var callDurationTimer: Timer? + lazy var callDurationFormatter = CallDurationFormatter() + + // MARK: Actions + + @IBAction func unwindForDialCallSegue(_ segue: UIStoryboardSegue) { + let dialOptionsViewController = segue.source as! DialOptionsViewController + + if let handle = dialOptionsViewController.handle { + let video = dialOptionsViewController.video + callManager?.startCall(handle: handle, video: video) + } + } + + @IBAction func unwindForSimulateIncomingCallSegue(_ segue: UIStoryboardSegue) { + let simulateIncomingCallViewController = segue.source as! SimulateIncomingCallViewController + + guard let handle = simulateIncomingCallViewController.handle else { return } + let video = simulateIncomingCallViewController.video + let delay = simulateIncomingCallViewController.delay + + /* + Since the app may be suspended while waiting for the delayed action to begin, + start a background task. + */ + let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) + DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + delay) { + AppDelegate.shared.displayIncomingCall(uuid: UUID(), handle: handle, hasVideo: video) { _ in + UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) + } + } + } + + // MARK: Helpers + + private func updateCallsDependentUI(animated: Bool) { + updateCallDurationTimer() + } + + private func call(at indexPath: IndexPath) -> SpeakerboxCall? { + return callManager?.calls[indexPath.row] + } + + // MARK: Call Duration Timer + + private func updateCallDurationTimer() { + let callCount = callManager?.calls.count ?? 0 + + if callCount > 0 && callDurationTimer == nil { + callDurationTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(callDurationTimerFired), userInfo: nil, repeats: true) + } else if callCount == 0 && callDurationTimer != nil { + callDurationTimer?.invalidate() + callDurationTimer = nil + } + } + + func callDurationTimerFired() { + updateCallDurationForVisibleCells() + } + + private func updateCallDurationForVisibleCells() { + /* + Modify all the visible cells directly, since -[UITableView reloadData] resets a lot + of things on the table view like selection & editing states + */ + let visibleCells = tableView.visibleCells as! [CallSummaryTableViewCell] + guard let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows else { return } + + for index in 0.. Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return callManager?.calls.count ?? 0 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "CallSummary") as! CallSummaryTableViewCell + + guard let call = call(at: indexPath) else { + return cell + } + + cell.handleLabel?.text = call.handle + + let accessoryLabelsTextColor = call.isOnHold ? UIColor.gray : cell.tintColor + + if call.hasConnected { + cell.callStatusTextLabel?.text = call.isOnHold ? NSLocalizedString("CALL_STATUS_HELD", comment: "Call status label for on hold") : NSLocalizedString("CALL_STATUS_ACTIVE", comment: "Call status label for active") + } else if call.hasStartedConnecting { + cell.callStatusTextLabel?.text = NSLocalizedString("CALL_STATUS_CONNECTING", comment: "Call status label for on hold") + } else { + cell.callStatusTextLabel?.text = call.isOutgoing ? NSLocalizedString("CALL_STATUS_SENDING", comment: "Call status label for sending") : NSLocalizedString("CALL_STATUS_RINGING", comment: "Call status label for ringing") + } + + cell.callStatusTextLabel?.textColor = accessoryLabelsTextColor + + cell.durationLabel?.text = durationLabelText(forCall: call) + cell.durationLabel?.font = cell.durationLabel?.font.addingMonospacedNumberAttributes + cell.durationLabel?.textColor = accessoryLabelsTextColor + + return cell + } + + private func durationLabelText(forCall call: SpeakerboxCall) -> String? { + return call.hasConnected ? callDurationFormatter.format(timeInterval: call.duration) : nil + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let call = call(at: indexPath) else { + return + } + + call.isOnHold = !call.isOnHold + callManager?.setHeld(call: call, onHold: call.isOnHold) + + tableView.reloadData() + } + + override func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? { + return NSLocalizedString("TABLE_CELL_EDIT_ACTION_END", comment: "End button in call summary table view cell") + } + + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { + if editingStyle == .delete { + if let call = call(at: indexPath) { + print("Requesting to end call: \(call)") + callManager?.end(call: call) + } else { + print("No call found at indexPath: \(indexPath)") + } + } + } + + // MARK: CXCallObserverDelegate + + func handleCallsChangedNotification(notification: NSNotification) { + tableView.reloadData() + updateCallsDependentUI(animated: true) + } + +} diff --git a/Speakerbox/Speakerbox/DialOptionsViewController.swift b/Speakerbox/Speakerbox/DialOptionsViewController.swift new file mode 100644 index 00000000..cc56299b --- /dev/null +++ b/Speakerbox/Speakerbox/DialOptionsViewController.swift @@ -0,0 +1,97 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller which controls dialing an outgoing call +*/ + +import UIKit + +class DialOptionsViewController: UIViewController { + + @IBOutlet private weak var destinationTextField: UITextField! + @IBOutlet private weak var dialButton: UIBarButtonItem! + @IBOutlet private weak var videoSwitch: UISwitch! + @IBOutlet private weak var videoSwitchLabel: UILabel! + + private struct DefaultsKeys { + static let OutgoingCallHandleKey = "OutgoingCallHandleKey" + static let OutgoingCallVideoCallKey = "OutgoingCallVideoCallKey" + } + + var handle: String? { + return destinationTextField.text + } + + var video: Bool { + return videoSwitch.isOn + } + + // MARK: Actions + + @IBAction func cancel(_ cancel: UIBarButtonItem?) { + dismiss(animated: true, completion: nil) + } + + // MARK: Helpers + + private func updateDialButton() { + guard let handle = handle else { + dialButton.isEnabled = false + return + } + + dialButton.isEnabled = !handle.isEmpty + } + + private func restoreValues() { + let defaults = UserDefaults.standard + + destinationTextField.text = defaults.string(forKey: DefaultsKeys.OutgoingCallHandleKey) + videoSwitch.isOn = defaults.bool(forKey: DefaultsKeys.OutgoingCallVideoCallKey) + + updateDialButton() + } + + private func saveValues() { + let defaults = UserDefaults.standard + + defaults.set(destinationTextField.text, forKey: DefaultsKeys.OutgoingCallHandleKey) + defaults.set(videoSwitch.isOn, forKey: DefaultsKeys.OutgoingCallVideoCallKey) + } + + // MARK: Observers + + func textFieldDidChange(textField: UITextField?) { + updateDialButton() + } + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.prompt = NSLocalizedString("DIAL_OPTIONS_NAVIGATION_PROMPT", comment: "Navigation item prompt for Dial options UI") + videoSwitchLabel.text = NSLocalizedString("CALL_VIDEO_SWITCH_LABEL", comment: "Label for simulating outgoing video call switch") + + updateDialButton() + + destinationTextField.addTarget(self, action: #selector(textFieldDidChange), for: [.editingChanged]) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + destinationTextField.becomeFirstResponder() + + restoreValues() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + saveValues() + } + +} diff --git a/Speakerbox/Speakerbox/Info.plist b/Speakerbox/Speakerbox/Info.plist new file mode 100644 index 00000000..f0815d5f --- /dev/null +++ b/Speakerbox/Speakerbox/Info.plist @@ -0,0 +1,76 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + $(PRODUCT_BUNDLE_IDENTIFIER).url-scheme.dial + CFBundleURLSchemes + + speakerbox + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSMicrophoneUsageDescription + $(PRODUCT_NAME) uses the Microphone for call audio + NSUserActivityTypes + + INStartAudioCallIntent + + SBIconVisibilityDefaultVisible + + SBIconVisibilitySetByAppPreference + + UIBackgroundModes + + audio + voip + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Speakerbox/Speakerbox/NSUserActivity+StartCallConvertible.swift b/Speakerbox/Speakerbox/NSUserActivity+StartCallConvertible.swift new file mode 100644 index 00000000..6975e6b7 --- /dev/null +++ b/Speakerbox/Speakerbox/NSUserActivity+StartCallConvertible.swift @@ -0,0 +1,44 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extension to allow creating a CallKit CXStartCallAction from an NSUserActivity which the app was launched with +*/ + +import Foundation +import Intents + +extension NSUserActivity: StartCallConvertible { + + var startCallHandle: String? { + guard + let interaction = interaction, + let startCallIntent = interaction.intent as? SupportedStartCallIntent, + let contact = startCallIntent.contacts?.first + else { + return nil + } + + return contact.personHandle?.value + } + + var video: Bool? { + guard + let interaction = interaction, + let startCallIntent = interaction.intent as? SupportedStartCallIntent + else { + return nil + } + + return startCallIntent is INStartVideoCallIntent + } + +} + +protocol SupportedStartCallIntent { + var contacts: [INPerson]? { get } +} + +extension INStartAudioCallIntent: SupportedStartCallIntent {} +extension INStartVideoCallIntent: SupportedStartCallIntent {} diff --git a/Speakerbox/Speakerbox/ProviderDelegate.swift b/Speakerbox/Speakerbox/ProviderDelegate.swift new file mode 100644 index 00000000..094ba733 --- /dev/null +++ b/Speakerbox/Speakerbox/ProviderDelegate.swift @@ -0,0 +1,212 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + CallKit provider delegate class, which conforms to CXProviderDelegate protocol +*/ + +import Foundation +import UIKit +import CallKit + +final class ProviderDelegate: NSObject, CXProviderDelegate { + + let callManager: SpeakerboxCallManager + private let provider: CXProvider + + init(callManager: SpeakerboxCallManager) { + self.callManager = callManager + provider = CXProvider(configuration: type(of: self).providerConfiguration) + + super.init() + + provider.setDelegate(self, queue: nil) + } + + /// The app's provider configuration, representing its CallKit capabilities + static var providerConfiguration: CXProviderConfiguration { + let localizedName = NSLocalizedString("APPLICATION_NAME", comment: "Name of application") + let providerConfiguration = CXProviderConfiguration(localizedName: localizedName) + + providerConfiguration.supportsVideo = true + + providerConfiguration.maximumCallsPerCallGroup = 1 + + providerConfiguration.supportedHandleTypes = [.phoneNumber] + + if let iconMaskImage = UIImage(named: "IconMask") { + providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(iconMaskImage) + } + + providerConfiguration.ringtoneSound = "Ringtone.caf" + + return providerConfiguration + } + + // MARK: Incoming Calls + + /// Use CXProvider to report the incoming call to the system + func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)? = nil) { + // Construct a CXCallUpdate describing the incoming call, including the caller. + let update = CXCallUpdate() + update.remoteHandle = CXHandle(type: .phoneNumber, value: handle) + update.hasVideo = hasVideo + + // Report the incoming call to the system + provider.reportNewIncomingCall(with: uuid, update: update) { error in + /* + Only add incoming call to the app's list of calls if the call was allowed (i.e. there was no error) + since calls may be "denied" for various legitimate reasons. See CXErrorCodeIncomingCallError. + */ + if error == nil { + let call = SpeakerboxCall(uuid: uuid) + call.handle = handle + + self.callManager.addCall(call) + } + + completion?(error as? NSError) + } + } + + // MARK: CXProviderDelegate + + func providerDidReset(_ provider: CXProvider) { + print("Provider did reset") + + stopAudio() + + /* + End any ongoing calls if the provider resets, and remove them from the app's list of calls, + since they are no longer valid. + */ + for call in callManager.calls { + call.endSpeakerboxCall() + } + + // Remove all calls from the app's list of calls. + callManager.removeAllCalls() + } + + func provider(_ provider: CXProvider, perform action: CXStartCallAction) { + // Create & configure an instance of SpeakerboxCall, the app's model class representing the new outgoing call. + let call = SpeakerboxCall(uuid: action.callUUID, isOutgoing: true) + call.handle = action.handle.value + + /* + Configure the audio session, but do not start call audio here, since it must be done once + the audio session has been activated by the system after having its priority elevated. + */ + configureAudioSession() + + /* + Set callback blocks for significant events in the call's lifecycle, so that the CXProvider may be updated + to reflect the updated state. + */ + call.hasStartedConnectingDidChange = { [weak self] in + self?.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: call.connectingDate) + } + call.hasConnectedDidChange = { [weak self] in + self?.provider.reportOutgoingCall(with: call.uuid, connectedAt: call.connectDate) + } + + // Trigger the call to be started via the underlying network service. + call.startSpeakerboxCall { success in + if success { + // Signal to the system that the action has been successfully performed. + action.fulfill() + + // Add the new outgoing call to the app's list of calls. + self.callManager.addCall(call) + } else { + // Signal to the system that the action was unable to be performed. + action.fail() + } + } + } + + func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { + // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID + guard let call = callManager.callWithUUID(uuid: action.callUUID) else { + action.fail() + return + } + + /* + Configure the audio session, but do not start call audio here, since it must be done once + the audio session has been activated by the system after having its priority elevated. + */ + configureAudioSession() + + // Trigger the call to be answered via the underlying network service. + call.answerSpeakerboxCall() + + // Signal to the system that the action has been successfully performed. + action.fulfill() + } + + func provider(_ provider: CXProvider, perform action: CXEndCallAction) { + // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID + guard let call = callManager.callWithUUID(uuid: action.callUUID) else { + action.fail() + return + } + + // Stop call audio whenever ending the call. + stopAudio() + + // Trigger the call to be ended via the underlying network service. + call.endSpeakerboxCall() + + // Signal to the system that the action has been successfully performed. + action.fulfill() + + // Remove the ended call from the app's list of calls. + callManager.removeCall(call) + } + + func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { + // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID + guard let call = callManager.callWithUUID(uuid: action.callUUID) else { + action.fail() + return + } + + // Update the SpeakerboxCall's underlying hold state. + call.isOnHold = action.isOnHold + + // Stop or start audio in response to holding or unholding the call. + if call.isOnHold { + stopAudio() + } else { + startAudio() + } + + // Signal to the system that the action has been successfully performed. + action.fulfill() + } + + func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { + print("Timed out \(#function)") + + // React to the action timeout if necessary, such as showing an error UI. + } + + func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { + print("Received \(#function)") + + // Start call audio media, now that the audio session has been activated after having its priority boosted. + startAudio() + } + + func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { + print("Received \(#function)") + + /* + Restart any non-call related audio now that the app's audio session has been + de-activated after having its priority restored to normal. + */ + } + +} diff --git a/Speakerbox/Speakerbox/Ringtone.caf b/Speakerbox/Speakerbox/Ringtone.caf new file mode 100644 index 00000000..fcbecb00 Binary files /dev/null and b/Speakerbox/Speakerbox/Ringtone.caf differ diff --git a/Speakerbox/Speakerbox/SimulateIncomingCallViewController.swift b/Speakerbox/Speakerbox/SimulateIncomingCallViewController.swift new file mode 100644 index 00000000..16ca9c24 --- /dev/null +++ b/Speakerbox/Speakerbox/SimulateIncomingCallViewController.swift @@ -0,0 +1,121 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + View controller which controls simulating an call +*/ + +import UIKit + +class SimulateIncomingCallViewController: UIViewController { + + @IBOutlet private weak var destinationTextField: UITextField! + @IBOutlet private weak var doneButton: UIBarButtonItem! + @IBOutlet private weak var videoSwitch: UISwitch! + @IBOutlet private weak var videoSwitchLabel: UILabel! + @IBOutlet private weak var delayStepper: UIStepper! + @IBOutlet private weak var delayStepperLabel: UILabel! + @IBOutlet private weak var delayExplanationLabel: UILabel! + + private let delayLabelTextFormat = NSLocalizedString("CALL_DELAY_STEPPER_LABEL", comment: "Label for simulating delayed incoming call switch") + + private struct DefaultsKeys { + static let IncomingCallDelayInSecondsKey = "IncomingCallDelayInSecondsKey" + static let IncomingCallHandleKey = "IncomingCallHandleKey" + static let IncomingCallVideoCallKey = "IncomingCallVideoCallKey" + } + + var handle: String? { + return destinationTextField.text + } + + var delay: TimeInterval { + return delayStepper.value + } + + var video: Bool { + return videoSwitch.isOn + } + + // MARK: Actions + + @IBAction func cancel(_ cancel: UIBarButtonItem?) { + dismiss(animated: true, completion: nil) + } + + // MARK: Helpers + + private func updateDialButton() { + guard let handle = handle else { + doneButton?.isEnabled = false + return + } + + doneButton?.isEnabled = !handle.isEmpty + } + + private func updateDelayStepperLabelText() { + let delayInSeconds = delayStepper.value + let delayLabelText = String(format: delayLabelTextFormat, delayInSeconds) + + delayStepperLabel.text = delayLabelText + } + + private func restoreValues() { + let defaults = UserDefaults.standard + + destinationTextField.text = defaults.string(forKey: DefaultsKeys.IncomingCallHandleKey) + + delayStepper.value = defaults.double(forKey: DefaultsKeys.IncomingCallDelayInSecondsKey) + updateDelayStepperLabelText() + + videoSwitch.isOn = defaults.bool(forKey: DefaultsKeys.IncomingCallVideoCallKey) + + updateDialButton() + } + + private func saveValues() { + let defaults = UserDefaults.standard + + defaults.set(destinationTextField.text, forKey: DefaultsKeys.IncomingCallHandleKey) + defaults.set(videoSwitch.isOn, forKey: DefaultsKeys.IncomingCallVideoCallKey) + defaults.set(delayStepper.value, forKey: DefaultsKeys.IncomingCallDelayInSecondsKey) + } + + // MARK: Observers + + func textFieldDidChange(textField: UITextField?) { + updateDialButton() + } + + @IBAction func stepperValueChanged(_ sender: AnyObject) { + updateDelayStepperLabelText() + } + + // MARK: UIViewController + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.prompt = NSLocalizedString("SIMULATE_INCOMING_CALL_NAVIGATION_PROMPT", comment: "Navigation item prompt for Incoming call options UI") + videoSwitchLabel.text = NSLocalizedString("CALL_VIDEO_SWITCH_LABEL", comment: "Label for simulating incoming video call switch") + delayExplanationLabel.text = NSLocalizedString("DELAY_EXPLANATION_LABEL", comment: "Label for explaining delay stepper usage") + destinationTextField.addTarget(self, action: #selector(textFieldDidChange), for: [.editingChanged]) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + destinationTextField?.becomeFirstResponder() + + restoreValues() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + saveValues() + } + +} diff --git a/Speakerbox/Speakerbox/Speakerbox-Bridging-Header.h b/Speakerbox/Speakerbox/Speakerbox-Bridging-Header.h new file mode 100644 index 00000000..40d0a099 --- /dev/null +++ b/Speakerbox/Speakerbox/Speakerbox-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "AudioController.h" diff --git a/Speakerbox/Speakerbox/SpeakerboxCall.swift b/Speakerbox/Speakerbox/SpeakerboxCall.swift new file mode 100644 index 00000000..6dd996f6 --- /dev/null +++ b/Speakerbox/Speakerbox/SpeakerboxCall.swift @@ -0,0 +1,128 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Model class representing a single call +*/ + +import Foundation + +final class SpeakerboxCall { + + // MARK: Metadata Properties + + let uuid: UUID + let isOutgoing: Bool + var handle: String? + + // MARK: Call State Properties + + var connectingDate: Date? { + didSet { + stateDidChange?() + hasStartedConnectingDidChange?() + } + } + var connectDate: Date? { + didSet { + stateDidChange?() + hasConnectedDidChange?() + } + } + var endDate: Date? { + didSet { + stateDidChange?() + hasEndedDidChange?() + } + } + var isOnHold = false { + didSet { + stateDidChange?() + } + } + + // MARK: State change callback blocks + + var stateDidChange: (() -> Void)? + var hasStartedConnectingDidChange: (() -> Void)? + var hasConnectedDidChange: (() -> Void)? + var hasEndedDidChange: (() -> Void)? + + // MARK: Derived Properties + + var hasStartedConnecting: Bool { + get { + return connectingDate != nil + } + set { + connectingDate = newValue ? Date() : nil + } + } + var hasConnected: Bool { + get { + return connectDate != nil + } + set { + connectDate = newValue ? Date() : nil + } + } + var hasEnded: Bool { + get { + return endDate != nil + } + set { + endDate = newValue ? Date() : nil + } + } + var duration: TimeInterval { + guard let connectDate = connectDate else { + return 0 + } + + return Date().timeIntervalSince(connectDate) + } + + // MARK: Initialization + + init(uuid: UUID, isOutgoing: Bool = false) { + self.uuid = uuid + self.isOutgoing = isOutgoing + } + + // MARK: Actions + + func startSpeakerboxCall(completion: ((_ success: Bool) -> Void)?) { + // Simulate the call starting successfully + completion?(true) + + /* + Simulate the "started connecting" and "connected" states using artificial delays, since + the example app is not backed by a real network service + */ + DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 3) { + self.hasStartedConnecting = true + + DispatchQueue.main.asyncAfter(wallDeadline: DispatchWallTime.now() + 1.5) { + self.hasConnected = true + } + } + } + + func answerSpeakerboxCall() { + /* + Simulate the answer becoming connected immediately, since + the example app is not backed by a real network service + */ + hasConnected = true + } + + func endSpeakerboxCall() { + /* + Simulate the end taking effect immediately, since + the example app is not backed by a real network service + */ + hasEnded = true + } + +} diff --git a/Speakerbox/Speakerbox/SpeakerboxCallManager.swift b/Speakerbox/Speakerbox/SpeakerboxCallManager.swift new file mode 100644 index 00000000..fbc17ef0 --- /dev/null +++ b/Speakerbox/Speakerbox/SpeakerboxCallManager.swift @@ -0,0 +1,99 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Manager of SpeakerboxCalls, which demonstrates using a CallKit CXCallController to request actions on calls +*/ + +import UIKit +import CallKit + +final class SpeakerboxCallManager: NSObject { + + let callController = CXCallController() + + // MARK: Actions + + func startCall(handle: String, video: Bool = false) { + let handle = CXHandle(type: .phoneNumber, value: handle) + let startCallAction = CXStartCallAction(call: UUID(), handle: handle) + + startCallAction.isVideo = video + + let transaction = CXTransaction() + transaction.addAction(startCallAction) + + requestTransaction(transaction) + } + + func end(call: SpeakerboxCall) { + let endCallAction = CXEndCallAction(call: call.uuid) + let transaction = CXTransaction() + transaction.addAction(endCallAction) + + requestTransaction(transaction) + } + + func setHeld(call: SpeakerboxCall, onHold: Bool) { + let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold) + let transaction = CXTransaction() + transaction.addAction(setHeldCallAction) + + requestTransaction(transaction) + } + + private func requestTransaction(_ transaction: CXTransaction) { + callController.request(transaction) { error in + if let error = error { + print("Error requesting transaction: \(error)") + } else { + print("Requested transaction successfully") + } + } + } + + // MARK: Call Management + + static let CallsChangedNotification = Notification.Name("CallManagerCallsChangedNotification") + + private(set) var calls = [SpeakerboxCall]() + + func callWithUUID(uuid: UUID) -> SpeakerboxCall? { + guard let index = calls.index(where: { $0.uuid == uuid }) else { + return nil + } + return calls[index] + } + + func addCall(_ call: SpeakerboxCall) { + calls.append(call) + + call.stateDidChange = { [weak self] in + self?.postCallsChangedNotification() + } + + postCallsChangedNotification() + } + + func removeCall(_ call: SpeakerboxCall) { + calls.removeFirst(where: { $0 === call }) + postCallsChangedNotification() + } + + func removeAllCalls() { + calls.removeAll() + postCallsChangedNotification() + } + + private func postCallsChangedNotification() { + NotificationCenter.default.post(name: type(of: self).CallsChangedNotification, object: self) + } + + // MARK: SpeakerboxCallDelegate + + func speakerboxCallDidChangeState(_ call: SpeakerboxCall) { + postCallsChangedNotification() + } + +} diff --git a/Speakerbox/Speakerbox/StartCallConvertible.swift b/Speakerbox/Speakerbox/StartCallConvertible.swift new file mode 100644 index 00000000..12e18724 --- /dev/null +++ b/Speakerbox/Speakerbox/StartCallConvertible.swift @@ -0,0 +1,20 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Protocol defining a type from which a call may be started. +*/ + +protocol StartCallConvertible { + var startCallHandle: String? { get } + var video: Bool? { get } +} + +extension StartCallConvertible { + + var video: Bool? { + return nil + } + +} diff --git a/Speakerbox/Speakerbox/UIFont+Speakerbox.swift b/Speakerbox/Speakerbox/UIFont+Speakerbox.swift new file mode 100644 index 00000000..efd12add --- /dev/null +++ b/Speakerbox/Speakerbox/UIFont+Speakerbox.swift @@ -0,0 +1,25 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extension of UIFont for creating monospaced font attributes suitable for displaying increasing call durations +*/ + +import UIKit +import CoreText + +extension UIFont { + + var addingMonospacedNumberAttributes: UIFont { + let attributes = [ + UIFontDescriptorFeatureSettingsAttribute: [[ + UIFontFeatureTypeIdentifierKey: kNumberSpacingType, + UIFontFeatureSelectorIdentifierKey: kMonospacedNumbersSelector + ]] + ] + let fontDescriptorWithAttributes = fontDescriptor.addingAttributes(attributes) + return UIFont(descriptor: fontDescriptorWithAttributes, size: pointSize) + } + +} diff --git a/Speakerbox/Speakerbox/URL+StartCallConvertible.swift b/Speakerbox/Speakerbox/URL+StartCallConvertible.swift new file mode 100644 index 00000000..60b6b1bf --- /dev/null +++ b/Speakerbox/Speakerbox/URL+StartCallConvertible.swift @@ -0,0 +1,25 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Extension to allow creating a CallKit CXStartCallAction from a URL which the app was launched with +*/ + +import Foundation + +extension URL: StartCallConvertible { + + private struct Constants { + static let URLScheme = "speakerbox" + } + + var startCallHandle: String? { + guard scheme == Constants.URLScheme else { + return nil + } + + return host + } + +} diff --git a/SpeedSketch/LICENSE.txt b/SpeedSketch/LICENSE.txt new file mode 100644 index 00000000..f913d616 --- /dev/null +++ b/SpeedSketch/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: SpeedSketch: Leveraging touch input for a drawing application +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) 2016 Apple Inc. All Rights Reserved. diff --git a/SpeedSketch/README.md b/SpeedSketch/README.md new file mode 100644 index 00000000..37998a43 --- /dev/null +++ b/SpeedSketch/README.md @@ -0,0 +1,15 @@ +# SpeedSketch: Leveraging touch input for a drawing application + +This sample demonstrates how to capture touch input on iOS into a series of strokes and rendering them in a fast, efficient and correct way on a Canvas. It shows how to get the highest fidelity and best latency when using Apple Pencil. It also touches on subclassing of UIGestureRecognizer, and on how to coordinate gestures between pencil and touches. + +## Requirements + +### Build + +Xcode 8.0 or later; iOS 10.0 SDK or later + +### Runtime + +iOS 10.0 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/SpeedSketch/SpeedSketch.xcodeproj/project.pbxproj b/SpeedSketch/SpeedSketch.xcodeproj/project.pbxproj new file mode 100644 index 00000000..09e38ec3 --- /dev/null +++ b/SpeedSketch/SpeedSketch.xcodeproj/project.pbxproj @@ -0,0 +1,365 @@ + +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + F224161F1D04337E003EB080 /* RingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F224161E1D04337E003EB080 /* RingView.swift */; }; + F2B07F471CED5DC300B03432 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D0CD921CDD78CC0097775C /* AppDelegate.swift */; }; + F2B07F481CED5DC600B03432 /* CanvasMainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D0CD941CDD78CC0097775C /* CanvasMainViewController.swift */; }; + F2B07F491CED5DC900B03432 /* CGDrawingEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = F280FF8E1CE04D1F00CF8D37 /* CGDrawingEngine.swift */; }; + F2B35B191CFCE5960014550C /* CanvasContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B35B181CFCE5960014550C /* CanvasContainerView.swift */; }; + F2C4FBAF1CEE9E4600F3A052 /* RingControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C4FBAE1CEE9E4600F3A052 /* RingControl.swift */; }; + F2D0CD9A1CDD78CC0097775C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F2D0CD991CDD78CC0097775C /* Assets.xcassets */; }; + F2D0CD9D1CDD78CC0097775C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F2D0CD9B1CDD78CC0097775C /* LaunchScreen.storyboard */; }; + F2D0CDA71CDD82900097775C /* StrokeGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D0CDA61CDD82900097775C /* StrokeGestureRecognizer.swift */; }; + F2D0CDA91CDD9C5F0097775C /* StrokeCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D0CDA81CDD9C5F0097775C /* StrokeCollection.swift */; }; + F2DE48981D022A7200C9E0DC /* CGMathExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2DE48971D022A7200C9E0DC /* CGMathExtensions.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + F214544C1CFB4E23003F77B0 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + F224161E1D04337E003EB080 /* RingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RingView.swift; sourceTree = ""; }; + F280FF8E1CE04D1F00CF8D37 /* CGDrawingEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGDrawingEngine.swift; sourceTree = ""; }; + F2B35B181CFCE5960014550C /* CanvasContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CanvasContainerView.swift; sourceTree = ""; }; + F2C4FBAE1CEE9E4600F3A052 /* RingControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RingControl.swift; sourceTree = ""; }; + F2D0CD8F1CDD78CC0097775C /* SpeedSketch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SpeedSketch.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F2D0CD921CDD78CC0097775C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + F2D0CD941CDD78CC0097775C /* CanvasMainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasMainViewController.swift; sourceTree = ""; }; + F2D0CD991CDD78CC0097775C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F2D0CD9C1CDD78CC0097775C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + F2D0CD9E1CDD78CC0097775C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F2D0CDA61CDD82900097775C /* StrokeGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StrokeGestureRecognizer.swift; sourceTree = ""; }; + F2D0CDA81CDD9C5F0097775C /* StrokeCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StrokeCollection.swift; sourceTree = ""; }; + F2DE48971D022A7200C9E0DC /* CGMathExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMathExtensions.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F2D0CD8C1CDD78CC0097775C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + F25618401CDEC7BD00C01A7C /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + F280FF901CE04D5300CF8D37 /* Drawing Engine */ = { + isa = PBXGroup; + children = ( + F280FF8E1CE04D1F00CF8D37 /* CGDrawingEngine.swift */, + ); + name = "Drawing Engine"; + sourceTree = ""; + }; + F280FF911CE04D7E00CF8D37 /* Stroke Model and Capture */ = { + isa = PBXGroup; + children = ( + F2D0CDA61CDD82900097775C /* StrokeGestureRecognizer.swift */, + F2D0CDA81CDD9C5F0097775C /* StrokeCollection.swift */, + ); + name = "Stroke Model and Capture"; + sourceTree = ""; + }; + F2D0CD861CDD78CC0097775C = { + isa = PBXGroup; + children = ( + F214544C1CFB4E23003F77B0 /* README.md */, + F2D0CD911CDD78CC0097775C /* SpeedSketch */, + F2D0CD901CDD78CC0097775C /* Products */, + F25618401CDEC7BD00C01A7C /* Frameworks */, + ); + sourceTree = ""; + }; + F2D0CD901CDD78CC0097775C /* Products */ = { + isa = PBXGroup; + children = ( + F2D0CD8F1CDD78CC0097775C /* SpeedSketch.app */, + ); + name = Products; + sourceTree = ""; + }; + F2D0CD911CDD78CC0097775C /* SpeedSketch */ = { + isa = PBXGroup; + children = ( + F2D0CD921CDD78CC0097775C /* AppDelegate.swift */, + F2D0CD941CDD78CC0097775C /* CanvasMainViewController.swift */, + F2B35B181CFCE5960014550C /* CanvasContainerView.swift */, + F2E932E51CEF870B00B1238F /* Controls */, + F280FF901CE04D5300CF8D37 /* Drawing Engine */, + F280FF911CE04D7E00CF8D37 /* Stroke Model and Capture */, + F2DE48971D022A7200C9E0DC /* CGMathExtensions.swift */, + F2D0CD991CDD78CC0097775C /* Assets.xcassets */, + F2D0CD9B1CDD78CC0097775C /* LaunchScreen.storyboard */, + F2D0CD9E1CDD78CC0097775C /* Info.plist */, + ); + path = SpeedSketch; + sourceTree = ""; + }; + F2E932E51CEF870B00B1238F /* Controls */ = { + isa = PBXGroup; + children = ( + F2C4FBAE1CEE9E4600F3A052 /* RingControl.swift */, + F224161E1D04337E003EB080 /* RingView.swift */, + ); + name = Controls; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F2D0CD8E1CDD78CC0097775C /* SpeedSketch */ = { + isa = PBXNativeTarget; + buildConfigurationList = F2D0CDA11CDD78CC0097775C /* Build configuration list for PBXNativeTarget "SpeedSketch" */; + buildPhases = ( + F2D0CD8B1CDD78CC0097775C /* Sources */, + F2D0CD8C1CDD78CC0097775C /* Frameworks */, + F2D0CD8D1CDD78CC0097775C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SpeedSketch; + productName = SpeedSketch; + productReference = F2D0CD8F1CDD78CC0097775C /* SpeedSketch.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F2D0CD871CDD78CC0097775C /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = UIKit; + TargetAttributes = { + F2D0CD8E1CDD78CC0097775C = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = F2D0CD8A1CDD78CC0097775C /* Build configuration list for PBXProject "SpeedSketch" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F2D0CD861CDD78CC0097775C; + productRefGroup = F2D0CD901CDD78CC0097775C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F2D0CD8E1CDD78CC0097775C /* SpeedSketch */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F2D0CD8D1CDD78CC0097775C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F2D0CD9D1CDD78CC0097775C /* LaunchScreen.storyboard in Resources */, + F2D0CD9A1CDD78CC0097775C /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F2D0CD8B1CDD78CC0097775C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F224161F1D04337E003EB080 /* RingView.swift in Sources */, + F2B07F471CED5DC300B03432 /* AppDelegate.swift in Sources */, + F2B07F481CED5DC600B03432 /* CanvasMainViewController.swift in Sources */, + F2DE48981D022A7200C9E0DC /* CGMathExtensions.swift in Sources */, + F2B07F491CED5DC900B03432 /* CGDrawingEngine.swift in Sources */, + F2D0CDA91CDD9C5F0097775C /* StrokeCollection.swift in Sources */, + F2C4FBAF1CEE9E4600F3A052 /* RingControl.swift in Sources */, + F2B35B191CFCE5960014550C /* CanvasContainerView.swift in Sources */, + F2D0CDA71CDD82900097775C /* StrokeGestureRecognizer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + F2D0CD9B1CDD78CC0097775C /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F2D0CD9C1CDD78CC0097775C /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + F2D0CD9F1CDD78CC0097775C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = lossless; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F2D0CDA01CDD78CC0097775C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = "respect-asset-catalog"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F2D0CDA21CDD78CC0097775C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = SpeedSketch/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SpeedSketch"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + VALIDATE_PRODUCT = NO; + }; + name = Debug; + }; + F2D0CDA31CDD78CC0097775C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = SpeedSketch/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.SpeedSketch"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + VALIDATE_PRODUCT = NO; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F2D0CD8A1CDD78CC0097775C /* Build configuration list for PBXProject "SpeedSketch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F2D0CD9F1CDD78CC0097775C /* Debug */, + F2D0CDA01CDD78CC0097775C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F2D0CDA11CDD78CC0097775C /* Build configuration list for PBXNativeTarget "SpeedSketch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F2D0CDA21CDD78CC0097775C /* Debug */, + F2D0CDA31CDD78CC0097775C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F2D0CD871CDD78CC0097775C /* Project object */; +} diff --git a/SpeedSketch/SpeedSketch.xcodeproj/xcshareddata/xcschemes/SpeedSketch.xcscheme b/SpeedSketch/SpeedSketch.xcodeproj/xcshareddata/xcschemes/SpeedSketch.xcscheme new file mode 100644 index 00000000..ea6f0aad --- /dev/null +++ b/SpeedSketch/SpeedSketch.xcodeproj/xcshareddata/xcschemes/SpeedSketch.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SpeedSketch/SpeedSketch/AppDelegate.swift b/SpeedSketch/SpeedSketch/AppDelegate.swift new file mode 100644 index 00000000..a07faed4 --- /dev/null +++ b/SpeedSketch/SpeedSketch/AppDelegate.swift @@ -0,0 +1,28 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application delegate. +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = [:]) -> Bool { + + // Minimal basic setup without a storyboard. + let localWindow = UIWindow(frame: UIScreen.main.bounds) + localWindow.rootViewController = CanvasMainViewController() + localWindow.makeKeyAndVisible() + window = localWindow + + return true + } + +} + diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPad.png b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPad.png new file mode 100644 index 00000000..2c7279f6 Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPad.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPad2x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPad2x.png new file mode 100644 index 00000000..96ec096e Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPad2x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPadPro.png b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPadPro.png new file mode 100644 index 00000000..f5f05cee Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPadPro.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPhone@2x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPhone@2x.png new file mode 100644 index 00000000..ebec74ed Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPhone@2x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPhone@3x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPhone@3x.png new file mode 100644 index 00000000..ab709af8 Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/AppIconiPhone@3x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/Contents.json b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..2a0e2008 --- /dev/null +++ b/SpeedSketch/SpeedSketch/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,78 @@ +{ + "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" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "AppIconiPhone@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "AppIconiPhone@3x.png", + "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" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "AppIconiPad.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "AppIconiPad2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "AppIconiPadPro.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Close.imageset/Close-iPad.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Close.imageset/Close-iPad.png new file mode 100644 index 00000000..426a7f7e Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Close.imageset/Close-iPad.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Close.imageset/Close-iPad@2x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Close.imageset/Close-iPad@2x.png new file mode 100644 index 00000000..565205de Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Close.imageset/Close-iPad@2x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Close.imageset/Contents.json b/SpeedSketch/SpeedSketch/Assets.xcassets/Close.imageset/Contents.json new file mode 100644 index 00000000..3622d909 --- /dev/null +++ b/SpeedSketch/SpeedSketch/Assets.xcassets/Close.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "ipad", + "filename" : "Close-iPad.png", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "filename" : "Close-iPad@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Contents.json b/SpeedSketch/SpeedSketch/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/SpeedSketch/SpeedSketch/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPad.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPad.png new file mode 100644 index 00000000..e2de46bc Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPad.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPad@2x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPad@2x.png new file mode 100644 index 00000000..edf03de3 Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPad@2x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPhone.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPhone.png new file mode 100644 index 00000000..0542b646 Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPhone.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPhone@2x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPhone@2x.png new file mode 100644 index 00000000..574aceaf Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPhone@2x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPhone@3x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPhone@3x.png new file mode 100644 index 00000000..ee0791ca Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Calligraphy-iPhone@3x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Contents.json b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Contents.json new file mode 100644 index 00000000..bee881c4 --- /dev/null +++ b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Calligraphy.imageset/Contents.json @@ -0,0 +1,33 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "filename" : "Calligraphy-iPhone.png", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "filename" : "Calligraphy-iPhone@2x.png", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "filename" : "Calligraphy-iPhone@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "filename" : "Calligraphy-iPad.png", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "filename" : "Calligraphy-iPad@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Contents.json b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Contents.json b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Contents.json new file mode 100644 index 00000000..30ef652c --- /dev/null +++ b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Contents.json @@ -0,0 +1,33 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "filename" : "Debug-iPhone.png", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "filename" : "Debug-iPhone@2x.png", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "filename" : "Debug-iPhone@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "filename" : "Debug-iPad.png", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "filename" : "Debug-iPad@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPad.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPad.png new file mode 100644 index 00000000..1c9ae8e0 Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPad.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPad@2x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPad@2x.png new file mode 100644 index 00000000..923057ed Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPad@2x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPhone.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPhone.png new file mode 100644 index 00000000..8677fc0c Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPhone.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPhone@2x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPhone@2x.png new file mode 100644 index 00000000..8fe2616c Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPhone@2x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPhone@3x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPhone@3x.png new file mode 100644 index 00000000..c73b3a5d Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Debug.imageset/Debug-iPhone@3x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Contents.json b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Contents.json new file mode 100644 index 00000000..551f065b --- /dev/null +++ b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Contents.json @@ -0,0 +1,33 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "filename" : "Ink-iPhone.png", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "filename" : "Ink-iPhone@2x.png", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "filename" : "Ink-iPhone@3x.png", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "filename" : "Ink-iPad.png", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "filename" : "Ink-iPad@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPad.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPad.png new file mode 100644 index 00000000..ccc0c3ea Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPad.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPad@2x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPad@2x.png new file mode 100644 index 00000000..ca1d1903 Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPad@2x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPhone.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPhone.png new file mode 100644 index 00000000..5220c574 Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPhone.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPhone@2x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPhone@2x.png new file mode 100644 index 00000000..e1bf0e45 Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPhone@2x.png differ diff --git a/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPhone@3x.png b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPhone@3x.png new file mode 100644 index 00000000..3af90fc8 Binary files /dev/null and b/SpeedSketch/SpeedSketch/Assets.xcassets/Tool Icons/Ink.imageset/Ink-iPhone@3x.png differ diff --git a/SpeedSketch/SpeedSketch/Base.lproj/LaunchScreen.storyboard b/SpeedSketch/SpeedSketch/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..13ea6a1e --- /dev/null +++ b/SpeedSketch/SpeedSketch/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SpeedSketch/SpeedSketch/CGDrawingEngine.swift b/SpeedSketch/SpeedSketch/CGDrawingEngine.swift new file mode 100644 index 00000000..851d3d1d --- /dev/null +++ b/SpeedSketch/SpeedSketch/CGDrawingEngine.swift @@ -0,0 +1,393 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The view that is responsible for the drawing. StrokeCGView can draw a StrokeCollection as .calligraphy, .ink or .debug. +*/ + +import UIKit + + +enum StrokeViewDisplayOptions { + case debug + case calligraphy + case ink +} + + +class StrokeCGView: UIView { + var displayOptions = StrokeViewDisplayOptions.calligraphy { + didSet { + if strokeCollection != nil { + setNeedsDisplay() + } + for view in dirtyRectViews { + view.isHidden = displayOptions != .debug + } + } + } + + var strokeCollection: StrokeCollection? { + didSet { + if oldValue !== strokeCollection { + setNeedsDisplay() + } + if let lastStroke = strokeCollection?.strokes.last { + setNeedsDisplay(for: lastStroke) + } + strokeToDraw = strokeCollection?.activeStroke + } + } + + var strokeToDraw: Stroke? { + didSet { + if oldValue !== strokeToDraw && oldValue != nil { + setNeedsDisplay() + } else { + if let stroke = strokeToDraw { + setNeedsDisplay(for: stroke) + } + } + } + } + + // MARK: Dirty rect calculation and handling. + var dirtyRectViews: [UIView]! + var lastEstimatedSample: (Int, StrokeSample)? + + func dirtyRects(for stroke:Stroke) -> [CGRect] { + var result = [CGRect]() + for range in stroke.updatedRanges() { + var lowerBound = range.lowerBound + if lowerBound > 0 { lowerBound -= 1 } + + if let (index, _) = lastEstimatedSample { + if index < lowerBound { + lowerBound = index + } + } + + let samples = stroke.samples + var upperBound = range.upperBound + if upperBound < samples.count { upperBound += 1 } + let dirtyRect = dirtyRectForSampleStride(stroke.samples[lowerBound.. 0 { + let dirtyRect = dirtyRectForSampleStride(stroke.predictedSamples[0..) -> CGRect { + var first = true + var frame = CGRect.zero + for sample in sampleStride { + let sampleFrame = CGRect(origin: sample.location, size: .zero) + if first { + first = false + frame = sampleFrame + } else { + frame = frame.union(sampleFrame) + } + } + let maxStrokeWidth = CGFloat(20.0) + return frame.insetBy(dx: -1 * maxStrokeWidth, dy: -1 * maxStrokeWidth) + } + + func setNeedsDisplay(for stroke:Stroke) { + for dirtyRect in dirtyRects(for: stroke) { + setNeedsDisplay(dirtyRect) + } + } + + // MARK: Inits + + override init(frame: CGRect) { + super.init(frame: frame) + + layer.drawsAsynchronously = true + + let dirtyRectView = { () -> UIView in + let view = UIView(frame: CGRect(x: -10, y: -10, width: 0, height: 0)) + view.layer.borderColor = UIColor.red.cgColor + view.layer.borderWidth = 0.5 + view.isUserInteractionEnabled = false + view.isHidden = true + self.addSubview(view) + return view + } + dirtyRectViews = [dirtyRectView(), dirtyRectView()] + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + // MARK: Drawing methods. + + + /** + Note: this is not a particularily efficient way to draw a great stroke path + with CoreGraphics. It is just a way to produce an interesting looking result. + For a real world example you would reuse and cache CGPaths and draw longer + paths instead of an aweful lot of tiny ones, etc. You would also respect the + draw rect to cull your draw requests. And you would use bezier paths to + interpolate between the points to get a smooother curve. + */ + func draw(stroke: Stroke, in rect:CGRect, isActive active: Bool) { + let displayOptions = self.displayOptions + + let updateRanges = stroke.updatedRanges() + if displayOptions == .debug { + for (index, dirtyRectView) in dirtyRectViews.enumerated() { + if index < updateRanges.count { + dirtyRectView.alpha = 1.0 + dirtyRectView.frame = dirtyRectForSampleStride(stroke.samples[updateRanges[index]]) + } else { + dirtyRectView.alpha = 0.0 + } + } + } + + lastEstimatedSample = nil + stroke.clearUpdateInfo() + let sampleCount = stroke.samples.count + guard sampleCount > 0 else { return } + guard let context = UIGraphicsGetCurrentContext() else { return } + let strokeColor = UIColor.black + + let lineSettings: (()->()) + let forceEstimatedLineSettings: (()->()) + if displayOptions == .debug { + lineSettings = { + context.setLineWidth(0.5) + context.setStrokeColor(UIColor.white.cgColor) + } + forceEstimatedLineSettings = { + context.setLineWidth(0.5) + context.setStrokeColor(UIColor.blue.cgColor) + } + } else { + lineSettings = { + context.setLineWidth(0.25) + context.setStrokeColor(strokeColor.cgColor) + } + forceEstimatedLineSettings = lineSettings + } + + let azimuthSettings = { + context.setLineWidth(1.5) + context.setStrokeColor(UIColor.orange.cgColor) + } + let altitudeSettings = { + context.setLineWidth(0.5) + context.setStrokeColor(strokeColor.cgColor) + } + var forceMultiplier = CGFloat(2.0) + var forceOffset = CGFloat(0.1) + + let fillColorRegular = UIColor.black.cgColor + let fillColorCoalesced = UIColor.lightGray.cgColor + let fillColorPredicted = UIColor.red.cgColor + + var lockedAzimuthUnitVector: CGVector? + let azimuthLockAltitudeThreshold = CGFloat.pi / 2.0 * 0.80 // locking azimuth at 80% altitude + + lineSettings() + + var forceAccessBlock = {(sample: StrokeSample) -> CGFloat in + sample.forceWithDefault + } + + if displayOptions == .ink { + forceAccessBlock = {(sample: StrokeSample) -> CGFloat in + return sample.perpendicularForce + } + } + + // Make the force influence less pronounced for the calligraphy pen. + if displayOptions == .calligraphy { + let previousGetter = forceAccessBlock + forceAccessBlock = {(sample: StrokeSample) -> CGFloat in + return max(previousGetter(sample), 1.0) + } + // make force value less pronounced + forceMultiplier = 1.0 + forceOffset = 10.0 + } + + let previousGetter = forceAccessBlock + forceAccessBlock = {(sample: StrokeSample) -> CGFloat in + return previousGetter(sample) * forceMultiplier + forceOffset + } + + var heldFromSample: StrokeSample? + var heldFromSampleUnitVector: CGVector? + + func draw(segment: StrokeSegment) { + if let toSample = segment.toSample { + let fromSample: StrokeSample = heldFromSample ?? segment.fromSample + + // Skip line segments that are too short. + if (fromSample.location - toSample.location).quadrance < 0.003 { + if heldFromSample == nil { + heldFromSample = fromSample + heldFromSampleUnitVector = segment.fromSampleUnitNormal + } + return + } + + if toSample.predicted { + if displayOptions == .debug { + context.setFillColor(fillColorPredicted) + } + } else { + if displayOptions == .debug && fromSample.coalesced { + context.setFillColor(fillColorCoalesced) + } else { + context.setFillColor(fillColorRegular) + } + } + + if displayOptions == .calligraphy { + + var fromAzimuthUnitVector = Stroke.calligraphyFallbackAzimuthUnitVector + var toAzimuthUnitVector = Stroke.calligraphyFallbackAzimuthUnitVector + + if fromSample.azimuth != nil { + + if lockedAzimuthUnitVector == nil { + lockedAzimuthUnitVector = fromSample.azimuthUnitVector + } + fromAzimuthUnitVector = fromSample.azimuthUnitVector + toAzimuthUnitVector = toSample.azimuthUnitVector + if fromSample.altitude! > azimuthLockAltitudeThreshold { + fromAzimuthUnitVector = lockedAzimuthUnitVector! + } + if toSample.altitude! > azimuthLockAltitudeThreshold { + toAzimuthUnitVector = lockedAzimuthUnitVector! + } else { + lockedAzimuthUnitVector = toAzimuthUnitVector + } + + } + // Rotate 90 degrees + let calligraphyTransform = CGAffineTransform(rotationAngle: CGFloat.pi / 2.0) + fromAzimuthUnitVector = fromAzimuthUnitVector.apply(transform: calligraphyTransform) + toAzimuthUnitVector = toAzimuthUnitVector.apply(transform: calligraphyTransform) + + let fromUnitVector = fromAzimuthUnitVector * forceAccessBlock(fromSample) + let toUnitVector = toAzimuthUnitVector * forceAccessBlock(toSample) + + context.beginPath() + context.move(to: fromSample.location + fromUnitVector) + context.addLine(to: toSample.location + toUnitVector) + context.addLine(to: toSample.location - toUnitVector) + context.addLine(to: fromSample.location - fromUnitVector) + context.closePath() + + context.drawPath(using: .fillStroke) + + } else { + + let fromUnitVector = (heldFromSampleUnitVector != nil ? heldFromSampleUnitVector! : segment.fromSampleUnitNormal) * forceAccessBlock(fromSample) + let toUnitVector = segment.toSampleUnitNormal * forceAccessBlock(toSample) + + let isForceEstimated = fromSample.estimatedProperties.contains(.force) || toSample.estimatedProperties.contains(.force) + if isForceEstimated { + if lastEstimatedSample == nil { + lastEstimatedSample = (segment.fromSampleIndex+1,toSample) + } + forceEstimatedLineSettings() + } else { + lineSettings() + } + + context.beginPath() + context.move(to: fromSample.location + fromUnitVector) + context.addLine(to: toSample.location + toUnitVector) + context.addLine(to: toSample.location - toUnitVector) + context.addLine(to: fromSample.location - fromUnitVector) + context.closePath() + context.drawPath(using: .fillStroke) + } + + let isEstimated = fromSample.estimatedProperties.contains(.azimuth) + if fromSample.azimuth != nil && (!fromSample.coalesced || isEstimated) && !fromSample.predicted && displayOptions == .debug { + + let length = CGFloat(20.0) + let azimuthUnitVector = fromSample.azimuthUnitVector + let azimuthTarget = fromSample.location + azimuthUnitVector * length + let altitudeStart = azimuthTarget + (azimuthUnitVector * (length / -2.0)) + let altitudeTarget = altitudeStart + (azimuthUnitVector * (length / 2.0)).apply(transform: CGAffineTransform(rotationAngle: fromSample.altitude!)) + + // Draw altitude as black line coming from the center of the azimuth. + altitudeSettings() + context.beginPath() + context.move(to: altitudeStart) + context.addLine(to: altitudeTarget) + context.strokePath() + + // Draw azimuth as orange (or blue if estimated) line. + azimuthSettings() + if isEstimated { + context.setStrokeColor(UIColor.blue.cgColor) + } + context.beginPath() + context.move(to: fromSample.location) + context.addLine(to: azimuthTarget) + context.strokePath() + + } + + if heldFromSample != nil { + heldFromSample = nil + heldFromSampleUnitVector = nil + } + } + } + + if stroke.samples.count == 1 { + // Construct a face segment to draw for a stroke that is only one point. + let sample = stroke.samples.first! + let tempSampleFrom = StrokeSample(timestamp: sample.timestamp, location: sample.location + CGVector(dx: -0.5, dy: 0.0), coalesced: false, predicted: false, force: sample.force, azimuth: sample.azimuth, altitude: sample.altitude, estimatedProperties: sample.estimatedProperties, estimatedPropertiesExpectingUpdates: []) + let tempSampleTo = StrokeSample(timestamp: sample.timestamp, location: sample.location + CGVector(dx: 0.5, dy: 0.0), coalesced: false, predicted: false, force: sample.force, azimuth: sample.azimuth, altitude: sample.altitude, estimatedProperties: sample.estimatedProperties, estimatedPropertiesExpectingUpdates: []) + let segment = StrokeSegment(sample: tempSampleFrom) + segment.advanceWithSample(incomingSample: tempSampleTo) + segment.advanceWithSample(incomingSample: nil) + + draw(segment: segment) + } else { + for segment in stroke { + draw(segment:segment) + } + } + + } + + override func draw(_ rect: CGRect) { + UIColor.white.set() + UIRectFill(rect) + + // Optimization opportunity: Draw the existing collection in a different view, + // and only draw each time we add a stroke. + if let strokeCollection = strokeCollection { + for stroke in strokeCollection.strokes { + draw(stroke: stroke, in: rect, isActive: false) + } + } + + if let stroke = strokeToDraw { + draw(stroke: stroke, in: rect, isActive: true) + } + } + +} diff --git a/SpeedSketch/SpeedSketch/CGMathExtensions.swift b/SpeedSketch/SpeedSketch/CGMathExtensions.swift new file mode 100644 index 00000000..b8850746 --- /dev/null +++ b/SpeedSketch/SpeedSketch/CGMathExtensions.swift @@ -0,0 +1,112 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Math extensions to Core Graphics structs. +*/ + +import Foundation +import CoreGraphics + + +// MARK: CGRect and Size +extension CGRect { + var center: CGPoint { + get { + return origin + CGVector(dx: width, dy: height) / 2.0 + } + set { + origin = center - CGVector(dx: width, dy: height) / 2 + } + } +} + +func +(left: CGSize, right: CGFloat) -> CGSize { + return CGSize(width: left.width + right, height: left.height + right) +} + +func -(left: CGSize, right: CGFloat) -> CGSize { + return left + (-1.0 * right) +} + + +// MARK: CGPoint and CGVector math +func -(left: CGPoint, right:CGPoint) -> CGVector { + return CGVector(dx: left.x - right.x, dy: left.y - right.y) +} + +func /(left: CGVector, right:CGFloat) -> CGVector { + return CGVector(dx: left.dx / right, dy: left.dy / right) +} + +func *(left: CGVector, right:CGFloat) -> CGVector { + return CGVector(dx: left.dx * right, dy: left.dy * right) +} + +func +(left: CGPoint, right: CGVector) -> CGPoint { + return CGPoint(x: left.x + right.dx, y: left.y + right.dy) +} + +func +(left: CGVector, right: CGVector) -> CGVector { + return CGVector(dx: left.dx + right.dx, dy: left.dy + right.dy) +} + +func +(left: CGVector?, right: CGVector?) -> CGVector? { + if let left = left, let right = right { + return CGVector(dx: left.dx + right.dx, dy: left.dy + right.dy) + } else { + return nil + } +} + + +func -(left: CGPoint, right: CGVector) -> CGPoint { + return CGPoint(x: left.x - right.dx, y: left.y - right.dy) +} + +extension CGPoint { + init(_ vector: CGVector) { + x = vector.dx + y = vector.dy + } +} + +extension CGVector { + init(_ point: CGPoint) { + dx = point.x + dy = point.y + } + + func apply(transform:CGAffineTransform) -> CGVector { + return CGVector(CGPoint(self).applying(transform)) + } + + func round(toScale scale: CGFloat) -> CGVector { + return CGVector(dx: CoreGraphics.round(dx * scale) / scale, + dy: CoreGraphics.round(dy * scale) / scale) + } + + var quadrance: CGFloat { + return dx * dx + dy * dy; + } + + var normal: CGVector? { + if !(dx.isZero && dy.isZero) { + return CGVector(dx: -dy, dy: dx) + } else { + return nil + } + } + + /// CGVector pointing in the same direction as self, with a length of 1.0 - or nil if the length is zero. + var normalize: CGVector? { + let quadrance = self.quadrance + if quadrance > 0.0 { + return self / sqrt(quadrance) + } else { + return nil + } + } +} + diff --git a/SpeedSketch/SpeedSketch/CanvasContainerView.swift b/SpeedSketch/SpeedSketch/CanvasContainerView.swift new file mode 100644 index 00000000..6d6f109c --- /dev/null +++ b/SpeedSketch/SpeedSketch/CanvasContainerView.swift @@ -0,0 +1,59 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The content of the scroll view. Adds some margin and a shadow. Setting the documentView places this view, and sizes it to the canvasSize. +*/ + +import UIKit + +class CanvasContainerView : UIView { + let canvasSize: CGSize + + let canvasView: UIView + + var documentView: UIView? { + willSet { + if let previousView = documentView { + previousView.removeFromSuperview() + } + } + didSet { + if let newView = documentView { + newView.frame = canvasView.bounds + canvasView.addSubview(newView) + } + } + } + + required init(canvasSize: CGSize) { + let screenBounds = UIScreen.main.bounds + let minDimension = max(screenBounds.width, screenBounds.height) + self.canvasSize = canvasSize + let baseInset = CGFloat(44.0) + var size = canvasSize + (baseInset * 2) + size.width = max(minDimension, size.width) + size.height = max(minDimension, size.height) + + let frame = CGRect(origin: .zero, size: size) + + let canvasOrigin = CGPoint(x: (frame.width - canvasSize.width) / 2.0, y: (frame.height - canvasSize.height) / 2.0) + let canvasFrame = CGRect(origin: canvasOrigin, size: canvasSize) + canvasView = UIView(frame:canvasFrame) + canvasView.backgroundColor = UIColor.white + canvasView.layer.shadowOffset = CGSize(width: 0.0, height: 3.0) + canvasView.layer.shadowRadius = 4.0 + canvasView.layer.shadowColor = UIColor.darkGray.cgColor + canvasView.layer.shadowOpacity = 1.0 + + super.init(frame:frame) + self.backgroundColor = UIColor.lightGray + self.addSubview(canvasView) + } + + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/SpeedSketch/SpeedSketch/CanvasMainViewController.swift b/SpeedSketch/SpeedSketch/CanvasMainViewController.swift new file mode 100644 index 00000000..8509fa27 --- /dev/null +++ b/SpeedSketch/SpeedSketch/CanvasMainViewController.swift @@ -0,0 +1,335 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The primary view controller. +*/ + +import UIKit + +class CanvasMainViewController: UIViewController, UIGestureRecognizerDelegate { + + var cgView: StrokeCGView! + var leftRingControl: RingControl! + + var fingerStrokeRecognizer: StrokeGestureRecognizer! + var pencilStrokeRecognizer: StrokeGestureRecognizer! + + var clearButton: UIButton! + var pencilButton: UIButton! + + var configurations = [() -> ()]() + + var strokeCollection = StrokeCollection() + var scrollView: UIScrollView! + var canvasContainerView: CanvasContainerView! + + + override func viewDidLoad() { + super.viewDidLoad() + let bounds = view.bounds + let screenBounds = UIScreen.main.bounds + let maxScreenDimension = max(screenBounds.width, screenBounds.height) + + let flexibleDimensions: UIViewAutoresizing = [.flexibleWidth, .flexibleHeight] + + let scrollView = UIScrollView(frame: bounds) + scrollView.autoresizingMask = flexibleDimensions + view.addSubview(scrollView) + self.scrollView = scrollView + + let cgView = StrokeCGView(frame: CGRect(origin: .zero, size: CGSize(width: maxScreenDimension, height:maxScreenDimension))) + cgView.autoresizingMask = flexibleDimensions + self.cgView = cgView + + + view.backgroundColor = UIColor.white + + let canvasContainerView = CanvasContainerView(canvasSize: cgView.frame.size) + canvasContainerView.documentView = cgView + self.canvasContainerView = canvasContainerView + scrollView.contentSize = canvasContainerView.frame.size + scrollView.contentOffset = CGPoint(x: (canvasContainerView.frame.width - scrollView.bounds.width) / 2.0, + y: (canvasContainerView.frame.height - scrollView.bounds.height) / 2.0) + scrollView.addSubview(canvasContainerView) + scrollView.backgroundColor = canvasContainerView.backgroundColor + scrollView.maximumZoomScale = 3.0 + scrollView.minimumZoomScale = 0.5 + scrollView.panGestureRecognizer.allowedTouchTypes = [UITouchType.direct.rawValue as NSNumber] + scrollView.pinchGestureRecognizer?.allowedTouchTypes = [UITouchType.direct.rawValue as NSNumber] + scrollView.delegate = self + // We put our UI elements on top of the scroll view, so we don't want any of the + // delay or cancel machinery in place. + scrollView.delaysContentTouches = false + + let fingerStrokeRecognizer = StrokeGestureRecognizer(target: self, action: #selector(strokeUpdated(_:))) + fingerStrokeRecognizer.delegate = self + fingerStrokeRecognizer.cancelsTouchesInView = false + scrollView.addGestureRecognizer(fingerStrokeRecognizer) + fingerStrokeRecognizer.coordinateSpaceView = cgView + fingerStrokeRecognizer.isForPencil = false + self.fingerStrokeRecognizer = fingerStrokeRecognizer + + let pencilStrokeRecognizer = StrokeGestureRecognizer(target: self, action: #selector(strokeUpdated(_:))) + pencilStrokeRecognizer.delegate = self + pencilStrokeRecognizer.cancelsTouchesInView = false + scrollView.addGestureRecognizer(pencilStrokeRecognizer) + pencilStrokeRecognizer.coordinateSpaceView = cgView + pencilStrokeRecognizer.isForPencil = true + self.pencilStrokeRecognizer = pencilStrokeRecognizer + + + + setupConfigurations() + + let onPhone = UIDevice.current.userInterfaceIdiom == .phone + + let ringDiameter = CGFloat(onPhone ? 66.0 : 74.0) + let ringImageInset = CGFloat(onPhone ? 12.0 : 14.0) + let borderWidth = CGFloat(1.0) + let ringOutset = ringDiameter / 2.0 - (floor(sqrt((ringDiameter * ringDiameter) / 8.0) - borderWidth)) + let ringFrame = CGRect(x: -ringOutset, y: self.view.bounds.height - ringDiameter + ringOutset, width: ringDiameter, height: ringDiameter) + let ringControl = RingControl(frame:ringFrame, itemCount:configurations.count) + ringControl.autoresizingMask = [.flexibleRightMargin, .flexibleTopMargin] + self.view.addSubview(ringControl) + leftRingControl = ringControl + let imageNames = ["Calligraphy", "Ink", "Debug"] + for (index, ringView) in leftRingControl.ringViews.enumerated() { + ringView.actionClosure = configurations[index] + let imageView = UIImageView(frame: ringView.bounds.insetBy(dx: ringImageInset, dy: ringImageInset)) + imageView.image = UIImage(named: imageNames[index]) + imageView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin] + ringView.addSubview(imageView) + } + + clearButton = addButton(title: "clear", action: #selector(clearButtonAction(_:)) ) + + setupPencilUI() + } + +// MARK: View setup helpers. + var buttons = [UIButton]() + func addButton(title: String, action: Selector) -> UIButton { + let bounds = view.bounds + let button = UIButton(type: .custom) + let maxX: CGFloat + if let lastButton = buttons.last { + maxX = lastButton.frame.minX + } else { + maxX = bounds.maxX + } + button.setTitleColor(UIColor.orange, for: []) + button.setTitleColor(UIColor.lightGray, for: .highlighted) + button.setTitle(title, for: []) + button.sizeToFit() + button.frame = button.frame.insetBy(dx: -20.0, dy: -4.0) + button.frame.origin = CGPoint(x: maxX - button.frame.width - 5.0, y: bounds.minY - 5.0) + button.autoresizingMask = [.flexibleLeftMargin, .flexibleBottomMargin] + button.addTarget(self, action: action, for: .touchUpInside) + let buttonLayer = button.layer + buttonLayer.cornerRadius = 5.0 + button.backgroundColor = UIColor(white: 1.0, alpha: 0.4) + view.addSubview(button) + buttons.append(button) + return button + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + scrollView.flashScrollIndicators() + } + + override var prefersStatusBarHidden: Bool { + return true + } + + func setupConfigurations() { + configurations = [ + { self.cgView.displayOptions = .calligraphy }, + { self.cgView.displayOptions = .ink }, + { self.cgView.displayOptions = .debug }, + ] + configurations.first?() + } + + func toggleConfiguration(_ sender: UIButton) { + if let index = Int(sender.titleLabel!.text!) { + let nextIndex = (index + 1) % configurations.count + configurations[nextIndex]() + sender.setTitle(String(nextIndex), for: []) + } + } + + func receivedAllUpdatesForStroke(_ stroke: Stroke) { + cgView.setNeedsDisplay(for: stroke) + stroke.clearUpdateInfo() + } + + func clearButtonAction(_ sender: AnyObject) { + self.strokeCollection = StrokeCollection() + cgView.strokeCollection = self.strokeCollection + } + + func strokeUpdated(_ strokeGesture: StrokeGestureRecognizer) { + + if strokeGesture === pencilStrokeRecognizer { + lastSeenPencilInteraction = Date.timeIntervalSinceReferenceDate + } + + var stroke: Stroke? + if strokeGesture.state != .cancelled { + stroke = strokeGesture.stroke + if strokeGesture.state == .began || + (strokeGesture.state == .ended && strokeCollection.activeStroke == nil) { + strokeCollection.activeStroke = stroke + leftRingControl.cancelInteraction() + } + } else { + strokeCollection.activeStroke = nil + } + + if let stroke = stroke { + if strokeGesture.state == .ended { + if strokeGesture === pencilStrokeRecognizer { + // Make sure we get the final stroke update if needed. + stroke.receivedAllNeededUpdatesBlock = { [weak self] in + self?.receivedAllUpdatesForStroke(stroke) + } + } + strokeCollection.takeActiveStroke() + } + } + + cgView.strokeCollection = strokeCollection + } + + + // MARK: Pencil Recognition and UI Adjustments + /* + Since usage of the Apple Pencil can be very temporary, the best way to + actually check for it being in use is to remember the last interaction. + Also make sure to provide an escape hatch if you modify your UI for + times when the pencil is in use vs. not. + */ + + // Timeout the pencil mode if no pencil has been seen for 5 minutes and the app is brought back in foreground. + let pencilResetInterval = TimeInterval(60.0 * 5) + + var lastSeenPencilInteraction: TimeInterval? { + didSet { + if lastSeenPencilInteraction != nil && !pencilMode { + pencilMode = true + } + } + } + + private func setupPencilUI() { + pencilButton = addButton(title: "pencil", action: #selector(stopPencilButtonAction(_:)) ) + pencilButton.titleLabel?.textAlignment = NSTextAlignment.left + let imageView = UIImageView(image: UIImage.init(named: "Close")) + let bounds = pencilButton.bounds + let dimension = bounds.height - 16.0 + pencilButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: dimension, bottom: 0, right: 0) + imageView.frame = CGRect(x: bounds.minX + 3.0, y: bounds.minY + (bounds.height - dimension) - 7.0, + width: dimension, height: dimension) + imageView.alpha = 0.7 + pencilButton.addSubview(imageView) + self.pencilMode = false + + notificationObservers.append( + NotificationCenter.default.addObserver(forName: .UIApplicationWillEnterForeground, object: UIApplication.shared, queue: nil) + { [unowned self](_) in + if self.pencilMode && + (self.lastSeenPencilInteraction == nil || + Date.timeIntervalSinceReferenceDate - self.lastSeenPencilInteraction! > self.pencilResetInterval) { + self.stopPencilButtonAction(nil) + } + } + ) + } + + var notificationObservers = [NSObjectProtocol]() + + deinit { + let defaultCenter = NotificationCenter.default + for closure in notificationObservers { + defaultCenter.removeObserver(closure) + } + } + + var pencilMode = false { + didSet { + if pencilMode { + scrollView.panGestureRecognizer.minimumNumberOfTouches = 1 + pencilButton.isHidden = false + if let view = fingerStrokeRecognizer.view { + view.removeGestureRecognizer(fingerStrokeRecognizer) + } + } else { + scrollView.panGestureRecognizer.minimumNumberOfTouches = 2 + pencilButton.isHidden = true + if fingerStrokeRecognizer.view == nil { + scrollView.addGestureRecognizer(fingerStrokeRecognizer) + } + } + } + } + + func stopPencilButtonAction(_ sender: AnyObject?) { + lastSeenPencilInteraction = nil + pencilMode = false + } + + // Since our gesture recognizer is beginning immediately, we do the hit test ambiguation here + // instead of adding failure requirements to the gesture for minimizing the delay + // to the first action sent and therefore the first lines drawn. + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + + if leftRingControl.hitTest(touch.location(in:leftRingControl), with: nil) != nil { + return false + } + + for button in buttons { + if button.hitTest(touch.location(in:clearButton), with: nil) != nil { + return false + } + } + + return true + } + + // We want the pencil to recognize simultaniously with all others. + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if gestureRecognizer === pencilStrokeRecognizer { + return otherGestureRecognizer !== fingerStrokeRecognizer + } + + return false + } + + +} + +extension CanvasMainViewController: UIScrollViewDelegate { + + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + return self.canvasContainerView + } + + func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { + var desiredScale = self.traitCollection.displayScale + let existingScale = cgView.contentScaleFactor + + if scale >= 2.0 { + desiredScale *= 2.0 + } + + if abs(desiredScale - existingScale) > 0.00001 { + cgView.contentScaleFactor = desiredScale + cgView.setNeedsDisplay() + } + } +} + + diff --git a/SpeedSketch/SpeedSketch/Info.plist b/SpeedSketch/SpeedSketch/Info.plist new file mode 100644 index 00000000..984f44e9 --- /dev/null +++ b/SpeedSketch/SpeedSketch/Info.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/SpeedSketch/SpeedSketch/RingControl.swift b/SpeedSketch/SpeedSketch/RingControl.swift new file mode 100644 index 00000000..26079099 --- /dev/null +++ b/SpeedSketch/SpeedSketch/RingControl.swift @@ -0,0 +1,195 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + A custom control using Gesture Recognizers and hit testing beyond its initial bounds. +*/ +import UIKit + +class RingControl: UIView { + var selectedView: RingView! + var tapRecognizer: UITapGestureRecognizer! + var ringViews = [RingView]() + + var ringRadius: CGFloat { + return bounds.width/2.0 + } + + init(frame: CGRect, itemCount: Int) { + super.init(frame:frame) + setupRings(itemCount: itemCount) + } + + func setupRings(itemCount: Int) { + // Define some nice colors. + let borderColorSelected = UIColor(hue:0.07, saturation:0.81, brightness:0.98, alpha:1.00).cgColor + let borderColorNormal = UIColor.darkGray.cgColor + let fillColorSelected = UIColor(hue:0.07, saturation:0.21, brightness:0.98, alpha:1.00) + let fillColorNormal = UIColor.white + + // We define generators to return closures which we use to define + // the different states of our item ring views. Since we add those + // to the view, they need to capture the view unowned to avoid a + // retain cycle. + let selectedGenerator = { (view: RingView) -> (()->()) in + return { [unowned view] in + view.layer.borderColor = borderColorSelected + view.backgroundColor = fillColorSelected + } + } + + let normalGenerator = { (view: RingView) -> (()->()) in + return { [unowned view] in + view.layer.borderColor = borderColorNormal + view.backgroundColor = fillColorNormal + } + } + + let startPosition = bounds.center + let locationNormalGenerator = { (view: RingView) -> (()->()) in + return { [unowned view] in + view.center = startPosition + if !view.selected { + view.alpha = 0.0 + } + } + } + + let locationFanGenerator = { (view: RingView, offset: CGVector) -> (()->()) in + return { [unowned view] in + view.center = startPosition + offset + view.alpha = 1.0 + } + } + + + // tau is a full circle in radians + let tau = CGFloat.pi * 2 + let absoluteRingSegment = tau / 4.0 + let requiredLengthPerRing = ringRadius * 2 + 5.0 + let totalRequiredCirlceSegment = requiredLengthPerRing * CGFloat(itemCount - 1) + let fannedControlRadius = max(requiredLengthPerRing, totalRequiredCirlceSegment / absoluteRingSegment) + let normalDistance = CGVector(dx: 0, dy: -1 * fannedControlRadius) + + let scale = UIScreen.main.scale + + // Setup our item views. + for index in 0..()]() + for view in ringViews { + if let state = view.selectionState { + stateTransitions.append(state) + } + if let state = view.locationState { + stateTransitions.append(state) + } + } + + let transition = { + for transition in stateTransitions { + transition() + } + } + + if animated { + UIView.animate(withDuration: 0.25, animations: transition) + } else { + transition() + } + } + + // MARK: Hit testing + + // Hit test on our ring views regardless of our own bounds. + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for view in self.subviews.reversed() { + let localPoint = view.convert(point, from: self) + if view.point(inside: localPoint, with: event) { + return view + } + } + // Don't hit-test ourself. + return nil + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + for view in self.subviews.reversed() { + if view.point(inside: view.convert(point, from: self), with: event) { + return true + } + } + return super.point(inside: point, with: event) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + + diff --git a/SpeedSketch/SpeedSketch/RingView.swift b/SpeedSketch/SpeedSketch/RingView.swift new file mode 100644 index 00000000..a997a59e --- /dev/null +++ b/SpeedSketch/SpeedSketch/RingView.swift @@ -0,0 +1,75 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The custom views used in the ring control. +*/ + +import UIKit + +enum RingControlState { + case selected + case normal + case locationFan + case locationOrigin +} + +class RingView: UIView { + /// Closures that configure the view for the corresponding state. + var stateClosures = [RingControlState : (()->())]() + var selected = false + var fannedOut = false + + /// The actionClosure will be executed on selection. + var actionClosure: (() -> ())? + + + var selectionState: (()->())? { + if selected { + return stateClosures[.selected] + } else { + return stateClosures[.normal] + } + } + var locationState: (()->())? { + if selected { + if fannedOut { + return stateClosures[.locationFan] + } else { + return stateClosures[.locationOrigin] + } + } else { + let fanState = stateClosures[.locationFan] + let transform = fannedOut ? CGAffineTransform.identity : CGAffineTransform.init(scaleX: 0.01, y: 0.01) + let alpha: CGFloat = fannedOut ? 1.0 : 0.0 + return { [unowned self]()->() in + fanState?() + self.transform = transform + self.alpha = alpha + } + } + } + + override init(frame: CGRect) { + super.init(frame:frame) + + let layer = self.layer + layer.cornerRadius = frame.width / 2.0 + layer.borderColor = UIColor.black.cgColor + layer.borderWidth = 2.0 + } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + // Quadrance as the square of the length requires less computation and cases + let quadrance = (bounds.center - point).quadrance + let maxQuadrance = pow(bounds.width/2.0, 2.0) + return quadrance < maxQuadrance + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + diff --git a/SpeedSketch/SpeedSketch/StrokeCollection.swift b/SpeedSketch/SpeedSketch/StrokeCollection.swift new file mode 100644 index 00000000..52fc2ccb --- /dev/null +++ b/SpeedSketch/SpeedSketch/StrokeCollection.swift @@ -0,0 +1,286 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The Stroke data model and math extensions for CG primitives for easier math +*/ + +import Foundation +import UIKit + + +class StrokeCollection { + var strokes: [Stroke] = [] + var activeStroke: Stroke? + + func takeActiveStroke() { + if let stroke = activeStroke { + strokes.append(stroke) + activeStroke = nil + } + } +} + +enum StrokePhase { + case began + case changed + case ended + case cancelled +} + +struct StrokeSample { + // Always. + let timestamp: TimeInterval + let location: CGPoint + + // 3D Touch or Pencil. + var force: CGFloat? + + // Pencil only. + var estimatedProperties: UITouchProperties = [] + var estimatedPropertiesExpectingUpdates: UITouchProperties = [] + var altitude: CGFloat? + var azimuth: CGFloat? + + var azimuthUnitVector: CGVector { + return CGVector(dx: 1.0, dy: 0.0).apply(transform: CGAffineTransform(rotationAngle: azimuth!)) + } + + init(timestamp: TimeInterval, location: CGPoint, + coalesced: Bool, predicted: Bool = false, + force: CGFloat? = nil, + azimuth: CGFloat? = nil, altitude: CGFloat? = nil, estimatedProperties: UITouchProperties = [], estimatedPropertiesExpectingUpdates: UITouchProperties = []) { + self.timestamp = timestamp + self.location = location + self.force = force + self.coalesced = coalesced + self.predicted = predicted + self.altitude = altitude + self.azimuth = azimuth + } + + /// Convenience accessor returns a non-optional (Default: 1.0) + var forceWithDefault: CGFloat { + return force ?? 1.0 + } + + /// Returns the force perpendicular to the screen. The regular stylus force is along the pencil axis. + var perpendicularForce: CGFloat { + let force = forceWithDefault + if let altitude = altitude { + let result = force / CGFloat(sin(Double(altitude))) + return result + } else { + return force + } + } + + // Values for debug display. + let coalesced: Bool + let predicted: Bool +} + +enum StrokeState { + case active + case done + case cancelled +} + +class Stroke { + static let calligraphyFallbackAzimuthUnitVector = CGVector(dx: 1.0, dy:1.0).normalize! + + var samples: [StrokeSample] = [] + var predictedSamples: [StrokeSample] = [] + var previousPredictedSamples: [StrokeSample]? + var state: StrokeState = .active + var sampleIndicesExpectingUpdates = Set() + var expectsAltitudeAzimuthBackfill = false + var hasUpdatesFromStartTo: Int? + var hasUpdatesAtEndFrom: Int? + + var receivedAllNeededUpdatesBlock: (() -> ())? + + func add(sample: StrokeSample) -> Int { + let resultIndex = samples.count + if hasUpdatesAtEndFrom == nil { + hasUpdatesAtEndFrom = resultIndex + } + samples.append(sample) + if previousPredictedSamples == nil { + previousPredictedSamples = predictedSamples + } + if sample.estimatedPropertiesExpectingUpdates != [] { + sampleIndicesExpectingUpdates.insert(resultIndex) + } + predictedSamples.removeAll() + return resultIndex + } + + func update(sample: StrokeSample, at index:Int) { + if index == 0 { + hasUpdatesFromStartTo = 0 + } else if hasUpdatesFromStartTo != nil && index == hasUpdatesFromStartTo! + 1 { + hasUpdatesFromStartTo = index + } else if hasUpdatesAtEndFrom == nil || hasUpdatesAtEndFrom! > index { + hasUpdatesAtEndFrom = index + } + samples[index] = sample + sampleIndicesExpectingUpdates.remove(index) + + if sampleIndicesExpectingUpdates.isEmpty { + if let block = receivedAllNeededUpdatesBlock { + receivedAllNeededUpdatesBlock = nil + block() + } + } + } + + func addPredicted(sample: StrokeSample) { + predictedSamples.append(sample) + } + + func clearUpdateInfo() { + hasUpdatesFromStartTo = nil + hasUpdatesAtEndFrom = nil + previousPredictedSamples = nil + } + + func updatedRanges() -> [CountableClosedRange] { + guard hasUpdatesFromStartTo != nil || hasUpdatesAtEndFrom != nil else { return [] } + if hasUpdatesFromStartTo == nil { + return [(hasUpdatesAtEndFrom!)...(samples.count - 1)] + } else if hasUpdatesAtEndFrom == nil { + return [0...(hasUpdatesFromStartTo!)] + } else { + return [0...(hasUpdatesFromStartTo!), hasUpdatesAtEndFrom!...(samples.count - 1)] + } + } + +} + +extension Stroke : Sequence { + func makeIterator() -> StrokeSegmentIterator { + return StrokeSegmentIterator(stroke: self) + } +} + +private func interpolatedNormalUnitVector(between vector1: CGVector, and vector2: CGVector) -> CGVector { + if let result = (vector1.normal + vector2.normal)?.normalize { + return result + } else { + // This means they resulted in a 0,0 vector, + // in this case one of the incoming vectors is a good result. + if let result = vector1.normalize { + return result + } else if let result = vector2.normalize { + return result + } else { + // This case should not happen. + return CGVector(dx:1.0, dy:0.0) + } + } +} + +class StrokeSegment { + var sampleBefore: StrokeSample? + var fromSample: StrokeSample! + var toSample: StrokeSample! + var sampleAfter: StrokeSample? + var fromSampleIndex: Int + + + var segmentUnitNormal: CGVector { + return segmentStrokeVector.normal!.normalize! + } + + var fromSampleUnitNormal: CGVector { + return interpolatedNormalUnitVector(between: previousSegmentStrokeVector, and: segmentStrokeVector) + } + + var toSampleUnitNormal: CGVector { + return interpolatedNormalUnitVector(between: segmentStrokeVector, and: nextSegmentStrokeVector) + } + + var previousSegmentStrokeVector: CGVector { + if let sampleBefore = self.sampleBefore { + return fromSample.location - sampleBefore.location + } else { + return segmentStrokeVector + } + } + + var segmentStrokeVector: CGVector { + return toSample.location - fromSample.location + } + + var nextSegmentStrokeVector: CGVector { + if let sampleAfter = self.sampleAfter { + return sampleAfter.location - toSample.location + } else { + return segmentStrokeVector + } + } + + + init(sample: StrokeSample) { + self.sampleAfter = sample + self.fromSampleIndex = -2 + } + + @discardableResult + func advanceWithSample(incomingSample:StrokeSample?) -> Bool { + if let sampleAfter = self.sampleAfter { + self.sampleBefore = fromSample + self.fromSample = toSample + self.toSample = sampleAfter + self.sampleAfter = incomingSample + self.fromSampleIndex += 1 + return true + } + return false + } +} + +class StrokeSegmentIterator: IteratorProtocol { + private let stroke: Stroke + private var nextIndex: Int + private let sampleCount: Int + private let predictedSampleCount: Int + private var segment: StrokeSegment! + + init(stroke: Stroke) { + self.stroke = stroke + nextIndex = 1 + sampleCount = stroke.samples.count + predictedSampleCount = stroke.predictedSamples.count + if (predictedSampleCount + sampleCount > 1) { + segment = StrokeSegment(sample: sampleAt(0)!) + segment.advanceWithSample(incomingSample: sampleAt(1)) + } + } + + func sampleAt(_ index: Int) -> StrokeSample? { + if (index < sampleCount) { + return stroke.samples[index] + } + let predictedIndex = index - sampleCount + if predictedIndex < predictedSampleCount { + return stroke.predictedSamples[predictedIndex] + } else { + return nil + } + } + + func next() -> StrokeSegment? { + nextIndex += 1 + if let segment = self.segment { + if segment.advanceWithSample(incomingSample: sampleAt(nextIndex)) { + return segment + } + } + return nil + } +} + + diff --git a/SpeedSketch/SpeedSketch/StrokeGestureRecognizer.swift b/SpeedSketch/SpeedSketch/StrokeGestureRecognizer.swift new file mode 100644 index 00000000..af65805f --- /dev/null +++ b/SpeedSketch/SpeedSketch/StrokeGestureRecognizer.swift @@ -0,0 +1,219 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The custom UIGestureRecognizer subclass to capture strokes. +*/ + +import UIKit +import UIKit.UIGestureRecognizerSubclass + + +class StrokeGestureRecognizer: UIGestureRecognizer { + // MARK: Configuration. + var collectsCoalescedTouches = true + var usesPredictedSamples = true + var isForPencil: Bool = false { + didSet { + if (isForPencil) { + allowedTouchTypes = [UITouchType.stylus.rawValue as NSNumber] + } else { + allowedTouchTypes = [UITouchType.direct.rawValue as NSNumber] + } + } + } + + // MARK: Data. + var stroke = Stroke() + var outstandingUpdateIndexes = [Int:(Stroke, Int)]() + var coordinateSpaceView: UIView? + + // MARK: State. + var trackedTouch: UITouch? + var initialTimestamp: TimeInterval? + var collectForce = false + + var fingerStartTimer: Timer? = nil + private let cancellationTimeInterval = TimeInterval(0.1) + + var ensuredReferenceView: UIView { + if let view = coordinateSpaceView { + return view + } else { + return view! + } + } + + // MARK: Stroke data collection. + func append(touches: Set, event: UIEvent?) -> Bool { + if let touchToAppend = trackedTouch { + + // Cancel the stroke recognition if we get a second touch during cancellation period. + for touch in touches { + if touch !== touchToAppend && + touch.timestamp - initialTimestamp! < cancellationTimeInterval { + if state == .possible { + state = .failed + } else { + state = .cancelled + } + return false + } + } + + // See if those touches contain our tracked touch. If not, ignore gracefully. + if touches.contains(touchToAppend) { + + let collector = { (stroke: Stroke, touch: UITouch, view: UIView, coalesced: Bool , predicted: Bool ) in + + // Only collect samples that actually moved in 2D space. + let location = touch.preciseLocation(in: view) + if let previousSample = stroke.samples.last { + if (previousSample.location - location).quadrance < 0.003 { + return + } + } + + var sample = StrokeSample(timestamp: touch.timestamp, location: location, coalesced: coalesced, predicted: predicted, force: self.collectForce ? touch.force : nil) + if touch.type == .stylus { + let estimatedProperties = touch.estimatedProperties + sample.estimatedProperties = estimatedProperties + sample.estimatedPropertiesExpectingUpdates = touch.estimatedPropertiesExpectingUpdates + sample.altitude = touch.altitudeAngle + sample.azimuth = touch.azimuthAngle(in: view) + if stroke.samples.count == 0 && + estimatedProperties.contains(.azimuth) { + stroke.expectsAltitudeAzimuthBackfill = true + } else if stroke.expectsAltitudeAzimuthBackfill && + !estimatedProperties.contains(.azimuth) { + for (index, priorSample) in stroke.samples.enumerated() { + var updatedSample = priorSample + if updatedSample.estimatedProperties.contains(.altitude) { + updatedSample.estimatedProperties.remove(.altitude) + updatedSample.altitude = sample.altitude + } + if updatedSample.estimatedProperties.contains(.azimuth) { + updatedSample.estimatedProperties.remove(.azimuth) + updatedSample.azimuth = sample.azimuth + } + stroke.update(sample: updatedSample, at: index) + } + stroke.expectsAltitudeAzimuthBackfill = false + } + } + if predicted { + stroke.addPredicted(sample: sample) + } else { + let index = stroke.add(sample: sample) + if touch.estimatedPropertiesExpectingUpdates != [] { + self.outstandingUpdateIndexes[Int(touch.estimationUpdateIndex!)] = (stroke, index) + } + } + } + + let view = ensuredReferenceView + if collectsCoalescedTouches { + if let event = event { + let coalescedTouches = event.coalescedTouches(for: touchToAppend)! + let lastIndex = coalescedTouches.count - 1 + for index in 0.., with event: UIEvent?) { + if trackedTouch == nil { + trackedTouch = touches.first + initialTimestamp = trackedTouch?.timestamp + collectForce = trackedTouch!.type == .stylus || view?.traitCollection.forceTouchCapability == .available + if !isForPencil { + fingerStartTimer = Timer.scheduledTimer(timeInterval: cancellationTimeInterval, target: self, selector: #selector(beginIfNeeded(_:)), userInfo: nil, repeats: false) + } + } + if append(touches: touches, event:event) { + if isForPencil { + state = .began + } + } + } + + // If not for pencil we give other gestures (pan, pinch) a chance by delaying our begin just a little. + func beginIfNeeded(_ timer: Timer) { + if state == .possible { + state = .began + } + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + if append(touches: touches, event:event) { + if state == .began { + state = .changed + } + } + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + if append(touches: touches, event:event) { + stroke.state = .done + state = .ended + } + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + if append(touches: touches, event:event) { + stroke.state = .cancelled + state = .failed + } + } + + override func touchesEstimatedPropertiesUpdated(_ touches: Set) { + for touch in touches { + if let (stroke, sampleIndex) = outstandingUpdateIndexes[Int(touch.estimationUpdateIndex!)] { + var sample = stroke.samples[sampleIndex] + let expectedUpdates = sample.estimatedPropertiesExpectingUpdates + // Only force is reported this way as of iOS 10.0 + if expectedUpdates.contains(.force) { + sample.force = touch.force + if !touch.estimatedProperties.contains(.force) { + // Only remove the estimate flag if the new value isn't estimated as well. + sample.estimatedProperties.remove(.force) + } + } + sample.estimatedPropertiesExpectingUpdates = touch.estimatedPropertiesExpectingUpdates + if touch.estimatedPropertiesExpectingUpdates == [] { + outstandingUpdateIndexes.removeValue(forKey: sampleIndex) + } + stroke.update(sample: sample, at: sampleIndex) + } + } + } + + override func reset() { + stroke = Stroke() + trackedTouch = nil + if let timer = fingerStartTimer { + timer.invalidate() + fingerStartTimer = nil + } + super.reset() + } +} diff --git a/ToolbarSample/LICENSE.txt b/ToolbarSample/LICENSE.txt new file mode 100644 index 00000000..1ec8aea0 --- /dev/null +++ b/ToolbarSample/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: ToolbarSample: Using NSToolbar to construct a window toolbar +Version: 3.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) 2016 Apple Inc. All Rights Reserved. diff --git a/ToolbarSample/ReadMe.md b/ToolbarSample/ReadMe.md new file mode 100644 index 00000000..35b8e1fb --- /dev/null +++ b/ToolbarSample/ReadMe.md @@ -0,0 +1,17 @@ +# ToolbarSample: Using NSToolbar to construct a window toolbar + +## Description + +This sample shows how to use the AppKit's NSToolbar and NSToolbarItem classes. These are used to add customizable toolbars to windows. This sample also includes more advanced use of custom views in NSToolbarItems, where you can put a variety of controls, given a little more coding. This sample implements the NSTouchBar API to work in conjunction with the toolbar. + +## Requirements + +### Build + +Xcode 8.1, macOS 10.12.1 SDK or later + +### Runtime + +macOS 10.11 or later + +Copyright (C) 2001-2016 Apple Inc. All rights reserved. diff --git a/ToolbarSample/ToolbarSample.xcodeproj/project.pbxproj b/ToolbarSample/ToolbarSample.xcodeproj/project.pbxproj new file mode 100644 index 00000000..a045e328 --- /dev/null +++ b/ToolbarSample/ToolbarSample.xcodeproj/project.pbxproj @@ -0,0 +1,335 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 536F82381D8862FA0029689C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536F82371D8862FA0029689C /* AppDelegate.swift */; }; + 536F823A1D8863010029689C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536F82391D8863010029689C /* ViewController.swift */; }; + 536F823D1D88630C0029689C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 536F823B1D88630C0029689C /* Main.storyboard */; }; + 536F82421D8863110029689C /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 536F823E1D8863110029689C /* Credits.rtf */; }; + 536F82431D8863110029689C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 536F82401D8863110029689C /* InfoPlist.strings */; }; + 536F82461D88631C0029689C /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 536F82451D88631C0029689C /* Images.xcassets */; }; + 53A35EAD1D88677A00A84CDE /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A35EAC1D88677A00A84CDE /* WindowController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 533FE5AD1DA86A0A0042B582 /* ReadMe.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ReadMe.md; sourceTree = ""; }; + 5341010D1AFD23B2003BC1D6 /* ToolbarSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ToolbarSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 536F82371D8862FA0029689C /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ToolbarSample/AppDelegate.swift; sourceTree = SOURCE_ROOT; }; + 536F82391D8863010029689C /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ViewController.swift; path = ToolbarSample/ViewController.swift; sourceTree = SOURCE_ROOT; }; + 536F823C1D88630C0029689C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = ToolbarSample/Base.lproj/Main.storyboard; sourceTree = SOURCE_ROOT; }; + 536F823F1D8863110029689C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = ToolbarSample/en.lproj/Credits.rtf; sourceTree = SOURCE_ROOT; }; + 536F82411D8863110029689C /* en */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = en; path = ToolbarSample/en.lproj/InfoPlist.strings; sourceTree = SOURCE_ROOT; }; + 536F82441D8863170029689C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ToolbarSample/Info.plist; sourceTree = SOURCE_ROOT; }; + 536F82451D88631C0029689C /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ToolbarSample/Images.xcassets; sourceTree = SOURCE_ROOT; }; + 53A35EAC1D88677A00A84CDE /* WindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WindowController.swift; path = ToolbarSample/WindowController.swift; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5341010A1AFD23B2003BC1D6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 534101041AFD23B2003BC1D6 = { + isa = PBXGroup; + children = ( + 533FE5AD1DA86A0A0042B582 /* ReadMe.md */, + 5341010F1AFD23B2003BC1D6 /* ToolbarSample */, + 5341010E1AFD23B2003BC1D6 /* Products */, + ); + sourceTree = ""; + }; + 5341010E1AFD23B2003BC1D6 /* Products */ = { + isa = PBXGroup; + children = ( + 5341010D1AFD23B2003BC1D6 /* ToolbarSample.app */, + ); + name = Products; + sourceTree = ""; + }; + 5341010F1AFD23B2003BC1D6 /* ToolbarSample */ = { + isa = PBXGroup; + children = ( + 536F82371D8862FA0029689C /* AppDelegate.swift */, + 53A35EAC1D88677A00A84CDE /* WindowController.swift */, + 536F82391D8863010029689C /* ViewController.swift */, + 534101101AFD23B2003BC1D6 /* Supporting Files */, + ); + name = ToolbarSample; + path = SimpleApp; + sourceTree = ""; + }; + 534101101AFD23B2003BC1D6 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 536F82451D88631C0029689C /* Images.xcassets */, + 536F82441D8863170029689C /* Info.plist */, + 536F823B1D88630C0029689C /* Main.storyboard */, + 536F823E1D8863110029689C /* Credits.rtf */, + 536F82401D8863110029689C /* InfoPlist.strings */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 5341010C1AFD23B2003BC1D6 /* ToolbarSample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 534101291AFD23B3003BC1D6 /* Build configuration list for PBXNativeTarget "ToolbarSample" */; + buildPhases = ( + 534101091AFD23B2003BC1D6 /* Sources */, + 5341010A1AFD23B2003BC1D6 /* Frameworks */, + 5341010B1AFD23B2003BC1D6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ToolbarSample; + productName = SimpleApp; + productReference = 5341010D1AFD23B2003BC1D6 /* ToolbarSample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 534101051AFD23B2003BC1D6 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0810; + ORGANIZATIONNAME = keith; + TargetAttributes = { + 5341010C1AFD23B2003BC1D6 = { + CreatedOnToolsVersion = 6.3.1; + LastSwiftMigration = 0810; + }; + }; + }; + buildConfigurationList = 534101081AFD23B2003BC1D6 /* Build configuration list for PBXProject "ToolbarSample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 534101041AFD23B2003BC1D6; + productRefGroup = 5341010E1AFD23B2003BC1D6 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5341010C1AFD23B2003BC1D6 /* ToolbarSample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5341010B1AFD23B2003BC1D6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 536F823D1D88630C0029689C /* Main.storyboard in Resources */, + 536F82461D88631C0029689C /* Images.xcassets in Resources */, + 536F82431D8863110029689C /* InfoPlist.strings in Resources */, + 536F82421D8863110029689C /* Credits.rtf in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 534101091AFD23B2003BC1D6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 536F823A1D8863010029689C /* ViewController.swift in Sources */, + 53A35EAD1D88677A00A84CDE /* WindowController.swift in Sources */, + 536F82381D8862FA0029689C /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 536F823B1D88630C0029689C /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 536F823C1D88630C0029689C /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 536F823E1D8863110029689C /* Credits.rtf */ = { + isa = PBXVariantGroup; + children = ( + 536F823F1D8863110029689C /* en */, + ); + name = Credits.rtf; + sourceTree = ""; + }; + 536F82401D8863110029689C /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 536F82411D8863110029689C /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 534101271AFD23B3003BC1D6 /* 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_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 534101281AFD23B3003BC1D6 /* 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_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + }; + name = Release; + }; + 5341012A1AFD23B3003BC1D6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = ToolbarSample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.ToolbarSample"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 5341012B1AFD23B3003BC1D6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = ToolbarSample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.ToolbarSample"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 534101081AFD23B2003BC1D6 /* Build configuration list for PBXProject "ToolbarSample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 534101271AFD23B3003BC1D6 /* Debug */, + 534101281AFD23B3003BC1D6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 534101291AFD23B3003BC1D6 /* Build configuration list for PBXNativeTarget "ToolbarSample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5341012A1AFD23B3003BC1D6 /* Debug */, + 5341012B1AFD23B3003BC1D6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 534101051AFD23B2003BC1D6 /* Project object */; +} diff --git a/ToolbarSample/ToolbarSample/AppDelegate.swift b/ToolbarSample/ToolbarSample/AppDelegate.swift new file mode 100644 index 00000000..cafdc368 --- /dev/null +++ b/ToolbarSample/ToolbarSample/AppDelegate.swift @@ -0,0 +1,29 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The main NSApplicationDelegate to this sample. + */ + +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Insert code here to initialize your application + + // Here we just opt-in for allowing our instance of the NSTouchBar class to be customized throughout the app. + if #available(OSX 10.12.1, *) { + if ((NSClassFromString("NSTouchBar")) != nil) { + NSApplication.shared().isAutomaticCustomizeTouchBarMenuItemEnabled = true + } + } + } + + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } + +} diff --git a/ToolbarSample/ToolbarSample/Base.lproj/Main.storyboard b/ToolbarSample/ToolbarSample/Base.lproj/Main.storyboard new file mode 100644 index 00000000..d2e2c5dd --- /dev/null +++ b/ToolbarSample/ToolbarSample/Base.lproj/Main.storyboard @@ -0,0 +1,581 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ToolbarSample/ToolbarSample/Images.xcassets/AppIcon.appiconset/Contents.json b/ToolbarSample/ToolbarSample/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..2db2b1c7 --- /dev/null +++ b/ToolbarSample/ToolbarSample/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "16x16", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "32x32", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "128x128", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "256x256", + "scale" : "2x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "1x" + }, + { + "idiom" : "mac", + "size" : "512x512", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ToolbarSample/ToolbarSample/Images.xcassets/Contents.json b/ToolbarSample/ToolbarSample/Images.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/ToolbarSample/ToolbarSample/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ToolbarSample/ToolbarSample/Info.plist b/ToolbarSample/ToolbarSample/Info.plist new file mode 100644 index 00000000..7b66ecc3 --- /dev/null +++ b/ToolbarSample/ToolbarSample/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 3.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + + diff --git a/ToolbarSample/ToolbarSample/ViewController.swift b/ToolbarSample/ToolbarSample/ViewController.swift new file mode 100644 index 00000000..10f81457 --- /dev/null +++ b/ToolbarSample/ToolbarSample/ViewController.swift @@ -0,0 +1,50 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The primary view controller holding the toolbar and text view. + */ + +import Cocoa + +class ViewController: NSViewController, NSTextViewDelegate { + + @IBOutlet var textView: NSTextView! + + // MARK: - View Controller Life Cycle + + override func viewDidLoad() + { + super.viewDidLoad() + + self.textView.delegate = self + if #available(OSX 10.12.1, *) { + // Opt-out of text completion in this simplified version. + if ((NSClassFromString("NSTouchBar")) != nil) { + self.textView?.isAutomaticTextCompletionEnabled = false + } + } else { + // Fallback on earlier versions + } + + self.view.window?.makeFirstResponder(self.textView) + + // Do any additional setup after loading the view. + } + + override var representedObject: Any? + { + didSet { + // Update the view, if already loaded. + } + } + + // MARK: - NSTextViewDelegate + + func textView(_ textView: NSTextView, shouldUpdateTouchBarItemIdentifiers identifiers: [NSTouchBarItemIdentifier]) -> [NSTouchBarItemIdentifier] { + + return [] // We want to show only our NSTouchBarItem instances. + } + +} diff --git a/ToolbarSample/ToolbarSample/WindowController.swift b/ToolbarSample/ToolbarSample/WindowController.swift new file mode 100644 index 00000000..21b50313 --- /dev/null +++ b/ToolbarSample/ToolbarSample/WindowController.swift @@ -0,0 +1,419 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The primary window controller for this sample. + */ + +import Cocoa + +fileprivate extension NSTouchBarCustomizationIdentifier { + + static let touchBar = NSTouchBarCustomizationIdentifier("com.ToolbarSample.touchBar") +} + +fileprivate extension NSTouchBarItemIdentifier { + + static let popover = NSTouchBarItemIdentifier("com.ToolbarSample.TouchBarItem.popover") + static let fontStyle = NSTouchBarItemIdentifier("com.ToolbarSample.TouchBarItem.fontStyle") + static let popoverSlider = NSTouchBarItemIdentifier("com.ToolbarSample.popoverBar.slider") +} + +class WindowController: NSWindowController, NSToolbarDelegate { + + let FontSizeToolbarItemID = "FontSize" + let FontStyleToolbarItemID = "FontStyle" + let DefaultFontSize : Int = 18 + + @IBOutlet weak var toolbar: NSToolbar! + + // Font style toolbar item. + @IBOutlet var styleSegmentView: NSView! // The font style changing view (ends up in an NSToolbarItem). + + // Font size toolbar item. + @IBOutlet var fontSizeView: NSView! // The font size changing view (ends up in an NSToolbarItem). + @IBOutlet var fontSizeStepper: NSStepper! + @IBOutlet var fontSizeField: NSTextField! + + var currentFontSize: Int = 0 + + // MARK: - Window Controller Life Cycle + + override func windowDidLoad() { + + super.windowDidLoad() + + self.currentFontSize = DefaultFontSize + + // Configure our toolbar (note: this can also be done in Interface Builder). + + /* If you pass NO here, you turn off the customization palette. The palette is normally handled + automatically for you by NSWindow's -runToolbarCustomizationPalette: function; you'll notice + that the "Customize Toolbar" menu item is hooked up to that method in Interface Builder. + */ + self.toolbar.allowsUserCustomization = true + + /* Tell the toolbar that it should save any configuration changes to user defaults, i.e. mode + changes, or reordering will persist. Specifically they will be written in the app domain using + the toolbar identifier as the key. + */ + self.toolbar.autosavesConfiguration = true + + // Tell the toolbar to show icons only by default. + self.toolbar.displayMode = .iconOnly + + // Initialize our font size control here to 18-point font, and set our view controller's NSTextView to that size. + self.fontSizeStepper.integerValue = Int(DefaultFontSize) + self.fontSizeField.stringValue = String(DefaultFontSize) + let font = NSFont(name: "Helvetica", size: CGFloat(DefaultFontSize)) + self.contentTextView().font = font + + if #available(OSX 10.12.1, *) { + if ((NSClassFromString("NSTouchBar")) != nil) { + let fontSizeTouchBarItem = self.touchBar!.item(forIdentifier: .popover) as! NSPopoverTouchBarItem + let sliderTouchBar = fontSizeTouchBarItem.popoverTouchBar + let sliderTouchBarItem = sliderTouchBar.item(forIdentifier: .popoverSlider) as! NSSliderTouchBarItem + let slider = sliderTouchBarItem.slider + + // Make the font size slider a bit narrowed, about 250 pixels. + let views = ["slider" : slider] + let theConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:[slider(250)]", options: NSLayoutFormatOptions(), metrics: nil, views:views) + NSLayoutConstraint.activate(theConstraints) + + // Set the font size for the slider item to the same value as the stepper. + slider.integerValue = DefaultFontSize + } + } + } + + // Convenince accessor to our NSTextView found in our content view controller. + func contentTextView() -> NSTextView { + return (self.contentViewController as! ViewController).textView + } + + // MARK: - Font and Size setters + + func setTextViewFontSize(fontSize: Float) { + + fontSizeField.floatValue = round(fontSize) + + let attrs = self.contentTextView().typingAttributes + var theFont : NSFont = attrs["NSFont"] as! NSFont + + theFont = NSFontManager.shared().convert(theFont, toSize: CGFloat(fontSize)) + + if (self.contentTextView().selectedRange().length > 0) { + // We have a selection, change the selected text + self.contentTextView().setFont(theFont, range: self.contentTextView().selectedRange()) + } + else { + // No selection, so just change the font size at insertion. + let attributesDict = [ NSFontAttributeName: theFont ] + self.contentTextView().typingAttributes = attributesDict + } + } + + /** + This action is called to change the font style. + It is called through it's popup toolbar item and segmented control item. + */ + func setTextViewFont(index: Int) { + + let attrs = self.contentTextView().typingAttributes + var theFont : NSFont = attrs["NSFont"] as! NSFont + + // Set the font properties depending upon what was selected. + switch (index) { + case 0: // plain + theFont = NSFontManager.shared().convert(theFont, toNotHaveTrait:.italicFontMask) + theFont = NSFontManager.shared().convert(theFont, toNotHaveTrait:.boldFontMask) + theFont = NSFontManager.shared().convert(theFont, toNotHaveTrait:.boldFontMask) + + // No underline attribute. + let selectedRange = self.contentTextView().selectedRange() + let textStorage = self.contentTextView().textStorage + textStorage?.removeAttribute(NSForegroundColorAttributeName, range: selectedRange) + textStorage?.addAttribute(NSUnderlineStyleAttributeName, value: NSNumber.init(value: 0), range: selectedRange) + + case 1: // bold + theFont = NSFontManager.shared().convert(theFont, toNotHaveTrait:.italicFontMask) + theFont = NSFontManager.shared().convert(theFont, toHaveTrait:.boldFontMask) + + case 2: // italic + theFont = NSFontManager.shared().convert(theFont, toNotHaveTrait:.boldFontMask) + theFont = NSFontManager.shared().convert(theFont, toHaveTrait:.italicFontMask) + + default: + print("invalid selection") + } + + if (self.contentTextView().selectedRange().length > 0) { + // We have a selection, change the selected text + self.contentTextView().setFont(theFont, range: self.contentTextView().selectedRange()) + } + else { + // No selection, so just change the font style at insertion. + let attributesDict = [ NSFontAttributeName: theFont ] + self.contentTextView().typingAttributes = attributesDict + } + } + + // MARK: - Action Functions + + /** + This action is called to change the font size. + It is called by the NSStepper in the toolbar item's custom view and the slider item. + */ + @IBAction func changeFontSize(_ sender: NSStepper) { + + self.setTextViewFontSize(fontSize: sender.floatValue) + } + + /// This action is called to change the font size from the slider item found in the NSTouchBar instance. + @IBAction func changeFontSizeBySlider(_ sender: NSSlider) { + + self.setTextViewFontSize(fontSize: sender.floatValue) + } + + /// This action is called from the change font style toolbar item, from the segmented control in the custom view. + @IBAction func changeFontStyleBySegment(_ sender: NSSegmentedControl) { + + let style = sender.selectedSegment + self.setTextViewFont(index: style) + } + + /// This is called by the appropriate toolbar item to toggle blue text on/off. + @IBAction func blueText(_ sender: AnyObject) { + + if (self.contentTextView().selectedRange().length > 0) { + // We have a selection, change the selected text + let textStorage = self.contentTextView().textStorage + textStorage?.removeAttribute(NSForegroundColorAttributeName, range: self.contentTextView().selectedRange()) + textStorage?.addAttribute(NSForegroundColorAttributeName, value: NSColor.blue, range: self.contentTextView().selectedRange()) + } + else { + // No selection, so just change the font size at insertion. + var attrs = self.contentTextView().typingAttributes + attrs["NSColor"] = NSColor.blue + self.contentTextView().typingAttributes = attrs + } + } + + /** + The NSToolbarPrintItem NSToolbarItem will send the -printDocument: message to its target. + Since we wired its target to be ourselves in -toolbarWillAddItem:, we get called here when + the user tries to print by clicking the toolbar item. + */ + func printDocument(_ sender: AnyObject) { + + let printOperation = NSPrintOperation(view: self.contentTextView()) + printOperation.runModal(for: self.window!, delegate: nil, didRun: nil, contextInfo: nil) + } + + // Called when the user chooses a font style from the segmented control inside the NSTouchBar instance. + @IBAction func touchBarFontStyleAction(_ sender: NSSegmentedControl) { + + self.setTextViewFont(index: sender.selectedSegment) + } + + // MARK: - NSToolbarDelegate + + /** + Factory method to create NSToolbarItems. + + All NSToolbarItems have a unique identifer associated with them, used to tell your delegate/controller + what toolbar items to initialize and return at various points. Typically, for a given identifier, + you need to generate a copy of your "master" toolbar item, and return. The function + creates an NSToolbarItem with a bunch of NSToolbarItem paramenters. + + It's easy to call this function repeatedly to generate lots of NSToolbarItems for your toolbar. + + The label, palettelabel, toolTip, action, and menu can all be nil, depending upon what you want + the item to do. + */ + func customToolbarItem(itemForItemIdentifier itemIdentifier: String, label: String, paletteLabel: String, toolTip: String, target: AnyObject, itemContent: AnyObject, action: Selector?, menu: NSMenu?) -> NSToolbarItem? { + + let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier) + + toolbarItem.label = label + toolbarItem.paletteLabel = paletteLabel + toolbarItem.toolTip = toolTip + toolbarItem.target = target + toolbarItem.action = action + + // Set the right attribute, depending on if we were given an image or a view. + if (itemContent is NSImage) { + let image: NSImage = itemContent as! NSImage + toolbarItem.image = image + } + else if (itemContent is NSView) { + let view: NSView = itemContent as! NSView + toolbarItem.view = view + } + else { + assertionFailure("Invalid itemContent: object") + } + + /* If this NSToolbarItem is supposed to have a menu "form representation" associated with it + (for text-only mode), we set it up here. Actually, you have to hand an NSMenuItem + (not a complete NSMenu) to the toolbar item, so we create a dummy NSMenuItem that has our real + menu as a submenu. + */ + // We actually need an NSMenuItem here, so we construct one. + let menuItem: NSMenuItem = NSMenuItem() + menuItem.submenu = menu + menuItem.title = label + toolbarItem.menuFormRepresentation = menuItem + + return toolbarItem + } + + /** + This is an optional delegate function, called when a new item is about to be added to the toolbar. + This is a good spot to set up initial state information for toolbar items, particularly items + that you don't directly control yourself (like with NSToolbarPrintItemIdentifier here). + The notification's object is the toolbar, and the "item" key in the userInfo is the toolbar item + being added. + */ + func toolbarWillAddItem(_ notification: Notification) { + + let userInfo = notification.userInfo! + let addedItem = userInfo["item"] as! NSToolbarItem + + let itemIdentifier = addedItem.itemIdentifier + + if itemIdentifier == "NSToolbarPrintItem" { + addedItem.toolTip = "Print your document" + addedItem.target = self + } + } + + /** + NSToolbar delegates require this function. + It takes an identifier, and returns the matching NSToolbarItem. It also takes a parameter telling + whether this toolbar item is going into an actual toolbar, or whether it's going to be displayed + in a customization palette. + */ + func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: String, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + + var toolbarItem: NSToolbarItem = NSToolbarItem() + + /* We create a new NSToolbarItem, and then go through the process of setting up its + attributes from the master toolbar item matching that identifier in our dictionary of items. + */ + if (itemIdentifier == FontStyleToolbarItemID) { + // 1) Font style toolbar item. + toolbarItem = customToolbarItem(itemForItemIdentifier: FontStyleToolbarItemID, label: "Font Style", paletteLabel:"Font Style", toolTip: "Change your font style", target: self, itemContent: self.styleSegmentView, action: nil, menu: nil)! + } + else if (itemIdentifier == FontSizeToolbarItemID) { + // 2) Font size toolbar item. + toolbarItem = customToolbarItem(itemForItemIdentifier: FontSizeToolbarItemID, label: "Font Size", paletteLabel: "Font Size", toolTip: "Grow or shrink the size of your font", target: self, itemContent: self.fontSizeView, action: nil, menu: nil)! + } + + return toolbarItem + } + + /** + NSToolbar delegates require this function. It returns an array holding identifiers for the default + set of toolbar items. It can also be called by the customization palette to display the default toolbar. + */ + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [String] { + + return [FontStyleToolbarItemID, FontSizeToolbarItemID] + /* Note: + That since our toolbar is defined from Interface Builder, an additional separator and customize + toolbar items will be automatically added to the "default" list of items. + */ + } + + /** + NSToolbar delegates require this function. It returns an array holding identifiers for all allowed + toolbar items in this toolbar. Any not listed here will not be available in the customization palette. + */ + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [String] { + + return [ FontStyleToolbarItemID, + FontSizeToolbarItemID, + NSToolbarSpaceItemIdentifier, + NSToolbarFlexibleSpaceItemIdentifier, + NSToolbarPrintItemIdentifier ] + } + + // MARK: - NSTouchBar + + @available(OSX 10.12.1, *) + override func makeTouchBar() -> NSTouchBar? { + + let touchBar = NSTouchBar() + touchBar.delegate = self + touchBar.customizationIdentifier = .touchBar + touchBar.defaultItemIdentifiers = [.fontStyle, .popover, .otherItemsProxy] + touchBar.customizationAllowedItemIdentifiers = [.fontStyle, .popover] + + return touchBar + } + +} + +extension WindowController: NSTouchBarDelegate { + + @available(OSX 10.12.1, *) + func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? { + + switch identifier { + + case NSTouchBarItemIdentifier.popover: + + let popoverItem = NSPopoverTouchBarItem(identifier: identifier) + popoverItem.customizationLabel = "Font Size" + popoverItem.collapsedRepresentationLabel = "Font Size" + + let secondaryTouchBar = NSTouchBar() + secondaryTouchBar.delegate = self + secondaryTouchBar.defaultItemIdentifiers = [.popoverSlider]; + + // We can setup a different NSTouchBar instance for popoverTouchBar and pressAndHoldTouchBar property + // Here we just use the same instance. + // + popoverItem.pressAndHoldTouchBar = secondaryTouchBar + popoverItem.popoverTouchBar = secondaryTouchBar + + return popoverItem + + case NSTouchBarItemIdentifier.fontStyle: + + let fontStyleItem = NSCustomTouchBarItem(identifier: identifier) + fontStyleItem.customizationLabel = "Font Style" + + let fontStyleSegment = NSSegmentedControl(labels: ["Plain", "Bold", "Italic"], trackingMode: .momentary, target: self, action: #selector(changeFontStyleBySegment)) + + fontStyleItem.view = fontStyleSegment + + return fontStyleItem; + + case NSTouchBarItemIdentifier.popoverSlider: + + let sliderItem = NSSliderTouchBarItem(identifier: identifier) + sliderItem.label = "Size" + sliderItem.customizationLabel = "Font Size" + + let slider = sliderItem.slider + slider.minValue = 6.0 + slider.maxValue = 100.0 + slider.target = self + slider.action = #selector(changeFontSizeBySlider) + + // Set the font size for the slider item to the same value as the stepper. + slider.integerValue = DefaultFontSize + + slider.bind(NSValueBinding, to: self, withKeyPath: "currentFontSize", options: nil) + + return sliderItem + + default: return nil + } + } + +} + diff --git a/ToolbarSample/ToolbarSample/en.lproj/Credits.rtf b/ToolbarSample/ToolbarSample/en.lproj/Credits.rtf new file mode 100644 index 00000000..8909f134 --- /dev/null +++ b/ToolbarSample/ToolbarSample/en.lproj/Credits.rtf @@ -0,0 +1,9 @@ +{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350 +{\fonttbl\f0\fnil\fcharset0 LucidaGrande;} +{\colortbl;\red255\green255\blue255;} +\pard\tx960\tx1920\tx2880\tx3840\tx4800\tx5760\tx6720\tx7680\tx8640\tx9600\qc + +\f0\fs20 \cf0 Demonstrates how to use\ +NSToolbar and NSToolbarItem\ +to build a toolbar in your application.\ +} \ No newline at end of file diff --git a/ToolbarSample/ToolbarSample/en.lproj/InfoPlist.strings b/ToolbarSample/ToolbarSample/en.lproj/InfoPlist.strings new file mode 100755 index 00000000..4d0c5890 Binary files /dev/null and b/ToolbarSample/ToolbarSample/en.lproj/InfoPlist.strings differ diff --git a/UICatalog/LICENSE.txt b/UICatalog/LICENSE.txt index 0b752a20..a7ba7340 100644 --- a/UICatalog/LICENSE.txt +++ b/UICatalog/LICENSE.txt @@ -1,5 +1,5 @@ Sample code project: UIKit Catalog (iOS): Creating and Customizing UIKit Controls -Version: 13.2 +Version: 13.3 IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following diff --git a/UICatalog/Objective-C/UIKitCatalog.xcodeproj/project.pbxproj b/UICatalog/Objective-C/UIKitCatalog.xcodeproj/project.pbxproj index cb4cb298..1b2bb421 100644 --- a/UICatalog/Objective-C/UIKitCatalog.xcodeproj/project.pbxproj +++ b/UICatalog/Objective-C/UIKitCatalog.xcodeproj/project.pbxproj @@ -302,7 +302,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0800; + LastUpgradeCheck = 0810; ORGANIZATIONNAME = f; }; buildConfigurationList = 5356823518F3656900BAAD62 /* Build configuration list for PBXProject "UIKitCatalog" */; @@ -418,8 +418,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 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_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -461,8 +463,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 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_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; diff --git a/UICatalog/README.md b/UICatalog/README.md index 70354ac6..9cf88fbe 100644 --- a/UICatalog/README.md +++ b/UICatalog/README.md @@ -8,7 +8,7 @@ You will also notice this sample shows how to localize string content by using t ## Build Requirements -Xcode 8.0 and iOS 10.0 SDK or later +Xcode 8.1 and iOS 10.0 SDK or later ## Runtime Requirements diff --git a/UICatalog/Swift/UIKitCatalog.xcodeproj/project.pbxproj b/UICatalog/Swift/UIKitCatalog.xcodeproj/project.pbxproj index 9ddf08d2..9c2b48eb 100644 --- a/UICatalog/Swift/UIKitCatalog.xcodeproj/project.pbxproj +++ b/UICatalog/Swift/UIKitCatalog.xcodeproj/project.pbxproj @@ -244,11 +244,11 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0800; + LastUpgradeCheck = 0810; ORGANIZATIONNAME = Apple; TargetAttributes = { 228DB9F218BC53F1002BA12A = { - LastSwiftMigration = 0800; + LastSwiftMigration = 0810; }; }; }; @@ -365,8 +365,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 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_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -391,6 +393,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 9.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + SWIFT_VERSION = ""; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -408,8 +411,10 @@ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 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_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -426,6 +431,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; SDKROOT = iphoneos; + SWIFT_VERSION = ""; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -441,7 +447,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -456,7 +462,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 2.3; + SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/UICatalog/Swift/UIKitCatalog/ActivityIndicatorViewController.swift b/UICatalog/Swift/UIKitCatalog/ActivityIndicatorViewController.swift index 0a11c2aa..5aa0dd75 100644 --- a/UICatalog/Swift/UIKitCatalog/ActivityIndicatorViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/ActivityIndicatorViewController.swift @@ -29,7 +29,7 @@ class ActivityIndicatorViewController: UITableViewController { // MARK: - Configuration func configureGrayActivityIndicatorView() { - grayStyleActivityIndicatorView.activityIndicatorViewStyle = .Gray + grayStyleActivityIndicatorView.activityIndicatorViewStyle = .gray grayStyleActivityIndicatorView.startAnimating() @@ -37,7 +37,7 @@ class ActivityIndicatorViewController: UITableViewController { } func configureTintedActivityIndicatorView() { - tintedActivityIndicatorView.activityIndicatorViewStyle = .Gray + tintedActivityIndicatorView.activityIndicatorViewStyle = .gray tintedActivityIndicatorView.color = UIColor.applicationPurpleColor diff --git a/UICatalog/Swift/UIKitCatalog/AlertControllerViewController.swift b/UICatalog/Swift/UIKitCatalog/AlertControllerViewController.swift index 5bbeda3a..7401f952 100644 --- a/UICatalog/Swift/UIKitCatalog/AlertControllerViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/AlertControllerViewController.swift @@ -17,7 +17,7 @@ class AlertControllerViewController : UITableViewController { A matrix of closures that should be invoked based on which table view cell is tapped (index by section, row). */ - var actionMap: [[(selectedIndexPath: NSIndexPath) -> Void]] { + var actionMap: [[(_ selectedIndexPath: IndexPath) -> Void]] { return [ // Alert style alerts. [ @@ -38,39 +38,39 @@ class AlertControllerViewController : UITableViewController { // MARK: - UIAlertControllerStyleAlert Style Alerts /// Show an alert with an "Okay" button. - func showSimpleAlert(_: NSIndexPath) { + func showSimpleAlert(_: IndexPath) { let title = NSLocalizedString("A Short Title is Best", comment: "") let message = NSLocalizedString("A message should be a short, complete sentence.", comment: "") let cancelButtonTitle = NSLocalizedString("OK", comment: "") - let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) // Create the action. - let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .Cancel) { action in + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { action in NSLog("The simple alert's cancel action occured.") } // Add the action. alertController.addAction(cancelAction) - presentViewController(alertController, animated: true, completion: nil) + present(alertController, animated: true, completion: nil) } /// Show an alert with an "Okay" and "Cancel" button. - func showOkayCancelAlert(_: NSIndexPath) { + func showOkayCancelAlert(_: IndexPath) { let title = NSLocalizedString("A Short Title is Best", comment: "") let message = NSLocalizedString("A message should be a short, complete sentence.", comment: "") let cancelButtonTitle = NSLocalizedString("Cancel", comment: "") let otherButtonTitle = NSLocalizedString("OK", comment: "") - let alertCotroller = UIAlertController(title: title, message: message, preferredStyle: .Alert) + let alertCotroller = UIAlertController(title: title, message: message, preferredStyle: .alert) // Create the actions. - let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .Cancel) { _ in + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in NSLog("The \"Okay/Cancel\" alert's cancel action occured.") } - let otherAction = UIAlertAction(title: otherButtonTitle, style: .Default) { _ in + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in NSLog("The \"Okay/Cancel\" alert's other action occured.") } @@ -78,29 +78,29 @@ class AlertControllerViewController : UITableViewController { alertCotroller.addAction(cancelAction) alertCotroller.addAction(otherAction) - presentViewController(alertCotroller, animated: true, completion: nil) + present(alertCotroller, animated: true, completion: nil) } /// Show an alert with two custom buttons. - func showOtherAlert(_: NSIndexPath) { + func showOtherAlert(_: IndexPath) { let title = NSLocalizedString("A Short Title is Best", comment: "") let message = NSLocalizedString("A message should be a short, complete sentence.", comment: "") let cancelButtonTitle = NSLocalizedString("Cancel", comment: "") let otherButtonTitleOne = NSLocalizedString("Choice One", comment: "") let otherButtonTitleTwo = NSLocalizedString("Choice Two", comment: "") - let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) // Create the actions. - let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .Cancel) { action in + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { action in NSLog("The \"Other\" alert's cancel action occured.") } - let otherButtonOneAction = UIAlertAction(title: otherButtonTitleOne, style: .Default) { _ in + let otherButtonOneAction = UIAlertAction(title: otherButtonTitleOne, style: .default) { _ in NSLog("The \"Other\" alert's other button one action occured.") } - let otherButtonTwoAction = UIAlertAction(title: otherButtonTitleTwo, style: .Default) { _ in + let otherButtonTwoAction = UIAlertAction(title: otherButtonTitleTwo, style: .default) { _ in NSLog("The \"Other\" alert's other button two action occured.") } @@ -109,29 +109,29 @@ class AlertControllerViewController : UITableViewController { alertController.addAction(otherButtonOneAction) alertController.addAction(otherButtonTwoAction) - presentViewController(alertController, animated: true, completion: nil) + present(alertController, animated: true, completion: nil) } /// Show a text entry alert with two custom buttons. - func showTextEntryAlert(_: NSIndexPath) { + func showTextEntryAlert(_: IndexPath) { let title = NSLocalizedString("A Short Title is Best", comment: "") let message = NSLocalizedString("A message should be a short, complete sentence.", comment: "") let cancelButtonTitle = NSLocalizedString("Cancel", comment: "") let otherButtonTitle = NSLocalizedString("OK", comment: "") - let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) // Add the text field for text entry. - alertController.addTextFieldWithConfigurationHandler { textField in + alertController.addTextField { textField in // If you need to customize the text field, you can do so here. } // Create the actions. - let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .Cancel) { _ in + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in NSLog("The \"Text Entry\" alert's cancel action occured.") } - let otherAction = UIAlertAction(title: otherButtonTitle, style: .Default) { _ in + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in NSLog("The \"Text Entry\" alert's other action occured.") } @@ -139,53 +139,53 @@ class AlertControllerViewController : UITableViewController { alertController.addAction(cancelAction) alertController.addAction(otherAction) - presentViewController(alertController, animated: true, completion: nil) + present(alertController, animated: true, completion: nil) } /// Show a secure text entry alert with two custom buttons. - func showSecureTextEntryAlert(_: NSIndexPath) { + func showSecureTextEntryAlert(_: IndexPath) { let title = NSLocalizedString("A Short Title is Best", comment: "") let message = NSLocalizedString("A message should be a short, complete sentence.", comment: "") let cancelButtonTitle = NSLocalizedString("Cancel", comment: "") let otherButtonTitle = NSLocalizedString("OK", comment: "") - let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert) + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) // Add the text field for the secure text entry. - alertController.addTextFieldWithConfigurationHandler { textField in + alertController.addTextField { textField in /* Listen for changes to the text field's text so that we can toggle the current action's enabled property based on whether the user has entered a sufficiently secure entry. */ - NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(AlertControllerViewController.handleTextFieldTextDidChangeNotification(_:)), name: UITextFieldTextDidChangeNotification, object: textField) + NotificationCenter.default.addObserver(self, selector: #selector(AlertControllerViewController.handleTextFieldTextDidChangeNotification(_:)), name: NSNotification.Name.UITextFieldTextDidChange, object: textField) - textField.secureTextEntry = true + textField.isSecureTextEntry = true } /* Stop listening for text change notifications on the text field. This closure will be called in the two action handlers. */ - let removeTextFieldObserver: Void -> Void = { - NSNotificationCenter.defaultCenter().removeObserver(self, name: UITextFieldTextDidChangeNotification, object: alertController.textFields!.first) + let removeTextFieldObserver: (Void) -> Void = { + NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UITextFieldTextDidChange, object: alertController.textFields!.first) } // Create the actions. - let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .Cancel) { _ in + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in NSLog("The \"Secure Text Entry\" alert's cancel action occured.") removeTextFieldObserver() } - let otherAction = UIAlertAction(title: otherButtonTitle, style: .Default) { _ in + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in NSLog("The \"Secure Text Entry\" alert's other action occured.") removeTextFieldObserver() } // The text field initially has no text in the text field, so we'll disable it. - otherAction.enabled = false + otherAction.isEnabled = false /* Hold onto the secure text alert action to toggle the enabled / disabled @@ -197,24 +197,24 @@ class AlertControllerViewController : UITableViewController { alertController.addAction(cancelAction) alertController.addAction(otherAction) - presentViewController(alertController, animated: true, completion: nil) + present(alertController, animated: true, completion: nil) } // MARK: - UIAlertControllerStyleActionSheet Style Alerts /// Show a dialog with an "Okay" and "Cancel" button. - func showOkayCancelActionSheet(selectedIndexPath: NSIndexPath) { + func showOkayCancelActionSheet(_ selectedIndexPath: IndexPath) { let cancelButtonTitle = NSLocalizedString("Cancel", comment: "OK") let destructiveButtonTitle = NSLocalizedString("OK", comment: "") - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .ActionSheet) + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) // Create the actions. - let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .Cancel) { _ in + let cancelAction = UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in NSLog("The \"Okay/Cancel\" alert action sheet's cancel action occured.") } - let destructiveAction = UIAlertAction(title: destructiveButtonTitle, style: .Destructive) { _ in + let destructiveAction = UIAlertAction(title: destructiveButtonTitle, style: .destructive) { _ in NSLog("The \"Okay/Cancel\" alert action sheet's destructive action occured.") } @@ -225,28 +225,28 @@ class AlertControllerViewController : UITableViewController { // Configure the alert controller's popover presentation controller if it has one. if let popoverPresentationController = alertController.popoverPresentationController { // This method expects a valid cell to display from. - let selectedCell = tableView.cellForRowAtIndexPath(selectedIndexPath)! + let selectedCell = tableView.cellForRow(at: selectedIndexPath)! popoverPresentationController.sourceRect = selectedCell.frame popoverPresentationController.sourceView = view - popoverPresentationController.permittedArrowDirections = .Up + popoverPresentationController.permittedArrowDirections = .up } - presentViewController(alertController, animated: true, completion: nil) + present(alertController, animated: true, completion: nil) } /// Show a dialog with two custom buttons. - func showOtherActionSheet(selectedIndexPath: NSIndexPath) { + func showOtherActionSheet(_ selectedIndexPath: IndexPath) { let destructiveButtonTitle = NSLocalizedString("Destructive Choice", comment: "") let otherButtonTitle = NSLocalizedString("Safe Choice", comment: "") - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .ActionSheet) + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) // Create the actions. - let destructiveAction = UIAlertAction(title: destructiveButtonTitle, style: .Destructive) { _ in + let destructiveAction = UIAlertAction(title: destructiveButtonTitle, style: .destructive) { _ in NSLog("The \"Other\" alert action sheet's destructive action occured.") } - let otherAction = UIAlertAction(title: otherButtonTitle, style: .Default) { _ in + let otherAction = UIAlertAction(title: otherButtonTitle, style: .default) { _ in NSLog("The \"Other\" alert action sheet's other action occured.") } @@ -257,36 +257,36 @@ class AlertControllerViewController : UITableViewController { // Configure the alert controller's popover presentation controller if it has one. if let popoverPresentationController = alertController.popoverPresentationController { // This method expects a valid cell to display from. - let selectedCell = tableView.cellForRowAtIndexPath(selectedIndexPath)! + let selectedCell = tableView.cellForRow(at: selectedIndexPath)! popoverPresentationController.sourceRect = selectedCell.frame popoverPresentationController.sourceView = view - popoverPresentationController.permittedArrowDirections = .Up + popoverPresentationController.permittedArrowDirections = .up } - presentViewController(alertController, animated: true, completion: nil) + present(alertController, animated: true, completion: nil) } // MARK: - UITextFieldTextDidChangeNotification - func handleTextFieldTextDidChangeNotification(notification: NSNotification) { + func handleTextFieldTextDidChangeNotification(_ notification: Notification) { let textField = notification.object as! UITextField // Enforce a minimum length of >= 5 characters for secure text alerts. if let text = textField.text { - secureTextAlertAction!.enabled = text.characters.count >= 5 + secureTextAlertAction!.isEnabled = text.characters.count >= 5 } else { - secureTextAlertAction!.enabled = false + secureTextAlertAction!.isEnabled = false } } // MARK: - UITableViewDelegate - override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let action = actionMap[indexPath.section][indexPath.row] - action(selectedIndexPath: indexPath) + action(indexPath) - tableView.deselectRowAtIndexPath(indexPath, animated: true) + tableView.deselectRow(at: indexPath, animated: true) } } diff --git a/UICatalog/Swift/UIKitCatalog/AppDelegate.swift b/UICatalog/Swift/UIKitCatalog/AppDelegate.swift index 9466e845..dcbaa0bf 100644 --- a/UICatalog/Swift/UIKitCatalog/AppDelegate.swift +++ b/UICatalog/Swift/UIKitCatalog/AppDelegate.swift @@ -16,18 +16,18 @@ class AppDelegate: NSObject, UIApplicationDelegate, UISplitViewControllerDelegat // MARK: - UIApplicationDelegate - func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { let splitViewController = window!.rootViewController as! UISplitViewController splitViewController.delegate = self - splitViewController.preferredDisplayMode = .AllVisible + splitViewController.preferredDisplayMode = .allVisible return true } // MARK: - UISplitViewControllerDelegate - func targetDisplayModeForActionInSplitViewController(splitViewController: UISplitViewController) -> UISplitViewControllerDisplayMode { - return .AllVisible + func targetDisplayModeForAction(in splitViewController: UISplitViewController) -> UISplitViewControllerDisplayMode { + return .allVisible } } diff --git a/UICatalog/Swift/UIKitCatalog/ButtonViewController.swift b/UICatalog/Swift/UIKitCatalog/ButtonViewController.swift index 8dde8c33..cb77bef9 100644 --- a/UICatalog/Swift/UIKitCatalog/ButtonViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/ButtonViewController.swift @@ -39,38 +39,38 @@ class ButtonViewController: UITableViewController { func configureSystemTextButton() { let buttonTitle = NSLocalizedString("Button", comment: "") - systemTextButton.setTitle(buttonTitle, forState: .Normal) + systemTextButton.setTitle(buttonTitle, for: UIControlState()) - systemTextButton.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), forControlEvents: .TouchUpInside) + systemTextButton.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) } func configureSystemContactAddButton() { - systemContactAddButton.backgroundColor = UIColor.clearColor() + systemContactAddButton.backgroundColor = UIColor.clear - systemContactAddButton.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), forControlEvents: .TouchUpInside) + systemContactAddButton.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) } func configureSystemDetailDisclosureButton() { - systemDetailDisclosureButton.backgroundColor = UIColor.clearColor() + systemDetailDisclosureButton.backgroundColor = UIColor.clear - systemDetailDisclosureButton.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), forControlEvents: .TouchUpInside) + systemDetailDisclosureButton.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) } func configureImageButton() { // To create this button in code you can use UIButton.buttonWithType() with a parameter value of .Custom. // Remove the title text. - imageButton.setTitle("", forState: .Normal) + imageButton.setTitle("", for: UIControlState()) imageButton.tintColor = UIColor.applicationPurpleColor let imageButtonNormalImage = UIImage(named: "x_icon") - imageButton.setImage(imageButtonNormalImage, forState: .Normal) + imageButton.setImage(imageButtonNormalImage, for: UIControlState()) // Add an accessibility label to the image. imageButton.accessibilityLabel = NSLocalizedString("X Button", comment: "") - imageButton.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), forControlEvents: .TouchUpInside) + imageButton.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) } func configureAttributedTextSystemButton() { @@ -79,25 +79,25 @@ class ButtonViewController: UITableViewController { // Set the button's title for normal state. let normalTitleAttributes = [ NSForegroundColorAttributeName: UIColor.applicationBlueColor, - NSStrikethroughStyleAttributeName: NSUnderlineStyle.StyleSingle.rawValue - ] + NSStrikethroughStyleAttributeName: NSUnderlineStyle.styleSingle.rawValue + ] as [String : Any] let normalAttributedTitle = NSAttributedString(string: buttonTitle, attributes: normalTitleAttributes) - attributedTextButton.setAttributedTitle(normalAttributedTitle, forState: .Normal) + attributedTextButton.setAttributedTitle(normalAttributedTitle, for: UIControlState()) // Set the button's title for highlighted state. let highlightedTitleAttributes = [ - NSForegroundColorAttributeName: UIColor.greenColor(), - NSStrikethroughStyleAttributeName: NSUnderlineStyle.StyleThick.rawValue - ] + NSForegroundColorAttributeName: UIColor.green, + NSStrikethroughStyleAttributeName: NSUnderlineStyle.styleThick.rawValue + ] as [String : Any] let highlightedAttributedTitle = NSAttributedString(string: buttonTitle, attributes: highlightedTitleAttributes) - attributedTextButton.setAttributedTitle(highlightedAttributedTitle, forState: .Highlighted) + attributedTextButton.setAttributedTitle(highlightedAttributedTitle, for: .highlighted) - attributedTextButton.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), forControlEvents: .TouchUpInside) + attributedTextButton.addTarget(self, action: #selector(ButtonViewController.buttonClicked(_:)), for: .touchUpInside) } // MARK: - Actions - func buttonClicked(sender: UIButton) { + func buttonClicked(_ sender: UIButton) { NSLog("A button was clicked: \(sender).") } } diff --git a/UICatalog/Swift/UIKitCatalog/CustomSearchBarViewController.swift b/UICatalog/Swift/UIKitCatalog/CustomSearchBarViewController.swift index ac077e13..be49465a 100644 --- a/UICatalog/Swift/UIKitCatalog/CustomSearchBarViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/CustomSearchBarViewController.swift @@ -33,27 +33,27 @@ class CustomSearchBarViewController: UIViewController, UISearchBarDelegate { // Set the bookmark image for both normal and highlighted states. let bookmarkImage = UIImage(named: "bookmark_icon") - searchBar.setImage(bookmarkImage, forSearchBarIcon: .Bookmark, state: .Normal) + searchBar.setImage(bookmarkImage, for: .bookmark, state: UIControlState()) let bookmarkHighlightedImage = UIImage(named: "bookmark_icon_highlighted") - searchBar.setImage(bookmarkHighlightedImage, forSearchBarIcon: .Bookmark, state: .Highlighted) + searchBar.setImage(bookmarkHighlightedImage, for: .bookmark, state: .highlighted) } // MARK: - UISearchBarDelegate - func searchBarSearchButtonClicked(searchBar: UISearchBar) { + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { NSLog("The custom search bar keyboard search button was tapped: \(searchBar).") searchBar.resignFirstResponder() } - func searchBarCancelButtonClicked(searchBar: UISearchBar) { + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { NSLog("The custom search bar cancel button was tapped.") searchBar.resignFirstResponder() } - func searchBarBookmarkButtonClicked(searchBar: UISearchBar) { + func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) { NSLog("The custom bookmark button inside the search bar was tapped.") } } diff --git a/UICatalog/Swift/UIKitCatalog/CustomToolbarViewController.swift b/UICatalog/Swift/UIKitCatalog/CustomToolbarViewController.swift index 7d9b69b0..ce17c1e3 100644 --- a/UICatalog/Swift/UIKitCatalog/CustomToolbarViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/CustomToolbarViewController.swift @@ -25,7 +25,7 @@ class CustomToolbarViewController: UIViewController { func configureToolbar() { let toolbarBackgroundImage = UIImage(named: "toolbar_background") - toolbar.setBackgroundImage(toolbarBackgroundImage, forToolbarPosition: .Bottom, barMetrics: .Default) + toolbar.setBackgroundImage(toolbarBackgroundImage, forToolbarPosition: .bottom, barMetrics: .default) let toolbarButtonItems = [ customImageBarButtonItem, @@ -41,7 +41,7 @@ class CustomToolbarViewController: UIViewController { var customImageBarButtonItem: UIBarButtonItem { let customBarButtonItemImage = UIImage(named: "tools_icon") - let customImageBarButtonItem = UIBarButtonItem(image: customBarButtonItemImage, style: .Plain, target: self, action: #selector(CustomToolbarViewController.barButtonItemClicked(_:))) + let customImageBarButtonItem = UIBarButtonItem(image: customBarButtonItemImage, style: .plain, target: self, action: #selector(CustomToolbarViewController.barButtonItemClicked(_:))) customImageBarButtonItem.tintColor = UIColor.applicationPurpleColor @@ -50,26 +50,26 @@ class CustomToolbarViewController: UIViewController { var flexibleSpaceBarButtonItem: UIBarButtonItem { // Note that there's no target/action since this represents empty space. - return UIBarButtonItem(barButtonSystemItem: .FlexibleSpace, target: nil, action: nil) + return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) } var customBarButtonItem: UIBarButtonItem { - let barButtonItem = UIBarButtonItem(title: NSLocalizedString("Button", comment: ""), style: .Plain, target: self, action: #selector(CustomToolbarViewController.barButtonItemClicked(_:))) + let barButtonItem = UIBarButtonItem(title: NSLocalizedString("Button", comment: ""), style: .plain, target: self, action: #selector(CustomToolbarViewController.barButtonItemClicked(_:))) let backgroundImage = UIImage(named: "WhiteButton") - barButtonItem.setBackgroundImage(backgroundImage, forState: .Normal, barMetrics: .Default) + barButtonItem.setBackgroundImage(backgroundImage, for: UIControlState(), barMetrics: .default) let attributes = [ NSForegroundColorAttributeName: UIColor.applicationPurpleColor ] - barButtonItem.setTitleTextAttributes(attributes, forState: .Normal) + barButtonItem.setTitleTextAttributes(attributes, for: UIControlState()) return barButtonItem } // MARK: - Actions - func barButtonItemClicked(barButtonItem: UIBarButtonItem) { + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { NSLog("A bar button item on the custom toolbar was clicked: \(barButtonItem).") } } diff --git a/UICatalog/Swift/UIKitCatalog/DatePickerController.swift b/UICatalog/Swift/UIKitCatalog/DatePickerController.swift index 1d7724ff..5e612825 100644 --- a/UICatalog/Swift/UIKitCatalog/DatePickerController.swift +++ b/UICatalog/Swift/UIKitCatalog/DatePickerController.swift @@ -16,11 +16,11 @@ class DatePickerController: UIViewController { @IBOutlet weak var dateLabel: UILabel! /// A date formatter to format the `date` property of `datePicker`. - lazy var dateFormatter: NSDateFormatter = { - let dateFormatter = NSDateFormatter() + lazy var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .MediumStyle - dateFormatter.timeStyle = .ShortStyle + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short return dateFormatter }() @@ -36,26 +36,26 @@ class DatePickerController: UIViewController { // MARK: - Configuration func configureDatePicker() { - datePicker.datePickerMode = .DateAndTime + datePicker.datePickerMode = .dateAndTime /* Set min/max date for the date picker. As an example we will limit the date between now and 7 days from now. */ - let now = NSDate() + let now = Date() datePicker.minimumDate = now - let currentCalendar = NSCalendar.currentCalendar() + let currentCalendar = Calendar.current - let dateComponents = NSDateComponents() + var dateComponents = DateComponents() dateComponents.day = 7 - let sevenDaysFromNow = currentCalendar.dateByAddingComponents(dateComponents, toDate: now, options: []) + let sevenDaysFromNow = (currentCalendar as NSCalendar).date(byAdding: dateComponents, to: now, options: []) datePicker.maximumDate = sevenDaysFromNow datePicker.minuteInterval = 2 - datePicker.addTarget(self, action: #selector(DatePickerController.updateDatePickerLabel), forControlEvents: .ValueChanged) + datePicker.addTarget(self, action: #selector(DatePickerController.updateDatePickerLabel), for: .valueChanged) updateDatePickerLabel() } @@ -63,6 +63,6 @@ class DatePickerController: UIViewController { // MARK: - Actions func updateDatePickerLabel() { - dateLabel.text = dateFormatter.stringFromDate(datePicker.date) + dateLabel.text = dateFormatter.string(from: datePicker.date) } } diff --git a/UICatalog/Swift/UIKitCatalog/DefaultSearchBarViewController.swift b/UICatalog/Swift/UIKitCatalog/DefaultSearchBarViewController.swift index 6e9cbdc8..d6ed17e3 100644 --- a/UICatalog/Swift/UIKitCatalog/DefaultSearchBarViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/DefaultSearchBarViewController.swift @@ -35,17 +35,17 @@ class DefaultSearchBarViewController: UIViewController, UISearchBarDelegate { // MARK: - UISearchBarDelegate - func searchBar(searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { + func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { NSLog("The default search selected scope button index changed to \(selectedScope).") } - func searchBarSearchButtonClicked(searchBar: UISearchBar) { + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { NSLog("The default search bar keyboard search button was tapped: \(searchBar.text).") searchBar.resignFirstResponder() } - func searchBarCancelButtonClicked(searchBar: UISearchBar) { + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { NSLog("The default search bar cancel button was tapped.") searchBar.resignFirstResponder() diff --git a/UICatalog/Swift/UIKitCatalog/DefaultToolbarViewController.swift b/UICatalog/Swift/UIKitCatalog/DefaultToolbarViewController.swift index d2077bf5..4217bfd3 100644 --- a/UICatalog/Swift/UIKitCatalog/DefaultToolbarViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/DefaultToolbarViewController.swift @@ -36,22 +36,22 @@ class DefaultToolbarViewController: UIViewController { // MARK: - UIBarButtonItem Creation and Configuration var trashBarButtonItem: UIBarButtonItem { - return UIBarButtonItem(barButtonSystemItem: .Trash, target: self, action: #selector(DefaultToolbarViewController.barButtonItemClicked(_:))) + return UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(DefaultToolbarViewController.barButtonItemClicked(_:))) } var flexibleSpaceBarButtonItem: UIBarButtonItem { - return UIBarButtonItem(barButtonSystemItem: .FlexibleSpace, target: nil, action: nil) + return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) } var customTitleBarButtonItem: UIBarButtonItem { let customTitle = NSLocalizedString("Action", comment: "") - return UIBarButtonItem(title: customTitle, style: .Plain, target: self, action: #selector(DefaultToolbarViewController.barButtonItemClicked(_:))) + return UIBarButtonItem(title: customTitle, style: .plain, target: self, action: #selector(DefaultToolbarViewController.barButtonItemClicked(_:))) } // MARK: - Actions - func barButtonItemClicked(barButtonItem: UIBarButtonItem) { + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { NSLog("A bar button item on the default toolbar was clicked: \(barButtonItem).") } } diff --git a/UICatalog/Swift/UIKitCatalog/ImageViewController.swift b/UICatalog/Swift/UIKitCatalog/ImageViewController.swift index 509ef887..b9016da0 100644 --- a/UICatalog/Swift/UIKitCatalog/ImageViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/ImageViewController.swift @@ -27,13 +27,13 @@ class ImageViewController: UIViewController { imageView.animationImages = (1...5).map { UIImage(named: "image_animal_\($0)")! } // We want the image to be scaled to the correct aspect ratio within imageView's bounds. - imageView.contentMode = .ScaleAspectFit + imageView.contentMode = .scaleAspectFit /* If the image does not have the same aspect ratio as imageView's bounds, then imageView's backgroundColor will be applied to the "empty" space. */ - imageView.backgroundColor = UIColor.whiteColor() + imageView.backgroundColor = UIColor.white imageView.animationDuration = 5 imageView.startAnimating() diff --git a/UICatalog/Swift/UIKitCatalog/PageControlViewController.swift b/UICatalog/Swift/UIKitCatalog/PageControlViewController.swift index 949635ba..972866ae 100644 --- a/UICatalog/Swift/UIKitCatalog/PageControlViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/PageControlViewController.swift @@ -17,16 +17,16 @@ class PageControlViewController: UIViewController { /// Colors that correspond to the selected page. Used as the background color for `colorView`. let colors = [ - UIColor.blackColor(), - UIColor.grayColor(), - UIColor.redColor(), - UIColor.greenColor(), - UIColor.blueColor(), - UIColor.cyanColor(), - UIColor.yellowColor(), - UIColor.magentaColor(), - UIColor.orangeColor(), - UIColor.purpleColor() + UIColor.black, + UIColor.gray, + UIColor.red, + UIColor.green, + UIColor.blue, + UIColor.cyan, + UIColor.yellow, + UIColor.magenta, + UIColor.orange, + UIColor.purple ] // MARK: - View Life Cycle @@ -49,7 +49,7 @@ class PageControlViewController: UIViewController { pageControl.pageIndicatorTintColor = UIColor.applicationGreenColor pageControl.currentPageIndicatorTintColor = UIColor.applicationPurpleColor - pageControl.addTarget(self, action: #selector(PageControlViewController.pageControlValueDidChange), forControlEvents: .ValueChanged) + pageControl.addTarget(self, action: #selector(PageControlViewController.pageControlValueDidChange), for: .valueChanged) } // MARK: - Actions diff --git a/UICatalog/Swift/UIKitCatalog/PickerViewController.swift b/UICatalog/Swift/UIKitCatalog/PickerViewController.swift index 65e739db..a6126cbc 100644 --- a/UICatalog/Swift/UIKitCatalog/PickerViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/PickerViewController.swift @@ -12,10 +12,10 @@ class PickerViewController: UIViewController, UIPickerViewDataSource, UIPickerVi // MARK: - Types enum ColorComponent: Int { - case Red = 0, Green, Blue + case red = 0, green, blue static var count: Int { - return ColorComponent.Blue.rawValue + 1 + return ColorComponent.blue.rawValue + 1 } } @@ -72,7 +72,7 @@ class PickerViewController: UIViewController, UIPickerViewDataSource, UIPickerVi pickerView.showsSelectionIndicator = true // Set the default selected rows (the desired rows to initially select will vary from app to app). - let selectedRows: [ColorComponent: Int] = [.Red: 13, .Green: 41, .Blue: 24] + let selectedRows: [ColorComponent: Int] = [.red: 13, .green: 41, .blue: 24] for (colorComponent, selectedRow) in selectedRows { /* @@ -87,17 +87,17 @@ class PickerViewController: UIViewController, UIPickerViewDataSource, UIPickerVi // MARK: - UIPickerViewDataSource - func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int { + func numberOfComponents(in pickerView: UIPickerView) -> Int { return ColorComponent.count } - func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return numberOfColorValuesPerComponent } // MARK: - UIPickerViewDelegate - func pickerView(pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { + func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { let colorValue = CGFloat(row) * RGB.offset let value = CGFloat(colorValue) / RGB.max @@ -106,13 +106,13 @@ class PickerViewController: UIViewController, UIPickerViewDataSource, UIPickerVi var blueColorComponent = RGB.min switch ColorComponent(rawValue: component)! { - case .Red: + case .red: redColorComponent = value - case .Green: + case .green: greenColorComponent = value - case .Blue: + case .blue: blueColorComponent = value } @@ -128,32 +128,32 @@ class PickerViewController: UIViewController, UIPickerViewDataSource, UIPickerVi return title } - func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { let colorComponentValue = RGB.offset * CGFloat(row) / RGB.max switch ColorComponent(rawValue: component)! { - case .Red: + case .red: redColor = colorComponentValue - case .Green: + case .green: greenColor = colorComponentValue - case .Blue: + case .blue: blueColor = colorComponentValue } } // MARK: - UIPickerViewAccessibilityDelegate - func pickerView(pickerView: UIPickerView, accessibilityLabelForComponent component: Int) -> String? { + func pickerView(_ pickerView: UIPickerView, accessibilityLabelForComponent component: Int) -> String? { switch ColorComponent(rawValue: component)! { - case .Red: + case .red: return NSLocalizedString("Red color component value", comment: "") - case .Green: + case .green: return NSLocalizedString("Green color component value", comment: "") - case .Blue: + case .blue: return NSLocalizedString("Blue color component value", comment: "") } } diff --git a/UICatalog/Swift/UIKitCatalog/ProgressViewController.swift b/UICatalog/Swift/UIKitCatalog/ProgressViewController.swift index e0ac2839..02da2318 100644 --- a/UICatalog/Swift/UIKitCatalog/ProgressViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/ProgressViewController.swift @@ -29,13 +29,13 @@ class ProgressViewController: UITableViewController { An `NSProgress` object who's `fractionCompleted` is observed using KVO to update the `UIProgressView`s' `progress` properties. */ - private let progress = NSProgress(totalUnitCount: 10) + fileprivate let progress = Progress(totalUnitCount: 10) /** A repeating timer that, when fired, updates the `NSProgress` object's `completedUnitCount` property. */ - private var updateTimer: NSTimer? + fileprivate var updateTimer: Timer? // MARK: - Initialization @@ -43,7 +43,7 @@ class ProgressViewController: UITableViewController { super.init(coder: aDecoder) // Register as an observer of the `NSProgress`'s `fractionCompleted` property. - progress.addObserver(self, forKeyPath: "fractionCompleted", options: [.New], context: &progressViewKVOContext) + progress.addObserver(self, forKeyPath: "fractionCompleted", options: [.new], context: &progressViewKVOContext) } deinit { @@ -61,7 +61,7 @@ class ProgressViewController: UITableViewController { configureTintedProgressView() } - override func viewDidAppear(animated: Bool) { + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // Reset the completed progress of the `UIProgressView`s. @@ -74,10 +74,10 @@ class ProgressViewController: UITableViewController { a repeating timer to increment it over time. */ progress.completedUnitCount = 0 - updateTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(ProgressViewController.timerDidFire), userInfo: nil, repeats: true) + updateTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(ProgressViewController.timerDidFire), userInfo: nil, repeats: true) } - override func viewDidDisappear(animated: Bool) { + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) // Stop the timer from firing. @@ -86,31 +86,31 @@ class ProgressViewController: UITableViewController { // MARK: - Key Value Observing (KVO) - override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String: AnyObject]?, context: UnsafeMutablePointer) { + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { // Check if this is the KVO notification for our `NSProgress` object. - if context == &progressViewKVOContext && keyPath == "fractionCompleted" && object === progress { + if context == &progressViewKVOContext && keyPath == "fractionCompleted" && object as AnyObject? === progress { // Update the progress views. for progressView in progressViews { progressView.setProgress(Float(progress.fractionCompleted), animated: true) } } else { - super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context) + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } } // MARK: - Configuration func configureDefaultStyleProgressView() { - defaultStyleProgressView.progressViewStyle = .Default + defaultStyleProgressView.progressViewStyle = .default } func configureBarStyleProgressView() { - barStyleProgressView.progressViewStyle = .Bar + barStyleProgressView.progressViewStyle = .bar } func configureTintedProgressView() { - tintedProgressView.progressViewStyle = .Default + tintedProgressView.progressViewStyle = .default tintedProgressView.trackTintColor = UIColor.applicationBlueColor tintedProgressView.progressTintColor = UIColor.applicationPurpleColor diff --git a/UICatalog/Swift/UIKitCatalog/SearchBarEmbeddedInNavigationBarViewController.swift b/UICatalog/Swift/UIKitCatalog/SearchBarEmbeddedInNavigationBarViewController.swift index 4639be20..6795b107 100644 --- a/UICatalog/Swift/UIKitCatalog/SearchBarEmbeddedInNavigationBarViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/SearchBarEmbeddedInNavigationBarViewController.swift @@ -20,7 +20,7 @@ class SearchBarEmbeddedInNavigationBarViewController: SearchControllerBaseViewCo super.viewDidLoad() // Create the search results view controller and use it for the `UISearchController`. - let searchResultsController = storyboard!.instantiateViewControllerWithIdentifier(SearchResultsViewController.StoryboardConstants.identifier) as! SearchResultsViewController + let searchResultsController = storyboard!.instantiateViewController(withIdentifier: SearchResultsViewController.StoryboardConstants.identifier) as! SearchResultsViewController // Create the search controller and make it perform the results updating. searchController = UISearchController(searchResultsController: searchResultsController) @@ -31,7 +31,7 @@ class SearchBarEmbeddedInNavigationBarViewController: SearchControllerBaseViewCo Configure the search controller's search bar. For more information on how to configure search bars, see the "Search Bar" group under "Search". */ - searchController.searchBar.searchBarStyle = .Minimal + searchController.searchBar.searchBarStyle = .minimal searchController.searchBar.placeholder = NSLocalizedString("Search", comment: "") // Include the search bar within the navigation bar. diff --git a/UICatalog/Swift/UIKitCatalog/SearchControllerBaseViewController.swift b/UICatalog/Swift/UIKitCatalog/SearchControllerBaseViewController.swift index fdd8d0eb..a23dd04e 100644 --- a/UICatalog/Swift/UIKitCatalog/SearchControllerBaseViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/SearchControllerBaseViewController.swift @@ -31,7 +31,7 @@ class SearchControllerBaseViewController: UITableViewController { // Filter the results using a predicate based on the filter string. let filterPredicate = NSPredicate(format: "self contains[c] %@", argumentArray: [filterString!]) - visibleResults = allResults.filter { filterPredicate.evaluateWithObject($0) } + visibleResults = allResults.filter { filterPredicate.evaluate(with: $0) } } tableView.reloadData() @@ -40,15 +40,15 @@ class SearchControllerBaseViewController: UITableViewController { // MARK: - UITableViewDataSource - override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return visibleResults.count } - override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { - return tableView.dequeueReusableCellWithIdentifier(TableViewConstants.tableViewCellIdentifier, forIndexPath: indexPath) + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return tableView.dequeueReusableCell(withIdentifier: TableViewConstants.tableViewCellIdentifier, for: indexPath) } - override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { cell.textLabel!.text = visibleResults[indexPath.row] } } diff --git a/UICatalog/Swift/UIKitCatalog/SearchPresentOverNavigationBarViewController.swift b/UICatalog/Swift/UIKitCatalog/SearchPresentOverNavigationBarViewController.swift index 731f0518..af456528 100644 --- a/UICatalog/Swift/UIKitCatalog/SearchPresentOverNavigationBarViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/SearchPresentOverNavigationBarViewController.swift @@ -16,9 +16,9 @@ class SearchPresentOverNavigationBarViewController: SearchControllerBaseViewCont // MARK: - Actions - @IBAction func searchButtonClicked(button: UIBarButtonItem) { + @IBAction func searchButtonClicked(_ button: UIBarButtonItem) { // Create the search results view controller and use it for the `UISearchController`. - let searchResultsController = storyboard!.instantiateViewControllerWithIdentifier(SearchResultsViewController.StoryboardConstants.identifier) as! SearchResultsViewController + let searchResultsController = storyboard!.instantiateViewController(withIdentifier: SearchResultsViewController.StoryboardConstants.identifier) as! SearchResultsViewController // Create the search controller and make it perform the results updating. searchController = UISearchController(searchResultsController: searchResultsController) @@ -26,6 +26,6 @@ class SearchPresentOverNavigationBarViewController: SearchControllerBaseViewCont searchController.hidesNavigationBarDuringPresentation = false // Present the view controller. - presentViewController(searchController, animated: true, completion: nil) + present(searchController, animated: true, completion: nil) } } diff --git a/UICatalog/Swift/UIKitCatalog/SearchResultsViewController.swift b/UICatalog/Swift/UIKitCatalog/SearchResultsViewController.swift index 720c4d1e..8c0103a5 100644 --- a/UICatalog/Swift/UIKitCatalog/SearchResultsViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/SearchResultsViewController.swift @@ -21,14 +21,14 @@ class SearchResultsViewController: SearchControllerBaseViewController, UISearchR // MARK: - UISearchResultsUpdating - func updateSearchResultsForSearchController(searchController: UISearchController) { + func updateSearchResults(for searchController: UISearchController) { /* `updateSearchResultsForSearchController(_:)` is called when the controller is being dismissed to allow those who are using the controller they are search as the results controller a chance to reset their state. No need to update anything if we're being dismissed. */ - guard searchController.active else { return } + guard searchController.isActive else { return } filterString = searchController.searchBar.text } diff --git a/UICatalog/Swift/UIKitCatalog/SegmentedControlViewController.swift b/UICatalog/Swift/UIKitCatalog/SegmentedControlViewController.swift index 85c57f73..a26b2384 100644 --- a/UICatalog/Swift/UIKitCatalog/SegmentedControlViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/SegmentedControlViewController.swift @@ -34,9 +34,9 @@ class SegmentedControlViewController: UITableViewController { func configureDefaultSegmentedControl() { - defaultSegmentedControl.setEnabled(false, forSegmentAtIndex: 0) + defaultSegmentedControl.setEnabled(false, forSegmentAt: 0) - defaultSegmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), forControlEvents: .ValueChanged) + defaultSegmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) } func configureTintedSegmentedControl() { @@ -44,7 +44,7 @@ class SegmentedControlViewController: UITableViewController { tintedSegmentedControl.selectedSegmentIndex = 1 - tintedSegmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), forControlEvents: .ValueChanged) + tintedSegmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) } func configureCustomSegmentsSegmentedControl() { @@ -56,21 +56,21 @@ class SegmentedControlViewController: UITableViewController { // Guarantee that the segments show up in the same order. var sortedSegmentImageNames = Array(imageToAccessibilityLabelMappings.keys) - sortedSegmentImageNames.sortInPlace { lhs, rhs in - return lhs.localizedStandardCompare(rhs) == NSComparisonResult.OrderedAscending + sortedSegmentImageNames.sort { lhs, rhs in + return lhs.localizedStandardCompare(rhs) == ComparisonResult.orderedAscending } - for (idx, segmentImageName) in sortedSegmentImageNames.enumerate() { + for (idx, segmentImageName) in sortedSegmentImageNames.enumerated() { let image = UIImage(named: segmentImageName)! image.accessibilityLabel = imageToAccessibilityLabelMappings[segmentImageName] - customSegmentsSegmentedControl.setImage(image, forSegmentAtIndex: idx) + customSegmentsSegmentedControl.setImage(image, forSegmentAt: idx) } customSegmentsSegmentedControl.selectedSegmentIndex = 0 - customSegmentsSegmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), forControlEvents: .ValueChanged) + customSegmentsSegmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) } @@ -79,40 +79,40 @@ class SegmentedControlViewController: UITableViewController { // Set the background images for each control state. let normalSegmentBackgroundImage = UIImage(named: "stepper_and_segment_background") - customBackgroundSegmentedControl.setBackgroundImage(normalSegmentBackgroundImage, forState: .Normal, barMetrics: .Default) + customBackgroundSegmentedControl.setBackgroundImage(normalSegmentBackgroundImage, for: UIControlState(), barMetrics: .default) let disabledSegmentBackgroundImage = UIImage(named: "stepper_and_segment_background_disabled") - customBackgroundSegmentedControl.setBackgroundImage(disabledSegmentBackgroundImage, forState: .Disabled, barMetrics: .Default) + customBackgroundSegmentedControl.setBackgroundImage(disabledSegmentBackgroundImage, for: .disabled, barMetrics: .default) let highlightedSegmentBackgroundImage = UIImage(named: "stepper_and_segment_background_highlighted") - customBackgroundSegmentedControl.setBackgroundImage(highlightedSegmentBackgroundImage, forState: .Highlighted, barMetrics: .Default) + customBackgroundSegmentedControl.setBackgroundImage(highlightedSegmentBackgroundImage, for: .highlighted, barMetrics: .default) // Set the divider image. let segmentDividerImage = UIImage(named: "stepper_and_segment_divider") - customBackgroundSegmentedControl.setDividerImage(segmentDividerImage, forLeftSegmentState: .Normal, rightSegmentState: .Normal, barMetrics: .Default) + customBackgroundSegmentedControl.setDividerImage(segmentDividerImage, forLeftSegmentState: UIControlState(), rightSegmentState: UIControlState(), barMetrics: .default) // Create a font to use for the attributed title (both normal and highlighted states). - let captionFontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleCaption1) + let captionFontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.caption1) let font = UIFont(descriptor: captionFontDescriptor, size: 0) let normalTextAttributes = [ NSForegroundColorAttributeName: UIColor.applicationPurpleColor, NSFontAttributeName: font ] - customBackgroundSegmentedControl.setTitleTextAttributes(normalTextAttributes, forState: .Normal) + customBackgroundSegmentedControl.setTitleTextAttributes(normalTextAttributes, for: UIControlState()) let highlightedTextAttributes = [ NSForegroundColorAttributeName: UIColor.applicationGreenColor, NSFontAttributeName: font ] - customBackgroundSegmentedControl.setTitleTextAttributes(highlightedTextAttributes, forState: .Highlighted) + customBackgroundSegmentedControl.setTitleTextAttributes(highlightedTextAttributes, for: .highlighted) - customBackgroundSegmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), forControlEvents: .ValueChanged) + customBackgroundSegmentedControl.addTarget(self, action: #selector(SegmentedControlViewController.selectedSegmentDidChange(_:)), for: .valueChanged) } // MARK: - Actions - func selectedSegmentDidChange(segmentedControl: UISegmentedControl) { + func selectedSegmentDidChange(_ segmentedControl: UISegmentedControl) { NSLog("The selected segment changed for: \(segmentedControl).") } } diff --git a/UICatalog/Swift/UIKitCatalog/SliderViewController.swift b/UICatalog/Swift/UIKitCatalog/SliderViewController.swift index f18691d0..c8e18938 100644 --- a/UICatalog/Swift/UIKitCatalog/SliderViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/SliderViewController.swift @@ -33,39 +33,39 @@ class SliderViewController: UITableViewController { defaultSlider.minimumValue = 0 defaultSlider.maximumValue = 100 defaultSlider.value = 42 - defaultSlider.continuous = true + defaultSlider.isContinuous = true - defaultSlider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), forControlEvents: .ValueChanged) + defaultSlider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) } func configureTintedSlider() { tintedSlider.minimumTrackTintColor = UIColor.applicationBlueColor tintedSlider.maximumTrackTintColor = UIColor.applicationPurpleColor - tintedSlider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), forControlEvents: .ValueChanged) + tintedSlider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) } func configureCustomSlider() { let leftTrackImage = UIImage(named: "slider_blue_track") - customSlider.setMinimumTrackImage(leftTrackImage, forState: .Normal) + customSlider.setMinimumTrackImage(leftTrackImage, for: UIControlState()) let rightTrackImage = UIImage(named: "slider_green_track") - customSlider.setMaximumTrackImage(rightTrackImage, forState: .Normal) + customSlider.setMaximumTrackImage(rightTrackImage, for: UIControlState()) let thumbImage = UIImage(named: "slider_thumb") - customSlider.setThumbImage(thumbImage, forState: .Normal) + customSlider.setThumbImage(thumbImage, for: UIControlState()) customSlider.minimumValue = 0 customSlider.maximumValue = 100 - customSlider.continuous = false + customSlider.isContinuous = false customSlider.value = 84 - customSlider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), forControlEvents: .ValueChanged) + customSlider.addTarget(self, action: #selector(SliderViewController.sliderValueDidChange(_:)), for: .valueChanged) } // MARK: - Actions - func sliderValueDidChange(slider: UISlider) { + func sliderValueDidChange(_ slider: UISlider) { NSLog("A slider changed its value: \(slider).") } } diff --git a/UICatalog/Swift/UIKitCatalog/StackViewController.swift b/UICatalog/Swift/UIKitCatalog/StackViewController.swift index e5a91c7c..eb9d138f 100644 --- a/UICatalog/Swift/UIKitCatalog/StackViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/StackViewController.swift @@ -25,11 +25,11 @@ class StackViewController: UIViewController { // MARK: - View Life Cycle - override func viewWillAppear(animated: Bool) { + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - furtherDetailStackView.hidden = true - plusButton.hidden = false + furtherDetailStackView.isHidden = true + plusButton.isHidden = false updateAddRemoveButtons() } @@ -37,30 +37,30 @@ class StackViewController: UIViewController { @IBAction func showFurtherDetail(_: AnyObject) { // Animate the changes by performing them in a `UIView` animation block. - UIView.animateWithDuration(0.25) { + UIView.animate(withDuration: 0.25, animations: { // Reveal the further details stack view and hide the plus button. - self.furtherDetailStackView.hidden = false - self.plusButton.hidden = true - } + self.furtherDetailStackView.isHidden = false + self.plusButton.isHidden = true + }) } @IBAction func hideFurtherDetail(_: AnyObject) { // Animate the changes by performing them in a `UIView` animation block. - UIView.animateWithDuration(0.25) { + UIView.animate(withDuration: 0.25, animations: { // Hide the further details stack view and reveal the plus button. - self.furtherDetailStackView.hidden = true - self.plusButton.hidden = false - } + self.furtherDetailStackView.isHidden = true + self.plusButton.isHidden = false + }) } @IBAction func addArrangedSubviewToStack(_: AnyObject) { // Create a simple, fixed-size, square view to add to the stack view let newViewSize = CGSize(width: 50, height: 50) - let newView = UIView(frame: CGRect(origin: CGPointZero, size: newViewSize)) + let newView = UIView(frame: CGRect(origin: CGPoint.zero, size: newViewSize)) newView.backgroundColor = randomColor() - newView.widthAnchor.constraintEqualToConstant(newViewSize.width).active = true - newView.heightAnchor.constraintEqualToConstant(newViewSize.height).active = true + newView.widthAnchor.constraint(equalToConstant: newViewSize.width).isActive = true + newView.heightAnchor.constraint(equalToConstant: newViewSize.height).isActive = true /* Adding an arranged subview automatically adds it as a child of the @@ -89,14 +89,14 @@ class StackViewController: UIViewController { // MARK: - Convenience - private func updateAddRemoveButtons() { + fileprivate func updateAddRemoveButtons() { let arrangedSubviewCount = addRemoveExampleStackView.arrangedSubviews.count - addArrangedViewButton.enabled = arrangedSubviewCount < maximumArrangedSubviewCount - removeArrangedViewButton.enabled = arrangedSubviewCount > 0 + addArrangedViewButton.isEnabled = arrangedSubviewCount < maximumArrangedSubviewCount + removeArrangedViewButton.isEnabled = arrangedSubviewCount > 0 } - private func randomColor() -> UIColor { + fileprivate func randomColor() -> UIColor { let red = CGFloat(arc4random_uniform(255)) / 255.0 let green = CGFloat(arc4random_uniform(255)) / 255.0 let blue = CGFloat(arc4random_uniform(255)) / 255.0 diff --git a/UICatalog/Swift/UIKitCatalog/StepperViewController.swift b/UICatalog/Swift/UIKitCatalog/StepperViewController.swift index ae7f051b..26c2b7b0 100644 --- a/UICatalog/Swift/UIKitCatalog/StepperViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/StepperViewController.swift @@ -42,49 +42,49 @@ class StepperViewController: UITableViewController { defaultStepper.stepValue = 1 defaultStepperLabel.text = "\(Int(defaultStepper.value))" - defaultStepper.addTarget(self, action: #selector(StepperViewController.stepperValueDidChange(_:)), forControlEvents: .ValueChanged) + defaultStepper.addTarget(self, action: #selector(StepperViewController.stepperValueDidChange(_:)), for: .valueChanged) } func configureTintedStepper() { tintedStepper.tintColor = UIColor.applicationBlueColor tintedStepperLabel.text = "\(Int(tintedStepper.value))" - tintedStepper.addTarget(self, action: #selector(StepperViewController.stepperValueDidChange(_:)), forControlEvents: .ValueChanged) + tintedStepper.addTarget(self, action: #selector(StepperViewController.stepperValueDidChange(_:)), for: .valueChanged) } func configureCustomStepper() { // Set the background image. let stepperBackgroundImage = UIImage(named: "stepper_and_segment_background") - customStepper.setBackgroundImage(stepperBackgroundImage, forState: .Normal) + customStepper.setBackgroundImage(stepperBackgroundImage, for: UIControlState()) let stepperHighlightedBackgroundImage = UIImage(named: "stepper_and_segment_background_highlighted") - customStepper.setBackgroundImage(stepperHighlightedBackgroundImage, forState: .Highlighted) + customStepper.setBackgroundImage(stepperHighlightedBackgroundImage, for: .highlighted) let stepperDisabledBackgroundImage = UIImage(named: "stepper_and_segment_background_disabled") - customStepper.setBackgroundImage(stepperDisabledBackgroundImage, forState: .Disabled) + customStepper.setBackgroundImage(stepperDisabledBackgroundImage, for: .disabled) /* Set the image which will be painted in between the two stepper segments (depends on the states of both segments). */ let stepperSegmentDividerImage = UIImage(named: "stepper_and_segment_divider") - customStepper.setDividerImage(stepperSegmentDividerImage, forLeftSegmentState: .Normal, rightSegmentState: .Normal) + customStepper.setDividerImage(stepperSegmentDividerImage, forLeftSegmentState: UIControlState(), rightSegmentState: UIControlState()) // Set the image for the + button. let stepperIncrementImage = UIImage(named: "stepper_increment") - customStepper.setIncrementImage(stepperIncrementImage, forState: .Normal) + customStepper.setIncrementImage(stepperIncrementImage, for: UIControlState()) // Set the image for the - button. let stepperDecrementImage = UIImage(named: "stepper_decrement") - customStepper.setDecrementImage(stepperDecrementImage, forState: .Normal) + customStepper.setDecrementImage(stepperDecrementImage, for: UIControlState()) customStepperLabel.text = "\(Int(customStepper.value))" - customStepper.addTarget(self, action: #selector(StepperViewController.stepperValueDidChange(_:)), forControlEvents: .ValueChanged) + customStepper.addTarget(self, action: #selector(StepperViewController.stepperValueDidChange(_:)), for: .valueChanged) } // MARK: - Actions - func stepperValueDidChange(stepper: UIStepper) { + func stepperValueDidChange(_ stepper: UIStepper) { NSLog("A stepper changed its value: \(stepper).") // A mapping from a stepper to its associated label. @@ -94,6 +94,6 @@ class StepperViewController: UITableViewController { customStepper: customStepperLabel ] - stepperMapping[stepper]!.text = "\(Int(stepper.value))" + stepperMapping[stepper]!?.text = "\(Int(stepper.value))" } } diff --git a/UICatalog/Swift/UIKitCatalog/SwitchViewController.swift b/UICatalog/Swift/UIKitCatalog/SwitchViewController.swift index 06b356da..f400c936 100644 --- a/UICatalog/Swift/UIKitCatalog/SwitchViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/SwitchViewController.swift @@ -29,7 +29,7 @@ class SwitchViewController: UITableViewController { func configureDefaultSwitch() { defaultSwitch.setOn(true, animated: false) - defaultSwitch.addTarget(self, action: #selector(SwitchViewController.switchValueDidChange(_:)), forControlEvents: .ValueChanged) + defaultSwitch.addTarget(self, action: #selector(SwitchViewController.switchValueDidChange(_:)), for: .valueChanged) } func configureTintedSwitch() { @@ -37,12 +37,12 @@ class SwitchViewController: UITableViewController { tintedSwitch.onTintColor = UIColor.applicationGreenColor tintedSwitch.thumbTintColor = UIColor.applicationPurpleColor - tintedSwitch.addTarget(self, action: #selector(SwitchViewController.switchValueDidChange(_:)), forControlEvents: .ValueChanged) + tintedSwitch.addTarget(self, action: #selector(SwitchViewController.switchValueDidChange(_:)), for: .valueChanged) } // MARK: - Actions - func switchValueDidChange(aSwitch: UISwitch) { + func switchValueDidChange(_ aSwitch: UISwitch) { NSLog("A switch changed its value: \(aSwitch).") } } diff --git a/UICatalog/Swift/UIKitCatalog/TextFieldViewController.swift b/UICatalog/Swift/UIKitCatalog/TextFieldViewController.swift index b230f2a7..28e2ce22 100644 --- a/UICatalog/Swift/UIKitCatalog/TextFieldViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/TextFieldViewController.swift @@ -37,9 +37,9 @@ class TextFieldViewController: UITableViewController, UITextFieldDelegate { func configureTextField() { textField.placeholder = NSLocalizedString("Placeholder text", comment: "") - textField.autocorrectionType = .Yes - textField.returnKeyType = .Done - textField.clearButtonMode = .Never + textField.autocorrectionType = .yes + textField.returnKeyType = .done + textField.clearButtonMode = .never } func configureTintedTextField() { @@ -47,16 +47,16 @@ class TextFieldViewController: UITableViewController, UITextFieldDelegate { tintedTextField.textColor = UIColor.applicationGreenColor tintedTextField.placeholder = NSLocalizedString("Placeholder text", comment: "") - tintedTextField.returnKeyType = .Done - tintedTextField.clearButtonMode = .Never + tintedTextField.returnKeyType = .done + tintedTextField.clearButtonMode = .never } func configureSecureTextField() { - secureTextField.secureTextEntry = true + secureTextField.isSecureTextEntry = true secureTextField.placeholder = NSLocalizedString("Placeholder text", comment: "") - secureTextField.returnKeyType = .Done - secureTextField.clearButtonMode = .Always + secureTextField.returnKeyType = .done + secureTextField.clearButtonMode = .always } /** @@ -65,15 +65,15 @@ class TextFieldViewController: UITableViewController, UITextFieldDelegate { This example shows how to display a keyboard to help enter email addresses. */ func configureSpecificKeyboardTextField() { - specificKeyboardTextField.keyboardType = .EmailAddress + specificKeyboardTextField.keyboardType = .emailAddress specificKeyboardTextField.placeholder = NSLocalizedString("Placeholder text", comment: "") - specificKeyboardTextField.returnKeyType = .Done + specificKeyboardTextField.returnKeyType = .done } func configureCustomTextField() { // Text fields with custom image backgrounds must have no border. - customTextField.borderStyle = .None + customTextField.borderStyle = .none customTextField.background = UIImage(named: "text_field_background") @@ -82,28 +82,28 @@ class TextFieldViewController: UITableViewController, UITextFieldDelegate { text color to purple. */ let purpleImage = UIImage(named: "text_field_purple_right_view")! - let purpleImageButton = UIButton(type: .Custom) + let purpleImageButton = UIButton(type: .custom) purpleImageButton.bounds = CGRect(x: 0, y: 0, width: purpleImage.size.width, height: purpleImage.size.height) purpleImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 5) - purpleImageButton.setImage(purpleImage, forState: .Normal) - purpleImageButton.addTarget(self, action: #selector(TextFieldViewController.customTextFieldPurpleButtonClicked), forControlEvents: .TouchUpInside) + purpleImageButton.setImage(purpleImage, for: UIControlState()) + purpleImageButton.addTarget(self, action: #selector(TextFieldViewController.customTextFieldPurpleButtonClicked), for: .touchUpInside) customTextField.rightView = purpleImageButton - customTextField.rightViewMode = .Always + customTextField.rightViewMode = .always // Add an empty view as the left view to ensure inset between the text and the bounding rectangle. let leftPaddingView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0)) - leftPaddingView.backgroundColor = UIColor.clearColor() + leftPaddingView.backgroundColor = UIColor.clear customTextField.leftView = leftPaddingView - customTextField.leftViewMode = .Always + customTextField.leftViewMode = .always customTextField.placeholder = NSLocalizedString("Placeholder text", comment: "") - customTextField.autocorrectionType = .No - customTextField.returnKeyType = .Done + customTextField.autocorrectionType = .no + customTextField.returnKeyType = .done } // MARK: - UITextFieldDelegate - func textFieldShouldReturn(textField: UITextField) -> Bool { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true diff --git a/UICatalog/Swift/UIKitCatalog/TextViewController.swift b/UICatalog/Swift/UIKitCatalog/TextViewController.swift index 1bc53fcf..b56b090f 100644 --- a/UICatalog/Swift/UIKitCatalog/TextViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/TextViewController.swift @@ -24,44 +24,44 @@ class TextViewController: UIViewController, UITextViewDelegate { configureTextView() } - override func viewWillAppear(animated: Bool) { + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Listen for changes to keyboard visibility so that we can adjust the text view accordingly. - let notificationCenter = NSNotificationCenter.defaultCenter() + let notificationCenter = NotificationCenter.default - notificationCenter.addObserver(self, selector: #selector(TextViewController.handleKeyboardNotification(_:)), name: UIKeyboardWillShowNotification, object: nil) + notificationCenter.addObserver(self, selector: #selector(TextViewController.handleKeyboardNotification(_:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil) - notificationCenter.addObserver(self, selector: #selector(TextViewController.handleKeyboardNotification(_:)), name: UIKeyboardWillHideNotification, object: nil) + notificationCenter.addObserver(self, selector: #selector(TextViewController.handleKeyboardNotification(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil) } - override func viewDidDisappear(animated: Bool) { + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - let notificationCenter = NSNotificationCenter.defaultCenter() + let notificationCenter = NotificationCenter.default - notificationCenter.removeObserver(self, name: UIKeyboardWillShowNotification, object: nil) + notificationCenter.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil) - notificationCenter.removeObserver(self, name: UIKeyboardWillHideNotification, object: nil) + notificationCenter.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil) } // MARK: - Keyboard Event Notifications - func handleKeyboardNotification(notification: NSNotification) { + func handleKeyboardNotification(_ notification: Notification) { let userInfo = notification.userInfo! // Get information about the animation. - let animationDuration: NSTimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue + let animationDuration: TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue - let rawAnimationCurveValue = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).unsignedLongValue + let rawAnimationCurveValue = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).uintValue let animationCurve = UIViewAnimationOptions(rawValue: rawAnimationCurveValue) // Convert the keyboard frame from screen to view coordinates. - let keyboardScreenBeginFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue() - let keyboardScreenEndFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue() + let keyboardScreenBeginFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue + let keyboardScreenEndFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue - let keyboardViewBeginFrame = view.convertRect(keyboardScreenBeginFrame, fromView: view.window) - let keyboardViewEndFrame = view.convertRect(keyboardScreenEndFrame, fromView: view.window) + let keyboardViewBeginFrame = view.convert(keyboardScreenBeginFrame, from: view.window) + let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window) let originDelta = keyboardViewEndFrame.origin.y - keyboardViewBeginFrame.origin.y // The text view should be adjusted, update the constant for this constraint. @@ -71,8 +71,8 @@ class TextViewController: UIViewController, UITextViewDelegate { view.setNeedsUpdateConstraints() // Animate updating the view's layout by calling layoutIfNeeded inside a UIView animation block. - let animationOptions: UIViewAnimationOptions = [animationCurve, .BeginFromCurrentState] - UIView.animateWithDuration(animationDuration, delay: 0, options: animationOptions, animations: { + let animationOptions: UIViewAnimationOptions = [animationCurve, .beginFromCurrentState] + UIView.animate(withDuration: animationDuration, delay: 0, options: animationOptions, animations: { self.view.layoutIfNeeded() }, completion: nil) @@ -84,13 +84,13 @@ class TextViewController: UIViewController, UITextViewDelegate { // MARK: - Configuration func configureTextView() { - let bodyFontDescriptor = UIFontDescriptor.preferredFontDescriptorWithTextStyle(UIFontTextStyleBody) + let bodyFontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFontTextStyle.body) let bodyFont = UIFont(descriptor: bodyFontDescriptor, size: 0) textView.font = bodyFont - textView.textColor = UIColor.blackColor() - textView.backgroundColor = UIColor.whiteColor() - textView.scrollEnabled = true + textView.textColor = UIColor.black + textView.backgroundColor = UIColor.white + textView.isScrollEnabled = true /* Let's modify some of the attributes of the attributed string. @@ -107,16 +107,16 @@ class TextViewController: UIViewController, UITextViewDelegate { let text = textView.text! as NSString // Find the range of each element to modify. - let boldRange = text.rangeOfString(NSLocalizedString("bold", comment: "")) - let highlightedRange = text.rangeOfString(NSLocalizedString("highlighted", comment: "")) - let underlinedRange = text.rangeOfString(NSLocalizedString("underlined", comment: "")) - let tintedRange = text.rangeOfString(NSLocalizedString("tinted", comment: "")) + let boldRange = text.range(of: NSLocalizedString("bold", comment: "")) + let highlightedRange = text.range(of: NSLocalizedString("highlighted", comment: "")) + let underlinedRange = text.range(of: NSLocalizedString("underlined", comment: "")) + let tintedRange = text.range(of: NSLocalizedString("tinted", comment: "")) /* Add bold. Take the current font descriptor and create a new font descriptor with an additional bold trait. */ - let boldFontDescriptor = textView.font!.fontDescriptor().fontDescriptorWithSymbolicTraits(.TraitBold) + let boldFontDescriptor = textView.font!.fontDescriptor.withSymbolicTraits(.traitBold) let boldFont = UIFont(descriptor: boldFontDescriptor!, size: 0) attributedText.addAttribute(NSFontAttributeName, value: boldFont, range: boldRange) @@ -124,7 +124,7 @@ class TextViewController: UIViewController, UITextViewDelegate { attributedText.addAttribute(NSBackgroundColorAttributeName, value: UIColor.applicationGreenColor, range: highlightedRange) // Add underline. - attributedText.addAttribute(NSUnderlineStyleAttributeName, value: NSUnderlineStyle.StyleSingle.rawValue, range: underlinedRange) + attributedText.addAttribute(NSUnderlineStyleAttributeName, value: NSUnderlineStyle.styleSingle.rawValue, range: underlinedRange) // Add tint. attributedText.addAttribute(NSForegroundColorAttributeName, value: UIColor.applicationBlueColor, range: tintedRange) @@ -133,29 +133,29 @@ class TextViewController: UIViewController, UITextViewDelegate { let textAttachment = NSTextAttachment() let image = UIImage(named: "text_view_attachment")! textAttachment.image = image - textAttachment.bounds = CGRect(origin: CGPointZero, size: image.size) + textAttachment.bounds = CGRect(origin: CGPoint.zero, size: image.size) let textAttachmentString = NSAttributedString(attachment: textAttachment) - attributedText.appendAttributedString(textAttachmentString) + attributedText.append(textAttachmentString) // Append a space with matching font of the rest of the body text. let appendedSpace = NSMutableAttributedString.init(string: " ") appendedSpace.addAttribute(NSFontAttributeName, value: bodyFont, range: NSMakeRange(0, 1)) - attributedText.appendAttributedString(appendedSpace) + attributedText.append(appendedSpace) textView.attributedText = attributedText } // MARK: - UITextViewDelegate - func textViewDidBeginEditing(textView: UITextView) { + func textViewDidBeginEditing(_ textView: UITextView) { /* Provide a "Done" button for the user to select to signify completion with writing text in the text view. */ - let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: #selector(TextViewController.doneBarButtonItemClicked)) + let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(TextViewController.doneBarButtonItemClicked)) - navigationItem.setRightBarButtonItem(doneBarButtonItem, animated: true) + navigationItem.setRightBarButton(doneBarButtonItem, animated: true) } // MARK: - Actions @@ -164,6 +164,6 @@ class TextViewController: UIViewController, UITextViewDelegate { // Dismiss the keyboard by removing it as the first responder. textView.resignFirstResponder() - navigationItem.setRightBarButtonItem(nil, animated: true) + navigationItem.setRightBarButton(nil, animated: true) } } diff --git a/UICatalog/Swift/UIKitCatalog/TintedToolbarViewController.swift b/UICatalog/Swift/UIKitCatalog/TintedToolbarViewController.swift index c3ec914a..e77e72eb 100644 --- a/UICatalog/Swift/UIKitCatalog/TintedToolbarViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/TintedToolbarViewController.swift @@ -25,7 +25,7 @@ class TintedToolbarViewController: UIViewController { func configureToolbar() { // See the `UIBarStyle` enum for more styles, including `.Default`. - toolbar.barStyle = .BlackTranslucent + toolbar.barStyle = .blackTranslucent toolbar.tintColor = UIColor.applicationGreenColor toolbar.backgroundColor = UIColor.applicationBlueColor @@ -42,21 +42,21 @@ class TintedToolbarViewController: UIViewController { // MARK: - UIBarButtonItem Creation and Configuration var refreshBarButtonItem: UIBarButtonItem { - return UIBarButtonItem(barButtonSystemItem: .Refresh, target: self, action: #selector(TintedToolbarViewController.barButtonItemClicked(_:))) + return UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(TintedToolbarViewController.barButtonItemClicked(_:))) } var flexibleSpaceBarButtonItem: UIBarButtonItem { // Note that there's no target/action since this represents empty space. - return UIBarButtonItem(barButtonSystemItem: .FlexibleSpace, target: nil, action: nil) + return UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) } var actionBarButtonItem: UIBarButtonItem { - return UIBarButtonItem(barButtonSystemItem: .Action, target: self, action: #selector(TintedToolbarViewController.barButtonItemClicked(_:))) + return UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(TintedToolbarViewController.barButtonItemClicked(_:))) } // MARK: - Actions - func barButtonItemClicked(barButtonItem: UIBarButtonItem) { + func barButtonItemClicked(_ barButtonItem: UIBarButtonItem) { NSLog("A bar button item on the tinted toolbar was clicked: \(barButtonItem).") } } diff --git a/UICatalog/Swift/UIKitCatalog/WebViewController.swift b/UICatalog/Swift/UIKitCatalog/WebViewController.swift index 5f35ba43..b00dee43 100644 --- a/UICatalog/Swift/UIKitCatalog/WebViewController.swift +++ b/UICatalog/Swift/UIKitCatalog/WebViewController.swift @@ -24,17 +24,17 @@ class WebViewController: UIViewController, UIWebViewDelegate, UITextFieldDelegat loadAddressURL() } - override func viewWillDisappear(animated: Bool) { + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - UIApplication.sharedApplication().networkActivityIndicatorVisible = false + UIApplication.shared.isNetworkActivityIndicatorVisible = false } // MARK: - Convenience func loadAddressURL() { - if let text = addressTextField.text, requestURL = NSURL(string: text) { - let request = NSURLRequest(URL: requestURL) + if let text = addressTextField.text, let requestURL = URL(string: text) { + let request = URLRequest(url: requestURL) webView.loadRequest(request) } } @@ -42,22 +42,22 @@ class WebViewController: UIViewController, UIWebViewDelegate, UITextFieldDelegat // MARK: - Configuration func configureWebView() { - webView.backgroundColor = UIColor.whiteColor() + webView.backgroundColor = UIColor.white webView.scalesPageToFit = true - webView.dataDetectorTypes = .All + webView.dataDetectorTypes = .all } // MARK: - UIWebViewDelegate - func webViewDidStartLoad(webView: UIWebView) { - UIApplication.sharedApplication().networkActivityIndicatorVisible = true + func webViewDidStartLoad(_ webView: UIWebView) { + UIApplication.shared.isNetworkActivityIndicatorVisible = true } - func webViewDidFinishLoad(webView: UIWebView) { - UIApplication.sharedApplication().networkActivityIndicatorVisible = false + func webViewDidFinishLoad(_ webView: UIWebView) { + UIApplication.shared.isNetworkActivityIndicatorVisible = false } - func webView(webView: UIWebView, didFailLoadWithError error: NSError) { + func webView(_ webView: UIWebView, didFailLoadWithError error: Error) { // Report the error inside the web view. let localizedErrorMessage = NSLocalizedString("An error occured:", comment: "") @@ -65,13 +65,13 @@ class WebViewController: UIViewController, UIWebViewDelegate, UITextFieldDelegat webView.loadHTMLString(errorHTML, baseURL: nil) - UIApplication.sharedApplication().networkActivityIndicatorVisible = false + UIApplication.shared.isNetworkActivityIndicatorVisible = false } // MARK: - UITextFieldDelegate /// Dismisses the keyboard when the "Done" button is clicked. - func textFieldShouldReturn(textField: UITextField) -> Bool { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() loadAddressURL() diff --git a/UnicornChat/LICENSE.txt b/UnicornChat/LICENSE.txt new file mode 100644 index 00000000..a34b75f7 --- /dev/null +++ b/UnicornChat/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: UnicornChat: Extending Your Apps with SiriKit +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) 2016 Apple Inc. All Rights Reserved. diff --git a/UnicornChat/README.md b/UnicornChat/README.md new file mode 100644 index 00000000..82cac511 --- /dev/null +++ b/UnicornChat/README.md @@ -0,0 +1,17 @@ +# UnicornChat: Extending Your Apps with SiriKit + +UnicornChat is a messaging application for unicorns to chat with each other. + +As a sample app, this illustrates how to adopt SiriKit by adding an Intents extension and an Intents UI extension. + +## Requirements + +### Build + +Xcode 8.0 or later; iOS 10.0 SDK or later + +### Runtime + +iOS 10.0 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/UnicornChat/SiriExtension/Info.plist b/UnicornChat/SiriExtension/Info.plist new file mode 100644 index 00000000..130d2839 --- /dev/null +++ b/UnicornChat/SiriExtension/Info.plist @@ -0,0 +1,44 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + SiriExtension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + INSendMessageIntent + + IntentsSupported + + INSendMessageIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).UCIntentsHandler + + + diff --git a/UnicornChat/SiriExtension/UCIntentsHandler.swift b/UnicornChat/SiriExtension/UCIntentsHandler.swift new file mode 100644 index 00000000..a04c4362 --- /dev/null +++ b/UnicornChat/SiriExtension/UCIntentsHandler.swift @@ -0,0 +1,20 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The main entry point to the Intents extension. +*/ + +import Intents + +class UCIntentsHandler: INExtension { + + override func handler(for intent: INIntent) -> Any? { + if intent is INSendMessageIntent { + return UCSendMessageIntentHandler() + } + + return nil + } +} diff --git a/UnicornChat/SiriExtension/UCSendMessageIntentHandler.swift b/UnicornChat/SiriExtension/UCSendMessageIntentHandler.swift new file mode 100644 index 00000000..b9b320d7 --- /dev/null +++ b/UnicornChat/SiriExtension/UCSendMessageIntentHandler.swift @@ -0,0 +1,88 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The handler class for INSendMessageIntent. +*/ + +import Foundation +import Intents +import UnicornCore + +class UCSendMessageIntentHandler: NSObject, INSendMessageIntentHandling { + + // MARK: 1. Resolve + func resolveRecipients(forSendMessage intent: INSendMessageIntent, with completion: @escaping ([INPersonResolutionResult]) -> Swift.Void) { + + if let recipients = intent.recipients { + var resolutionResults = [INPersonResolutionResult]() + + for recipient in recipients { + let matchingContacts = UCAddressBookManager().contacts(matchingName: recipient.displayName) + + switch matchingContacts.count { + case 2 ... Int.max: + // We need Siri's help to ask user to pick one from the matches. + let disambiguationOptions: [INPerson] = matchingContacts.map { contact in + return contact.inPerson() + } + + resolutionResults += [.disambiguation(with: disambiguationOptions)] + + case 1: + let recipientMatched = matchingContacts[0].inPerson() + resolutionResults += [.success(with: recipientMatched)] + + case 0: + resolutionResults += [.unsupported()] + + default: + break + } + } + + completion(resolutionResults) + + } else { + // No recipients are provided. We need to prompt for a value. + completion([INPersonResolutionResult.needsValue()]) + } + } + + func resolveContent(forSendMessage intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Swift.Void) { + if let text = intent.content, !text.isEmpty { + completion(INStringResolutionResult.success(with: text)) + } + else { + completion(INStringResolutionResult.needsValue()) + } + } + + // MARK: 2. Confirm + func confirm(sendMessage intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Swift.Void) { + + if UCAccount.shared().hasValidAuthentication { + completion(INSendMessageIntentResponse(code: .success, userActivity: nil)) + } + else { + // Creating our own user activity to include error information. + let userActivity = NSUserActivity(activityType: String(describing: INSendMessageIntent.self)) + userActivity.userInfo = [NSString(string: "error"):NSString(string: "UserLoggedOut")] + + completion(INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity)) + } + } + + // MARK: 3. Handle + func handle(sendMessage intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Swift.Void) { + if intent.recipients != nil && intent.content != nil { + // Send the message. + let success = UCAccount.shared().sendMessage(intent.content, toRecipients: intent.recipients) + completion(INSendMessageIntentResponse(code: success ? .success : .failure, userActivity: nil)) + } + else { + completion(INSendMessageIntentResponse(code: .failure, userActivity: nil)) + } + } +} diff --git a/UnicornChat/SiriUIExtension/Base.lproj/MainInterface.storyboard b/UnicornChat/SiriUIExtension/Base.lproj/MainInterface.storyboard new file mode 100644 index 00000000..831ee403 --- /dev/null +++ b/UnicornChat/SiriUIExtension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UnicornChat/SiriUIExtension/Info.plist b/UnicornChat/SiriUIExtension/Info.plist new file mode 100644 index 00000000..51c0c25f --- /dev/null +++ b/UnicornChat/SiriUIExtension/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + SiriUIExtension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + IntentsSupported + + INSendMessageIntent + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.intents-ui-service + + + diff --git a/UnicornChat/SiriUIExtension/IntentViewController.swift b/UnicornChat/SiriUIExtension/IntentViewController.swift new file mode 100644 index 00000000..37c80acd --- /dev/null +++ b/UnicornChat/SiriUIExtension/IntentViewController.swift @@ -0,0 +1,56 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The view controller providing a user interface for the intent. +*/ + +import IntentsUI +import UnicornCore + +class IntentViewController: UIViewController, INUIHostedViewControlling, INUIHostedViewSiriProviding { + + // MARK: INUIHostedViewControlling + + func configure(with interaction: INInteraction!, context: INUIHostedViewContext, completion: ((CGSize) -> Void)!) { + var size: CGSize + + // Check if the interaction describes a SendMessageIntent. + if interaction.representsSendMessageIntent { + // If it is, let's set up a view controller. + let chatViewController = UCChatViewController() + chatViewController.messageContent = interaction.messageContent + + let contact = UCContact() + contact.name = interaction.recipientName + chatViewController.recipient = contact + + switch interaction.intentHandlingStatus { + case .unspecified, .inProgress, .ready, .failure: + chatViewController.isSent = false + + case .success, .deferredToApplication: + chatViewController.isSent = true + } + + present(chatViewController, animated: false, completion: nil) + + size = desiredSize + } + else { + // Otherwise, we'll tell the host to draw us at zero size. + size = CGSize.zero + } + + completion(size) + } + + var desiredSize: CGSize { + return extensionContext!.hostedViewMaximumAllowedSize + } + + var displaysMessage: Bool { + return true + } +} diff --git a/UnicornChat/UnicornChat.xcodeproj/project.pbxproj b/UnicornChat/UnicornChat.xcodeproj/project.pbxproj new file mode 100644 index 00000000..d20743af --- /dev/null +++ b/UnicornChat/UnicornChat.xcodeproj/project.pbxproj @@ -0,0 +1,808 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 522FAE651D08855B009BC1A0 /* chatmockdraft.png in Resources */ = {isa = PBXBuildFile; fileRef = 522FAE641D08855B009BC1A0 /* chatmockdraft.png */; }; + 522FAE661D08855B009BC1A0 /* chatmockdraft.png in Resources */ = {isa = PBXBuildFile; fileRef = 522FAE641D08855B009BC1A0 /* chatmockdraft.png */; }; + 522FAE681D0885A4009BC1A0 /* chatmock.png in Resources */ = {isa = PBXBuildFile; fileRef = 522FAE671D0885A4009BC1A0 /* chatmock.png */; }; + 522FAE691D0885A4009BC1A0 /* chatmock.png in Resources */ = {isa = PBXBuildFile; fileRef = 522FAE671D0885A4009BC1A0 /* chatmock.png */; }; + 5253B67C1CFDD60600F67727 /* UCChatViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 5253B67A1CFDD60600F67727 /* UCChatViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5253B67D1CFDD60600F67727 /* UCChatViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5253B67B1CFDD60600F67727 /* UCChatViewController.m */; }; + 5253B6831CFDD73800F67727 /* UCChatView.h in Headers */ = {isa = PBXBuildFile; fileRef = 5253B6811CFDD73800F67727 /* UCChatView.h */; }; + 5253B6841CFDD73800F67727 /* UCChatView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5253B6821CFDD73800F67727 /* UCChatView.m */; }; + 5253B68A1CFDDD3D00F67727 /* IntentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 520CB0D21CEA2617003E80C1 /* IntentsUI.framework */; }; + 5253B68D1CFDDD3D00F67727 /* IntentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5253B68C1CFDDD3D00F67727 /* IntentViewController.swift */; }; + 5253B6901CFDDD3D00F67727 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5253B68E1CFDDD3D00F67727 /* MainInterface.storyboard */; }; + 5253B6941CFDDD3D00F67727 /* SiriUIExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 5253B6891CFDDD3D00F67727 /* SiriUIExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 843FAC4B1CFE99CE0079D035 /* UnicornCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84AC3F1C1CF9170F00D3D08A /* UnicornCore.framework */; }; + 84AC3F1F1CF9170F00D3D08A /* UnicornCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84AC3F1E1CF9170F00D3D08A /* UnicornCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84AC3F241CF9170F00D3D08A /* UnicornCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 84AC3F1C1CF9170F00D3D08A /* UnicornCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 84AC3F2E1CF9187F00D3D08A /* UnicornCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84AC3F1C1CF9170F00D3D08A /* UnicornCore.framework */; }; + 84AC3F311CF9195400D3D08A /* UCContact.h in Headers */ = {isa = PBXBuildFile; fileRef = 84AC3F2F1CF9195400D3D08A /* UCContact.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84AC3F321CF9195400D3D08A /* UCContact.m in Sources */ = {isa = PBXBuildFile; fileRef = 84AC3F301CF9195400D3D08A /* UCContact.m */; }; + 84C6A78A1CE433A200E22048 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C6A7891CE433A200E22048 /* AppDelegate.swift */; }; + 84C6A78C1CE433A200E22048 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C6A78B1CE433A200E22048 /* ViewController.swift */; }; + 84C6A78F1CE433A200E22048 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C6A78D1CE433A200E22048 /* Main.storyboard */; }; + 84C6A7911CE433A200E22048 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84C6A7901CE433A200E22048 /* Assets.xcassets */; }; + 84C6A7941CE433A200E22048 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C6A7921CE433A200E22048 /* LaunchScreen.storyboard */; }; + 84C6A7A21CE4413400E22048 /* UCIntentsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C6A7A11CE4413400E22048 /* UCIntentsHandler.swift */; }; + 84C6A7A61CE4413400E22048 /* SiriExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 84C6A79F1CE4413400E22048 /* SiriExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 84C6A7AC1CE4573E00E22048 /* UCSendMessageIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C6A7AB1CE4573E00E22048 /* UCSendMessageIntentHandler.swift */; }; + 84D39A4E1CFD0DBB00C8241D /* UCAddressBookManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 84D39A4C1CFD0DBB00C8241D /* UCAddressBookManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D39A4F1CFD0DBB00C8241D /* UCAddressBookManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 84D39A4D1CFD0DBB00C8241D /* UCAddressBookManager.m */; }; + 84D39A521CFD5D9000C8241D /* UCAccount.h in Headers */ = {isa = PBXBuildFile; fileRef = 84D39A501CFD5D9000C8241D /* UCAccount.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D39A531CFD5D9000C8241D /* UCAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 84D39A511CFD5D9000C8241D /* UCAccount.m */; }; + 92508E351D02381800944860 /* INInteraction+UnicornCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 92508E331D02381800944860 /* INInteraction+UnicornCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 92508E361D02381800944860 /* INInteraction+UnicornCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 92508E341D02381800944860 /* INInteraction+UnicornCore.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 5253B6921CFDDD3D00F67727 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 84C6A77E1CE433A200E22048 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5253B6881CFDDD3D00F67727; + remoteInfo = SiriUIExtension; + }; + 84AC3F211CF9170F00D3D08A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 84C6A77E1CE433A200E22048 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 84AC3F1B1CF9170F00D3D08A; + remoteInfo = UnicornCore; + }; + 84AC3F2C1CF9183500D3D08A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 84C6A77E1CE433A200E22048 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 84AC3F1B1CF9170F00D3D08A; + remoteInfo = UnicornCore; + }; + 84C6A7A41CE4413400E22048 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 84C6A77E1CE433A200E22048 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 84C6A79E1CE4413400E22048; + remoteInfo = SiriExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 84AC3F281CF9170F00D3D08A /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 84AC3F241CF9170F00D3D08A /* UnicornCore.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 84C6A7AA1CE4413400E22048 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 84C6A7A61CE4413400E22048 /* SiriExtension.appex in Embed App Extensions */, + 5253B6941CFDDD3D00F67727 /* SiriUIExtension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 520CB0D21CEA2617003E80C1 /* IntentsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntentsUI.framework; path = System/Library/Frameworks/IntentsUI.framework; sourceTree = SDKROOT; }; + 522FAE641D08855B009BC1A0 /* chatmockdraft.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = chatmockdraft.png; sourceTree = ""; }; + 522FAE671D0885A4009BC1A0 /* chatmock.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = chatmock.png; sourceTree = ""; }; + 5253B67A1CFDD60600F67727 /* UCChatViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UCChatViewController.h; sourceTree = ""; }; + 5253B67B1CFDD60600F67727 /* UCChatViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UCChatViewController.m; sourceTree = ""; }; + 5253B6811CFDD73800F67727 /* UCChatView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UCChatView.h; sourceTree = ""; }; + 5253B6821CFDD73800F67727 /* UCChatView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UCChatView.m; sourceTree = ""; }; + 5253B6891CFDDD3D00F67727 /* SiriUIExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SiriUIExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 5253B68C1CFDDD3D00F67727 /* IntentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentViewController.swift; sourceTree = ""; }; + 5253B68F1CFDDD3D00F67727 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + 5253B6911CFDDD3D00F67727 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 84AC3F1C1CF9170F00D3D08A /* UnicornCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UnicornCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 84AC3F1E1CF9170F00D3D08A /* UnicornCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UnicornCore.h; sourceTree = ""; }; + 84AC3F201CF9170F00D3D08A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 84AC3F2F1CF9195400D3D08A /* UCContact.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UCContact.h; sourceTree = ""; }; + 84AC3F301CF9195400D3D08A /* UCContact.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UCContact.m; sourceTree = ""; }; + 84C6A7861CE433A200E22048 /* UnicornChat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UnicornChat.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 84C6A7891CE433A200E22048 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 84C6A78B1CE433A200E22048 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 84C6A78E1CE433A200E22048 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 84C6A7901CE433A200E22048 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 84C6A7931CE433A200E22048 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 84C6A7951CE433A200E22048 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 84C6A79F1CE4413400E22048 /* SiriExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SiriExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 84C6A7A11CE4413400E22048 /* UCIntentsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UCIntentsHandler.swift; sourceTree = ""; }; + 84C6A7A31CE4413400E22048 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 84C6A7AB1CE4573E00E22048 /* UCSendMessageIntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UCSendMessageIntentHandler.swift; sourceTree = ""; }; + 84D39A4C1CFD0DBB00C8241D /* UCAddressBookManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UCAddressBookManager.h; sourceTree = ""; }; + 84D39A4D1CFD0DBB00C8241D /* UCAddressBookManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UCAddressBookManager.m; sourceTree = ""; }; + 84D39A501CFD5D9000C8241D /* UCAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UCAccount.h; sourceTree = ""; }; + 84D39A511CFD5D9000C8241D /* UCAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UCAccount.m; sourceTree = ""; }; + 92508E331D02381800944860 /* INInteraction+UnicornCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "INInteraction+UnicornCore.h"; sourceTree = ""; }; + 92508E341D02381800944860 /* INInteraction+UnicornCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "INInteraction+UnicornCore.m"; sourceTree = ""; }; + B5E465061D9F11E900F114F1 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5253B6861CFDDD3D00F67727 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 843FAC4B1CFE99CE0079D035 /* UnicornCore.framework in Frameworks */, + 5253B68A1CFDDD3D00F67727 /* IntentsUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84AC3F181CF9170F00D3D08A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84C6A7831CE433A200E22048 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84C6A79C1CE4413400E22048 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 84AC3F2E1CF9187F00D3D08A /* UnicornCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 520CB0D11CEA2617003E80C1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 84AC3F1D1CF9170F00D3D08A /* UnicornCore */, + 520CB0D21CEA2617003E80C1 /* IntentsUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 5253B6801CFDD72200F67727 /* Internal */ = { + isa = PBXGroup; + children = ( + 522FAE641D08855B009BC1A0 /* chatmockdraft.png */, + 522FAE671D0885A4009BC1A0 /* chatmock.png */, + 5253B6811CFDD73800F67727 /* UCChatView.h */, + 5253B6821CFDD73800F67727 /* UCChatView.m */, + ); + name = Internal; + sourceTree = ""; + }; + 5253B68B1CFDDD3D00F67727 /* SiriUIExtension */ = { + isa = PBXGroup; + children = ( + 5253B68C1CFDDD3D00F67727 /* IntentViewController.swift */, + 5253B68E1CFDDD3D00F67727 /* MainInterface.storyboard */, + 5253B6911CFDDD3D00F67727 /* Info.plist */, + ); + path = SiriUIExtension; + sourceTree = ""; + }; + 84AC3F1D1CF9170F00D3D08A /* UnicornCore */ = { + isa = PBXGroup; + children = ( + 92508E331D02381800944860 /* INInteraction+UnicornCore.h */, + 92508E341D02381800944860 /* INInteraction+UnicornCore.m */, + 5253B67A1CFDD60600F67727 /* UCChatViewController.h */, + 5253B67B1CFDD60600F67727 /* UCChatViewController.m */, + 84D39A4C1CFD0DBB00C8241D /* UCAddressBookManager.h */, + 84D39A4D1CFD0DBB00C8241D /* UCAddressBookManager.m */, + 84AC3F1E1CF9170F00D3D08A /* UnicornCore.h */, + 84AC3F2F1CF9195400D3D08A /* UCContact.h */, + 84AC3F301CF9195400D3D08A /* UCContact.m */, + 84D39A501CFD5D9000C8241D /* UCAccount.h */, + 84D39A511CFD5D9000C8241D /* UCAccount.m */, + 5253B6801CFDD72200F67727 /* Internal */, + 84AC3F201CF9170F00D3D08A /* Info.plist */, + ); + path = UnicornCore; + sourceTree = ""; + }; + 84C6A77D1CE433A200E22048 = { + isa = PBXGroup; + children = ( + B5E465061D9F11E900F114F1 /* README.md */, + 84C6A7881CE433A200E22048 /* UnicornChat */, + 84C6A7A01CE4413400E22048 /* SiriExtension */, + 5253B68B1CFDDD3D00F67727 /* SiriUIExtension */, + 520CB0D11CEA2617003E80C1 /* Frameworks */, + 84C6A7871CE433A200E22048 /* Products */, + ); + sourceTree = ""; + }; + 84C6A7871CE433A200E22048 /* Products */ = { + isa = PBXGroup; + children = ( + 84C6A7861CE433A200E22048 /* UnicornChat.app */, + 84C6A79F1CE4413400E22048 /* SiriExtension.appex */, + 84AC3F1C1CF9170F00D3D08A /* UnicornCore.framework */, + 5253B6891CFDDD3D00F67727 /* SiriUIExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 84C6A7881CE433A200E22048 /* UnicornChat */ = { + isa = PBXGroup; + children = ( + 84C6A7891CE433A200E22048 /* AppDelegate.swift */, + 84C6A78B1CE433A200E22048 /* ViewController.swift */, + 84C6A78D1CE433A200E22048 /* Main.storyboard */, + 84C6A7901CE433A200E22048 /* Assets.xcassets */, + 84C6A7921CE433A200E22048 /* LaunchScreen.storyboard */, + 84C6A7951CE433A200E22048 /* Info.plist */, + ); + path = UnicornChat; + sourceTree = ""; + }; + 84C6A7A01CE4413400E22048 /* SiriExtension */ = { + isa = PBXGroup; + children = ( + 84C6A7A11CE4413400E22048 /* UCIntentsHandler.swift */, + 84C6A7A31CE4413400E22048 /* Info.plist */, + 84C6A7AB1CE4573E00E22048 /* UCSendMessageIntentHandler.swift */, + ); + path = SiriExtension; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 84AC3F191CF9170F00D3D08A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 84D39A4E1CFD0DBB00C8241D /* UCAddressBookManager.h in Headers */, + 84AC3F1F1CF9170F00D3D08A /* UnicornCore.h in Headers */, + 92508E351D02381800944860 /* INInteraction+UnicornCore.h in Headers */, + 84D39A521CFD5D9000C8241D /* UCAccount.h in Headers */, + 5253B6831CFDD73800F67727 /* UCChatView.h in Headers */, + 5253B67C1CFDD60600F67727 /* UCChatViewController.h in Headers */, + 84AC3F311CF9195400D3D08A /* UCContact.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 5253B6881CFDDD3D00F67727 /* SiriUIExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5253B6971CFDDD3D00F67727 /* Build configuration list for PBXNativeTarget "SiriUIExtension" */; + buildPhases = ( + 5253B6851CFDDD3D00F67727 /* Sources */, + 5253B6861CFDDD3D00F67727 /* Frameworks */, + 5253B6871CFDDD3D00F67727 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SiriUIExtension; + productName = SiriUIExtension; + productReference = 5253B6891CFDDD3D00F67727 /* SiriUIExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 84AC3F1B1CF9170F00D3D08A /* UnicornCore */ = { + isa = PBXNativeTarget; + buildConfigurationList = 84AC3F271CF9170F00D3D08A /* Build configuration list for PBXNativeTarget "UnicornCore" */; + buildPhases = ( + 84AC3F171CF9170F00D3D08A /* Sources */, + 84AC3F181CF9170F00D3D08A /* Frameworks */, + 84AC3F191CF9170F00D3D08A /* Headers */, + 84AC3F1A1CF9170F00D3D08A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = UnicornCore; + productName = UnicornCore; + productReference = 84AC3F1C1CF9170F00D3D08A /* UnicornCore.framework */; + productType = "com.apple.product-type.framework"; + }; + 84C6A7851CE433A200E22048 /* UnicornChat */ = { + isa = PBXNativeTarget; + buildConfigurationList = 84C6A7981CE433A200E22048 /* Build configuration list for PBXNativeTarget "UnicornChat" */; + buildPhases = ( + 84C6A7821CE433A200E22048 /* Sources */, + 84C6A7831CE433A200E22048 /* Frameworks */, + 84C6A7841CE433A200E22048 /* Resources */, + 84C6A7AA1CE4413400E22048 /* Embed App Extensions */, + 84AC3F281CF9170F00D3D08A /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 84C6A7A51CE4413400E22048 /* PBXTargetDependency */, + 84AC3F221CF9170F00D3D08A /* PBXTargetDependency */, + 5253B6931CFDDD3D00F67727 /* PBXTargetDependency */, + ); + name = UnicornChat; + productName = UnicornChat; + productReference = 84C6A7861CE433A200E22048 /* UnicornChat.app */; + productType = "com.apple.product-type.application"; + }; + 84C6A79E1CE4413400E22048 /* SiriExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 84C6A7A71CE4413400E22048 /* Build configuration list for PBXNativeTarget "SiriExtension" */; + buildPhases = ( + 84C6A79B1CE4413400E22048 /* Sources */, + 84C6A79C1CE4413400E22048 /* Frameworks */, + 84C6A79D1CE4413400E22048 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 84AC3F2D1CF9183500D3D08A /* PBXTargetDependency */, + ); + name = SiriExtension; + productName = SiriExtension; + productReference = 84C6A79F1CE4413400E22048 /* SiriExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 84C6A77E1CE433A200E22048 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = "Apple Inc"; + TargetAttributes = { + 5253B6881CFDDD3D00F67727 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + 84AC3F1B1CF9170F00D3D08A = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + 84C6A7851CE433A200E22048 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + 84C6A79E1CE4413400E22048 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 84C6A7811CE433A200E22048 /* Build configuration list for PBXProject "UnicornChat" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 84C6A77D1CE433A200E22048; + productRefGroup = 84C6A7871CE433A200E22048 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 84C6A7851CE433A200E22048 /* UnicornChat */, + 84C6A79E1CE4413400E22048 /* SiriExtension */, + 5253B6881CFDDD3D00F67727 /* SiriUIExtension */, + 84AC3F1B1CF9170F00D3D08A /* UnicornCore */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5253B6871CFDDD3D00F67727 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5253B6901CFDDD3D00F67727 /* MainInterface.storyboard in Resources */, + 522FAE651D08855B009BC1A0 /* chatmockdraft.png in Resources */, + 522FAE681D0885A4009BC1A0 /* chatmock.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84AC3F1A1CF9170F00D3D08A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 522FAE691D0885A4009BC1A0 /* chatmock.png in Resources */, + 522FAE661D08855B009BC1A0 /* chatmockdraft.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84C6A7841CE433A200E22048 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 84C6A7941CE433A200E22048 /* LaunchScreen.storyboard in Resources */, + 84C6A7911CE433A200E22048 /* Assets.xcassets in Resources */, + 84C6A78F1CE433A200E22048 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84C6A79D1CE4413400E22048 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5253B6851CFDDD3D00F67727 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5253B68D1CFDDD3D00F67727 /* IntentViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84AC3F171CF9170F00D3D08A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5253B67D1CFDD60600F67727 /* UCChatViewController.m in Sources */, + 5253B6841CFDD73800F67727 /* UCChatView.m in Sources */, + 84D39A4F1CFD0DBB00C8241D /* UCAddressBookManager.m in Sources */, + 92508E361D02381800944860 /* INInteraction+UnicornCore.m in Sources */, + 84AC3F321CF9195400D3D08A /* UCContact.m in Sources */, + 84D39A531CFD5D9000C8241D /* UCAccount.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84C6A7821CE433A200E22048 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 84C6A78C1CE433A200E22048 /* ViewController.swift in Sources */, + 84C6A78A1CE433A200E22048 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84C6A79B1CE4413400E22048 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 84C6A7A21CE4413400E22048 /* UCIntentsHandler.swift in Sources */, + 84C6A7AC1CE4573E00E22048 /* UCSendMessageIntentHandler.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 5253B6931CFDDD3D00F67727 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5253B6881CFDDD3D00F67727 /* SiriUIExtension */; + targetProxy = 5253B6921CFDDD3D00F67727 /* PBXContainerItemProxy */; + }; + 84AC3F221CF9170F00D3D08A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 84AC3F1B1CF9170F00D3D08A /* UnicornCore */; + targetProxy = 84AC3F211CF9170F00D3D08A /* PBXContainerItemProxy */; + }; + 84AC3F2D1CF9183500D3D08A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 84AC3F1B1CF9170F00D3D08A /* UnicornCore */; + targetProxy = 84AC3F2C1CF9183500D3D08A /* PBXContainerItemProxy */; + }; + 84C6A7A51CE4413400E22048 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 84C6A79E1CE4413400E22048 /* SiriExtension */; + targetProxy = 84C6A7A41CE4413400E22048 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 5253B68E1CFDDD3D00F67727 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5253B68F1CFDDD3D00F67727 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; + 84C6A78D1CE433A200E22048 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 84C6A78E1CE433A200E22048 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 84C6A7921CE433A200E22048 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 84C6A7931CE433A200E22048 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 5253B6951CFDDD3D00F67727 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = SiriUIExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.UnicornChat.SiriUIExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 5253B6961CFDDD3D00F67727 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = SiriUIExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.UnicornChat.SiriUIExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + 84AC3F251CF9170F00D3D08A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = UnicornCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.UnicornChat.UnicornCore"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 84AC3F261CF9170F00D3D08A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = UnicornCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.UnicornChat.UnicornCore"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 84C6A7961CE433A200E22048 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = lossless; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 84C6A7971CE433A200E22048 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPRESSION = "respect-asset-catalog"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 84C6A7991CE433A200E22048 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + INFOPLIST_FILE = UnicornChat/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.UnicornChat"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 84C6A79A1CE433A200E22048 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + INFOPLIST_FILE = UnicornChat/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.UnicornChat"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; + 84C6A7A81CE4413400E22048 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = SiriExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.UnicornChat.SiriExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 84C6A7A91CE4413400E22048 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = SiriExtension/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.UnicornChat.SiriExtension"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5253B6971CFDDD3D00F67727 /* Build configuration list for PBXNativeTarget "SiriUIExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5253B6951CFDDD3D00F67727 /* Debug */, + 5253B6961CFDDD3D00F67727 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 84AC3F271CF9170F00D3D08A /* Build configuration list for PBXNativeTarget "UnicornCore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 84AC3F251CF9170F00D3D08A /* Debug */, + 84AC3F261CF9170F00D3D08A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 84C6A7811CE433A200E22048 /* Build configuration list for PBXProject "UnicornChat" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 84C6A7961CE433A200E22048 /* Debug */, + 84C6A7971CE433A200E22048 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 84C6A7981CE433A200E22048 /* Build configuration list for PBXNativeTarget "UnicornChat" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 84C6A7991CE433A200E22048 /* Debug */, + 84C6A79A1CE433A200E22048 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 84C6A7A71CE4413400E22048 /* Build configuration list for PBXNativeTarget "SiriExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 84C6A7A81CE4413400E22048 /* Debug */, + 84C6A7A91CE4413400E22048 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 84C6A77E1CE433A200E22048 /* Project object */; +} diff --git a/UnicornChat/UnicornChat/AppDelegate.swift b/UnicornChat/UnicornChat/AppDelegate.swift new file mode 100644 index 00000000..9c884d00 --- /dev/null +++ b/UnicornChat/UnicornChat/AppDelegate.swift @@ -0,0 +1,21 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application delegate +*/ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool { + // implement to handle user activity created by Siri or by our SiriExtension + + return true + } +} diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 120-1.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 120-1.png new file mode 100644 index 00000000..166cbe4e Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 120-1.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 120-2.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 120-2.png new file mode 100644 index 00000000..166cbe4e Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 120-2.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 152.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 152.png new file mode 100644 index 00000000..5deea725 Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 152.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 167.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 167.png new file mode 100644 index 00000000..e1c9f20f Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 167.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 180.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 180.png new file mode 100644 index 00000000..2edc2337 Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 180.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 29.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 29.png new file mode 100644 index 00000000..854c60c9 Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 29.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 40.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 40.png new file mode 100644 index 00000000..a30a3812 Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 40.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 58-1.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 58-1.png new file mode 100644 index 00000000..bbbb5233 Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 58-1.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 58.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 58.png new file mode 100644 index 00000000..ffcd8041 Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 58.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 76.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 76.png new file mode 100644 index 00000000..9beb896c Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 76.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 80-2.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 80-2.png new file mode 100644 index 00000000..a8d41c25 Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 80-2.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 80.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 80.png new file mode 100644 index 00000000..a8d41c25 Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 80.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 87.png b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 87.png new file mode 100644 index 00000000..bb13431c Binary files /dev/null and b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Carl 87.png differ diff --git a/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Contents.json b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..4c123c2a --- /dev/null +++ b/UnicornChat/UnicornChat/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,86 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Carl 58-1.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Carl 87.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Carl 80.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Carl 120-1.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Carl 120-2.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Carl 180.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Carl 29.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Carl 58.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Carl 40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Carl 80-2.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Carl 76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Carl 152.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Carl 167.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/UnicornChat/UnicornChat/Assets.xcassets/Contents.json b/UnicornChat/UnicornChat/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/UnicornChat/UnicornChat/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/UnicornChat/UnicornChat/Base.lproj/LaunchScreen.storyboard b/UnicornChat/UnicornChat/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..2e721e18 --- /dev/null +++ b/UnicornChat/UnicornChat/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UnicornChat/UnicornChat/Base.lproj/Main.storyboard b/UnicornChat/UnicornChat/Base.lproj/Main.storyboard new file mode 100644 index 00000000..3a2a49ba --- /dev/null +++ b/UnicornChat/UnicornChat/Base.lproj/Main.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UnicornChat/UnicornChat/Info.plist b/UnicornChat/UnicornChat/Info.plist new file mode 100644 index 00000000..1493ba3d --- /dev/null +++ b/UnicornChat/UnicornChat/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundleDisplayName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/UnicornChat/UnicornChat/ViewController.swift b/UnicornChat/UnicornChat/ViewController.swift new file mode 100644 index 00000000..6976c5f3 --- /dev/null +++ b/UnicornChat/UnicornChat/ViewController.swift @@ -0,0 +1,12 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The application's view controller +*/ + +import UIKit + +class ViewController: UIViewController { +} diff --git a/UnicornChat/UnicornCore/INInteraction+UnicornCore.h b/UnicornChat/UnicornCore/INInteraction+UnicornCore.h new file mode 100644 index 00000000..c7ed7f1c --- /dev/null +++ b/UnicornChat/UnicornCore/INInteraction+UnicornCore.h @@ -0,0 +1,17 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Convinience category on INInteraction to get information relevant to UnicornCore. +*/ + +#import + +@interface INInteraction (UnicornCore) + +@property (nonatomic, assign, readonly) BOOL representsSendMessageIntent; +@property (nonatomic, copy, readonly) NSString *recipientName; +@property (nonatomic, copy, readonly) NSString *messageContent; + +@end diff --git a/UnicornChat/UnicornCore/INInteraction+UnicornCore.m b/UnicornChat/UnicornCore/INInteraction+UnicornCore.m new file mode 100644 index 00000000..acfb4f3a --- /dev/null +++ b/UnicornChat/UnicornCore/INInteraction+UnicornCore.m @@ -0,0 +1,56 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Convinience category on INInteraction to get information relevant to UnicornCore. +*/ + +#import "INInteraction+UnicornCore.h" + +@interface INIntent (UnicornCore) + +- (BOOL)isSendMessageIntent; +- (INSendMessageIntent *)sendMessageIntent; + +@end + +@implementation INIntent (UnicornCore) + +- (BOOL)isSendMessageIntent { + return NO; +} + +- (INSendMessageIntent *)sendMessageIntent { + return nil; +} + +@end + +@implementation INSendMessageIntent (UnicornCore) + +- (BOOL)isSendMessageIntent { + return YES; +} + +- (INSendMessageIntent *)sendMessageIntent { + return self; +} + +@end + +@implementation INInteraction (UnicornCore) + +- (BOOL)representsSendMessageIntent { + return [[self intent] isSendMessageIntent]; +} + +- (NSString *)messageContent { + return [[[self intent] sendMessageIntent] content]; +} + +- (NSString *)recipientName { + return [[[[[self intent] sendMessageIntent] recipients] firstObject] displayName]; +} + +@end diff --git a/UnicornChat/UnicornCore/Info.plist b/UnicornChat/UnicornCore/Info.plist new file mode 100644 index 00000000..d3de8eef --- /dev/null +++ b/UnicornChat/UnicornCore/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/UnicornChat/UnicornCore/UCAccount.h b/UnicornChat/UnicornCore/UCAccount.h new file mode 100644 index 00000000..51025cff --- /dev/null +++ b/UnicornChat/UnicornCore/UCAccount.h @@ -0,0 +1,21 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The class that manages the current user account status, and sending/receiving messages. +*/ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UCAccount : NSObject +@property (nonatomic) BOOL hasValidAuthentication; + ++ (instancetype)sharedAccount; + +- (BOOL)sendMessage:(nullable NSString *)message toRecipients:(nullable NSArray *)recipients; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnicornChat/UnicornCore/UCAccount.m b/UnicornChat/UnicornCore/UCAccount.m new file mode 100644 index 00000000..0cbc6da3 --- /dev/null +++ b/UnicornChat/UnicornCore/UCAccount.m @@ -0,0 +1,25 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The class that manages the current user account status, and sending/receiving messages. +*/ + +#import "UCAccount.h" + +@implementation UCAccount + ++ (instancetype)sharedAccount { + UCAccount *shared = [[UCAccount alloc] init]; + [shared setHasValidAuthentication:YES]; + return shared; +} + +- (BOOL)sendMessage:(NSString *)message toRecipients:(NSArray *)recipients { + // Sending a message here... + + return YES; +} + +@end diff --git a/UnicornChat/UnicornCore/UCAddressBookManager.h b/UnicornChat/UnicornCore/UCAddressBookManager.h new file mode 100644 index 00000000..7c62bbc5 --- /dev/null +++ b/UnicornChat/UnicornCore/UCAddressBookManager.h @@ -0,0 +1,19 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The class that manages UnicornChat's own address book. +*/ + +#import + +@class UCContact; + +NS_ASSUME_NONNULL_BEGIN + +@interface UCAddressBookManager : NSObject +- (NSArray *)contactsMatchingName:(NSString *)name; +@end + +NS_ASSUME_NONNULL_END diff --git a/UnicornChat/UnicornCore/UCAddressBookManager.m b/UnicornChat/UnicornCore/UCAddressBookManager.m new file mode 100644 index 00000000..119abe57 --- /dev/null +++ b/UnicornChat/UnicornCore/UCAddressBookManager.m @@ -0,0 +1,50 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The class that manages UnicornChat's own address book. +*/ + +#import "UCAddressBookManager.h" +#import "UCContact.h" + +@implementation UCAddressBookManager + +- (NSArray *)contactsMatchingName:(NSString *)name { + NSMutableArray *results = [[NSMutableArray alloc] init]; + for (UCContact *contact in [self allContacts]) { + if ([[[contact name] lowercaseString] containsString:[name lowercaseString]]) { + [results addObject:contact]; + } + } + return results; +} + + +- (NSArray *)allContacts { + UCContact *contact1 = [[UCContact alloc] init]; + [contact1 setName:@"Bill James"]; + [contact1 setUnicornName:@"Sparkle Sparkly"]; + + UCContact *contact2 = [[UCContact alloc] init]; + [contact2 setName:@"Tom Clark"]; + [contact2 setUnicornName:@"Celestra"]; + + UCContact *contact3 = [[UCContact alloc] init]; + [contact3 setName:@"Juan Chavez"]; + [contact3 setUnicornName:@"Dandelion Prince"]; + + UCContact *contact4 = [[UCContact alloc] init]; + [contact4 setName:@"Anne Johnson"]; + [contact4 setUnicornName:@"Pinky Nose"]; + + NSArray *allContacts = @[contact1, + contact2, + contact3, + contact4, + ]; + return allContacts; +} + +@end diff --git a/UnicornChat/UnicornCore/UCChatView.h b/UnicornChat/UnicornCore/UCChatView.h new file mode 100644 index 00000000..1d584d04 --- /dev/null +++ b/UnicornChat/UnicornCore/UCChatView.h @@ -0,0 +1,17 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The view that displays messages in UnicornChat. +*/ + +#import + +@interface UCChatView : UIView + +@property (nonatomic, copy) NSString *recipientName; +@property (nonatomic, copy) NSString *content; +@property (nonatomic, assign, getter=isSent) BOOL sent; + +@end diff --git a/UnicornChat/UnicornCore/UCChatView.m b/UnicornChat/UnicornCore/UCChatView.m new file mode 100644 index 00000000..07e9e993 --- /dev/null +++ b/UnicornChat/UnicornCore/UCChatView.m @@ -0,0 +1,85 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The view that displays messages in UnicornChat. +*/ + +#import "UCChatView.h" + +@implementation UCChatView { + UILabel *_recipientLabel; + UILabel *_contentLabel; + UIImageView *_mockView; + + UIImage *_draftMock; + UIImage *_sentMock; +} + +- (instancetype)initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + if (self) { + _draftMock = [UIImage imageNamed:@"chatmockdraft.png"]; + _sentMock = [UIImage imageNamed:@"chatmock.png"]; + + _mockView = [[UIImageView alloc] initWithImage:_draftMock]; + [_mockView setContentMode:UIViewContentModeScaleToFill]; + [self addSubview:_mockView]; + + _recipientLabel = [[UILabel alloc] init]; + [_recipientLabel setNumberOfLines:0]; + [_recipientLabel setLineBreakMode:NSLineBreakByWordWrapping]; + [_recipientLabel setTextColor:[UIColor whiteColor]]; + [self addSubview:_recipientLabel]; + + _contentLabel = [[UILabel alloc] init]; + [_contentLabel setNumberOfLines:0]; + [_contentLabel setLineBreakMode:NSLineBreakByWordWrapping]; + [self addSubview:_contentLabel]; + } + return self; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + [_mockView setFrame:[self bounds]]; + + [_recipientLabel setText:_recipientName]; + [_recipientLabel setFrame:CGRectMake(65.0, 22.0, 62.0, 30.0)]; + + [_contentLabel setText:_content]; + [_contentLabel setFrame:CGRectMake(113.0, 85.0, 150.0, 75.0)]; +} + +- (void)setSent:(BOOL)sent { + if (_sent == sent) { + return; + } + + _sent = sent; + + UIImage *mockImage = (_sent ? _sentMock : _draftMock); + [_mockView setImage:mockImage]; +} + +- (void)setContent:(NSString *)content { + if ([_content isEqualToString:content]) { + return; + } + _content = content; + + [self setNeedsLayout]; +} + +- (void)setRecipientName:(NSString *)recipientName { + if ([_recipientName isEqualToString:recipientName]) { + return; + } + _recipientName = recipientName; + + [self setNeedsLayout]; +} + +@end diff --git a/UnicornChat/UnicornCore/UCChatViewController.h b/UnicornChat/UnicornCore/UCChatViewController.h new file mode 100644 index 00000000..f82e61cd --- /dev/null +++ b/UnicornChat/UnicornCore/UCChatViewController.h @@ -0,0 +1,20 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The view controller to display messages. +*/ + +#import + +@class NSString; +@class UCContact; + +@interface UCChatViewController : UIViewController + +@property (nonatomic, strong) UCContact *recipient; +@property (nonatomic, strong) NSString *messageContent; +@property (nonatomic, assign, getter=isSent) BOOL sent; + +@end diff --git a/UnicornChat/UnicornCore/UCChatViewController.m b/UnicornChat/UnicornCore/UCChatViewController.m new file mode 100644 index 00000000..6f5c71e1 --- /dev/null +++ b/UnicornChat/UnicornCore/UCChatViewController.m @@ -0,0 +1,54 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The view controller to display messages. +*/ + +#import "UCChatViewController.h" + +#import "UCChatView.h" +#import "UCContact.h" + +#import + +@interface UCChatViewController () + +@property (null_resettable, nonatomic, strong) UCChatView *view; + +@end + +@implementation UCChatViewController + +@dynamic view; + +- (void)loadView { + UCChatView *chatView = [[UCChatView alloc] init]; + [self setView:chatView]; +} + +- (void)setRecipient:(UCContact *)recipient { + if (![_recipient isEqual:recipient]) { + _recipient = recipient; + [[self view] setRecipientName:[recipient name]]; + } +} + +- (NSString *)messageContent { + return [[self view] content]; +} + +- (void)setMessageContent:(NSString *)messageContent { + [[self view] setContent:messageContent]; +} + +- (void)setSent:(BOOL)sent { + [[self view] setSent:sent]; +} + +- (BOOL)isSent { + return [[self view] isSent]; +} + +@end diff --git a/UnicornChat/UnicornCore/UCContact.h b/UnicornChat/UnicornCore/UCContact.h new file mode 100644 index 00000000..34497fab --- /dev/null +++ b/UnicornChat/UnicornCore/UCContact.h @@ -0,0 +1,26 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Data model class for contact object in UnicornChat. +*/ + +#import + +@class INPerson; + +NS_ASSUME_NONNULL_BEGIN + +@interface UCContact : NSObject + +@property (nonatomic, copy, nullable) NSString *name; +@property (nonatomic, copy, nullable) NSString *unicornName; + +@property (nonatomic) BOOL favorite; + +- (INPerson *)inPerson; + +@end + +NS_ASSUME_NONNULL_END diff --git a/UnicornChat/UnicornCore/UCContact.m b/UnicornChat/UnicornCore/UCContact.m new file mode 100644 index 00000000..ee49f5ec --- /dev/null +++ b/UnicornChat/UnicornCore/UCContact.m @@ -0,0 +1,19 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Data model class for contact object in UnicornChat. +*/ + +#import "UCContact.h" +#import + +@implementation UCContact + +- (INPerson *)inPerson { + INPersonHandle *handle = [[INPersonHandle alloc] initWithValue:_unicornName type:INPersonHandleTypeUnknown]; + return [[INPerson alloc] initWithPersonHandle:handle nameComponents:nil displayName:_name image:nil contactIdentifier:_unicornName customIdentifier:nil]; +} + +@end diff --git a/UnicornChat/UnicornCore/UnicornCore.h b/UnicornChat/UnicornCore/UnicornCore.h new file mode 100644 index 00000000..96cb495a --- /dev/null +++ b/UnicornChat/UnicornCore/UnicornCore.h @@ -0,0 +1,21 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Embedded framework used among the main application and the extensions. +*/ + +#import + +//! Project version number for UnicornCore. +FOUNDATION_EXPORT double UnicornCoreVersionNumber; + +//! Project version string for UnicornCore. +FOUNDATION_EXPORT const unsigned char UnicornCoreVersionString[]; + +#import +#import +#import +#import +#import diff --git a/UnicornChat/UnicornCore/chatmock.png b/UnicornChat/UnicornCore/chatmock.png new file mode 100644 index 00000000..26217eb1 Binary files /dev/null and b/UnicornChat/UnicornCore/chatmock.png differ diff --git a/UnicornChat/UnicornCore/chatmockdraft.png b/UnicornChat/UnicornCore/chatmockdraft.png new file mode 100644 index 00000000..a3ce0b1c Binary files /dev/null and b/UnicornChat/UnicornCore/chatmockdraft.png differ diff --git a/WatchBackgroundRefresh/LICENSE.txt b/WatchBackgroundRefresh/LICENSE.txt new file mode 100644 index 00000000..3fc6b4f1 --- /dev/null +++ b/WatchBackgroundRefresh/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: WatchBackgroundRefresh: Using WKRefreshBackgroundTask to update WatchKit apps in the background +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) 2016 Apple Inc. All Rights Reserved. diff --git a/WatchBackgroundRefresh/README.md b/WatchBackgroundRefresh/README.md new file mode 100644 index 00000000..4deafb08 --- /dev/null +++ b/WatchBackgroundRefresh/README.md @@ -0,0 +1,24 @@ +# WatchBackgroundRrefresh: Using WKRefreshBackgroundTask to update WatchKit apps in the background + +This sample demonstrates a common background refresh pattern: + +1. First schedule an application task by pressing the button on the UI +2. Now background the app using the crown +3. Wait for that applicaiton task to arrive +4. Using the application task's runtime, start a background URL session to download a file +5. When that file arrives, update the label on the UI to the current time and schedule a snapshot +6. When the snapshot completes, check the dock and you'll see the new timestamp + +Schedule runtime -> do some work -> snapshot your UI + +## Requirements + +### Build + +Xcode 8.0 or later; iOS 10.0 SDK or later, watchOS 3.0 SDK or later + +### Runtime + +iOS 10.0 or later, watchOS 3.0 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..dd221ba5 --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,55 @@ +{ + "images" : [ + { + "size" : "24x24", + "idiom" : "watch", + "scale" : "2x", + "role" : "notificationCenter", + "subtype" : "38mm" + }, + { + "size" : "27.5x27.5", + "idiom" : "watch", + "scale" : "2x", + "role" : "notificationCenter", + "subtype" : "42mm" + }, + { + "size" : "29x29", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "watch", + "scale" : "2x", + "role" : "appLauncher", + "subtype" : "38mm" + }, + { + "size" : "86x86", + "idiom" : "watch", + "scale" : "2x", + "role" : "quickLook", + "subtype" : "38mm" + }, + { + "size" : "98x98", + "idiom" : "watch", + "scale" : "2x", + "role" : "quickLook", + "subtype" : "42mm" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit App/Base.lproj/Interface.storyboard b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit App/Base.lproj/Interface.storyboard new file mode 100644 index 00000000..7ea687e9 --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit App/Base.lproj/Interface.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit App/Info.plist b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit App/Info.plist new file mode 100644 index 00000000..2d1a05f3 --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit App/Info.plist @@ -0,0 +1,35 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + WatchBackgroundRrefresh WatchKit App + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + WKCompanionAppBundleIdentifier + com.example.apple-samplecode.watchbackgroundrefresh + WKWatchKitApp + + + diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json new file mode 100644 index 00000000..9be9adbf --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "watch", + "screenWidth" : "{130,145}", + "scale" : "2x" + }, + { + "idiom" : "watch", + "screenWidth" : "{146,165}", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json new file mode 100644 index 00000000..2bf25222 --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json @@ -0,0 +1,23 @@ +{ + "assets" : [ + { + "idiom" : "watch", + "filename" : "Circular.imageset", + "role" : "circular" + }, + { + "idiom" : "watch", + "filename" : "Modular.imageset", + "role" : "modular" + }, + { + "idiom" : "watch", + "filename" : "Utilitarian.imageset", + "role" : "utilitarian" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json new file mode 100644 index 00000000..9be9adbf --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "watch", + "screenWidth" : "{130,145}", + "scale" : "2x" + }, + { + "idiom" : "watch", + "screenWidth" : "{146,165}", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json new file mode 100644 index 00000000..9be9adbf --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "idiom" : "watch", + "screenWidth" : "{130,145}", + "scale" : "2x" + }, + { + "idiom" : "watch", + "screenWidth" : "{146,165}", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/ExtensionDelegate.swift b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/ExtensionDelegate.swift new file mode 100644 index 00000000..cbb38d75 --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/ExtensionDelegate.swift @@ -0,0 +1,12 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The Extension Delegate. + */ + +import WatchKit + +class ExtensionDelegate: NSObject, WKExtensionDelegate { +} diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Info.plist b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Info.plist new file mode 100644 index 00000000..50a1741d --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + WatchBackgroundRrefresh WatchKit Extension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + WKAppBundleIdentifier + com.example.apple-samplecode.watchbackgroundrefresh.watchkitapp + + NSExtensionPointIdentifier + com.apple.watchkit + + WKExtensionDelegateClassName + $(PRODUCT_MODULE_NAME).ExtensionDelegate + + diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/MainInterfaceController.swift b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/MainInterfaceController.swift new file mode 100644 index 00000000..ff824cd3 --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh WatchKit Extension/MainInterfaceController.swift @@ -0,0 +1,110 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The main interface controller. + */ + +import WatchKit +import Foundation + + +class MainInterfaceController: WKInterfaceController, WKExtensionDelegate, URLSessionDownloadDelegate { + // MARK: Properties + + let sampleDownloadURL = URL(string: "http://devstreaming.apple.com/videos/wwdc/2015/802mpzd3nzovlygpbg/802/802_designing_for_apple_watch.pdf?dl=1")! + + @IBOutlet var timeDisplayLabel: WKInterfaceLabel! + + private let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .long + + return formatter + }() + + // MARK: WKInterfaceController + + override func awake(withContext context: Any?) { + super.awake(withContext: context) + + // Configure interface objects here. + WKExtension.shared().delegate = self + updateDateLabel() + } + + // MARK: WKExtensionDelegate + func handle(_ backgroundTasks: Set) { + for task : WKRefreshBackgroundTask in backgroundTasks { + print("received background task: ", task) + // only handle these while running in the background + if (WKExtension.shared().applicationState == .background) { + if task is WKApplicationRefreshBackgroundTask { + // this task is completed below, our app will then suspend while the download session runs + print("application task received, start URL session") + scheduleURLSession() + } + } + else if let urlTask = task as? WKURLSessionRefreshBackgroundTask { + let backgroundConfigObject = URLSessionConfiguration.background(withIdentifier: urlTask.sessionIdentifier) + let backgroundSession = URLSession(configuration: backgroundConfigObject, delegate: self, delegateQueue: nil) + + print("Rejoining session ", backgroundSession) + } + // make sure to complete all tasks, even ones you don't handle + task.setTaskCompleted() + } + } + + // MARK: Snapshot and UI updating + + func scheduleSnapshot() { + // fire now, we're ready + let fireDate = Date() + WKExtension.shared().scheduleSnapshotRefresh(withPreferredDate: fireDate, userInfo: nil) { error in + if (error == nil) { + print("successfully scheduled snapshot. All background work completed.") + } + } + } + + func updateDateLabel() { + let currentDate = Date() + timeDisplayLabel.setText(dateFormatter.string(from: currentDate)) + } + + // MARK: URLSession handling + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + print("NSURLSession finished to url: ", location) + updateDateLabel() + scheduleSnapshot() + } + + func scheduleURLSession() { + let backgroundConfigObject = URLSessionConfiguration.background(withIdentifier: NSUUID().uuidString) + backgroundConfigObject.sessionSendsLaunchEvents = true + let backgroundSession = URLSession(configuration: backgroundConfigObject) + + let downloadTask = backgroundSession.downloadTask(with: sampleDownloadURL) + downloadTask.resume() + } + + // MARK: IB actions + + @IBAction func ScheduleRefreshButtonTapped() { + // fire in 20 seconds + let fireDate = Date(timeIntervalSinceNow: 20.0) + // optional, any SecureCoding compliant data can be passed here + let userInfo = ["reason" : "background update"] as NSDictionary + + WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate: fireDate, userInfo: userInfo) { (error) in + if (error == nil) { + print("successfully scheduled background task, use the crown to send the app to the background and wait for handle:BackgroundTasks to fire.") + } + } + } + +} diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh.xcodeproj/project.pbxproj b/WatchBackgroundRefresh/WatchBackgroundRrefresh.xcodeproj/project.pbxproj new file mode 100644 index 00000000..9d64764a --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh.xcodeproj/project.pbxproj @@ -0,0 +1,570 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 72002E401CF15480005AAD20 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72002E3F1CF15480005AAD20 /* AppDelegate.swift */; }; + 72002E421CF15480005AAD20 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72002E411CF15480005AAD20 /* ViewController.swift */; }; + 72002E451CF15480005AAD20 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 72002E431CF15480005AAD20 /* Main.storyboard */; }; + 72002E471CF15480005AAD20 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72002E461CF15480005AAD20 /* Assets.xcassets */; }; + 72002E4A1CF15480005AAD20 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 72002E481CF15480005AAD20 /* LaunchScreen.storyboard */; }; + 72002E4F1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 72002E4E1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit App.app */; }; + 72002E551CF15480005AAD20 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 72002E531CF15480005AAD20 /* Interface.storyboard */; }; + 72002E571CF15480005AAD20 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72002E561CF15480005AAD20 /* Assets.xcassets */; }; + 72002E5E1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 72002E5D1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 72002E631CF15480005AAD20 /* MainInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72002E621CF15480005AAD20 /* MainInterfaceController.swift */; }; + 72002E651CF15480005AAD20 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72002E641CF15480005AAD20 /* ExtensionDelegate.swift */; }; + 72002E671CF15480005AAD20 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 72002E661CF15480005AAD20 /* Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 72002E501CF15480005AAD20 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 72002E341CF15480005AAD20 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 72002E4D1CF15480005AAD20; + remoteInfo = "backgroundwatchkitapprefresh WatchKit App"; + }; + 72002E5F1CF15480005AAD20 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 72002E341CF15480005AAD20 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 72002E5C1CF15480005AAD20; + remoteInfo = "backgroundwatchkitapprefresh WatchKit Extension"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 72002E6E1CF15480005AAD20 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 72002E5E1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit Extension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 72002E721CF15480005AAD20 /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + 72002E4F1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit App.app in Embed Watch Content */, + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 72002E3C1CF15480005AAD20 /* WatchBackgroundRrefresh.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WatchBackgroundRrefresh.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 72002E3F1CF15480005AAD20 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 72002E411CF15480005AAD20 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 72002E441CF15480005AAD20 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 72002E461CF15480005AAD20 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 72002E491CF15480005AAD20 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 72002E4B1CF15480005AAD20 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 72002E4E1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WatchBackgroundRrefresh WatchKit App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 72002E541CF15480005AAD20 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; }; + 72002E561CF15480005AAD20 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 72002E581CF15480005AAD20 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 72002E5D1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "WatchBackgroundRrefresh WatchKit Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 72002E621CF15480005AAD20 /* MainInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainInterfaceController.swift; sourceTree = ""; }; + 72002E641CF15480005AAD20 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = ""; }; + 72002E661CF15480005AAD20 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 72002E681CF15480005AAD20 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B56A639B1CF71DB500D55B59 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 72002E391CF15480005AAD20 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 72002E5A1CF15480005AAD20 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 72002E331CF15480005AAD20 = { + isa = PBXGroup; + children = ( + B56A639B1CF71DB500D55B59 /* README.md */, + 72002E3E1CF15480005AAD20 /* WatchBackgroundRrefresh */, + 72002E521CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit App */, + 72002E611CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit Extension */, + 72002E3D1CF15480005AAD20 /* Products */, + ); + sourceTree = ""; + }; + 72002E3D1CF15480005AAD20 /* Products */ = { + isa = PBXGroup; + children = ( + 72002E3C1CF15480005AAD20 /* WatchBackgroundRrefresh.app */, + 72002E4E1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit App.app */, + 72002E5D1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit Extension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 72002E3E1CF15480005AAD20 /* WatchBackgroundRrefresh */ = { + isa = PBXGroup; + children = ( + 72002E3F1CF15480005AAD20 /* AppDelegate.swift */, + 72002E411CF15480005AAD20 /* ViewController.swift */, + 72002E431CF15480005AAD20 /* Main.storyboard */, + 72002E461CF15480005AAD20 /* Assets.xcassets */, + 72002E481CF15480005AAD20 /* LaunchScreen.storyboard */, + 72002E4B1CF15480005AAD20 /* Info.plist */, + ); + path = WatchBackgroundRrefresh; + sourceTree = ""; + }; + 72002E521CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit App */ = { + isa = PBXGroup; + children = ( + 72002E531CF15480005AAD20 /* Interface.storyboard */, + 72002E561CF15480005AAD20 /* Assets.xcassets */, + 72002E581CF15480005AAD20 /* Info.plist */, + ); + path = "WatchBackgroundRrefresh WatchKit App"; + sourceTree = ""; + }; + 72002E611CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit Extension */ = { + isa = PBXGroup; + children = ( + 72002E621CF15480005AAD20 /* MainInterfaceController.swift */, + 72002E641CF15480005AAD20 /* ExtensionDelegate.swift */, + 72002E661CF15480005AAD20 /* Assets.xcassets */, + 72002E681CF15480005AAD20 /* Info.plist */, + ); + path = "WatchBackgroundRrefresh WatchKit Extension"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 72002E3B1CF15480005AAD20 /* WatchBackgroundRrefresh */ = { + isa = PBXNativeTarget; + buildConfigurationList = 72002E731CF15480005AAD20 /* Build configuration list for PBXNativeTarget "WatchBackgroundRrefresh" */; + buildPhases = ( + 72002E381CF15480005AAD20 /* Sources */, + 72002E391CF15480005AAD20 /* Frameworks */, + 72002E3A1CF15480005AAD20 /* Resources */, + 72002E721CF15480005AAD20 /* Embed Watch Content */, + ); + buildRules = ( + ); + dependencies = ( + 72002E511CF15480005AAD20 /* PBXTargetDependency */, + ); + name = WatchBackgroundRrefresh; + productName = backgroundwatchkitapprefresh; + productReference = 72002E3C1CF15480005AAD20 /* WatchBackgroundRrefresh.app */; + productType = "com.apple.product-type.application"; + }; + 72002E4D1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit App */ = { + isa = PBXNativeTarget; + buildConfigurationList = 72002E6F1CF15480005AAD20 /* Build configuration list for PBXNativeTarget "WatchBackgroundRrefresh WatchKit App" */; + buildPhases = ( + 72002E4C1CF15480005AAD20 /* Resources */, + 72002E6E1CF15480005AAD20 /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 72002E601CF15480005AAD20 /* PBXTargetDependency */, + ); + name = "WatchBackgroundRrefresh WatchKit App"; + productName = "backgroundwatchkitapprefresh WatchKit App"; + productReference = 72002E4E1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit App.app */; + productType = "com.apple.product-type.application.watchapp2"; + }; + 72002E5C1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 72002E6B1CF15480005AAD20 /* Build configuration list for PBXNativeTarget "WatchBackgroundRrefresh WatchKit Extension" */; + buildPhases = ( + 72002E591CF15480005AAD20 /* Sources */, + 72002E5A1CF15480005AAD20 /* Frameworks */, + 72002E5B1CF15480005AAD20 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "WatchBackgroundRrefresh WatchKit Extension"; + productName = "backgroundwatchkitapprefresh WatchKit Extension"; + productReference = 72002E5D1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit Extension.appex */; + productType = "com.apple.product-type.watchkit2-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 72002E341CF15480005AAD20 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Apple; + TargetAttributes = { + 72002E3B1CF15480005AAD20 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + 72002E4D1CF15480005AAD20 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + 72002E5C1CF15480005AAD20 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 72002E371CF15480005AAD20 /* Build configuration list for PBXProject "WatchBackgroundRrefresh" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 72002E331CF15480005AAD20; + productRefGroup = 72002E3D1CF15480005AAD20 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 72002E3B1CF15480005AAD20 /* WatchBackgroundRrefresh */, + 72002E4D1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit App */, + 72002E5C1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit Extension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 72002E3A1CF15480005AAD20 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 72002E4A1CF15480005AAD20 /* LaunchScreen.storyboard in Resources */, + 72002E471CF15480005AAD20 /* Assets.xcassets in Resources */, + 72002E451CF15480005AAD20 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 72002E4C1CF15480005AAD20 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 72002E571CF15480005AAD20 /* Assets.xcassets in Resources */, + 72002E551CF15480005AAD20 /* Interface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 72002E5B1CF15480005AAD20 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 72002E671CF15480005AAD20 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 72002E381CF15480005AAD20 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 72002E421CF15480005AAD20 /* ViewController.swift in Sources */, + 72002E401CF15480005AAD20 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 72002E591CF15480005AAD20 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 72002E651CF15480005AAD20 /* ExtensionDelegate.swift in Sources */, + 72002E631CF15480005AAD20 /* MainInterfaceController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 72002E511CF15480005AAD20 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 72002E4D1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit App */; + targetProxy = 72002E501CF15480005AAD20 /* PBXContainerItemProxy */; + }; + 72002E601CF15480005AAD20 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 72002E5C1CF15480005AAD20 /* WatchBackgroundRrefresh WatchKit Extension */; + targetProxy = 72002E5F1CF15480005AAD20 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 72002E431CF15480005AAD20 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 72002E441CF15480005AAD20 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 72002E481CF15480005AAD20 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 72002E491CF15480005AAD20 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 72002E531CF15480005AAD20 /* Interface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 72002E541CF15480005AAD20 /* Base */, + ); + name = Interface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 72002E691CF15480005AAD20 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 72002E6A1CF15480005AAD20 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 72002E6C1CF15480005AAD20 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + INFOPLIST_FILE = "WatchBackgroundRrefresh WatchKit Extension/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.watchbackgroundrefresh.watchkitapp.watchkitextension"; + PRODUCT_NAME = "${TARGET_NAME}"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 3.0; + }; + name = Debug; + }; + 72002E6D1CF15480005AAD20 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + INFOPLIST_FILE = "WatchBackgroundRrefresh WatchKit Extension/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.watchbackgroundrefresh.watchkitapp.watchkitextension"; + PRODUCT_NAME = "${TARGET_NAME}"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 3.0; + }; + name = Release; + }; + 72002E701CF15480005AAD20 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + IBSC_MODULE = backgroundwatchkitapprefresh_WatchKit_Extension; + INFOPLIST_FILE = "WatchBackgroundRrefresh WatchKit App/Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.watchbackgroundrefresh.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 3.0; + }; + name = Debug; + }; + 72002E711CF15480005AAD20 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + IBSC_MODULE = backgroundwatchkitapprefresh_WatchKit_Extension; + INFOPLIST_FILE = "WatchBackgroundRrefresh WatchKit App/Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.watchbackgroundrefresh.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 3.0; + }; + name = Release; + }; + 72002E741CF15480005AAD20 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = WatchBackgroundRrefresh/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.watchbackgroundrefresh"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 72002E751CF15480005AAD20 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = WatchBackgroundRrefresh/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.watchbackgroundrefresh"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 72002E371CF15480005AAD20 /* Build configuration list for PBXProject "WatchBackgroundRrefresh" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 72002E691CF15480005AAD20 /* Debug */, + 72002E6A1CF15480005AAD20 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 72002E6B1CF15480005AAD20 /* Build configuration list for PBXNativeTarget "WatchBackgroundRrefresh WatchKit Extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 72002E6C1CF15480005AAD20 /* Debug */, + 72002E6D1CF15480005AAD20 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 72002E6F1CF15480005AAD20 /* Build configuration list for PBXNativeTarget "WatchBackgroundRrefresh WatchKit App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 72002E701CF15480005AAD20 /* Debug */, + 72002E711CF15480005AAD20 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 72002E731CF15480005AAD20 /* Build configuration list for PBXNativeTarget "WatchBackgroundRrefresh" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 72002E741CF15480005AAD20 /* Debug */, + 72002E751CF15480005AAD20 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 72002E341CF15480005AAD20 /* Project object */; +} diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh/AppDelegate.swift b/WatchBackgroundRefresh/WatchBackgroundRrefresh/AppDelegate.swift new file mode 100644 index 00000000..fe1bc68d --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh/AppDelegate.swift @@ -0,0 +1,16 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The App Delegate. + */ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + +} + diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh/Assets.xcassets/AppIcon.appiconset/Contents.json b/WatchBackgroundRefresh/WatchBackgroundRrefresh/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..36d2c80d --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh/Base.lproj/LaunchScreen.storyboard b/WatchBackgroundRefresh/WatchBackgroundRrefresh/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..06a12191 --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh/Base.lproj/Main.storyboard b/WatchBackgroundRefresh/WatchBackgroundRrefresh/Base.lproj/Main.storyboard new file mode 100644 index 00000000..3a2a49ba --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh/Base.lproj/Main.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh/Info.plist b/WatchBackgroundRefresh/WatchBackgroundRrefresh/Info.plist new file mode 100644 index 00000000..40c6215d --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/WatchBackgroundRefresh/WatchBackgroundRrefresh/ViewController.swift b/WatchBackgroundRefresh/WatchBackgroundRrefresh/ViewController.swift new file mode 100644 index 00000000..fee1cfc0 --- /dev/null +++ b/WatchBackgroundRefresh/WatchBackgroundRrefresh/ViewController.swift @@ -0,0 +1,15 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The View Controller. + */ + +import UIKit + +class ViewController: UIViewController { + + +} + diff --git a/WatchPuzzle/LICENSE.txt b/WatchPuzzle/LICENSE.txt new file mode 100644 index 00000000..ac1ba928 --- /dev/null +++ b/WatchPuzzle/LICENSE.txt @@ -0,0 +1,42 @@ +Sample code project: WatchPuzzle: Using SceneKit and SpriteKit on watchOS +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) 2016 Apple Inc. All Rights Reserved. diff --git a/WatchPuzzle/README.md b/WatchPuzzle/README.md new file mode 100644 index 00000000..c3de7d48 --- /dev/null +++ b/WatchPuzzle/README.md @@ -0,0 +1,15 @@ +# WatchPuzzle: Using SceneKit and SpriteKit on watchOS + +This sample demonstrate how to use SceneKit with watchOS. Refer to this sample if you want to see how to use, configure, and interact with a 3D scene using SceneKit. This project shows you how to load a 3D SceneKit scene, add overlays using SpriteKit and setup gesture recognizer to manipulate the scene graph. + +## Requirements + +### Build + +Xcode 8.0 or later; iOS 10.0 SDK or later; watchOS 3.0 SDK or later + +### Runtime + +iOS 10.0 or later; watchOS 3.0 or later + +Copyright (C) 2016 Apple Inc. All rights reserved. diff --git a/WatchPuzzle/WatchPuzzle WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json b/WatchPuzzle/WatchPuzzle WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..dd221ba5 --- /dev/null +++ b/WatchPuzzle/WatchPuzzle WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,55 @@ +{ + "images" : [ + { + "size" : "24x24", + "idiom" : "watch", + "scale" : "2x", + "role" : "notificationCenter", + "subtype" : "38mm" + }, + { + "size" : "27.5x27.5", + "idiom" : "watch", + "scale" : "2x", + "role" : "notificationCenter", + "subtype" : "42mm" + }, + { + "size" : "29x29", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "watch", + "scale" : "2x", + "role" : "appLauncher", + "subtype" : "38mm" + }, + { + "size" : "86x86", + "idiom" : "watch", + "scale" : "2x", + "role" : "quickLook", + "subtype" : "38mm" + }, + { + "size" : "98x98", + "idiom" : "watch", + "scale" : "2x", + "role" : "quickLook", + "subtype" : "42mm" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/WatchPuzzle/WatchPuzzle WatchKit App/Base.lproj/Interface.storyboard b/WatchPuzzle/WatchPuzzle WatchKit App/Base.lproj/Interface.storyboard new file mode 100644 index 00000000..abdb331e --- /dev/null +++ b/WatchPuzzle/WatchPuzzle WatchKit App/Base.lproj/Interface.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WatchPuzzle/WatchPuzzle WatchKit App/Info.plist b/WatchPuzzle/WatchPuzzle WatchKit App/Info.plist new file mode 100644 index 00000000..347b1eee --- /dev/null +++ b/WatchPuzzle/WatchPuzzle WatchKit App/Info.plist @@ -0,0 +1,35 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + WatchPuzzle + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + WKCompanionAppBundleIdentifier + com.example.apple-samplecode.WatchPuzzle + WKWatchKitApp + + + diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json b/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json new file mode 100644 index 00000000..f2a7f51c --- /dev/null +++ b/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : "<=145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">145" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json b/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json new file mode 100644 index 00000000..2bf25222 --- /dev/null +++ b/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json @@ -0,0 +1,23 @@ +{ + "assets" : [ + { + "idiom" : "watch", + "filename" : "Circular.imageset", + "role" : "circular" + }, + { + "idiom" : "watch", + "filename" : "Modular.imageset", + "role" : "modular" + }, + { + "idiom" : "watch", + "filename" : "Utilitarian.imageset", + "role" : "utilitarian" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json b/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json new file mode 100644 index 00000000..f2a7f51c --- /dev/null +++ b/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : "<=145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">145" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json b/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json new file mode 100644 index 00000000..f2a7f51c --- /dev/null +++ b/WatchPuzzle/WatchPuzzle WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "watch", + "scale" : "2x" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : "<=145" + }, + { + "idiom" : "watch", + "scale" : "2x", + "screen-width" : ">145" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/ExtensionDelegate.swift b/WatchPuzzle/WatchPuzzle WatchKit Extension/ExtensionDelegate.swift new file mode 100644 index 00000000..9b9a1fe0 --- /dev/null +++ b/WatchPuzzle/WatchPuzzle WatchKit Extension/ExtensionDelegate.swift @@ -0,0 +1,12 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + The WatchOS implementation of the app extension delegate. + */ + +import WatchKit + +class ExtensionDelegate: NSObject, WKExtensionDelegate { +} diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/Info.plist b/WatchPuzzle/WatchPuzzle WatchKit Extension/Info.plist new file mode 100644 index 00000000..d46f4df7 --- /dev/null +++ b/WatchPuzzle/WatchPuzzle WatchKit Extension/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + WatchPuzzle WatchKit Extension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + WKAppBundleIdentifier + com.example.apple-samplecode.WatchPuzzle.watchkitapp + + NSExtensionPointIdentifier + com.apple.watchkit + + WKExtensionDelegateClassName + $(PRODUCT_MODULE_NAME).ExtensionDelegate + + diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/InterfaceController.swift b/WatchPuzzle/WatchPuzzle WatchKit Extension/InterfaceController.swift new file mode 100644 index 00000000..41f62b48 --- /dev/null +++ b/WatchPuzzle/WatchPuzzle WatchKit Extension/InterfaceController.swift @@ -0,0 +1,382 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + WatchOS WKInterfaceController implementation of the game. + */ + +import WatchKit +import Foundation +import simd +import SceneKit +import SpriteKit + +class InterfaceController: WKInterfaceController { + // MARK: Types + + /// A struct containing all the `SCNNode`s used in the game. + struct GameNodes { + let object: SCNNode + + let objectMaterial: SCNMaterial + + let confetti: SCNNode + + let camera: SCNCamera + + let countdownLabel: SKLabelNode + + let congratulationsLabel: SKLabelNode + + /// Queries the root node for the expected nodes. + init?(sceneRoot: SCNNode) { + guard let object = sceneRoot.childNode(withName: "teapot", recursively:true), let objectMaterial = object.geometry?.firstMaterial else { return nil } + guard let confetti = sceneRoot.childNode(withName: "particles", recursively: true) else { return nil } + guard let camera = sceneRoot.childNode(withName: "camera", recursively: true)!.camera else { return nil } + + self.object = object + self.objectMaterial = objectMaterial + self.confetti = confetti + self.camera = camera + + countdownLabel = SKLabelNode() + countdownLabel.horizontalAlignmentMode = .center + + congratulationsLabel = SKLabelNode(text: "You Win!") + congratulationsLabel.fontColor = InterfaceController.GameColors.defaultFont + congratulationsLabel.fontSize = 45; + } + } + + /// Defines the colors used in the game. + struct GameColors { + static let defaultFont = UIColor(red:31.0/255, green:226.0/255.0, blue:63.0/255.0, alpha:1.0) + static let warning = UIColor.orange + static let danger = UIColor.red + } + + + // MARK: Properties + + @IBOutlet var sceneInterface: WKInterfaceSCNScene! + + var gameNodes: GameNodes? + + var gameStarted = false + + var initialObject3DRotation = SCNMatrix4Identity + + var initialSphereLocation = float3() + + var countdown = 0 + + weak var textUpdateTimer: Timer? + + weak var particleRemovalTimer: Timer? + + // MARK: WKInterfaceController + + override func awake(withContext context: Any?) { + super.awake(withContext: context) + + setupGame() + } + + override func willActivate() { + // Start the game if not already started. + if !gameStarted { + startGame() + } + + super.willActivate() + } + + // MARK: IB Actions + + @IBAction func handleTap(sender: AnyObject) { + if let tapGesture = sender as? WKTapGestureRecognizer { + if tapGesture.numberOfTapsRequired == 1 && !gameStarted { + // Restart the game on single tap only if presenting congratulation screen. + startGame() + } + } + } + + // MARK: Gesture reconginzer handling + + /** + Handle rotation of the 3D object by computing rotations of a virtual + trackball using the pan gesture touch locations. + + On state ended, end the game if the object has the right orientation. + */ + @IBAction func handlePan(panGesture: WKPanGestureRecognizer) { + guard let gameNodes = gameNodes, gameStarted else { return } + + let location = panGesture.locationInObject() + let bounds = panGesture.objectBounds() + + // Compute the projection of the interface point to the virtual trackball. + let sphereLocation = sphereProjection(forInterfaceLocation: location, inBounds: bounds) + + switch panGesture.state { + case .began: + // Record initial states. + initialSphereLocation = sphereLocation + initialObject3DRotation = gameNodes.object.transform + + case .cancelled, .ended, .changed: + // Compute the rotation and apply to the object. + let currentRotation = rotationFromPoint(initialSphereLocation, to: sphereLocation) + gameNodes.object.transform = SCNMatrix4Mult(initialObject3DRotation, currentRotation) + + default: + debugPrint("Unhandled gesture state: \(panGesture.state)") + } + + // End the game if the object has the initial orientation. + if panGesture.state == .ended { + endGameOnCorrectOrientation() + } + } + + // MARK: Game flow + + /// Setup overlays and lookup scene objects. + func setupGame() { + guard let sceneRoot = sceneInterface.scene?.rootNode, let gameNodes = GameNodes(sceneRoot: sceneRoot) else { fatalError("Unable to load game nodes") } + self.gameNodes = gameNodes + + gameNodes.object.transform = SCNMatrix4Identity + gameNodes.objectMaterial.transparency = 0.0 + + gameNodes.confetti.isHidden = true + + let skScene = SKScene(size: CGSize(width: contentFrame.size.width, height: contentFrame.size.height)) + skScene.scaleMode = SKSceneScaleMode.resizeFill + skScene.addChild(gameNodes.countdownLabel) + + sceneInterface.overlaySKScene = skScene + } + + /// Start the game. + func startGame() { + guard let gameNodes = gameNodes else { fatalError("Nodes not set") } + + let startSequence = SCNAction.sequence([ + // Wait for 1 second. + SCNAction.wait(duration: 1.0), + + SCNAction.group([ + // Fade in. + SCNAction.fadeIn(duration: 0.3), + + // Start the game. + SCNAction.run({ [weak self] (node: SCNNode) in + guard let gameNodes = self?.gameNodes else { return } + + // Compute a random orientation for the object3D. + let theta = Float(M_PI) * (Float(arc4random()) / 0x100000000) + let phi = acosf(2.0 * Float(arc4random()) / 0x100000000 - 1) / Float(M_PI) + var axis = float3() + axis.x = cosf(theta) * sinf(phi) + axis.y = sinf(theta) * sinf(phi) + axis.z = cosf(theta) + let angle = 2.0 * Float(M_PI) * (Float(arc4random()) / 0x100000000) + + SCNTransaction.begin() + SCNTransaction.animationDuration = 0.3 + SCNTransaction.completionBlock = { + self?.gameStarted = true + } + + gameNodes.objectMaterial.transparency = 1.0 + gameNodes.object.transform = SCNMatrix4MakeRotation(angle, axis.x, axis.y, axis.z) + + SCNTransaction.commit() + }), + ]) + ]) + gameNodes.object.runAction(startSequence) + + // Load and set the background image. + let backgroundImage = UIImage(named:"art.scnassets/background.png") + sceneInterface.scene?.background.contents = backgroundImage + + // Hide particles, set camera projection to orthographic. + particleRemovalTimer?.invalidate() + gameNodes.congratulationsLabel.removeFromParent() + gameNodes.confetti.isHidden = true + gameNodes.camera.usesOrthographicProjection = true + + // Reset the countdown. + countdown = 30 + gameNodes.countdownLabel.text = "\(countdown)" + gameNodes.countdownLabel.fontColor = InterfaceController.GameColors.defaultFont + + gameNodes.countdownLabel.position = CGPoint(x: contentFrame.size.width / 2, y: contentFrame.size.height - 30) + + textUpdateTimer?.invalidate() + textUpdateTimer = Timer.scheduledTimer(timeInterval: 1, + target: self, + selector: #selector(updateText(timer:)), + userInfo: nil, + repeats: true) + } + + /// Update countdown timer. + func updateText(timer: Timer) { + guard let gameNodes = gameNodes else { fatalError("Nodes not set") } + + gameNodes.countdownLabel.text = "\(countdown)" + sceneInterface.isPlaying = true + sceneInterface.isPlaying = false + countdown -= 1 + + if countdown < 0 { + gameNodes.countdownLabel.fontColor = InterfaceController.GameColors.danger + textUpdateTimer?.invalidate() + return + } + else if countdown < 10 { + gameNodes.countdownLabel.fontColor = InterfaceController.GameColors.warning + } + } + + /** + End the game by showing the congratulation screen after fading the object + to white. + */ + func endGame() { + guard let gameNodes = gameNodes else { fatalError("Nodes not set") } + + textUpdateTimer?.invalidate() + + SCNTransaction.begin() + SCNTransaction.animationDuration = 0.5 + SCNTransaction.completionBlock = { () in + SCNTransaction.begin() + SCNTransaction.animationDuration = 0.3 + SCNTransaction.completionBlock = { [weak self] () in + self?.showCongratulation() + gameNodes.objectMaterial.emission.contents = UIColor.black + self?.gameStarted = false + } + SCNTransaction.commit() + } + + gameNodes.object.transform = SCNMatrix4Identity + gameNodes.objectMaterial.emission.contents = UIColor.white + gameNodes.objectMaterial.transparency = 0.0 + + SCNTransaction.commit() + } + + // MARK: Convenience + + /// Compute the projection of screen points to unit sphere points. + func sphereProjection(forInterfaceLocation location: CGPoint, inBounds bounds: CGRect) -> float3 { + let screenLocation = screenProjection(forInterfaceLocation: location, inBounds: bounds) + return sphereProjection(forScreenLocation: screenLocation) + } + + /// Compute projection from object interface to virtual screen on the range [-1, 1]. + func screenProjection(forInterfaceLocation location: CGPoint, inBounds bounds: CGRect) -> CGPoint { + let w = bounds.size.width + let h = bounds.size.height + let aspectRatioCorrection = (h - w) / 2 + var screenCoord = CGPoint(x: location.x / w * 2.0 - 1.0, + y: ((h - location.y) - aspectRatioCorrection) / w * 2.0 - 1.0) + screenCoord.x = min(1.0, max(-1.0, screenCoord.x)) + screenCoord.y = min(1.0, max(-1.0, screenCoord.y)) + return screenCoord + } + + /// Compute projection of virtual screen point to unit sphere. + func sphereProjection(forScreenLocation location: CGPoint) -> float3 { + var sphereCoord = float3() + let squaredLenght = location.x * location.x + location.y * location.y + + if squaredLenght <= 1.0 { + sphereCoord.x = Float(location.x) + sphereCoord.y = Float(location.y) + sphereCoord.z = sqrtf(1.0 - Float(squaredLenght)) + } else { + let n = 1.0 / sqrtf(Float(squaredLenght)) + sphereCoord.x = n * Float(location.x) + sphereCoord.y = n * Float(location.y) + sphereCoord.z = 0 + } + + return sphereCoord + } + + /// Compute the rotation matrix from one point to another on a unit sphere. + func rotationFromPoint(_ start: float3, to end: float3) -> SCNMatrix4 { + let axis = cross(start, end) + let angle = atan2f(length(axis), dot(start, end)) + + return SCNMatrix4MakeRotation(angle, axis.x, axis.y, axis.z) + } + + /// End the game if the object has its initial orientation with a 10 degree tolerance. + func endGameOnCorrectOrientation() { + guard let gameNodes = gameNodes, gameStarted else { return } + + let transform = SCNMatrix4ToMat4(gameNodes.object.transform) + let unitX: float4 = [1 , 0, 0, 0] + let unitY: float4 = [0 , 1, 0, 0] + let tX: float4 = matrix_multiply(unitX, transform) + let tY: float4 = matrix_multiply(unitY, transform) + + let toleranceDegree : Float = 10.0 + let max_cos_angle = cosf(toleranceDegree * Float(M_PI) / 180) + let cos_angleX = dot(unitX, tX) + let cos_angleY = dot(unitY, tY) + + if cos_angleX >= max_cos_angle && cos_angleY >= max_cos_angle { + endGame() + } + } + + // Show the congratulation screen. + func showCongratulation() { + guard let gameNodes = gameNodes else { fatalError("Nodes not set") } + + gameNodes.camera.usesOrthographicProjection = false + + sceneInterface.scene?.background.contents = UIColor.black + + gameNodes.confetti.isHidden = false + particleRemovalTimer?.invalidate() + particleRemovalTimer = Timer.scheduledTimer(timeInterval: 30, + target: self, + selector: #selector(removeParticles(timer:)), + userInfo: nil, + repeats:false) + + gameNodes.congratulationsLabel.removeFromParent() + gameNodes.congratulationsLabel.position = CGPoint(x: contentFrame.size.width/2 , y: contentFrame.size.height/2) + gameNodes.congratulationsLabel.xScale = 0; + gameNodes.congratulationsLabel.yScale = 0; + gameNodes.congratulationsLabel.alpha = 0; + gameNodes.congratulationsLabel.run( + SKAction.group([ + SKAction.fadeIn(withDuration:0.25), + SKAction.sequence([ + SKAction.scale(to: 0.70, duration:0.25), + SKAction.scale(to: 0.80, duration:0.2)]), + ]) + ) + + sceneInterface.overlaySKScene?.addChild(gameNodes.congratulationsLabel) + } + + // Remove the confetti particles. + func removeParticles(timer: Timer) { + guard let gameNodes = gameNodes else { fatalError("Nodes not set") } + + gameNodes.confetti.isHidden = true + } +} diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/background.png b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/background.png new file mode 100644 index 00000000..69cda022 Binary files /dev/null and b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/background.png differ diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/confetti.png b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/confetti.png new file mode 100644 index 00000000..28f46f50 Binary files /dev/null and b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/confetti.png differ diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/congratulations.png b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/congratulations.png new file mode 100644 index 00000000..186a386e Binary files /dev/null and b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/congratulations.png differ diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/sample.scn b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/sample.scn new file mode 100644 index 00000000..6349b62e Binary files /dev/null and b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/sample.scn differ diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/teapot_AO.png b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/teapot_AO.png new file mode 100644 index 00000000..a906ed56 Binary files /dev/null and b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/teapot_AO.png differ diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/texture-wood2.png b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/texture-wood2.png new file mode 100644 index 00000000..8cfea1a7 Binary files /dev/null and b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/texture-wood2.png differ diff --git a/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/watch_reflection.png b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/watch_reflection.png new file mode 100644 index 00000000..9ca82031 Binary files /dev/null and b/WatchPuzzle/WatchPuzzle WatchKit Extension/art.scnassets/watch_reflection.png differ diff --git a/WatchPuzzle/WatchPuzzle.xcodeproj/project.pbxproj b/WatchPuzzle/WatchPuzzle.xcodeproj/project.pbxproj new file mode 100644 index 00000000..60f1b8ea --- /dev/null +++ b/WatchPuzzle/WatchPuzzle.xcodeproj/project.pbxproj @@ -0,0 +1,572 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 290ED2DB1CF33D7B003CB7F2 /* art.scnassets in Resources */ = {isa = PBXBuildFile; fileRef = 290ED2DA1CF33D7B003CB7F2 /* art.scnassets */; }; + 29EEC4D31CECC14D0048E528 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EEC4D21CECC14D0048E528 /* AppDelegate.swift */; }; + 29EEC4D51CECC14D0048E528 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EEC4D41CECC14D0048E528 /* ViewController.swift */; }; + 29EEC4D81CECC14D0048E528 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 29EEC4D61CECC14D0048E528 /* Main.storyboard */; }; + 29EEC4DA1CECC14D0048E528 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 29EEC4D91CECC14D0048E528 /* Assets.xcassets */; }; + 29EEC4DD1CECC14D0048E528 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 29EEC4DB1CECC14D0048E528 /* LaunchScreen.storyboard */; }; + 29EEC4E21CECC14D0048E528 /* WatchPuzzle WatchKit App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 29EEC4E11CECC14D0048E528 /* WatchPuzzle WatchKit App.app */; }; + 29EEC4E81CECC14D0048E528 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 29EEC4E61CECC14D0048E528 /* Interface.storyboard */; }; + 29EEC4EA1CECC14D0048E528 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 29EEC4E91CECC14D0048E528 /* Assets.xcassets */; }; + 29EEC4F11CECC14D0048E528 /* WatchPuzzle WatchKit Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 29EEC4F01CECC14D0048E528 /* WatchPuzzle WatchKit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 29EEC4F61CECC14D0048E528 /* InterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EEC4F51CECC14D0048E528 /* InterfaceController.swift */; }; + 29EEC4F81CECC14D0048E528 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29EEC4F71CECC14D0048E528 /* ExtensionDelegate.swift */; }; + 29EEC4FA1CECC14D0048E528 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 29EEC4F91CECC14D0048E528 /* Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 29EEC4E31CECC14D0048E528 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 29EEC4C71CECC14D0048E528 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 29EEC4E01CECC14D0048E528; + remoteInfo = "teapot_swift WatchKit App"; + }; + 29EEC4F21CECC14D0048E528 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 29EEC4C71CECC14D0048E528 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 29EEC4EF1CECC14D0048E528; + remoteInfo = "teapot_swift WatchKit Extension"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 29EEC5011CECC14D0048E528 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 29EEC4F11CECC14D0048E528 /* WatchPuzzle WatchKit Extension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 29EEC5051CECC14D0048E528 /* Embed Watch Content */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; + dstSubfolderSpec = 16; + files = ( + 29EEC4E21CECC14D0048E528 /* WatchPuzzle WatchKit App.app in Embed Watch Content */, + ); + name = "Embed Watch Content"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 290ED2DA1CF33D7B003CB7F2 /* art.scnassets */ = {isa = PBXFileReference; lastKnownFileType = wrapper.scnassets; path = art.scnassets; sourceTree = ""; }; + 29EEC4CF1CECC14D0048E528 /* WatchPuzzle.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WatchPuzzle.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 29EEC4D21CECC14D0048E528 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 29EEC4D41CECC14D0048E528 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 29EEC4D71CECC14D0048E528 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 29EEC4D91CECC14D0048E528 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 29EEC4DC1CECC14D0048E528 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 29EEC4DE1CECC14D0048E528 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 29EEC4E11CECC14D0048E528 /* WatchPuzzle WatchKit App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "WatchPuzzle WatchKit App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 29EEC4E71CECC14D0048E528 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; }; + 29EEC4E91CECC14D0048E528 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 29EEC4EB1CECC14D0048E528 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 29EEC4F01CECC14D0048E528 /* WatchPuzzle WatchKit Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "WatchPuzzle WatchKit Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 29EEC4F51CECC14D0048E528 /* InterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceController.swift; sourceTree = ""; }; + 29EEC4F71CECC14D0048E528 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = ""; }; + 29EEC4F91CECC14D0048E528 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 29EEC4FB1CECC14D0048E528 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B59526461DA84C34005E4553 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 29EEC4CC1CECC14D0048E528 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 29EEC4ED1CECC14D0048E528 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 29EEC4C61CECC14D0048E528 = { + isa = PBXGroup; + children = ( + B59526461DA84C34005E4553 /* README.md */, + 29EEC4D11CECC14D0048E528 /* WatchPuzzle */, + 29EEC4E51CECC14D0048E528 /* WatchPuzzle WatchKit App */, + 29EEC4F41CECC14D0048E528 /* WatchPuzzle WatchKit Extension */, + 29EEC4D01CECC14D0048E528 /* Products */, + ); + sourceTree = ""; + }; + 29EEC4D01CECC14D0048E528 /* Products */ = { + isa = PBXGroup; + children = ( + 29EEC4CF1CECC14D0048E528 /* WatchPuzzle.app */, + 29EEC4E11CECC14D0048E528 /* WatchPuzzle WatchKit App.app */, + 29EEC4F01CECC14D0048E528 /* WatchPuzzle WatchKit Extension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 29EEC4D11CECC14D0048E528 /* WatchPuzzle */ = { + isa = PBXGroup; + children = ( + 29EEC4D21CECC14D0048E528 /* AppDelegate.swift */, + 29EEC4D41CECC14D0048E528 /* ViewController.swift */, + 29EEC4D61CECC14D0048E528 /* Main.storyboard */, + 29EEC4D91CECC14D0048E528 /* Assets.xcassets */, + 29EEC4DB1CECC14D0048E528 /* LaunchScreen.storyboard */, + 29EEC4DE1CECC14D0048E528 /* Info.plist */, + ); + path = WatchPuzzle; + sourceTree = ""; + }; + 29EEC4E51CECC14D0048E528 /* WatchPuzzle WatchKit App */ = { + isa = PBXGroup; + children = ( + 29EEC4E61CECC14D0048E528 /* Interface.storyboard */, + 29EEC4E91CECC14D0048E528 /* Assets.xcassets */, + 29EEC4EB1CECC14D0048E528 /* Info.plist */, + ); + path = "WatchPuzzle WatchKit App"; + sourceTree = ""; + }; + 29EEC4F41CECC14D0048E528 /* WatchPuzzle WatchKit Extension */ = { + isa = PBXGroup; + children = ( + 290ED2DA1CF33D7B003CB7F2 /* art.scnassets */, + 29EEC4F51CECC14D0048E528 /* InterfaceController.swift */, + 29EEC4F71CECC14D0048E528 /* ExtensionDelegate.swift */, + 29EEC4F91CECC14D0048E528 /* Assets.xcassets */, + 29EEC4FB1CECC14D0048E528 /* Info.plist */, + ); + path = "WatchPuzzle WatchKit Extension"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 29EEC4CE1CECC14D0048E528 /* WatchPuzzle */ = { + isa = PBXNativeTarget; + buildConfigurationList = 29EEC5061CECC14D0048E528 /* Build configuration list for PBXNativeTarget "WatchPuzzle" */; + buildPhases = ( + 29EEC4CB1CECC14D0048E528 /* Sources */, + 29EEC4CC1CECC14D0048E528 /* Frameworks */, + 29EEC4CD1CECC14D0048E528 /* Resources */, + 29EEC5051CECC14D0048E528 /* Embed Watch Content */, + ); + buildRules = ( + ); + dependencies = ( + 29EEC4E41CECC14D0048E528 /* PBXTargetDependency */, + ); + name = WatchPuzzle; + productName = teapot_swift; + productReference = 29EEC4CF1CECC14D0048E528 /* WatchPuzzle.app */; + productType = "com.apple.product-type.application"; + }; + 29EEC4E01CECC14D0048E528 /* WatchPuzzle WatchKit App */ = { + isa = PBXNativeTarget; + buildConfigurationList = 29EEC5021CECC14D0048E528 /* Build configuration list for PBXNativeTarget "WatchPuzzle WatchKit App" */; + buildPhases = ( + 29EEC4DF1CECC14D0048E528 /* Resources */, + 29EEC5011CECC14D0048E528 /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 29EEC4F31CECC14D0048E528 /* PBXTargetDependency */, + ); + name = "WatchPuzzle WatchKit App"; + productName = "teapot_swift WatchKit App"; + productReference = 29EEC4E11CECC14D0048E528 /* WatchPuzzle WatchKit App.app */; + productType = "com.apple.product-type.application.watchapp2"; + }; + 29EEC4EF1CECC14D0048E528 /* WatchPuzzle WatchKit Extension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 29EEC4FE1CECC14D0048E528 /* Build configuration list for PBXNativeTarget "WatchPuzzle WatchKit Extension" */; + buildPhases = ( + 29EEC4EC1CECC14D0048E528 /* Sources */, + 29EEC4ED1CECC14D0048E528 /* Frameworks */, + 29EEC4EE1CECC14D0048E528 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "WatchPuzzle WatchKit Extension"; + productName = "teapot_swift WatchKit Extension"; + productReference = 29EEC4F01CECC14D0048E528 /* WatchPuzzle WatchKit Extension.appex */; + productType = "com.apple.product-type.watchkit2-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 29EEC4C71CECC14D0048E528 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0800; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Apple; + TargetAttributes = { + 29EEC4CE1CECC14D0048E528 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + 29EEC4E01CECC14D0048E528 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + 29EEC4EF1CECC14D0048E528 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 29EEC4CA1CECC14D0048E528 /* Build configuration list for PBXProject "WatchPuzzle" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 29EEC4C61CECC14D0048E528; + productRefGroup = 29EEC4D01CECC14D0048E528 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 29EEC4CE1CECC14D0048E528 /* WatchPuzzle */, + 29EEC4E01CECC14D0048E528 /* WatchPuzzle WatchKit App */, + 29EEC4EF1CECC14D0048E528 /* WatchPuzzle WatchKit Extension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 29EEC4CD1CECC14D0048E528 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 29EEC4DD1CECC14D0048E528 /* LaunchScreen.storyboard in Resources */, + 29EEC4DA1CECC14D0048E528 /* Assets.xcassets in Resources */, + 29EEC4D81CECC14D0048E528 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 29EEC4DF1CECC14D0048E528 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 29EEC4EA1CECC14D0048E528 /* Assets.xcassets in Resources */, + 29EEC4E81CECC14D0048E528 /* Interface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 29EEC4EE1CECC14D0048E528 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 29EEC4FA1CECC14D0048E528 /* Assets.xcassets in Resources */, + 290ED2DB1CF33D7B003CB7F2 /* art.scnassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 29EEC4CB1CECC14D0048E528 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 29EEC4D51CECC14D0048E528 /* ViewController.swift in Sources */, + 29EEC4D31CECC14D0048E528 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 29EEC4EC1CECC14D0048E528 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 29EEC4F81CECC14D0048E528 /* ExtensionDelegate.swift in Sources */, + 29EEC4F61CECC14D0048E528 /* InterfaceController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 29EEC4E41CECC14D0048E528 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 29EEC4E01CECC14D0048E528 /* WatchPuzzle WatchKit App */; + targetProxy = 29EEC4E31CECC14D0048E528 /* PBXContainerItemProxy */; + }; + 29EEC4F31CECC14D0048E528 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 29EEC4EF1CECC14D0048E528 /* WatchPuzzle WatchKit Extension */; + targetProxy = 29EEC4F21CECC14D0048E528 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 29EEC4D61CECC14D0048E528 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 29EEC4D71CECC14D0048E528 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 29EEC4DB1CECC14D0048E528 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 29EEC4DC1CECC14D0048E528 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 29EEC4E61CECC14D0048E528 /* Interface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 29EEC4E71CECC14D0048E528 /* Base */, + ); + name = Interface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 29EEC4FC1CECC14D0048E528 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = watchos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 29EEC4FD1CECC14D0048E528 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = watchos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 29EEC4FF1CECC14D0048E528 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + INFOPLIST_FILE = "WatchPuzzle WatchKit Extension/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.WatchPuzzle.watchkitapp.watchkitextension"; + PRODUCT_NAME = "${TARGET_NAME}"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 3.0; + }; + name = Debug; + }; + 29EEC5001CECC14D0048E528 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; + INFOPLIST_FILE = "WatchPuzzle WatchKit Extension/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.WatchPuzzle.watchkitapp.watchkitextension"; + PRODUCT_NAME = "${TARGET_NAME}"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 3.0; + }; + name = Release; + }; + 29EEC5031CECC14D0048E528 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + IBSC_MODULE = WatchPuzzle_WatchKit_Extension; + INFOPLIST_FILE = "WatchPuzzle WatchKit App/Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.WatchPuzzle.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 3.0; + }; + name = Debug; + }; + 29EEC5041CECC14D0048E528 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + IBSC_MODULE = WatchPuzzle_WatchKit_Extension; + INFOPLIST_FILE = "WatchPuzzle WatchKit App/Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.WatchPuzzle.watchkitapp"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 3.0; + }; + name = Release; + }; + 29EEC5071CECC14D0048E528 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = WatchPuzzle/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.WatchPuzzle"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos10.0; + SWIFT_VERSION = 3.0; + }; + name = Debug; + }; + 29EEC5081CECC14D0048E528 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = WatchPuzzle/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.WatchPuzzle"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos10.0; + SWIFT_VERSION = 3.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 29EEC4CA1CECC14D0048E528 /* Build configuration list for PBXProject "WatchPuzzle" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 29EEC4FC1CECC14D0048E528 /* Debug */, + 29EEC4FD1CECC14D0048E528 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 29EEC4FE1CECC14D0048E528 /* Build configuration list for PBXNativeTarget "WatchPuzzle WatchKit Extension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 29EEC4FF1CECC14D0048E528 /* Debug */, + 29EEC5001CECC14D0048E528 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 29EEC5021CECC14D0048E528 /* Build configuration list for PBXNativeTarget "WatchPuzzle WatchKit App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 29EEC5031CECC14D0048E528 /* Debug */, + 29EEC5041CECC14D0048E528 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 29EEC5061CECC14D0048E528 /* Build configuration list for PBXNativeTarget "WatchPuzzle" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 29EEC5071CECC14D0048E528 /* Debug */, + 29EEC5081CECC14D0048E528 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 29EEC4C71CECC14D0048E528 /* Project object */; +} diff --git a/WatchPuzzle/WatchPuzzle/AppDelegate.swift b/WatchPuzzle/WatchPuzzle/AppDelegate.swift new file mode 100644 index 00000000..48305e5f --- /dev/null +++ b/WatchPuzzle/WatchPuzzle/AppDelegate.swift @@ -0,0 +1,21 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + iOS application delegate. + */ + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + return true + } + +} + diff --git a/WatchPuzzle/WatchPuzzle/Assets.xcassets/AppIcon.appiconset/Contents.json b/WatchPuzzle/WatchPuzzle/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..eeea76c2 --- /dev/null +++ b/WatchPuzzle/WatchPuzzle/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,73 @@ +{ + "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" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/WatchPuzzle/WatchPuzzle/Base.lproj/LaunchScreen.storyboard b/WatchPuzzle/WatchPuzzle/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..2e721e18 --- /dev/null +++ b/WatchPuzzle/WatchPuzzle/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WatchPuzzle/WatchPuzzle/Base.lproj/Main.storyboard b/WatchPuzzle/WatchPuzzle/Base.lproj/Main.storyboard new file mode 100644 index 00000000..3a2a49ba --- /dev/null +++ b/WatchPuzzle/WatchPuzzle/Base.lproj/Main.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WatchPuzzle/WatchPuzzle/Info.plist b/WatchPuzzle/WatchPuzzle/Info.plist new file mode 100644 index 00000000..40c6215d --- /dev/null +++ b/WatchPuzzle/WatchPuzzle/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/WatchPuzzle/WatchPuzzle/ViewController.swift b/WatchPuzzle/WatchPuzzle/ViewController.swift new file mode 100644 index 00000000..c1cda813 --- /dev/null +++ b/WatchPuzzle/WatchPuzzle/ViewController.swift @@ -0,0 +1,13 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + iOS application view controller. + */ + +import UIKit + +class ViewController: UIViewController { +} + diff --git a/_META/fetch-samplecode.sh b/_META/fetch-samplecode.sh new file mode 100755 index 00000000..0277a805 --- /dev/null +++ b/_META/fetch-samplecode.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +curl 'https://developer.apple.com/search/search_data.php?q=swift%203&results=500' -s -e developer.apple.com -o search_result.json + +ruby -e "require 'json'; +require 'csv'; +CSV.open('samplecode.csv', 'wb') do |csv| + JSON.parse(File.open('search_result.json').read)['results'].keep_if {|v| + v['type'] == 'sample_code' + }.sort_by {|x| x['title']}.each{|sc| + csv << [sc['url'].match('\/samplecode\/([^\/]*)\/')[1], 'https://developer.apple.com' + sc['url'] + 'Introduction/Intro.html'] + } +end +" diff --git a/_META/samplecode.csv b/_META/samplecode.csv new file mode 100644 index 00000000..eb8f0bcf --- /dev/null +++ b/_META/samplecode.csv @@ -0,0 +1,25 @@ +avloopplayer,https://developer.apple.com/library/content/samplecode/avloopplayer/Listings/../Introduction/Intro.html +avexporter,https://developer.apple.com/library/content/samplecode/avexporter/Listings/../Introduction/Intro.html +AVFoundationSimplePlayer-iOS,https://developer.apple.com/library/content/samplecode/AVFoundationSimplePlayer-iOS/Listings/../Introduction/Intro.html +ReaderWriter,https://developer.apple.com/library/content/samplecode/ReaderWriter/Listings/../Introduction/Intro.html +AdaptiveElements,https://developer.apple.com/library/content/samplecode/AdaptiveElements/Listings/../Introduction/Intro.html +AdoptingMetalII,https://developer.apple.com/library/content/samplecode/AdoptingMetalII/Listings/../Introduction/Intro.html +DemoBots,https://developer.apple.com/library/content/samplecode/DemoBots/Listings/../Introduction/Intro.html +Fox,https://developer.apple.com/library/content/samplecode/Fox/Listings/../Introduction/Intro.html +IntentHandling,https://developer.apple.com/library/prerelease/content/samplecode/IntentHandling/Listings/../Introduction/Intro.html +Lister,https://developer.apple.com/library/content/samplecode/Lister/Listings/../Introduction/Intro.html +MPSCNNHelloWorld,https://developer.apple.com/library/content/samplecode/MPSCNNHelloWorld/Listings/../Introduction/Intro.html +MPSMatrixMultiplicationSample,https://developer.apple.com/library/content/samplecode/MPSMatrixMultiplicationSample/Listings/../Introduction/Intro.html +MetalImageFilters,https://developer.apple.com/library/content/samplecode/MetalImageFilters/Listings/../Introduction/Intro.html +MetalImageRecognition,https://developer.apple.com/library/content/samplecode/MetalImageRecognition/Listings/../Introduction/Intro.html +PrintPhoto,https://developer.apple.com/library/content/samplecode/PrintPhoto/Listings/../Introduction/Intro.html +Scoreboard,https://developer.apple.com/library/content/samplecode/Scoreboard/Listings/../Introduction/Intro.html +ShapeEdit,https://developer.apple.com/library/content/samplecode/ShapeEdit/Listings/../Introduction/Intro.html +SimpleTunnel,https://developer.apple.com/library/content/samplecode/SimpleTunnel/Listings/../Introduction/Intro.html +SpeakToMe,https://developer.apple.com/library/content/samplecode/SpeakToMe/Listings/../Introduction/Intro.html +TVMLGuide,https://developer.apple.com/library/content/samplecode/TVMLGuide/Listings/../Introduction/Intro.html +TalkingToTheLiveView,https://developer.apple.com/library/content/samplecode/TalkingToTheLiveView/Listings/../Introduction/Intro.html +ToolbarSample,https://developer.apple.com/library/content/samplecode/ToolbarSample/Listings/../Introduction/Intro.html +TouchCanvas,https://developer.apple.com/library/prerelease/content/samplecode/TouchCanvas/Listings/../Introduction/Intro.html +UICatalog,https://developer.apple.com/library/content/samplecode/UICatalog/Listings/../Introduction/Intro.html +UnicornChat,https://developer.apple.com/library/content/samplecode/UnicornChat/Listings/../Introduction/Intro.html