First commit

This commit is contained in:
Sami Samhuri 2022-02-16 22:16:09 -08:00
commit 8b89ba572e
No known key found for this signature in database
GPG key ID: 4B4195422742FC16
86 changed files with 5257 additions and 0 deletions

65
.gitignore vendored Normal file
View file

@ -0,0 +1,65 @@
.DS_Store
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
build/
DerivedData
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
## Other
*.xccheckout
*.moved-aside
*.xcuserstate
*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
*.ipa
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
fastlane/report.xml
fastlane/screenshots

View file

@ -0,0 +1,609 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
550F72C41B15230E00E86A47 /* MoreInformationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550F72C31B15230E00E86A47 /* MoreInformationOperation.swift */; };
551344C01B029C7B004A1569 /* AlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551344BF1B029C7B004A1569 /* AlertOperation.swift */; };
551B9BE31AEB1C9700302388 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551B9BE21AEB1C9700302388 /* AppDelegate.swift */; };
551B9BE81AEB1C9700302388 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 551B9BE61AEB1C9700302388 /* Main.storyboard */; };
551B9BEA1AEB1C9700302388 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 551B9BE91AEB1C9700302388 /* Images.xcassets */; };
551B9BED1AEB1C9700302388 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 551B9BEB1AEB1C9700302388 /* LaunchScreen.xib */; };
5521B4821B700C87007089CE /* NSLock+Operations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5521B4811B700C87007089CE /* NSLock+Operations.swift */; };
553F500F1B081A9D005F991E /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553F500E1B081A9D005F991E /* NetworkObserver.swift */; };
553F50111B082BCF005F991E /* BackgroundObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553F50101B082BCF005F991E /* BackgroundObserver.swift */; };
553F50161B08E98A005F991E /* LoadModelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553F50151B08E98A005F991E /* LoadModelOperation.swift */; };
55817C3A1B18FDF8001C0CE2 /* OperationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551B9C061AEB2D7800302388 /* OperationQueue.swift */; };
55817C3B1B18FDF8001C0CE2 /* ExclusivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CD4D201AF5C05300E3A9E3 /* ExclusivityController.swift */; };
55817C3C1B18FDF8001C0CE2 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551B9C021AEB1CA900302388 /* Operation.swift */; };
55817C3D1B18FDF8001C0CE2 /* BlockOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55857B3A1AF20DE800219D5A /* BlockOperation.swift */; };
55817C3E1B18FDF8001C0CE2 /* GroupOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55727FB11AF2798C00EC6916 /* GroupOperation.swift */; };
55817C3F1B18FDF8001C0CE2 /* URLSessionTaskOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55727FB91AF2849E00EC6916 /* URLSessionTaskOperation.swift */; };
55817C401B18FDF8001C0CE2 /* LocationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553F50031B07FB5E005F991E /* LocationOperation.swift */; };
55817C411B18FDF8001C0CE2 /* DelayOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553F50131B0832D2005F991E /* DelayOperation.swift */; };
55817C421B18FDF8001C0CE2 /* OperationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553F50051B081760005F991E /* OperationObserver.swift */; };
55817C431B18FDF8001C0CE2 /* BlockObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553F50081B0817D2005F991E /* BlockObserver.swift */; };
55817C441B18FDF8001C0CE2 /* TimeoutObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55AD643C1B128CC3000EF5CB /* TimeoutObserver.swift */; };
55817C451B18FDF8001C0CE2 /* OperationErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553F2D651B00041100BF4093 /* OperationErrors.swift */; };
55817C461B18FDF8001C0CE2 /* OperationCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551B9C041AEB1CC800302388 /* OperationCondition.swift */; };
55817C471B18FDF8001C0CE2 /* SilentCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55857B331AF2055600219D5A /* SilentCondition.swift */; };
55817C481B18FDF8001C0CE2 /* NegatedCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E702721AFE95C00032742F /* NegatedCondition.swift */; };
55817C491B18FDF8001C0CE2 /* NoCancelledDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55857B3E1AF2116600219D5A /* NoCancelledDependencies.swift */; };
55817C4A1B18FDF8001C0CE2 /* MutuallyExclusive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55857B381AF20CC700219D5A /* MutuallyExclusive.swift */; };
55817C4B1B18FDF8001C0CE2 /* ReachabilityCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551B9C0D1AEBE52800302388 /* ReachabilityCondition.swift */; };
55817C4C1B18FDF8001C0CE2 /* CloudCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551B9C0B1AEBE4F300302388 /* CloudCondition.swift */; };
55817C4D1B18FDF8001C0CE2 /* PassbookCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551B9C101AEBE54D00302388 /* PassbookCondition.swift */; };
55817C4E1B18FDF8001C0CE2 /* LocationCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55857B311AF203C100219D5A /* LocationCondition.swift */; };
55817C4F1B18FDF8001C0CE2 /* CalendarCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55727FAB1AF216C800EC6916 /* CalendarCondition.swift */; };
55817C501B18FDF8001C0CE2 /* PhotosCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553C5AC41AF6C774002FC47A /* PhotosCondition.swift */; };
55817C511B18FDF8001C0CE2 /* HealthCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553C5AC61AF6D2EB002FC47A /* HealthCondition.swift */; };
55817C521B18FDF8001C0CE2 /* RemoteNotificationCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55727FAF1AF276A900EC6916 /* RemoteNotificationCondition.swift */; };
55817C531B18FDF8001C0CE2 /* UserNotificationCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CD4D261AF6709400E3A9E3 /* UserNotificationCondition.swift */; };
55817C551B18FDF8001C0CE2 /* Dictionary+Operations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551344C31B02D7BA004A1569 /* Dictionary+Operations.swift */; };
55817C561B18FDF8001C0CE2 /* NSOperation+Operations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55727FB31AF27BDB00EC6916 /* NSOperation+Operations.swift */; };
55817C571B18FDF8001C0CE2 /* CKContainer+Operations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55857B351AF2077700219D5A /* CKContainer+Operations.swift */; };
55817C581B18FDF8001C0CE2 /* UIUserNotifications+Operations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551344C11B02D34A004A1569 /* UIUserNotifications+Operations.swift */; };
55E7021D1AFC38920032742F /* EarthquakeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E7021C1AFC38920032742F /* EarthquakeTableViewController.swift */; };
55E7021F1AFD15C80032742F /* SplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E7021E1AFD15C80032742F /* SplitViewController.swift */; };
55E702221AFE587C0032742F /* Earthquakes.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 55E702201AFE587C0032742F /* Earthquakes.xcdatamodeld */; };
55E702241AFE58EF0032742F /* GetEarthquakesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E702231AFE58EF0032742F /* GetEarthquakesOperation.swift */; };
55E702261AFE59610032742F /* EarthquakesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E702251AFE59610032742F /* EarthquakesTableViewController.swift */; };
55E702651AFE5E590032742F /* EarthquakeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E702641AFE5E590032742F /* EarthquakeTableViewCell.swift */; };
55E702681AFE61700032742F /* Earthquake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E702671AFE61700032742F /* Earthquake.swift */; };
55E7026B1AFE63E70032742F /* DownloadEarthquakesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E7026A1AFE63E70032742F /* DownloadEarthquakesOperation.swift */; };
55E7026D1AFE64D40032742F /* ParseEarthquakesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E7026C1AFE64D40032742F /* ParseEarthquakesOperation.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
55E702481AFE59E80032742F /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
550F72C31B15230E00E86A47 /* MoreInformationOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MoreInformationOperation.swift; sourceTree = "<group>"; };
551344B81B016CA7004A1569 /* Earthquakes.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Earthquakes.entitlements; sourceTree = "<group>"; };
551344B91B016CAF004A1569 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
551344BF1B029C7B004A1569 /* AlertOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertOperation.swift; sourceTree = "<group>"; };
551344C11B02D34A004A1569 /* UIUserNotifications+Operations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIUserNotifications+Operations.swift"; sourceTree = "<group>"; };
551344C31B02D7BA004A1569 /* Dictionary+Operations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Operations.swift"; sourceTree = "<group>"; };
551B9BDD1AEB1C9700302388 /* Earthquakes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Earthquakes.app; sourceTree = BUILT_PRODUCTS_DIR; };
551B9BE11AEB1C9700302388 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
551B9BE21AEB1C9700302388 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
551B9BE71AEB1C9700302388 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
551B9BE91AEB1C9700302388 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
551B9BEC1AEB1C9700302388 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
551B9C021AEB1CA900302388 /* Operation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = "<group>"; };
551B9C041AEB1CC800302388 /* OperationCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationCondition.swift; sourceTree = "<group>"; };
551B9C061AEB2D7800302388 /* OperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationQueue.swift; sourceTree = "<group>"; };
551B9C0B1AEBE4F300302388 /* CloudCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudCondition.swift; sourceTree = "<group>"; };
551B9C0D1AEBE52800302388 /* ReachabilityCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityCondition.swift; sourceTree = "<group>"; };
551B9C101AEBE54D00302388 /* PassbookCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassbookCondition.swift; sourceTree = "<group>"; };
5521B4811B700C87007089CE /* NSLock+Operations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLock+Operations.swift"; sourceTree = "<group>"; };
553C5AC41AF6C774002FC47A /* PhotosCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosCondition.swift; sourceTree = "<group>"; };
553C5AC61AF6D2EB002FC47A /* HealthCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HealthCondition.swift; sourceTree = "<group>"; };
553F2D651B00041100BF4093 /* OperationErrors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationErrors.swift; sourceTree = "<group>"; };
553F50031B07FB5E005F991E /* LocationOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationOperation.swift; sourceTree = "<group>"; };
553F50051B081760005F991E /* OperationObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationObserver.swift; sourceTree = "<group>"; };
553F50081B0817D2005F991E /* BlockObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockObserver.swift; sourceTree = "<group>"; };
553F500E1B081A9D005F991E /* NetworkObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
553F50101B082BCF005F991E /* BackgroundObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundObserver.swift; sourceTree = "<group>"; };
553F50131B0832D2005F991E /* DelayOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DelayOperation.swift; sourceTree = "<group>"; };
553F50151B08E98A005F991E /* LoadModelOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadModelOperation.swift; sourceTree = "<group>"; };
55727FAB1AF216C800EC6916 /* CalendarCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarCondition.swift; sourceTree = "<group>"; };
55727FAF1AF276A900EC6916 /* RemoteNotificationCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteNotificationCondition.swift; sourceTree = "<group>"; };
55727FB11AF2798C00EC6916 /* GroupOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupOperation.swift; sourceTree = "<group>"; };
55727FB31AF27BDB00EC6916 /* NSOperation+Operations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSOperation+Operations.swift"; sourceTree = "<group>"; };
55727FB91AF2849E00EC6916 /* URLSessionTaskOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskOperation.swift; sourceTree = "<group>"; };
55857B311AF203C100219D5A /* LocationCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationCondition.swift; sourceTree = "<group>"; };
55857B331AF2055600219D5A /* SilentCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SilentCondition.swift; sourceTree = "<group>"; };
55857B351AF2077700219D5A /* CKContainer+Operations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CKContainer+Operations.swift"; sourceTree = "<group>"; };
55857B381AF20CC700219D5A /* MutuallyExclusive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutuallyExclusive.swift; sourceTree = "<group>"; };
55857B3A1AF20DE800219D5A /* BlockOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockOperation.swift; sourceTree = "<group>"; };
55857B3E1AF2116600219D5A /* NoCancelledDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoCancelledDependencies.swift; sourceTree = "<group>"; };
55AD643C1B128CC3000EF5CB /* TimeoutObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeoutObserver.swift; sourceTree = "<group>"; };
55CD4D201AF5C05300E3A9E3 /* ExclusivityController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExclusivityController.swift; sourceTree = "<group>"; };
55CD4D261AF6709400E3A9E3 /* UserNotificationCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotificationCondition.swift; sourceTree = "<group>"; };
55E7021C1AFC38920032742F /* EarthquakeTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EarthquakeTableViewController.swift; sourceTree = "<group>"; };
55E7021E1AFD15C80032742F /* SplitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplitViewController.swift; sourceTree = "<group>"; };
55E702211AFE587C0032742F /* Earthquakes.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Earthquakes.xcdatamodel; sourceTree = "<group>"; };
55E702231AFE58EF0032742F /* GetEarthquakesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetEarthquakesOperation.swift; sourceTree = "<group>"; };
55E702251AFE59610032742F /* EarthquakesTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EarthquakesTableViewController.swift; sourceTree = "<group>"; };
55E702641AFE5E590032742F /* EarthquakeTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EarthquakeTableViewCell.swift; sourceTree = "<group>"; };
55E702671AFE61700032742F /* Earthquake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Earthquake.swift; sourceTree = "<group>"; };
55E7026A1AFE63E70032742F /* DownloadEarthquakesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadEarthquakesOperation.swift; sourceTree = "<group>"; };
55E7026C1AFE64D40032742F /* ParseEarthquakesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseEarthquakesOperation.swift; sourceTree = "<group>"; };
55E702721AFE95C00032742F /* NegatedCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NegatedCondition.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
551B9BDA1AEB1C9700302388 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
551B9BD41AEB1C9700302388 = {
isa = PBXGroup;
children = (
551B9BDF1AEB1C9700302388 /* Earthquakes */,
551B9BDE1AEB1C9700302388 /* Products */,
);
sourceTree = "<group>";
};
551B9BDE1AEB1C9700302388 /* Products */ = {
isa = PBXGroup;
children = (
551B9BDD1AEB1C9700302388 /* Earthquakes.app */,
);
name = Products;
sourceTree = "<group>";
};
551B9BDF1AEB1C9700302388 /* Earthquakes */ = {
isa = PBXGroup;
children = (
551344B81B016CA7004A1569 /* Earthquakes.entitlements */,
551B9BE21AEB1C9700302388 /* AppDelegate.swift */,
55E7021E1AFD15C80032742F /* SplitViewController.swift */,
553F50171B08EBBC005F991E /* Earthquake List */,
553F50181B08EBC3005F991E /* Earthquake Detail */,
553F2D631B00025200BF4093 /* Model */,
55817C391B18FDD3001C0CE2 /* App Operations */,
55E702691AFE63C50032742F /* Operations */,
551B9BE01AEB1C9700302388 /* Supporting Files */,
553F50121B082D9E005F991E /* Frameworks */,
);
path = Earthquakes;
sourceTree = "<group>";
};
551B9BE01AEB1C9700302388 /* Supporting Files */ = {
isa = PBXGroup;
children = (
551B9BEB1AEB1C9700302388 /* LaunchScreen.xib */,
551B9BE61AEB1C9700302388 /* Main.storyboard */,
551B9BE91AEB1C9700302388 /* Images.xcassets */,
551B9BE11AEB1C9700302388 /* Info.plist */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
551B9C0F1AEBE53400302388 /* Conditions */ = {
isa = PBXGroup;
children = (
553F2D651B00041100BF4093 /* OperationErrors.swift */,
551B9C041AEB1CC800302388 /* OperationCondition.swift */,
55857B331AF2055600219D5A /* SilentCondition.swift */,
55E702721AFE95C00032742F /* NegatedCondition.swift */,
55857B3E1AF2116600219D5A /* NoCancelledDependencies.swift */,
55857B381AF20CC700219D5A /* MutuallyExclusive.swift */,
551B9C0D1AEBE52800302388 /* ReachabilityCondition.swift */,
551B9C0B1AEBE4F300302388 /* CloudCondition.swift */,
551B9C101AEBE54D00302388 /* PassbookCondition.swift */,
55857B311AF203C100219D5A /* LocationCondition.swift */,
55727FAB1AF216C800EC6916 /* CalendarCondition.swift */,
553C5AC41AF6C774002FC47A /* PhotosCondition.swift */,
553C5AC61AF6D2EB002FC47A /* HealthCondition.swift */,
55727FAF1AF276A900EC6916 /* RemoteNotificationCondition.swift */,
55CD4D261AF6709400E3A9E3 /* UserNotificationCondition.swift */,
);
name = Conditions;
sourceTree = "<group>";
};
553F2D621AFFED5300BF4093 /* Operation Queue */ = {
isa = PBXGroup;
children = (
551B9C061AEB2D7800302388 /* OperationQueue.swift */,
55CD4D201AF5C05300E3A9E3 /* ExclusivityController.swift */,
);
name = "Operation Queue";
sourceTree = "<group>";
};
553F2D631B00025200BF4093 /* Model */ = {
isa = PBXGroup;
children = (
55E702201AFE587C0032742F /* Earthquakes.xcdatamodeld */,
55E702671AFE61700032742F /* Earthquake.swift */,
);
name = Model;
sourceTree = "<group>";
};
553F50071B0817BB005F991E /* Observers */ = {
isa = PBXGroup;
children = (
553F50051B081760005F991E /* OperationObserver.swift */,
553F50081B0817D2005F991E /* BlockObserver.swift */,
55AD643C1B128CC3000EF5CB /* TimeoutObserver.swift */,
);
name = Observers;
sourceTree = "<group>";
};
553F50121B082D9E005F991E /* Frameworks */ = {
isa = PBXGroup;
children = (
551344B91B016CAF004A1569 /* CloudKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
553F50171B08EBBC005F991E /* Earthquake List */ = {
isa = PBXGroup;
children = (
55E702251AFE59610032742F /* EarthquakesTableViewController.swift */,
55E702641AFE5E590032742F /* EarthquakeTableViewCell.swift */,
);
name = "Earthquake List";
sourceTree = "<group>";
};
553F50181B08EBC3005F991E /* Earthquake Detail */ = {
isa = PBXGroup;
children = (
55E7021C1AFC38920032742F /* EarthquakeTableViewController.swift */,
);
name = "Earthquake Detail";
sourceTree = "<group>";
};
55817C391B18FDD3001C0CE2 /* App Operations */ = {
isa = PBXGroup;
children = (
551344BF1B029C7B004A1569 /* AlertOperation.swift */,
553F50151B08E98A005F991E /* LoadModelOperation.swift */,
55E702231AFE58EF0032742F /* GetEarthquakesOperation.swift */,
55E7026A1AFE63E70032742F /* DownloadEarthquakesOperation.swift */,
55E7026C1AFE64D40032742F /* ParseEarthquakesOperation.swift */,
550F72C31B15230E00E86A47 /* MoreInformationOperation.swift */,
553F500E1B081A9D005F991E /* NetworkObserver.swift */,
553F50101B082BCF005F991E /* BackgroundObserver.swift */,
);
name = "App Operations";
sourceTree = "<group>";
};
55857B301AF1E59900219D5A /* Operations */ = {
isa = PBXGroup;
children = (
551B9C021AEB1CA900302388 /* Operation.swift */,
55857B3A1AF20DE800219D5A /* BlockOperation.swift */,
55727FB11AF2798C00EC6916 /* GroupOperation.swift */,
55727FB91AF2849E00EC6916 /* URLSessionTaskOperation.swift */,
553F50031B07FB5E005F991E /* LocationOperation.swift */,
553F50131B0832D2005F991E /* DelayOperation.swift */,
);
name = Operations;
sourceTree = "<group>";
};
55857B371AF2077A00219D5A /* Convenience Extensions */ = {
isa = PBXGroup;
children = (
551344C31B02D7BA004A1569 /* Dictionary+Operations.swift */,
55727FB31AF27BDB00EC6916 /* NSOperation+Operations.swift */,
5521B4811B700C87007089CE /* NSLock+Operations.swift */,
55857B351AF2077700219D5A /* CKContainer+Operations.swift */,
551344C11B02D34A004A1569 /* UIUserNotifications+Operations.swift */,
);
name = "Convenience Extensions";
sourceTree = "<group>";
};
55E702691AFE63C50032742F /* Operations */ = {
isa = PBXGroup;
children = (
553F2D621AFFED5300BF4093 /* Operation Queue */,
55857B301AF1E59900219D5A /* Operations */,
553F50071B0817BB005F991E /* Observers */,
551B9C0F1AEBE53400302388 /* Conditions */,
55857B371AF2077A00219D5A /* Convenience Extensions */,
);
path = Operations;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
551B9BDC1AEB1C9700302388 /* Earthquakes */ = {
isa = PBXNativeTarget;
buildConfigurationList = 551B9BFC1AEB1C9800302388 /* Build configuration list for PBXNativeTarget "Earthquakes" */;
buildPhases = (
551B9BD91AEB1C9700302388 /* Sources */,
551B9BDA1AEB1C9700302388 /* Frameworks */,
551B9BDB1AEB1C9700302388 /* Resources */,
55E702481AFE59E80032742F /* Embed Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Earthquakes;
productName = AAPLOperations;
productReference = 551B9BDD1AEB1C9700302388 /* Earthquakes.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
551B9BD51AEB1C9700302388 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0700;
LastUpgradeCheck = 0700;
ORGANIZATIONNAME = "Apple, Inc.";
TargetAttributes = {
551B9BDC1AEB1C9700302388 = {
CreatedOnToolsVersion = 6.3;
SystemCapabilities = {
com.apple.iCloud = {
enabled = 1;
};
};
};
};
};
buildConfigurationList = 551B9BD81AEB1C9700302388 /* Build configuration list for PBXProject "Advanced NSOperations" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 551B9BD41AEB1C9700302388;
productRefGroup = 551B9BDE1AEB1C9700302388 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
551B9BDC1AEB1C9700302388 /* Earthquakes */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
551B9BDB1AEB1C9700302388 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
551B9BE81AEB1C9700302388 /* Main.storyboard in Resources */,
551B9BED1AEB1C9700302388 /* LaunchScreen.xib in Resources */,
551B9BEA1AEB1C9700302388 /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
551B9BD91AEB1C9700302388 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
55E7026B1AFE63E70032742F /* DownloadEarthquakesOperation.swift in Sources */,
55817C4B1B18FDF8001C0CE2 /* ReachabilityCondition.swift in Sources */,
55817C521B18FDF8001C0CE2 /* RemoteNotificationCondition.swift in Sources */,
55E702241AFE58EF0032742F /* GetEarthquakesOperation.swift in Sources */,
55E702221AFE587C0032742F /* Earthquakes.xcdatamodeld in Sources */,
550F72C41B15230E00E86A47 /* MoreInformationOperation.swift in Sources */,
55E7021F1AFD15C80032742F /* SplitViewController.swift in Sources */,
55817C4D1B18FDF8001C0CE2 /* PassbookCondition.swift in Sources */,
55817C561B18FDF8001C0CE2 /* NSOperation+Operations.swift in Sources */,
55817C3C1B18FDF8001C0CE2 /* Operation.swift in Sources */,
55817C3A1B18FDF8001C0CE2 /* OperationQueue.swift in Sources */,
55817C4A1B18FDF8001C0CE2 /* MutuallyExclusive.swift in Sources */,
55817C531B18FDF8001C0CE2 /* UserNotificationCondition.swift in Sources */,
55817C461B18FDF8001C0CE2 /* OperationCondition.swift in Sources */,
55817C441B18FDF8001C0CE2 /* TimeoutObserver.swift in Sources */,
55817C511B18FDF8001C0CE2 /* HealthCondition.swift in Sources */,
55817C501B18FDF8001C0CE2 /* PhotosCondition.swift in Sources */,
55817C571B18FDF8001C0CE2 /* CKContainer+Operations.swift in Sources */,
551B9BE31AEB1C9700302388 /* AppDelegate.swift in Sources */,
55817C401B18FDF8001C0CE2 /* LocationOperation.swift in Sources */,
55817C4E1B18FDF8001C0CE2 /* LocationCondition.swift in Sources */,
55817C4C1B18FDF8001C0CE2 /* CloudCondition.swift in Sources */,
553F50161B08E98A005F991E /* LoadModelOperation.swift in Sources */,
55817C3E1B18FDF8001C0CE2 /* GroupOperation.swift in Sources */,
55817C491B18FDF8001C0CE2 /* NoCancelledDependencies.swift in Sources */,
55817C581B18FDF8001C0CE2 /* UIUserNotifications+Operations.swift in Sources */,
55817C421B18FDF8001C0CE2 /* OperationObserver.swift in Sources */,
5521B4821B700C87007089CE /* NSLock+Operations.swift in Sources */,
553F500F1B081A9D005F991E /* NetworkObserver.swift in Sources */,
55817C551B18FDF8001C0CE2 /* Dictionary+Operations.swift in Sources */,
551344C01B029C7B004A1569 /* AlertOperation.swift in Sources */,
55E702261AFE59610032742F /* EarthquakesTableViewController.swift in Sources */,
55817C3F1B18FDF8001C0CE2 /* URLSessionTaskOperation.swift in Sources */,
55E7026D1AFE64D40032742F /* ParseEarthquakesOperation.swift in Sources */,
553F50111B082BCF005F991E /* BackgroundObserver.swift in Sources */,
55817C3B1B18FDF8001C0CE2 /* ExclusivityController.swift in Sources */,
55817C411B18FDF8001C0CE2 /* DelayOperation.swift in Sources */,
55E702651AFE5E590032742F /* EarthquakeTableViewCell.swift in Sources */,
55E702681AFE61700032742F /* Earthquake.swift in Sources */,
55817C4F1B18FDF8001C0CE2 /* CalendarCondition.swift in Sources */,
55817C481B18FDF8001C0CE2 /* NegatedCondition.swift in Sources */,
55817C3D1B18FDF8001C0CE2 /* BlockOperation.swift in Sources */,
55817C471B18FDF8001C0CE2 /* SilentCondition.swift in Sources */,
55E7021D1AFC38920032742F /* EarthquakeTableViewController.swift in Sources */,
55817C451B18FDF8001C0CE2 /* OperationErrors.swift in Sources */,
55817C431B18FDF8001C0CE2 /* BlockObserver.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
551B9BE61AEB1C9700302388 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
551B9BE71AEB1C9700302388 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
551B9BEB1AEB1C9700302388 /* LaunchScreen.xib */ = {
isa = PBXVariantGroup;
children = (
551B9BEC1AEB1C9700302388 /* Base */,
);
name = LaunchScreen.xib;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
551B9BFA1AEB1C9800302388 /* 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;
"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";
};
name = Debug;
};
551B9BFB1AEB1C9800302388 /* 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;
"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;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
551B9BFD1AEB1C9800302388 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Earthquakes/Earthquakes.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
INFOPLIST_FILE = Earthquakes/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = Earthquakes;
PROVISIONING_PROFILE = "";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
551B9BFE1AEB1C9800302388 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = Earthquakes/Earthquakes.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
INFOPLIST_FILE = Earthquakes/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = Earthquakes;
PROVISIONING_PROFILE = "";
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
551B9BD81AEB1C9700302388 /* Build configuration list for PBXProject "Advanced NSOperations" */ = {
isa = XCConfigurationList;
buildConfigurations = (
551B9BFA1AEB1C9800302388 /* Debug */,
551B9BFB1AEB1C9800302388 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
551B9BFC1AEB1C9800302388 /* Build configuration list for PBXNativeTarget "Earthquakes" */ = {
isa = XCConfigurationList;
buildConfigurations = (
551B9BFD1AEB1C9800302388 /* Debug */,
551B9BFE1AEB1C9800302388 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCVersionGroup section */
55E702201AFE587C0032742F /* Earthquakes.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
55E702211AFE587C0032742F /* Earthquakes.xcdatamodel */,
);
currentVersion = 55E702211AFE587C0032742F /* Earthquakes.xcdatamodel */;
path = Earthquakes.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
};
rootObject = 551B9BD51AEB1C9700302388 /* Project object */;
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0720"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "551B9BDC1AEB1C9700302388"
BuildableName = "Earthquakes.app"
BlueprintName = "Earthquakes"
ReferencedContainer = "container:Advanced NSOperations.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "551B9BDC1AEB1C9700302388"
BuildableName = "Earthquakes.app"
BlueprintName = "Earthquakes"
ReferencedContainer = "container:Advanced NSOperations.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "551B9BDC1AEB1C9700302388"
BuildableName = "Earthquakes.app"
BlueprintName = "Earthquakes"
ReferencedContainer = "container:Advanced NSOperations.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "551B9BDC1AEB1C9700302388"
BuildableName = "Earthquakes.app"
BlueprintName = "Earthquakes"
ReferencedContainer = "container:Advanced NSOperations.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,82 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows how to present an alert as part of an operation.
*/
import UIKit
class AlertOperation: Operation {
// MARK: Properties
private let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .Alert)
private let presentationContext: UIViewController?
var title: String? {
get {
return alertController.title
}
set {
alertController.title = newValue
name = newValue
}
}
var message: String? {
get {
return alertController.message
}
set {
alertController.message = newValue
}
}
// MARK: Initialization
init(presentationContext: UIViewController? = nil) {
self.presentationContext = presentationContext ?? UIApplication.sharedApplication().keyWindow?.rootViewController
super.init()
addCondition(AlertPresentation())
/*
This operation modifies the view controller hierarchy.
Doing this while other such operations are executing can lead to
inconsistencies in UIKit. So, let's make them mutally exclusive.
*/
addCondition(MutuallyExclusive<UIViewController>())
}
func addAction(title: String, style: UIAlertActionStyle = .Default, handler: AlertOperation -> Void = { _ in }) {
let action = UIAlertAction(title: title, style: style) { [weak self] _ in
if let strongSelf = self {
handler(strongSelf)
}
self?.finish()
}
alertController.addAction(action)
}
override func execute() {
guard let presentationContext = presentationContext else {
finish()
return
}
dispatch_async(dispatch_get_main_queue()) {
if self.alertController.actions.isEmpty {
self.addAction("OK")
}
presentationContext.presentViewController(self.alertController, animated: true, completion: nil)
}
}
}

View file

@ -0,0 +1,26 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
The app delegate. This, by design, has almost no implementation.
*/
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: Properties
var window: UIWindow?
// MARK: UIApplicationDelegate
func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) {
RemoteNotificationCondition.didFailToRegister(error)
}
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
RemoteNotificationCondition.didReceiveNotificationToken(deviceToken)
}
}

View file

@ -0,0 +1,82 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Contains the code related to automatic background tasks
*/
import UIKit
/**
`BackgroundObserver` is an `OperationObserver` that will automatically begin
and end a background task if the application transitions to the background.
This would be useful if you had a vital `Operation` whose execution *must* complete,
regardless of the activation state of the app. Some kinds network connections
may fall in to this category, for example.
*/
class BackgroundObserver: NSObject, OperationObserver {
// MARK: Properties
private var identifier = UIBackgroundTaskInvalid
private var isInBackground = false
override init() {
super.init()
// We need to know when the application moves to/from the background.
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(BackgroundObserver.didEnterBackground(_:)), name: UIApplicationDidEnterBackgroundNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(BackgroundObserver.didEnterForeground(_:)), name: UIApplicationDidBecomeActiveNotification, object: nil)
isInBackground = UIApplication.sharedApplication().applicationState == .Background
// If we're in the background already, immediately begin the background task.
if isInBackground {
startBackgroundTask()
}
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
@objc func didEnterBackground(notification: NSNotification) {
if !isInBackground {
isInBackground = true
startBackgroundTask()
}
}
@objc func didEnterForeground(notification: NSNotification) {
if isInBackground {
isInBackground = false
endBackgroundTask()
}
}
private func startBackgroundTask() {
if identifier == UIBackgroundTaskInvalid {
identifier = UIApplication.sharedApplication().beginBackgroundTaskWithName("BackgroundObserver", expirationHandler: {
self.endBackgroundTask()
})
}
}
private func endBackgroundTask() {
if identifier != UIBackgroundTaskInvalid {
UIApplication.sharedApplication().endBackgroundTask(identifier)
identifier = UIBackgroundTaskInvalid
}
}
// MARK: Operation Observer
func operationDidStart(operation: Operation) { }
func operation(operation: Operation, didProduceOperation newOperation: NSOperation) { }
func operationDidFinish(operation: Operation, errors: [NSError]) {
endBackgroundTask()
}
}

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="8121.17" systemVersion="15A178w" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<animations/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="548" y="455"/>
</view>
</objects>
</document>

View file

@ -0,0 +1,335 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8121.17" systemVersion="14D131" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="vxG-3R-aw3">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8101.14"/>
</dependencies>
<scenes>
<!--Navigation Controller-->
<scene sceneID="Cq3-O0-pey">
<objects>
<navigationController id="NLB-vB-4wf" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" id="U7d-uh-loD">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="CMa-LU-bLP" kind="relationship" relationship="rootViewController" id="ZQc-pb-d98"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="UhJ-eR-BI8" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="559" y="280"/>
</scene>
<!--Earthquake List-->
<scene sceneID="3I5-2C-86e">
<objects>
<tableViewController clearsSelectionOnViewWillAppear="NO" id="O5y-IL-rqn" userLabel="Earthquake List" customClass="EarthquakesTableViewController" customModule="Earthquakes" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="64" sectionHeaderHeight="22" sectionFooterHeight="22" id="OlO-ih-GDz">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<prototypes>
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="earthquakeCell" rowHeight="64" id="N5t-mf-X67" customClass="EarthquakeTableViewCell" customModule="Earthquakes" customModuleProvider="target">
<rect key="frame" x="0.0" y="86" width="600" height="64"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="N5t-mf-X67" id="9e1-nt-c0c">
<rect key="frame" x="0.0" y="0.0" width="600" height="64"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="5.0" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lng-VZ-CBl">
<rect key="frame" x="546" y="18" width="40" height="29"/>
<constraints>
<constraint firstAttribute="width" constant="40" id="By9-Md-R6C"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="24"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<color key="highlightedColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</label>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="yJW-yA-Q71">
<rect key="frame" x="448" y="11" width="90" height="43"/>
<constraints>
<constraint firstAttribute="width" constant="90" id="aZh-o8-tLY"/>
<constraint firstAttribute="height" constant="43" id="mnz-Sh-y94"/>
</constraints>
</imageView>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Date" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pvz-VT-bAa">
<rect key="frame" x="14" y="38" width="426" height="21"/>
<constraints>
<constraint firstAttribute="height" constant="21" id="NJM-7l-k3r"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<color key="highlightedColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Location" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="14" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="goq-NH-1sA">
<rect key="frame" x="14" y="9" width="460" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<color key="highlightedColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</label>
</subviews>
</tableViewCellContentView>
<constraints>
<constraint firstAttribute="centerY" secondItem="lng-VZ-CBl" secondAttribute="centerY" id="1gi-vn-57o"/>
<constraint firstItem="yJW-yA-Q71" firstAttribute="leading" secondItem="Pvz-VT-bAa" secondAttribute="trailing" constant="8" symbolic="YES" id="67i-yf-Tpb"/>
<constraint firstItem="goq-NH-1sA" firstAttribute="leading" secondItem="N5t-mf-X67" secondAttribute="leading" constant="14" id="7fR-ko-7F6"/>
<constraint firstItem="Pvz-VT-bAa" firstAttribute="leading" secondItem="N5t-mf-X67" secondAttribute="leading" constant="14" id="DH4-6m-rc3"/>
<constraint firstAttribute="trailing" secondItem="goq-NH-1sA" secondAttribute="trailing" constant="126" id="FiX-5u-CW1"/>
<constraint firstItem="lng-VZ-CBl" firstAttribute="leading" secondItem="yJW-yA-Q71" secondAttribute="trailing" constant="8" symbolic="YES" id="MSG-QS-LuN"/>
<constraint firstItem="Pvz-VT-bAa" firstAttribute="top" secondItem="goq-NH-1sA" secondAttribute="bottom" constant="8" symbolic="YES" id="PE3-7W-Xs2"/>
<constraint firstItem="goq-NH-1sA" firstAttribute="top" secondItem="N5t-mf-X67" secondAttribute="top" constant="9" id="VTr-Kl-inO"/>
<constraint firstItem="yJW-yA-Q71" firstAttribute="centerY" secondItem="lng-VZ-CBl" secondAttribute="centerY" id="lT0-24-sdL"/>
<constraint firstAttribute="trailing" secondItem="lng-VZ-CBl" secondAttribute="trailing" constant="14" id="omP-AZ-8Me"/>
</constraints>
<connections>
<outlet property="locationLabel" destination="goq-NH-1sA" id="mIh-FL-vEc"/>
<outlet property="magnitudeImage" destination="yJW-yA-Q71" id="Ybv-2X-E1C"/>
<outlet property="magnitudeLabel" destination="lng-VZ-CBl" id="wC0-P7-5h4"/>
<outlet property="timestampLabel" destination="Pvz-VT-bAa" id="uoL-lP-ROE"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="O5y-IL-rqn" id="gyz-XS-lQS"/>
<outlet property="delegate" destination="O5y-IL-rqn" id="VqS-OZ-liX"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Earthquakes" id="kc1-LW-flU"/>
<refreshControl key="refreshControl" opaque="NO" multipleTouchEnabled="YES" contentMode="center" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" id="mpF-VZ-eEO">
<autoresizingMask key="autoresizingMask"/>
<connections>
<action selector="startRefreshing:" destination="O5y-IL-rqn" eventType="valueChanged" id="Jtl-Wp-Jv9"/>
</connections>
</refreshControl>
<connections>
<segue destination="NLB-vB-4wf" kind="showDetail" identifier="showEarthquake" id="6II-IP-1jc"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="4ci-St-vxw" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1379" y="-444"/>
</scene>
<!--Earthquake-->
<scene sceneID="4F6-H2-TYF">
<objects>
<tableViewController id="CMa-LU-bLP" customClass="EarthquakeTableViewController" customModule="Earthquakes" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="10" sectionFooterHeight="10" id="zQC-LZ-9b1">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.93725490196078431" green="0.93725490196078431" blue="0.95686274509803926" alpha="1" colorSpace="calibratedRGB"/>
<mapView key="tableHeaderView" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" mapType="standard" zoomEnabled="NO" scrollEnabled="NO" rotateEnabled="NO" pitchEnabled="NO" showsBuildings="NO" showsPointsOfInterest="NO" id="LjX-Fv-ewm">
<rect key="frame" x="0.0" y="64" width="600" height="179"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<connections>
<outlet property="delegate" destination="CMa-LU-bLP" id="THU-ID-2bZ"/>
</connections>
</mapView>
<sections>
<tableViewSection headerTitle="Information" id="Hdn-Y9-iP6">
<cells>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="infoCell" textLabel="5CJ-52-Zpo" detailTextLabel="jlb-RD-Gtl" style="IBUITableViewCellStyleValue1" id="n9t-Mi-ZRh">
<rect key="frame" x="0.0" y="293" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="n9t-Mi-ZRh" id="96a-dM-TlV">
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Location" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="5CJ-52-Zpo">
<rect key="frame" x="15" y="12" width="62.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="jlb-RD-Gtl">
<rect key="frame" x="543.5" y="12" width="41.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.5568627451" green="0.5568627451" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="infoCell" textLabel="cMb-Ug-EP6" detailTextLabel="WHm-Pl-E7Q" style="IBUITableViewCellStyleValue1" id="6um-n1-LJ8">
<rect key="frame" x="0.0" y="337" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="6um-n1-LJ8" id="vpG-pa-taK">
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Magnitude" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="cMb-Ug-EP6">
<rect key="frame" x="15" y="12" width="77.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="WHm-Pl-E7Q">
<rect key="frame" x="543.5" y="12" width="41.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.5568627451" green="0.5568627451" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="infoCell" textLabel="exD-Br-rmu" detailTextLabel="xgy-SG-FJO" style="IBUITableViewCellStyleValue1" id="p6E-Ac-V8t">
<rect key="frame" x="0.0" y="381" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="p6E-Ac-V8t" id="X7P-Ef-lFv">
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Depth" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="exD-Br-rmu">
<rect key="frame" x="15" y="12" width="44.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xgy-SG-FJO">
<rect key="frame" x="543.5" y="12" width="41.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.5568627451" green="0.5568627451" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="infoCell" textLabel="tZW-Jq-8Kf" detailTextLabel="z8Q-a0-YzZ" style="IBUITableViewCellStyleValue1" id="Qmm-YO-4Lf">
<rect key="frame" x="0.0" y="425" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Qmm-YO-4Lf" id="auz-gV-oKt">
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Time" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tZW-Jq-8Kf">
<rect key="frame" x="15" y="12" width="36" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="z8Q-a0-YzZ">
<rect key="frame" x="543.5" y="12" width="41.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.5568627451" green="0.5568627451" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="infoCell" textLabel="gIF-V9-0df" detailTextLabel="tvX-an-y61" style="IBUITableViewCellStyleValue1" id="X9O-wy-rGS">
<rect key="frame" x="0.0" y="469" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="X9O-wy-rGS" id="8mc-CY-snv">
<rect key="frame" x="0.0" y="0.0" width="600" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Distance" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="gIF-V9-0df">
<rect key="frame" x="15" y="12" width="64" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Unknown" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tvX-an-y61">
<rect key="frame" x="516.5" y="12" width="68.5" height="19.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/>
<color key="textColor" red="0.5568627451" green="0.5568627451" blue="0.57647058819999997" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
<tableViewSection id="rnl-co-5eX">
<cells>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="moreInfoCell" id="Qaz-IF-Mnx" userLabel="moreInfoCell">
<rect key="frame" x="0.0" y="533" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Qaz-IF-Mnx" id="MKe-Iq-gam">
<rect key="frame" x="0.0" y="0.0" width="600" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="More Information" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zbI-ts-ijx">
<rect key="frame" x="234" y="11" width="132" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="centerY" secondItem="zbI-ts-ijx" secondAttribute="centerY" id="9BW-HJ-9Og"/>
<constraint firstAttribute="centerX" secondItem="zbI-ts-ijx" secondAttribute="centerX" id="smf-tq-C1y"/>
</constraints>
</tableViewCellContentView>
</tableViewCell>
</cells>
</tableViewSection>
</sections>
<connections>
<outlet property="dataSource" destination="CMa-LU-bLP" id="ep1-j9-D5B"/>
<outlet property="delegate" destination="CMa-LU-bLP" id="kQE-Yx-Uf5"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Earthquake" id="XM2-63-oEp">
<barButtonItem key="rightBarButtonItem" systemItem="action" id="WsD-yu-aWE">
<connections>
<action selector="shareEarthquake:" destination="CMa-LU-bLP" id="uuM-uF-CHQ"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="depthLabel" destination="xgy-SG-FJO" id="wU1-zM-pLO"/>
<outlet property="distanceLabel" destination="tvX-an-y61" id="tlN-iZ-sur"/>
<outlet property="magnitudeLabel" destination="WHm-Pl-E7Q" id="7TW-GJ-6Tx"/>
<outlet property="map" destination="LjX-Fv-ewm" id="wSh-Ub-mvY"/>
<outlet property="nameLabel" destination="jlb-RD-Gtl" id="zrZ-OU-erQ"/>
<outlet property="timeLabel" destination="z8Q-a0-YzZ" id="OrN-Qm-4zt"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8x3-yd-3ni" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1379" y="280"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="TU8-gY-8jv">
<objects>
<navigationController id="ADU-vk-Kwq" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" id="3rY-BT-niW">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="O5y-IL-rqn" kind="relationship" relationship="rootViewController" id="29l-No-Rkf"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Wmh-J9-j4i" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="559" y="-444"/>
</scene>
<!--Split View Controller-->
<scene sceneID="w40-KG-7Re">
<objects>
<splitViewController id="vxG-3R-aw3" customClass="SplitViewController" customModule="Earthquakes" customModuleProvider="target" sceneMemberID="viewController">
<connections>
<segue destination="ADU-vk-Kwq" kind="relationship" relationship="masterViewController" id="SHQ-kT-xut"/>
<segue destination="NLB-vB-4wf" kind="relationship" relationship="detailViewController" id="AWs-eU-9Bq"/>
</connections>
</splitViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="FZl-3d-dQu" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-261" y="-113"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="AWs-eU-9Bq"/>
</inferredMetricsTieBreakers>
</document>

View file

@ -0,0 +1,74 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file contains the code to download the feed of recent earthquakes.
*/
import Foundation
class DownloadEarthquakesOperation: GroupOperation {
// MARK: Properties
let cacheFile: NSURL
// MARK: Initialization
/// - parameter cacheFile: The file `NSURL` to which the earthquake feed will be downloaded.
init(cacheFile: NSURL) {
self.cacheFile = cacheFile
super.init(operations: [])
name = "Download Earthquakes"
/*
Since this server is out of our control and does not offer a secure
communication channel, we'll use the http version of the URL and have
added "earthquake.usgs.gov" to the "NSExceptionDomains" value in the
app's Info.plist file. When you communicate with your own servers,
or when the services you use offer secure communication options, you
should always prefer to use https.
*/
let url = NSURL(string: "http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_month.geojson")!
let task = NSURLSession.sharedSession().downloadTaskWithURL(url) { url, response, error in
self.downloadFinished(url, response: response as? NSHTTPURLResponse, error: error)
}
let taskOperation = URLSessionTaskOperation(task: task)
let reachabilityCondition = ReachabilityCondition(host: url)
taskOperation.addCondition(reachabilityCondition)
let networkObserver = NetworkObserver()
taskOperation.addObserver(networkObserver)
addOperation(taskOperation)
}
func downloadFinished(url: NSURL?, response: NSHTTPURLResponse?, error: NSError?) {
if let localURL = url {
do {
/*
If we already have a file at this location, just delete it.
Also, swallow the error, because we don't really care about it.
*/
try NSFileManager.defaultManager().removeItemAtURL(cacheFile)
}
catch { }
do {
try NSFileManager.defaultManager().moveItemAtURL(localURL, toURL: cacheFile)
}
catch let error as NSError {
aggregateError(error)
}
}
else if let error = error {
aggregateError(error)
}
else {
// Do nothing, and the operation will automatically finish.
}
}
}

View file

@ -0,0 +1,78 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
The Earthquake model object.
*/
import Foundation
import CoreData
import CoreLocation
/*
An `NSManagedObject` subclass to model basic earthquake properties. This also
contains some convenience methods to aid in formatting the information.
*/
class Earthquake: NSManagedObject {
// MARK: Static Properties
static let entityName = "Earthquake"
// MARK: Formatters
static let timestampFormatter: NSDateFormatter = {
let timestampFormatter = NSDateFormatter()
timestampFormatter.dateStyle = .MediumStyle
timestampFormatter.timeStyle = .MediumStyle
return timestampFormatter
}()
static let magnitudeFormatter: NSNumberFormatter = {
let magnitudeFormatter = NSNumberFormatter()
magnitudeFormatter.numberStyle = .DecimalStyle
magnitudeFormatter.maximumFractionDigits = 1
magnitudeFormatter.minimumFractionDigits = 1
return magnitudeFormatter
}()
static let depthFormatter: NSLengthFormatter = {
let depthFormatter = NSLengthFormatter()
depthFormatter.forPersonHeightUse = false
return depthFormatter
}()
static let distanceFormatter: NSLengthFormatter = {
let distanceFormatter = NSLengthFormatter()
distanceFormatter.forPersonHeightUse = false
distanceFormatter.numberFormatter.maximumFractionDigits = 2
return distanceFormatter
}()
// MARK: Properties
@NSManaged var identifier: String
@NSManaged var latitude: Double
@NSManaged var longitude: Double
@NSManaged var name: String
@NSManaged var magnitude: Double
@NSManaged var timestamp: NSDate
@NSManaged var depth: Double
@NSManaged var webLink: String
var coordinate: CLLocationCoordinate2D {
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
var location: CLLocation {
return CLLocation(coordinate: coordinate, altitude: -depth, horizontalAccuracy: kCLLocationAccuracyBest, verticalAccuracy: kCLLocationAccuracyBest, timestamp: timestamp)
}
}

View file

@ -0,0 +1,41 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
A UITableViewCell to display the high-level information of an earthquake
*/
import UIKit
class EarthquakeTableViewCell: UITableViewCell {
// MARK: Properties
@IBOutlet var locationLabel: UILabel!
@IBOutlet var timestampLabel: UILabel!
@IBOutlet var magnitudeLabel: UILabel!
@IBOutlet var magnitudeImage: UIImageView!
// MARK: Configuration
func configure(earthquake: Earthquake) {
timestampLabel.text = Earthquake.timestampFormatter.stringFromDate(earthquake.timestamp)
magnitudeLabel.text = Earthquake.magnitudeFormatter.stringFromNumber(earthquake.magnitude)
locationLabel.text = earthquake.name
let imageName: String
switch earthquake.magnitude {
case 0..<2: imageName = ""
case 2..<3: imageName = "2.0"
case 3..<4: imageName = "3.0"
case 4..<5: imageName = "4.0"
default: imageName = "5.0"
}
magnitudeImage.image = UIImage(named: imageName)
}
}

View file

@ -0,0 +1,162 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
A static UITableViewController to display details of an earthquake
*/
import UIKit
import MapKit
class EarthquakeTableViewController: UITableViewController {
// MARK: Properties
var queue: OperationQueue?
var earthquake: Earthquake?
var locationRequest: LocationOperation?
@IBOutlet var map: MKMapView!
@IBOutlet var nameLabel: UILabel!
@IBOutlet var magnitudeLabel: UILabel!
@IBOutlet var depthLabel: UILabel!
@IBOutlet var timeLabel: UILabel!
@IBOutlet var distanceLabel: UILabel!
// MARKL View Controller
override func viewDidLoad() {
super.viewDidLoad()
// Default all labels if there's no earthquake.
guard let earthquake = earthquake else {
nameLabel.text = ""
magnitudeLabel.text = ""
depthLabel.text = ""
timeLabel.text = ""
distanceLabel.text = ""
return
}
let span = MKCoordinateSpan(latitudeDelta: 15, longitudeDelta: 15)
map.region = MKCoordinateRegion(center: earthquake.coordinate, span: span)
let annotation = MKPointAnnotation()
annotation.coordinate = earthquake.coordinate
map.addAnnotation(annotation)
nameLabel.text = earthquake.name
magnitudeLabel.text = Earthquake.magnitudeFormatter.stringFromNumber(earthquake.magnitude)
depthLabel.text = Earthquake.depthFormatter.stringFromMeters(earthquake.depth)
timeLabel.text = Earthquake.timestampFormatter.stringFromDate(earthquake.timestamp)
/*
We can use a `LocationOperation` to retrieve the user's current location.
Once we have the location, we can compute how far they currently are
from the epicenter of the earthquake.
If this operation fails (ie, we are denied access to their location),
then the text in the `UILabel` will remain as what it is defined to
be in the storyboard.
*/
let locationOperation = LocationOperation(accuracy: kCLLocationAccuracyKilometer) { location in
if let earthquakeLocation = self.earthquake?.location {
let distance = location.distanceFromLocation(earthquakeLocation)
self.distanceLabel.text = Earthquake.distanceFormatter.stringFromMeters(distance)
}
self.locationRequest = nil
}
queue?.addOperation(locationOperation)
locationRequest = locationOperation
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
// If the LocationOperation is still going on, then cancel it.
locationRequest?.cancel()
}
@IBAction func shareEarthquake(sender: UIBarButtonItem) {
guard let earthquake = earthquake else { return }
guard let url = NSURL(string: earthquake.webLink) else { return }
let location = earthquake.location
let items = [url, location]
/*
We could present the share sheet manually, but by putting it inside
an `Operation`, we can make it mutually exclusive with other operations
that modify the view controller hierarchy.
*/
let shareOperation = BlockOperation { (continuation: Void -> Void) in
dispatch_async(dispatch_get_main_queue()) {
let shareSheet = UIActivityViewController(activityItems: items, applicationActivities: nil)
shareSheet.popoverPresentationController?.barButtonItem = sender
shareSheet.completionWithItemsHandler = { _ in
// End the operation when the share sheet completes.
continuation()
}
self.presentViewController(shareSheet, animated: true, completion: nil)
}
}
/*
Indicate that this operation modifies the View Controller hierarchy
and is thus mutually exclusive.
*/
shareOperation.addCondition(MutuallyExclusive<UIViewController>())
queue?.addOperation(shareOperation)
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.section == 1 && indexPath.row == 0 {
// The user has tapped the "More Information" button.
if let link = earthquake?.webLink, url = NSURL(string: link) {
// If we have a link, present the "More Information" dialog.
let moreInformation = MoreInformationOperation(URL: url)
queue?.addOperation(moreInformation)
}
else {
// No link; present an alert.
let alert = AlertOperation()
alert.title = "No Information"
alert.message = "No other information is available for this earthquake"
queue?.addOperation(alert)
}
}
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
extension EarthquakeTableViewController: MKMapViewDelegate {
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
guard let earthquake = earthquake else { return nil }
var view = mapView.dequeueReusableAnnotationViewWithIdentifier("pin") as? MKPinAnnotationView
view = view ?? MKPinAnnotationView(annotation: annotation, reuseIdentifier: "pin")
guard let pin = view else { return nil }
switch earthquake.magnitude {
case 0..<3: pin.pinTintColor = UIColor.grayColor()
case 3..<4: pin.pinTintColor = UIColor.blueColor()
case 4..<5: pin.pinTintColor = UIColor.orangeColor()
default: pin.pinTintColor = UIColor.redColor()
}
pin.enabled = false
return pin
}
}

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.$(CFBundleIdentifier)</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
</dict>
</plist>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="1" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="8154.4" systemVersion="15A216d" minimumToolsVersion="Automatic">
<entity name="Earthquake" representedClassName="Earthquakes.Earthquake" syncable="YES">
<attribute name="depth" optional="YES" attributeType="Double" defaultValueString="0.0" syncable="YES"/>
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="latitude" attributeType="Double" defaultValueString="0.0" syncable="YES"/>
<attribute name="longitude" optional="YES" attributeType="Double" defaultValueString="0.0" syncable="YES"/>
<attribute name="magnitude" optional="YES" attributeType="Double" defaultValueString="0.0" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="timestamp" optional="YES" attributeType="Date" syncable="YES"/>
<attribute name="webLink" optional="YES" attributeType="String" syncable="YES"/>
<compoundIndexes>
<compoundIndex>
<index value="identifier"/>
</compoundIndex>
</compoundIndexes>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="Earthquake" positionX="-63" positionY="-18" width="128" height="165"/>
</elements>
</model>

View file

@ -0,0 +1,158 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
The code in this file loads the data store, updates the model, and displays data in the UI.
*/
import UIKit
import CoreData
import CloudKit
class EarthquakesTableViewController: UITableViewController {
// MARK: Properties
var fetchedResultsController: NSFetchedResultsController?
let operationQueue = OperationQueue()
// MARK: View Controller
override func viewDidLoad() {
super.viewDidLoad()
let operation = LoadModelOperation { context in
// Now that we have a context, build our `FetchedResultsController`.
dispatch_async(dispatch_get_main_queue()) {
let request = NSFetchRequest(entityName: Earthquake.entityName)
request.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: false)]
request.fetchLimit = 100
let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
self.fetchedResultsController = controller
self.updateUI()
}
}
operationQueue.addOperation(operation)
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return fetchedResultsController?.sections?.count ?? 0
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section = fetchedResultsController?.sections?[section]
return section?.numberOfObjects ?? 0
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("earthquakeCell", forIndexPath: indexPath) as! EarthquakeTableViewCell
if let earthquake = fetchedResultsController?.objectAtIndexPath(indexPath) as? Earthquake {
cell.configure(earthquake)
}
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
/*
Instead of performing the segue directly, we can wrap it in a `BlockOperation`.
This allows us to attach conditions to the operation. For example, you
could make it so that you could only perform the segue if the network
is reachable and you have access to the user's Photos library.
If you decide to use this pattern in your apps, choose conditions that
are sensible and do not place onerous requirements on the user.
It's also worth noting that the Observer attached to the `BlockOperation`
will cause the tableview row to be deselected automatically if the
`Operation` fails.
You may choose to add your own observer to introspect the errors reported
as the operation finishes. Doing so would allow you to present a message
to the user about why you were unable to perform the requested action.
*/
let operation = BlockOperation {
self.performSegueWithIdentifier("showEarthquake", sender: nil)
}
operation.addCondition(MutuallyExclusive<UIViewController>())
let blockObserver = BlockObserver { _, errors in
/*
If the operation errored (ex: a condition failed) then the segue
isn't going to happen. We shouldn't leave the row selected.
*/
if !errors.isEmpty {
dispatch_async(dispatch_get_main_queue()) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
}
operation.addObserver(blockObserver)
operationQueue.addOperation(operation)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
guard let navigationVC = segue.destinationViewController as? UINavigationController,
detailVC = navigationVC.viewControllers.first as? EarthquakeTableViewController else {
return
}
detailVC.queue = operationQueue
if let indexPath = tableView.indexPathForSelectedRow {
detailVC.earthquake = fetchedResultsController?.objectAtIndexPath(indexPath) as? Earthquake
}
}
@IBAction func startRefreshing(sender: UIRefreshControl) {
getEarthquakes()
}
private func getEarthquakes(userInitiated: Bool = true) {
if let context = fetchedResultsController?.managedObjectContext {
let getEarthquakesOperation = GetEarthquakesOperation(context: context) {
dispatch_async(dispatch_get_main_queue()) {
self.refreshControl?.endRefreshing()
self.updateUI()
}
}
getEarthquakesOperation.userInitiated = userInitiated
operationQueue.addOperation(getEarthquakesOperation)
}
else {
/*
We don't have a context to operate on, so wait a bit and just make
the refresh control end.
*/
let when = dispatch_time(DISPATCH_TIME_NOW, Int64(0.3 * Double(NSEC_PER_SEC)))
dispatch_after(when, dispatch_get_main_queue()) {
self.refreshControl?.endRefreshing()
}
}
}
private func updateUI() {
do {
try fetchedResultsController?.performFetch()
}
catch {
print("Error in the fetched results controller: \(error).")
}
tableView.reloadData()
}
}

View file

@ -0,0 +1,104 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file sets up the operations to download and parse earthquake data. It will also decide to display an error message, if appropriate.
*/
import CoreData
/// A composite `Operation` to both download and parse earthquake data.
class GetEarthquakesOperation: GroupOperation {
// MARK: Properties
let downloadOperation: DownloadEarthquakesOperation
let parseOperation: ParseEarthquakesOperation
private var hasProducedAlert = false
/**
- parameter context: The `NSManagedObjectContext` into which the parsed
Earthquakes will be imported.
- parameter completionHandler: The handler to call after downloading and
parsing are complete. This handler will be
invoked on an arbitrary queue.
*/
init(context: NSManagedObjectContext, completionHandler: Void -> Void) {
let cachesFolder = try! NSFileManager.defaultManager().URLForDirectory(.CachesDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true)
let cacheFile = cachesFolder.URLByAppendingPathComponent("earthquakes.json")
/*
This operation is made of three child operations:
1. The operation to download the JSON feed
2. The operation to parse the JSON feed and insert the elements into the Core Data store
3. The operation to invoke the completion handler
*/
downloadOperation = DownloadEarthquakesOperation(cacheFile: cacheFile)
parseOperation = ParseEarthquakesOperation(cacheFile: cacheFile, context: context)
let finishOperation = NSBlockOperation(block: completionHandler)
// These operations must be executed in order
parseOperation.addDependency(downloadOperation)
finishOperation.addDependency(parseOperation)
super.init(operations: [downloadOperation, parseOperation, finishOperation])
name = "Get Earthquakes"
}
override func operationDidFinish(operation: NSOperation, withErrors errors: [NSError]) {
if let firstError = errors.first where (operation === downloadOperation || operation === parseOperation) {
produceAlert(firstError)
}
}
private func produceAlert(error: NSError) {
/*
We only want to show the first error, since subsequent errors might
be caused by the first.
*/
if hasProducedAlert { return }
let alert = AlertOperation()
let errorReason = (error.domain, error.code, error.userInfo[OperationConditionKey] as? String)
// These are examples of errors for which we might choose to display an error to the user
let failedReachability = (OperationErrorDomain, OperationErrorCode.ConditionFailed, ReachabilityCondition.name)
let failedJSON = (NSCocoaErrorDomain, NSPropertyListReadCorruptError, nil as String?)
switch errorReason {
case failedReachability:
// We failed because the network isn't reachable.
let hostURL = error.userInfo[ReachabilityCondition.hostKey] as! NSURL
alert.title = "Unable to Connect"
alert.message = "Cannot connect to \(hostURL.host!). Make sure your device is connected to the internet and try again."
case failedJSON:
// We failed because the JSON was malformed.
alert.title = "Unable to Download"
alert.message = "Cannot download earthquake data. Try again later."
default:
return
}
produceOperation(alert)
hasProducedAlert = true
}
}
// Operators to use in the switch statement.
private func ~=(lhs: (String, Int, String?), rhs: (String, Int, String?)) -> Bool {
return lhs.0 ~= rhs.0 && lhs.1 ~= rhs.1 && lhs.2 == rhs.2
}
private func ~=(lhs: (String, OperationErrorCode, String), rhs: (String, Int, String?)) -> Bool {
return lhs.0 ~= rhs.0 && lhs.1.rawValue ~= rhs.1 && lhs.2 == rhs.2
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

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

55
Earthquakes/Info.plist Normal file
View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocationAlwaysUsageDescription</key>
<string>Earthquakes uses your location to show how close you are to the epicenter of an earthquake.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Earthquakes uses your location to show how close you are to the epicenter of an earthquake.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>earthquake.usgs.gov</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,136 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file contains the code to create the Core Data stack.
*/
import CoreData
/**
An `Operation` subclass that loads the Core Data stack. If this operation fails,
it will produce an `AlertOperation` that will offer to retry the operation.
*/
class LoadModelOperation: Operation {
// MARK: Properties
let loadHandler: NSManagedObjectContext -> Void
// MARK: Initialization
init(loadHandler: NSManagedObjectContext -> Void) {
self.loadHandler = loadHandler
super.init()
// We only want one of these going at a time.
addCondition(MutuallyExclusive<LoadModelOperation>())
}
override func execute() {
/*
We're not going to handle catching the error here, because if we can't
get the Caches directory, then your entire sandbox is broken and
there's nothing we can possibly do to fix it.
*/
let cachesFolder = try! NSFileManager.defaultManager().URLForDirectory(.CachesDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true)
let storeURL = cachesFolder.URLByAppendingPathComponent("earthquakes.sqlite")
/*
Force unwrap this model, because this would only fail if we haven't
included the xcdatamodel in our app resources. If we forgot that step,
we deserve to crash. Plus, there's really no easy way to recover from
a missing model without reconstructing it programmatically
*/
let model = NSManagedObjectModel.mergedModelFromBundles(nil)!
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
let context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
context.persistentStoreCoordinator = persistentStoreCoordinator
var error = createStore(persistentStoreCoordinator, atURL: storeURL)
if persistentStoreCoordinator.persistentStores.isEmpty {
/*
Our persistent store does not contain irreplaceable data (which
is why it's in the Caches folder). If we fail to add it, we can
delete it and try again.
*/
destroyStore(persistentStoreCoordinator, atURL: storeURL)
error = createStore(persistentStoreCoordinator, atURL: storeURL)
}
if persistentStoreCoordinator.persistentStores.isEmpty {
print("Error creating SQLite store: \(error).")
print("Falling back to `.InMemory` store.")
error = createStore(persistentStoreCoordinator, atURL: nil, type: NSInMemoryStoreType)
}
if !persistentStoreCoordinator.persistentStores.isEmpty {
loadHandler(context)
error = nil
}
finishWithError(error)
}
private func createStore(persistentStoreCoordinator: NSPersistentStoreCoordinator, atURL URL: NSURL?, type: String = NSSQLiteStoreType) -> NSError? {
var error: NSError?
do {
let _ = try persistentStoreCoordinator.addPersistentStoreWithType(type, configuration: nil, URL: URL, options: nil)
}
catch let storeError as NSError {
error = storeError
}
return error
}
private func destroyStore(persistentStoreCoordinator: NSPersistentStoreCoordinator, atURL URL: NSURL, type: String = NSSQLiteStoreType) {
do {
let _ = try persistentStoreCoordinator.destroyPersistentStoreAtURL(URL, withType: type, options: nil)
}
catch { }
}
override func finished(errors: [NSError]) {
guard let firstError = errors.first where userInitiated else { return }
/*
We failed to load the model on a user initiated operation try and present
an error.
*/
let alert = AlertOperation()
alert.title = "Unable to load database"
alert.message = "An error occurred while loading the database. \(firstError.localizedDescription). Please try again later."
// No custom action for this button.
alert.addAction("Retry Later", style: .Cancel)
// Declare this as a local variable to avoid capturing self in the closure below.
let handler = loadHandler
/*
For this operation, the `loadHandler` is only ever invoked if there are
no errors, so if we get to this point we know that it was not executed.
This means that we can offer to the user to try loading the model again,
simply by creating a new copy of the operation and giving it the same
loadHandler.
*/
alert.addAction("Retry Now") { alertOperation in
let retryOperation = LoadModelOperation(loadHandler: handler)
retryOperation.userInitiated = true
alertOperation.produceOperation(retryOperation)
}
produceOperation(alert)
}
}

View file

@ -0,0 +1,54 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file contains the code to present more information about an earthquake as a modal sheet.
*/
import Foundation
import SafariServices
/// An `Operation` to display an `NSURL` in an app-modal `SFSafariViewController`.
class MoreInformationOperation: Operation {
// MARK: Properties
let URL: NSURL
// MARK: Initialization
init(URL: NSURL) {
self.URL = URL
super.init()
addCondition(MutuallyExclusive<UIViewController>())
}
// MARK: Overrides
override func execute() {
dispatch_async(dispatch_get_main_queue()) {
self.showSafariViewController()
}
}
private func showSafariViewController() {
if let context = UIApplication.sharedApplication().keyWindow?.rootViewController {
let safari = SFSafariViewController(URL: URL, entersReaderIfAvailable: false)
safari.delegate = self
context.presentViewController(safari, animated: true, completion: nil)
}
else {
finish()
}
}
}
extension MoreInformationOperation: SFSafariViewControllerDelegate {
func safariViewControllerDidFinish(controller: SFSafariViewController) {
controller.dismissViewControllerAnimated(true) {
self.finish()
}
}
}

View file

@ -0,0 +1,116 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Contains the code to manage the visibility of the network activity indicator
*/
import UIKit
/**
An `OperationObserver` that will cause the network activity indicator to appear
as long as the `Operation` to which it is attached is executing.
*/
struct NetworkObserver: OperationObserver {
// MARK: Initilization
init() { }
func operationDidStart(operation: Operation) {
dispatch_async(dispatch_get_main_queue()) {
// Increment the network indicator's "reference count"
NetworkIndicatorController.sharedIndicatorController.networkActivityDidStart()
}
}
func operation(operation: Operation, didProduceOperation newOperation: NSOperation) { }
func operationDidFinish(operation: Operation, errors: [NSError]) {
dispatch_async(dispatch_get_main_queue()) {
// Decrement the network indicator's "reference count".
NetworkIndicatorController.sharedIndicatorController.networkActivityDidEnd()
}
}
}
/// A singleton to manage a visual "reference count" on the network activity indicator.
private class NetworkIndicatorController {
// MARK: Properties
static let sharedIndicatorController = NetworkIndicatorController()
private var activityCount = 0
private var visibilityTimer: Timer?
// MARK: Methods
func networkActivityDidStart() {
assert(NSThread.isMainThread(), "Altering network activity indicator state can only be done on the main thread.")
activityCount += 1
updateIndicatorVisibility()
}
func networkActivityDidEnd() {
assert(NSThread.isMainThread(), "Altering network activity indicator state can only be done on the main thread.")
activityCount -= 1
updateIndicatorVisibility()
}
private func updateIndicatorVisibility() {
if activityCount > 0 {
showIndicator()
}
else {
/*
To prevent the indicator from flickering on and off, we delay the
hiding of the indicator by one second. This provides the chance
to come in and invalidate the timer before it fires.
*/
visibilityTimer = Timer(interval: 1.0) {
self.hideIndicator()
}
}
}
private func showIndicator() {
visibilityTimer?.cancel()
visibilityTimer = nil
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
}
private func hideIndicator() {
visibilityTimer?.cancel()
visibilityTimer = nil
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
}
/// Essentially a cancellable `dispatch_after`.
class Timer {
// MARK: Properties
private var isCancelled = false
// MARK: Initialization
init(interval: NSTimeInterval, handler: dispatch_block_t) {
let when = dispatch_time(DISPATCH_TIME_NOW, Int64(interval * Double(NSEC_PER_SEC)))
dispatch_after(when, dispatch_get_main_queue()) { [weak self] in
if self?.isCancelled == false {
handler()
}
}
}
func cancel() {
isCancelled = true
}
}

View file

@ -0,0 +1,41 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows how to implement the OperationObserver protocol.
*/
import Foundation
/**
The `BlockObserver` is a way to attach arbitrary blocks to significant events
in an `Operation`'s lifecycle.
*/
struct BlockObserver: OperationObserver {
// MARK: Properties
private let startHandler: (Operation -> Void)?
private let produceHandler: ((Operation, NSOperation) -> Void)?
private let finishHandler: ((Operation, [NSError]) -> Void)?
init(startHandler: (Operation -> Void)? = nil, produceHandler: ((Operation, NSOperation) -> Void)? = nil, finishHandler: ((Operation, [NSError]) -> Void)? = nil) {
self.startHandler = startHandler
self.produceHandler = produceHandler
self.finishHandler = finishHandler
}
// MARK: OperationObserver
func operationDidStart(operation: Operation) {
startHandler?(operation)
}
func operation(operation: Operation, didProduceOperation newOperation: NSOperation) {
produceHandler?(operation, newOperation)
}
func operationDidFinish(operation: Operation, errors: [NSError]) {
finishHandler?(operation, errors)
}
}

View file

@ -0,0 +1,59 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This code shows how to create a simple subclass of Operation.
*/
import Foundation
/// A closure type that takes a closure as its parameter.
typealias OperationBlock = (Void -> Void) -> Void
/// A sublcass of `Operation` to execute a closure.
class BlockOperation: Operation {
private let block: OperationBlock?
/**
The designated initializer.
- parameter block: The closure to run when the operation executes. This
closure will be run on an arbitrary queue. The parameter passed to the
block **MUST** be invoked by your code, or else the `BlockOperation`
will never finish executing. If this parameter is `nil`, the operation
will immediately finish.
*/
init(block: OperationBlock? = nil) {
self.block = block
super.init()
}
/**
A convenience initializer to execute a block on the main queue.
- parameter mainQueueBlock: The block to execute on the main queue. Note
that this block does not have a "continuation" block to execute (unlike
the designated initializer). The operation will be automatically ended
after the `mainQueueBlock` is executed.
*/
convenience init(mainQueueBlock: dispatch_block_t) {
self.init(block: { continuation in
dispatch_async(dispatch_get_main_queue()) {
mainQueueBlock()
continuation()
}
})
}
override func execute() {
guard let block = block else {
finish()
return
}
block {
self.finish()
}
}
}

View file

@ -0,0 +1,80 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
A convenient extension to CloudKit.CKContainer.
*/
import CloudKit
extension CKContainer {
/**
Verify that the current user has certain permissions for the `CKContainer`,
and potentially requesting the permission if necessary.
- parameter permission: The permissions to be verified on the container.
- parameter shouldRequest: If this value is `true` and the user does not
have the passed `permission`, then the user will be prompted for it.
- parameter completion: A closure that will be executed after verification
completes. The `NSError` passed in to the closure is the result of either
retrieving the account status, or requesting permission, if either
operation fails. If the verification was successful, this value will
be `nil`.
*/
func verifyPermission(permission: CKApplicationPermissions, requestingIfNecessary shouldRequest: Bool = false, completion: NSError? -> Void) {
verifyAccountStatus(self, permission: permission, shouldRequest: shouldRequest, completion: completion)
}
}
/**
Make these helper functions instead of helper methods, so we don't pollute
`CKContainer`.
*/
private func verifyAccountStatus(container: CKContainer, permission: CKApplicationPermissions, shouldRequest: Bool, completion: NSError? -> Void) {
container.accountStatusWithCompletionHandler { accountStatus, accountError in
if accountStatus == .Available {
if permission != [] {
verifyPermission(container, permission: permission, shouldRequest: shouldRequest, completion: completion)
}
else {
completion(nil)
}
}
else {
let error = accountError ?? NSError(domain: CKErrorDomain, code: CKErrorCode.NotAuthenticated.rawValue, userInfo: nil)
completion(error)
}
}
}
private func verifyPermission(container: CKContainer, permission: CKApplicationPermissions, shouldRequest: Bool, completion: NSError? -> Void) {
container.statusForApplicationPermission(permission) { permissionStatus, permissionError in
if permissionStatus == .Granted {
completion(nil)
}
else if permissionStatus == .InitialState && shouldRequest {
requestPermission(container, permission: permission, completion: completion)
}
else {
let error = permissionError ?? NSError(domain: CKErrorDomain, code: CKErrorCode.PermissionFailure.rawValue, userInfo: nil)
completion(error)
}
}
}
private func requestPermission(container: CKContainer, permission: CKApplicationPermissions, completion: NSError? -> Void) {
dispatch_async(dispatch_get_main_queue()) {
container.requestApplicationPermission(permission) { requestStatus, requestError in
if requestStatus == .Granted {
completion(nil)
}
else {
let error = requestError ?? NSError(domain: CKErrorDomain, code: CKErrorCode.PermissionFailure.rawValue, userInfo: nil)
completion(error)
}
}
}
}

View file

@ -0,0 +1,81 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows an example of implementing the OperationCondition protocol.
*/
import EventKit
/// A condition for verifying access to the user's calendar.
struct CalendarCondition: OperationCondition {
static let name = "Calendar"
static let entityTypeKey = "EKEntityType"
static let isMutuallyExclusive = false
let entityType: EKEntityType
init(entityType: EKEntityType) {
self.entityType = entityType
}
func dependencyForOperation(operation: Operation) -> NSOperation? {
return CalendarPermissionOperation(entityType: entityType)
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
switch EKEventStore.authorizationStatusForEntityType(entityType) {
case .Authorized:
completion(.Satisfied)
default:
// We are not authorized to access entities of this type.
let error = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name,
self.dynamicType.entityTypeKey: entityType.rawValue
])
completion(.Failed(error))
}
}
}
/**
`EKEventStore` takes a while to initialize, so we should create
one and then keep it around for future use, instead of creating
a new one every time a `CalendarPermissionOperation` runs.
*/
private let SharedEventStore = EKEventStore()
/**
A private `Operation` that will request access to the user's Calendar/Reminders,
if it has not already been granted.
*/
private class CalendarPermissionOperation: Operation {
let entityType: EKEntityType
init(entityType: EKEntityType) {
self.entityType = entityType
super.init()
addCondition(AlertPresentation())
}
override func execute() {
let status = EKEventStore.authorizationStatusForEntityType(entityType)
switch status {
case .NotDetermined:
dispatch_async(dispatch_get_main_queue()) {
SharedEventStore.requestAccessToEntityType(self.entityType) { granted, error in
self.finish()
}
}
default:
finish()
}
}
}

View file

@ -0,0 +1,90 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows an example of implementing the OperationCondition protocol.
*/
import CloudKit
/// A condition describing that the operation requires access to a specific CloudKit container.
struct CloudContainerCondition: OperationCondition {
static let name = "CloudContainer"
static let containerKey = "CKContainer"
/*
CloudKit has no problem handling multiple operations at the same time
so we will allow operations that use CloudKit to be concurrent with each
other.
*/
static let isMutuallyExclusive = false
let container: CKContainer // this is the container to which you need access.
let permission: CKApplicationPermissions
/**
- parameter container: the `CKContainer` to which you need access.
- parameter permission: the `CKApplicationPermissions` you need for the
container. This parameter has a default value of `[]`, which would get
you anonymized read/write access.
*/
init(container: CKContainer, permission: CKApplicationPermissions = []) {
self.container = container
self.permission = permission
}
func dependencyForOperation(operation: Operation) -> NSOperation? {
return CloudKitPermissionOperation(container: container, permission: permission)
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
container.verifyPermission(permission, requestingIfNecessary: false) { error in
if let error = error {
let conditionError = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name,
self.dynamicType.containerKey: self.container,
NSUnderlyingErrorKey: error
])
completion(.Failed(conditionError))
}
else {
completion(.Satisfied)
}
}
}
}
/**
This operation asks the user for permission to use CloudKit, if necessary.
If permission has already been granted, this operation will quickly finish.
*/
private class CloudKitPermissionOperation: Operation {
let container: CKContainer
let permission: CKApplicationPermissions
init(container: CKContainer, permission: CKApplicationPermissions) {
self.container = container
self.permission = permission
super.init()
if permission != [] {
/*
Requesting non-zero permissions means that this potentially presents
an alert, so it should not run at the same time as anything else
that presents an alert.
*/
addCondition(AlertPresentation())
}
}
override func execute() {
container.verifyPermission(permission, requestingIfNecessary: true) { error in
self.finishWithError(error)
}
}
}

View file

@ -0,0 +1,78 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows how to make an operation that efficiently waits.
*/
import Foundation
/**
`DelayOperation` is an `Operation` that will simply wait for a given time
interval, or until a specific `NSDate`.
It is important to note that this operation does **not** use the `sleep()`
function, since that is inefficient and blocks the thread on which it is called.
Instead, this operation uses `dispatch_after` to know when the appropriate amount
of time has passed.
If the interval is negative, or the `NSDate` is in the past, then this operation
immediately finishes.
*/
class DelayOperation: Operation {
// MARK: Types
private enum Delay {
case Interval(NSTimeInterval)
case Date(NSDate)
}
// MARK: Properties
private let delay: Delay
// MARK: Initialization
init(interval: NSTimeInterval) {
delay = .Interval(interval)
super.init()
}
init(until date: NSDate) {
delay = .Date(date)
super.init()
}
override func execute() {
let interval: NSTimeInterval
// Figure out how long we should wait for.
switch delay {
case .Interval(let theInterval):
interval = theInterval
case .Date(let date):
interval = date.timeIntervalSinceNow
}
guard interval > 0 else {
finish()
return
}
let when = dispatch_time(DISPATCH_TIME_NOW, Int64(interval * Double(NSEC_PER_SEC)))
dispatch_after(when, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)) {
// If we were cancelled, then finish() has already been called.
if !self.cancelled {
self.finish()
}
}
}
override func cancel() {
super.cancel()
// Cancelling the operation means we don't want to wait anymore.
self.finish()
}
}

View file

@ -0,0 +1,31 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
A convenient extension to Swift.Dictionary.
*/
extension Dictionary {
/**
It's not uncommon to want to turn a sequence of values into a dictionary,
where each value is keyed by some unique identifier. This initializer will
do that.
- parameter sequence: The sequence to be iterated
- parameter keyer: The closure that will be executed for each element in
the `sequence`. The return value of this closure, if there is one, will
be used as the key for the value in the `Dictionary`. If the closure
returns `nil`, then the value will be omitted from the `Dictionary`.
*/
init<Sequence: SequenceType where Sequence.Generator.Element == Value>(sequence: Sequence, @noescape keyMapper: Value -> Key?) {
self.init()
for item in sequence {
if let key = keyMapper(item) {
self[key] = item
}
}
}
}

View file

@ -0,0 +1,79 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
The file contains the code to automatically set up dependencies between mutually exclusive operations.
*/
import Foundation
/**
`ExclusivityController` is a singleton to keep track of all the in-flight
`Operation` instances that have declared themselves as requiring mutual exclusivity.
We use a singleton because mutual exclusivity must be enforced across the entire
app, regardless of the `OperationQueue` on which an `Operation` was executed.
*/
class ExclusivityController {
static let sharedExclusivityController = ExclusivityController()
private let serialQueue = dispatch_queue_create("Operations.ExclusivityController", DISPATCH_QUEUE_SERIAL)
private var operations: [String: [Operation]] = [:]
private init() {
/*
A private initializer effectively prevents any other part of the app
from accidentally creating an instance.
*/
}
/// Registers an operation as being mutually exclusive
func addOperation(operation: Operation, categories: [String]) {
/*
This needs to be a synchronous operation.
If this were async, then we might not get around to adding dependencies
until after the operation had already begun, which would be incorrect.
*/
dispatch_sync(serialQueue) {
for category in categories {
self.noqueue_addOperation(operation, category: category)
}
}
}
/// Unregisters an operation from being mutually exclusive.
func removeOperation(operation: Operation, categories: [String]) {
dispatch_async(serialQueue) {
for category in categories {
self.noqueue_removeOperation(operation, category: category)
}
}
}
// MARK: Operation Management
private func noqueue_addOperation(operation: Operation, category: String) {
var operationsWithThisCategory = operations[category] ?? []
if let last = operationsWithThisCategory.last {
operation.addDependency(last)
}
operationsWithThisCategory.append(operation)
operations[category] = operationsWithThisCategory
}
private func noqueue_removeOperation(operation: Operation, category: String) {
let matchingOperations = operations[category]
if var operationsWithThisCategory = matchingOperations,
let index = operationsWithThisCategory.indexOf(operation) {
operationsWithThisCategory.removeAtIndex(index)
operations[category] = operationsWithThisCategory
}
}
}

View file

@ -0,0 +1,111 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows how operations can be composed together to form new operations.
*/
import Foundation
/**
A subclass of `Operation` that executes zero or more operations as part of its
own execution. This class of operation is very useful for abstracting several
smaller operations into a larger operation. As an example, the `GetEarthquakesOperation`
is composed of both a `DownloadEarthquakesOperation` and a `ParseEarthquakesOperation`.
Additionally, `GroupOperation`s are useful if you establish a chain of dependencies,
but part of the chain may "loop". For example, if you have an operation that
requires the user to be authenticated, you may consider putting the "login"
operation inside a group operation. That way, the "login" operation may produce
subsequent operations (still within the outer `GroupOperation`) that will all
be executed before the rest of the operations in the initial chain of operations.
*/
class GroupOperation: Operation {
private let internalQueue = OperationQueue()
private let startingOperation = NSBlockOperation(block: {})
private let finishingOperation = NSBlockOperation(block: {})
private var aggregatedErrors = [NSError]()
convenience init(operations: NSOperation...) {
self.init(operations: operations)
}
init(operations: [NSOperation]) {
super.init()
internalQueue.suspended = true
internalQueue.delegate = self
internalQueue.addOperation(startingOperation)
for operation in operations {
internalQueue.addOperation(operation)
}
}
override func cancel() {
internalQueue.cancelAllOperations()
super.cancel()
}
override func execute() {
internalQueue.suspended = false
internalQueue.addOperation(finishingOperation)
}
func addOperation(operation: NSOperation) {
internalQueue.addOperation(operation)
}
/**
Note that some part of execution has produced an error.
Errors aggregated through this method will be included in the final array
of errors reported to observers and to the `finished(_:)` method.
*/
final func aggregateError(error: NSError) {
aggregatedErrors.append(error)
}
func operationDidFinish(operation: NSOperation, withErrors errors: [NSError]) {
// For use by subclassers.
}
}
extension GroupOperation: OperationQueueDelegate {
final func operationQueue(operationQueue: OperationQueue, willAddOperation operation: NSOperation) {
assert(!finishingOperation.finished && !finishingOperation.executing, "cannot add new operations to a group after the group has completed")
/*
Some operation in this group has produced a new operation to execute.
We want to allow that operation to execute before the group completes,
so we'll make the finishing operation dependent on this newly-produced operation.
*/
if operation !== finishingOperation {
finishingOperation.addDependency(operation)
}
/*
All operations should be dependent on the "startingOperation".
This way, we can guarantee that the conditions for other operations
will not evaluate until just before the operation is about to run.
Otherwise, the conditions could be evaluated at any time, even
before the internal operation queue is unsuspended.
*/
if operation !== startingOperation {
operation.addDependency(startingOperation)
}
}
final func operationQueue(operationQueue: OperationQueue, operationDidFinish operation: NSOperation, withErrors errors: [NSError]) {
aggregatedErrors.appendContentsOf(errors)
if operation === finishingOperation {
internalQueue.suspended = true
finish(aggregatedErrors)
}
else if operation !== startingOperation {
operationDidFinish(operation, withErrors: errors)
}
}
}

View file

@ -0,0 +1,129 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows an example of implementing the OperationCondition protocol.
*/
#if os(iOS)
import HealthKit
import UIKit
/**
A condition to indicate an `Operation` requires access to the user's health
data.
*/
struct HealthCondition: OperationCondition {
static let name = "Health"
static let healthDataAvailable = "HealthDataAvailable"
static let unauthorizedShareTypesKey = "UnauthorizedShareTypes"
static let isMutuallyExclusive = false
let shareTypes: Set<HKSampleType>
let readTypes: Set<HKSampleType>
/**
The designated initializer.
- parameter typesToWrite: An array of `HKSampleType` objects, indicating
the kinds of data you wish to save to HealthKit.
- parameter typesToRead: An array of `HKSampleType` objects, indicating
the kinds of data you wish to read from HealthKit.
*/
init(typesToWrite: Set<HKSampleType>, typesToRead: Set<HKSampleType>) {
shareTypes = typesToWrite
readTypes = typesToRead
}
func dependencyForOperation(operation: Operation) -> NSOperation? {
guard HKHealthStore.isHealthDataAvailable() else {
return nil
}
guard !shareTypes.isEmpty || !readTypes.isEmpty else {
return nil
}
return HealthPermissionOperation(shareTypes: shareTypes, readTypes: readTypes)
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
guard HKHealthStore.isHealthDataAvailable() else {
failed(shareTypes, completion: completion)
return
}
let store = HKHealthStore()
/*
Note that we cannot check to see if access to the "typesToRead"
has been granted or not, as that is sensitive data. For example,
a person with diabetes may choose to not allow access to Blood Glucose
data, and the fact that this request has denied is itself an indicator
that the user may have diabetes.
Thus, we can only check to see if we've been given permission to
write data to HealthKit.
*/
let unauthorizedShareTypes = shareTypes.filter { shareType in
return store.authorizationStatusForType(shareType) != .SharingAuthorized
}
if !unauthorizedShareTypes.isEmpty {
failed(Set(unauthorizedShareTypes), completion: completion)
}
else {
completion(.Satisfied)
}
}
// Break this out in to its own method so we don't clutter up the evaluate... method.
private func failed(unauthorizedShareTypes: Set<HKSampleType>, completion: OperationConditionResult -> Void) {
let error = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name,
self.dynamicType.healthDataAvailable: HKHealthStore.isHealthDataAvailable(),
self.dynamicType.unauthorizedShareTypesKey: unauthorizedShareTypes
])
completion(.Failed(error))
}
}
/**
A private `Operation` that will request access to the user's health data, if
it has not already been granted.
*/
private class HealthPermissionOperation: Operation {
let shareTypes: Set<HKSampleType>
let readTypes: Set<HKSampleType>
init(shareTypes: Set<HKSampleType>, readTypes: Set<HKSampleType>) {
self.shareTypes = shareTypes
self.readTypes = readTypes
super.init()
addCondition(MutuallyExclusive<HealthPermissionOperation>())
addCondition(MutuallyExclusive<UIViewController>())
addCondition(AlertPresentation())
}
override func execute() {
dispatch_async(dispatch_get_main_queue()) {
let store = HKHealthStore()
/*
This method is smart enough to not re-prompt for access if access
has already been granted.
*/
store.requestAuthorizationToShareTypes(self.shareTypes, readTypes: self.readTypes) { completed, error in
self.finish()
}
}
}
}
#endif

View file

@ -0,0 +1,143 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows an example of implementing the OperationCondition protocol.
*/
import CoreLocation
/// A condition for verifying access to the user's location.
struct LocationCondition: OperationCondition {
/**
Declare a new enum instead of using `CLAuthorizationStatus`, because that
enum has more case values than are necessary for our purposes.
*/
enum Usage {
case WhenInUse
case Always
}
static let name = "Location"
static let locationServicesEnabledKey = "CLLocationServicesEnabled"
static let authorizationStatusKey = "CLAuthorizationStatus"
static let isMutuallyExclusive = false
let usage: Usage
init(usage: Usage) {
self.usage = usage
}
func dependencyForOperation(operation: Operation) -> NSOperation? {
return LocationPermissionOperation(usage: usage)
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
let enabled = CLLocationManager.locationServicesEnabled()
let actual = CLLocationManager.authorizationStatus()
var error: NSError?
// There are several factors to consider when evaluating this condition
switch (enabled, usage, actual) {
case (true, _, .AuthorizedAlways):
// The service is enabled, and we have "Always" permission -> condition satisfied.
break
case (true, .WhenInUse, .AuthorizedWhenInUse):
/*
The service is enabled, and we have and need "WhenInUse"
permission -> condition satisfied.
*/
break
default:
/*
Anything else is an error. Maybe location services are disabled,
or maybe we need "Always" permission but only have "WhenInUse",
or maybe access has been restricted or denied,
or maybe access hasn't been request yet.
The last case would happen if this condition were wrapped in a `SilentCondition`.
*/
error = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name,
self.dynamicType.locationServicesEnabledKey: enabled,
self.dynamicType.authorizationStatusKey: Int(actual.rawValue)
])
}
if let error = error {
completion(.Failed(error))
}
else {
completion(.Satisfied)
}
}
}
/**
A private `Operation` that will request permission to access the user's location,
if permission has not already been granted.
*/
private class LocationPermissionOperation: Operation {
let usage: LocationCondition.Usage
var manager: CLLocationManager?
init(usage: LocationCondition.Usage) {
self.usage = usage
super.init()
/*
This is an operation that potentially presents an alert so it should
be mutually exclusive with anything else that presents an alert.
*/
addCondition(AlertPresentation())
}
override func execute() {
/*
Not only do we need to handle the "Not Determined" case, but we also
need to handle the "upgrade" (.WhenInUse -> .Always) case.
*/
switch (CLLocationManager.authorizationStatus(), usage) {
case (.NotDetermined, _), (.AuthorizedWhenInUse, .Always):
dispatch_async(dispatch_get_main_queue()) {
self.requestPermission()
}
default:
finish()
}
}
private func requestPermission() {
manager = CLLocationManager()
manager?.delegate = self
let key: String
switch usage {
case .WhenInUse:
key = "NSLocationWhenInUseUsageDescription"
manager?.requestWhenInUseAuthorization()
case .Always:
key = "NSLocationAlwaysUsageDescription"
manager?.requestAlwaysAuthorization()
}
// This is helpful when developing the app.
assert(NSBundle.mainBundle().objectForInfoDictionaryKey(key) != nil, "Requesting location permission requires the \(key) key in your Info.plist")
}
}
extension LocationPermissionOperation: CLLocationManagerDelegate {
@objc func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status: CLAuthorizationStatus) {
if manager == self.manager && executing && status != .NotDetermined {
finish()
}
}
}

View file

@ -0,0 +1,78 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Shows how to retrieve the user's location with an operation.
*/
import Foundation
import CoreLocation
/**
`LocationOperation` is an `Operation` subclass to do a "one-shot" request to
get the user's current location, with a desired accuracy. This operation will
prompt for `WhenInUse` location authorization, if the app does not already
have it.
*/
class LocationOperation: Operation, CLLocationManagerDelegate {
// MARK: Properties
private let accuracy: CLLocationAccuracy
private var manager: CLLocationManager?
private let handler: CLLocation -> Void
// MARK: Initialization
init(accuracy: CLLocationAccuracy, locationHandler: CLLocation -> Void) {
self.accuracy = accuracy
self.handler = locationHandler
super.init()
addCondition(LocationCondition(usage: .WhenInUse))
addCondition(MutuallyExclusive<CLLocationManager>())
}
override func execute() {
dispatch_async(dispatch_get_main_queue()) {
/*
`CLLocationManager` needs to be created on a thread with an active
run loop, so for simplicity we do this on the main queue.
*/
let manager = CLLocationManager()
manager.desiredAccuracy = self.accuracy
manager.delegate = self
manager.startUpdatingLocation()
self.manager = manager
}
}
override func cancel() {
dispatch_async(dispatch_get_main_queue()) {
self.stopLocationUpdates()
super.cancel()
}
}
private func stopLocationUpdates() {
manager?.stopUpdatingLocation()
manager = nil
}
// MARK: CLLocationManagerDelegate
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last where location.horizontalAccuracy <= accuracy else {
return
}
stopLocationUpdates()
handler(location)
finish()
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError) {
stopLocationUpdates()
finishWithError(error)
}
}

View file

@ -0,0 +1,39 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows an example of implementing the OperationCondition protocol.
*/
import Foundation
/// A generic condition for describing kinds of operations that may not execute concurrently.
struct MutuallyExclusive<T>: OperationCondition {
static var name: String {
return "MutuallyExclusive<\(T.self)>"
}
static var isMutuallyExclusive: Bool {
return true
}
init() { }
func dependencyForOperation(operation: Operation) -> NSOperation? {
return nil
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
completion(.Satisfied)
}
}
/**
The purpose of this enum is to simply provide a non-constructible
type to be used with `MutuallyExclusive<T>`.
*/
enum Alert { }
/// A condition describing that the targeted operation may present an alert.
typealias AlertPresentation = MutuallyExclusive<Alert>

View file

@ -0,0 +1,18 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
An extension to NSLock to simplify executing critical code.
*/
import Foundation
extension NSLock {
func withCriticalScope<T>(@noescape block: Void -> T) -> T {
lock()
let value = block()
unlock()
return value
}
}

View file

@ -0,0 +1,38 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
A convenient extension to Foundation.NSOperation.
*/
import Foundation
extension NSOperation {
/**
Add a completion block to be executed after the `NSOperation` enters the
"finished" state.
*/
func addCompletionBlock(block: Void -> Void) {
if let existing = completionBlock {
/*
If we already have a completion block, we construct a new one by
chaining them together.
*/
completionBlock = {
existing()
block()
}
}
else {
completionBlock = block
}
}
/// Add multiple depdendencies to the operation.
func addDependencies(dependencies: [NSOperation]) {
for dependency in dependencies {
addDependency(dependency)
}
}
}

View file

@ -0,0 +1,56 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
The file shows how to make an OperationCondition that composes another OperationCondition.
*/
import Foundation
/**
A simple condition that negates the evaluation of another condition.
This is useful (for example) if you want to only execute an operation if the
network is NOT reachable.
*/
struct NegatedCondition<T: OperationCondition>: OperationCondition {
static var name: String {
return "Not<\(T.name)>"
}
static var negatedConditionKey: String {
return "NegatedCondition"
}
static var isMutuallyExclusive: Bool {
return T.isMutuallyExclusive
}
let condition: T
init(condition: T) {
self.condition = condition
}
func dependencyForOperation(operation: Operation) -> NSOperation? {
return condition.dependencyForOperation(operation)
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
condition.evaluateForOperation(operation) { result in
if result == .Satisfied {
// If the composed condition succeeded, then this one failed.
let error = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name,
self.dynamicType.negatedConditionKey: self.condition.dynamicType.name
])
completion(.Failed(error))
}
else {
// If the composed condition failed, then this one succeeded.
completion(.Satisfied)
}
}
}
}

View file

@ -0,0 +1,46 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows an example of implementing the OperationCondition protocol.
*/
import Foundation
/**
A condition that specifies that every dependency must have succeeded.
If any dependency was cancelled, the target operation will be cancelled as
well.
*/
struct NoCancelledDependencies: OperationCondition {
static let name = "NoCancelledDependencies"
static let cancelledDependenciesKey = "CancelledDependencies"
static let isMutuallyExclusive = false
init() {
// No op.
}
func dependencyForOperation(operation: Operation) -> NSOperation? {
return nil
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
// Verify that all of the dependencies executed.
let cancelled = operation.dependencies.filter { $0.cancelled }
if !cancelled.isEmpty {
// At least one dependency was cancelled; the condition was not satisfied.
let error = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name,
self.dynamicType.cancelledDependenciesKey: cancelled
])
completion(.Failed(error))
}
else {
completion(.Satisfied)
}
}
}

View file

@ -0,0 +1,348 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file contains the foundational subclass of NSOperation.
*/
import Foundation
/**
The subclass of `NSOperation` from which all other operations should be derived.
This class adds both Conditions and Observers, which allow the operation to define
extended readiness requirements, as well as notify many interested parties
about interesting operation state changes
*/
class Operation: NSOperation {
// use the KVO mechanism to indicate that changes to "state" affect other properties as well
class func keyPathsForValuesAffectingIsReady() -> Set<NSObject> {
return ["state"]
}
class func keyPathsForValuesAffectingIsExecuting() -> Set<NSObject> {
return ["state"]
}
class func keyPathsForValuesAffectingIsFinished() -> Set<NSObject> {
return ["state"]
}
// MARK: State Management
private enum State: Int, Comparable {
/// The initial state of an `Operation`.
case Initialized
/// The `Operation` is ready to begin evaluating conditions.
case Pending
/// The `Operation` is evaluating conditions.
case EvaluatingConditions
/**
The `Operation`'s conditions have all been satisfied, and it is ready
to execute.
*/
case Ready
/// The `Operation` is executing.
case Executing
/**
Execution of the `Operation` has finished, but it has not yet notified
the queue of this.
*/
case Finishing
/// The `Operation` has finished executing.
case Finished
func canTransitionToState(target: State) -> Bool {
switch (self, target) {
case (.Initialized, .Pending):
return true
case (.Pending, .EvaluatingConditions):
return true
case (.EvaluatingConditions, .Ready):
return true
case (.Ready, .Executing):
return true
case (.Ready, .Finishing):
return true
case (.Executing, .Finishing):
return true
case (.Finishing, .Finished):
return true
default:
return false
}
}
}
/**
Indicates that the Operation can now begin to evaluate readiness conditions,
if appropriate.
*/
func willEnqueue() {
state = .Pending
}
/// Private storage for the `state` property that will be KVO observed.
private var _state = State.Initialized
/// A lock to guard reads and writes to the `_state` property
private let stateLock = NSLock()
private var state: State {
get {
return stateLock.withCriticalScope {
_state
}
}
set(newState) {
/*
It's important to note that the KVO notifications are NOT called from inside
the lock. If they were, the app would deadlock, because in the middle of
calling the `didChangeValueForKey()` method, the observers try to access
properties like "isReady" or "isFinished". Since those methods also
acquire the lock, then we'd be stuck waiting on our own lock. It's the
classic definition of deadlock.
*/
willChangeValueForKey("state")
stateLock.withCriticalScope { Void -> Void in
guard _state != .Finished else {
return
}
assert(_state.canTransitionToState(newState), "Performing invalid state transition.")
_state = newState
}
didChangeValueForKey("state")
}
}
// Here is where we extend our definition of "readiness".
override var ready: Bool {
switch state {
case .Initialized:
// If the operation has been cancelled, "isReady" should return true
return cancelled
case .Pending:
// If the operation has been cancelled, "isReady" should return true
guard !cancelled else {
return true
}
// If super isReady, conditions can be evaluated
if super.ready {
evaluateConditions()
}
// Until conditions have been evaluated, "isReady" returns false
return false
case .Ready:
return super.ready || cancelled
default:
return false
}
}
var userInitiated: Bool {
get {
return qualityOfService == .UserInitiated
}
set {
assert(state < .Executing, "Cannot modify userInitiated after execution has begun.")
qualityOfService = newValue ? .UserInitiated : .Default
}
}
override var executing: Bool {
return state == .Executing
}
override var finished: Bool {
return state == .Finished
}
private func evaluateConditions() {
assert(state == .Pending && !cancelled, "evaluateConditions() was called out-of-order")
state = .EvaluatingConditions
OperationConditionEvaluator.evaluate(conditions, operation: self) { failures in
self._internalErrors.appendContentsOf(failures)
self.state = .Ready
}
}
// MARK: Observers and Conditions
private(set) var conditions = [OperationCondition]()
func addCondition(condition: OperationCondition) {
assert(state < .EvaluatingConditions, "Cannot modify conditions after execution has begun.")
conditions.append(condition)
}
private(set) var observers = [OperationObserver]()
func addObserver(observer: OperationObserver) {
assert(state < .Executing, "Cannot modify observers after execution has begun.")
observers.append(observer)
}
override func addDependency(operation: NSOperation) {
assert(state < .Executing, "Dependencies cannot be modified after execution has begun.")
super.addDependency(operation)
}
// MARK: Execution and Cancellation
override final func start() {
// NSOperation.start() contains important logic that shouldn't be bypassed.
super.start()
// If the operation has been cancelled, we still need to enter the "Finished" state.
if cancelled {
finish()
}
}
override final func main() {
assert(state == .Ready, "This operation must be performed on an operation queue.")
if _internalErrors.isEmpty && !cancelled {
state = .Executing
for observer in observers {
observer.operationDidStart(self)
}
execute()
}
else {
finish()
}
}
/**
`execute()` is the entry point of execution for all `Operation` subclasses.
If you subclass `Operation` and wish to customize its execution, you would
do so by overriding the `execute()` method.
At some point, your `Operation` subclass must call one of the "finish"
methods defined below; this is how you indicate that your operation has
finished its execution, and that operations dependent on yours can re-evaluate
their readiness state.
*/
func execute() {
print("\(self.dynamicType) must override `execute()`.")
finish()
}
private var _internalErrors = [NSError]()
func cancelWithError(error: NSError? = nil) {
if let error = error {
_internalErrors.append(error)
}
cancel()
}
final func produceOperation(operation: NSOperation) {
for observer in observers {
observer.operation(self, didProduceOperation: operation)
}
}
// MARK: Finishing
/**
Most operations may finish with a single error, if they have one at all.
This is a convenience method to simplify calling the actual `finish()`
method. This is also useful if you wish to finish with an error provided
by the system frameworks. As an example, see `DownloadEarthquakesOperation`
for how an error from an `NSURLSession` is passed along via the
`finishWithError()` method.
*/
final func finishWithError(error: NSError?) {
if let error = error {
finish([error])
}
else {
finish()
}
}
/**
A private property to ensure we only notify the observers once that the
operation has finished.
*/
private var hasFinishedAlready = false
final func finish(errors: [NSError] = []) {
if !hasFinishedAlready {
hasFinishedAlready = true
state = .Finishing
let combinedErrors = _internalErrors + errors
finished(combinedErrors)
for observer in observers {
observer.operationDidFinish(self, errors: combinedErrors)
}
state = .Finished
}
}
/**
Subclasses may override `finished(_:)` if they wish to react to the operation
finishing with errors. For example, the `LoadModelOperation` implements
this method to potentially inform the user about an error when trying to
bring up the Core Data stack.
*/
func finished(errors: [NSError]) {
// No op.
}
override final func waitUntilFinished() {
/*
Waiting on operations is almost NEVER the right thing to do. It is
usually superior to use proper locking constructs, such as `dispatch_semaphore_t`
or `dispatch_group_notify`, or even `NSLocking` objects. Many developers
use waiting when they should instead be chaining discrete operations
together using dependencies.
To reinforce this idea, invoking `waitUntilFinished()` will crash your
app, as incentive for you to find a more appropriate way to express
the behavior you're wishing to create.
*/
fatalError("Waiting on operations is an anti-pattern. Remove this ONLY if you're absolutely sure there is No Other Way™.")
}
}
// Simple operator functions to simplify the assertions used above.
private func <(lhs: Operation.State, rhs: Operation.State) -> Bool {
return lhs.rawValue < rhs.rawValue
}
private func ==(lhs: Operation.State, rhs: Operation.State) -> Bool {
return lhs.rawValue == rhs.rawValue
}

View file

@ -0,0 +1,110 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file contains the fundamental logic relating to Operation conditions.
*/
import Foundation
let OperationConditionKey = "OperationCondition"
/**
A protocol for defining conditions that must be satisfied in order for an
operation to begin execution.
*/
protocol OperationCondition {
/**
The name of the condition. This is used in userInfo dictionaries of `.ConditionFailed`
errors as the value of the `OperationConditionKey` key.
*/
static var name: String { get }
/**
Specifies whether multiple instances of the conditionalized operation may
be executing simultaneously.
*/
static var isMutuallyExclusive: Bool { get }
/**
Some conditions may have the ability to satisfy the condition if another
operation is executed first. Use this method to return an operation that
(for example) asks for permission to perform the operation
- parameter operation: The `Operation` to which the Condition has been added.
- returns: An `NSOperation`, if a dependency should be automatically added. Otherwise, `nil`.
- note: Only a single operation may be returned as a dependency. If you
find that you need to return multiple operations, then you should be
expressing that as multiple conditions. Alternatively, you could return
a single `GroupOperation` that executes multiple operations internally.
*/
func dependencyForOperation(operation: Operation) -> NSOperation?
/// Evaluate the condition, to see if it has been satisfied or not.
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void)
}
/**
An enum to indicate whether an `OperationCondition` was satisfied, or if it
failed with an error.
*/
enum OperationConditionResult: Equatable {
case Satisfied
case Failed(NSError)
var error: NSError? {
if case .Failed(let error) = self {
return error
}
return nil
}
}
func ==(lhs: OperationConditionResult, rhs: OperationConditionResult) -> Bool {
switch (lhs, rhs) {
case (.Satisfied, .Satisfied):
return true
case (.Failed(let lError), .Failed(let rError)) where lError == rError:
return true
default:
return false
}
}
// MARK: Evaluate Conditions
struct OperationConditionEvaluator {
static func evaluate(conditions: [OperationCondition], operation: Operation, completion: [NSError] -> Void) {
// Check conditions.
let conditionGroup = dispatch_group_create()
var results = [OperationConditionResult?](count: conditions.count, repeatedValue: nil)
// Ask each condition to evaluate and store its result in the "results" array.
for (index, condition) in conditions.enumerate() {
dispatch_group_enter(conditionGroup)
condition.evaluateForOperation(operation) { result in
results[index] = result
dispatch_group_leave(conditionGroup)
}
}
// After all the conditions have evaluated, this block will execute.
dispatch_group_notify(conditionGroup, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)) {
// Aggregate the errors that occurred, in order.
var failures = results.flatMap { $0?.error }
/*
If any of the conditions caused this operation to be cancelled,
check for that.
*/
if operation.cancelled {
failures.append(NSError(code: .ConditionFailed))
}
completion(failures)
}
}
}

View file

@ -0,0 +1,31 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file defines the error codes and convenience functions for interacting with Operation-related errors.
*/
import Foundation
let OperationErrorDomain = "OperationErrors"
enum OperationErrorCode: Int {
case ConditionFailed = 1
case ExecutionFailed = 2
}
extension NSError {
convenience init(code: OperationErrorCode, userInfo: [NSObject: AnyObject]? = nil) {
self.init(domain: OperationErrorDomain, code: code.rawValue, userInfo: userInfo)
}
}
// This makes it easy to compare an `NSError.code` to an `OperationErrorCode`.
func ==(lhs: Int, rhs: OperationErrorCode) -> Bool {
return lhs == rhs.rawValue
}
func ==(lhs: OperationErrorCode, rhs: Int) -> Bool {
return lhs.rawValue == rhs
}

View file

@ -0,0 +1,29 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file defines the OperationObserver protocol.
*/
import Foundation
/**
The protocol that types may implement if they wish to be notified of significant
operation lifecycle events.
*/
protocol OperationObserver {
/// Invoked immediately prior to the `Operation`'s `execute()` method.
func operationDidStart(operation: Operation)
/// Invoked when `Operation.produceOperation(_:)` is executed.
func operation(operation: Operation, didProduceOperation newOperation: NSOperation)
/**
Invoked as an `Operation` finishes, along with any errors produced during
execution (or readiness evaluation).
*/
func operationDidFinish(operation: Operation, errors: [NSError])
}

View file

@ -0,0 +1,124 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file contains an NSOperationQueue subclass.
*/
import Foundation
/**
The delegate of an `OperationQueue` can respond to `Operation` lifecycle
events by implementing these methods.
In general, implementing `OperationQueueDelegate` is not necessary; you would
want to use an `OperationObserver` instead. However, there are a couple of
situations where using `OperationQueueDelegate` can lead to simpler code.
For example, `GroupOperation` is the delegate of its own internal
`OperationQueue` and uses it to manage dependencies.
*/
@objc protocol OperationQueueDelegate: NSObjectProtocol {
optional func operationQueue(operationQueue: OperationQueue, willAddOperation operation: NSOperation)
optional func operationQueue(operationQueue: OperationQueue, operationDidFinish operation: NSOperation, withErrors errors: [NSError])
}
/**
`OperationQueue` is an `NSOperationQueue` subclass that implements a large
number of "extra features" related to the `Operation` class:
- Notifying a delegate of all operation completion
- Extracting generated dependencies from operation conditions
- Setting up dependencies to enforce mutual exclusivity
*/
class OperationQueue: NSOperationQueue {
weak var delegate: OperationQueueDelegate?
override func addOperation(operation: NSOperation) {
if let op = operation as? Operation {
// Set up a `BlockObserver` to invoke the `OperationQueueDelegate` method.
let delegate = BlockObserver(
startHandler: nil,
produceHandler: { [weak self] in
self?.addOperation($1)
},
finishHandler: { [weak self] in
if let q = self {
q.delegate?.operationQueue?(q, operationDidFinish: $0, withErrors: $1)
}
}
)
op.addObserver(delegate)
// Extract any dependencies needed by this operation.
let dependencies = op.conditions.flatMap {
$0.dependencyForOperation(op)
}
for dependency in dependencies {
op.addDependency(dependency)
self.addOperation(dependency)
}
/*
With condition dependencies added, we can now see if this needs
dependencies to enforce mutual exclusivity.
*/
let concurrencyCategories: [String] = op.conditions.flatMap { condition in
if !condition.dynamicType.isMutuallyExclusive { return nil }
return "\(condition.dynamicType)"
}
if !concurrencyCategories.isEmpty {
// Set up the mutual exclusivity dependencies.
let exclusivityController = ExclusivityController.sharedExclusivityController
exclusivityController.addOperation(op, categories: concurrencyCategories)
op.addObserver(BlockObserver { operation, _ in
exclusivityController.removeOperation(operation, categories: concurrencyCategories)
})
}
/*
Indicate to the operation that we've finished our extra work on it
and it's now it a state where it can proceed with evaluating conditions,
if appropriate.
*/
op.willEnqueue()
}
else {
/*
For regular `NSOperation`s, we'll manually call out to the queue's
delegate we don't want to just capture "operation" because that
would lead to the operation strongly referencing itself and that's
the pure definition of a memory leak.
*/
operation.addCompletionBlock { [weak self, weak operation] in
guard let queue = self, let operation = operation else { return }
queue.delegate?.operationQueue?(queue, operationDidFinish: operation, withErrors: [])
}
}
delegate?.operationQueue?(self, willAddOperation: operation)
super.addOperation(operation)
}
override func addOperations(operations: [NSOperation], waitUntilFinished wait: Bool) {
/*
The base implementation of this method does not call `addOperation()`,
so we'll call it ourselves.
*/
for operation in operations {
addOperation(operation)
}
if wait {
for operation in operations {
operation.waitUntilFinished()
}
}
}
}

View file

@ -0,0 +1,43 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows an example of implementing the OperationCondition protocol.
*/
#if os(iOS)
import PassKit
/// A condition for verifying that Passbook exists and is accessible.
struct PassbookCondition: OperationCondition {
static let name = "Passbook"
static let isMutuallyExclusive = false
init() { }
func dependencyForOperation(operation: Operation) -> NSOperation? {
/*
There's nothing you can do to make Passbook available if it's not
on your device.
*/
return nil
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
if PKPassLibrary.isPassLibraryAvailable() {
completion(.Satisfied)
}
else {
let error = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name
])
completion(.Failed(error))
}
}
}
#endif

View file

@ -0,0 +1,67 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows an example of implementing the OperationCondition protocol.
*/
#if os(iOS)
import Photos
/// A condition for verifying access to the user's Photos library.
struct PhotosCondition: OperationCondition {
static let name = "Photos"
static let isMutuallyExclusive = false
init() { }
func dependencyForOperation(operation: Operation) -> NSOperation? {
return PhotosPermissionOperation()
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
switch PHPhotoLibrary.authorizationStatus() {
case .Authorized:
completion(.Satisfied)
default:
let error = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name
])
completion(.Failed(error))
}
}
}
/**
A private `Operation` that will request access to the user's Photos, if it
has not already been granted.
*/
private class PhotosPermissionOperation: Operation {
override init() {
super.init()
addCondition(AlertPresentation())
}
override func execute() {
switch PHPhotoLibrary.authorizationStatus() {
case .NotDetermined:
dispatch_async(dispatch_get_main_queue()) {
PHPhotoLibrary.requestAuthorization { status in
self.finish()
}
}
default:
finish()
}
}
}
#endif

View file

@ -0,0 +1,92 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows an example of implementing the OperationCondition protocol.
*/
import Foundation
import SystemConfiguration
/**
This is a condition that performs a very high-level reachability check.
It does *not* perform a long-running reachability check, nor does it respond to changes in reachability.
Reachability is evaluated once when the operation to which this is attached is asked about its readiness.
*/
struct ReachabilityCondition: OperationCondition {
static let hostKey = "Host"
static let name = "Reachability"
static let isMutuallyExclusive = false
let host: NSURL
init(host: NSURL) {
self.host = host
}
func dependencyForOperation(operation: Operation) -> NSOperation? {
return nil
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
ReachabilityController.requestReachability(host) { reachable in
if reachable {
completion(.Satisfied)
}
else {
let error = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name,
self.dynamicType.hostKey: self.host
])
completion(.Failed(error))
}
}
}
}
/// A private singleton that maintains a basic cache of `SCNetworkReachability` objects.
private class ReachabilityController {
static var reachabilityRefs = [String: SCNetworkReachability]()
static let reachabilityQueue = dispatch_queue_create("Operations.Reachability", DISPATCH_QUEUE_SERIAL)
static func requestReachability(url: NSURL, completionHandler: (Bool) -> Void) {
if let host = url.host {
dispatch_async(reachabilityQueue) {
var ref = self.reachabilityRefs[host]
if ref == nil {
let hostString = host as NSString
ref = SCNetworkReachabilityCreateWithName(nil, hostString.UTF8String)
}
if let ref = ref {
self.reachabilityRefs[host] = ref
var reachable = false
var flags: SCNetworkReachabilityFlags = []
if SCNetworkReachabilityGetFlags(ref, &flags) != false {
/*
Note that this is a very basic "is reachable" check.
Your app may choose to allow for other considerations,
such as whether or not the connection would require
VPN, a cellular connection, etc.
*/
reachable = flags.contains(.Reachable)
}
completionHandler(reachable)
}
else {
completionHandler(false)
}
}
}
else {
completionHandler(false)
}
}
}

View file

@ -0,0 +1,127 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows an example of implementing the OperationCondition protocol.
*/
#if os(iOS)
import UIKit
private let RemoteNotificationQueue = OperationQueue()
private let RemoteNotificationName = "RemoteNotificationPermissionNotification"
private enum RemoteRegistrationResult {
case Token(NSData)
case Error(NSError)
}
/// A condition for verifying that the app has the ability to receive push notifications.
struct RemoteNotificationCondition: OperationCondition {
static let name = "RemoteNotification"
static let isMutuallyExclusive = false
static func didReceiveNotificationToken(token: NSData) {
NSNotificationCenter.defaultCenter().postNotificationName(RemoteNotificationName, object: nil, userInfo: [
"token": token
])
}
static func didFailToRegister(error: NSError) {
NSNotificationCenter.defaultCenter().postNotificationName(RemoteNotificationName, object: nil, userInfo: [
"error": error
])
}
let application: UIApplication
init(application: UIApplication) {
self.application = application
}
func dependencyForOperation(operation: Operation) -> NSOperation? {
return RemoteNotificationPermissionOperation(application: application, handler: { _ in })
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
/*
Since evaluation requires executing an operation, use a private operation
queue.
*/
RemoteNotificationQueue.addOperation(RemoteNotificationPermissionOperation(application: application) { result in
switch result {
case .Token(_):
completion(.Satisfied)
case .Error(let underlyingError):
let error = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name,
NSUnderlyingErrorKey: underlyingError
])
completion(.Failed(error))
}
})
}
}
/**
A private `Operation` to request a push notification token from the `UIApplication`.
- note: This operation is used for *both* the generated dependency **and**
condition evaluation, since there is no "easy" way to retrieve the push
notification token other than to ask for it.
- note: This operation requires you to call either `RemoteNotificationCondition.didReceiveNotificationToken(_:)` or
`RemoteNotificationCondition.didFailToRegister(_:)` in the appropriate
`UIApplicationDelegate` method, as shown in the `AppDelegate.swift` file.
*/
private class RemoteNotificationPermissionOperation: Operation {
let application: UIApplication
private let handler: RemoteRegistrationResult -> Void
private init(application: UIApplication, handler: RemoteRegistrationResult -> Void) {
self.application = application
self.handler = handler
super.init()
/*
This operation cannot run at the same time as any other remote notification
permission operation.
*/
addCondition(MutuallyExclusive<RemoteNotificationPermissionOperation>())
}
override func execute() {
dispatch_async(dispatch_get_main_queue()) {
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: #selector(RemoteNotificationPermissionOperation.didReceiveResponse(_:)), name: RemoteNotificationName, object: nil)
self.application.registerForRemoteNotifications()
}
}
@objc func didReceiveResponse(notification: NSNotification) {
NSNotificationCenter.defaultCenter().removeObserver(self)
let userInfo = notification.userInfo
if let token = userInfo?["token"] as? NSData {
handler(.Token(token))
}
else if let error = userInfo?["error"] as? NSError {
handler(.Error(error))
}
else {
fatalError("Received a notification without a token and without an error.")
}
finish()
}
}
#endif

View file

@ -0,0 +1,40 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
The file shows how to make an OperationCondition that composes another OperationCondition.
*/
import Foundation
/**
A simple condition that causes another condition to not enqueue its dependency.
This is useful (for example) when you want to verify that you have access to
the user's location, but you do not want to prompt them for permission if you
do not already have it.
*/
struct SilentCondition<T: OperationCondition>: OperationCondition {
let condition: T
static var name: String {
return "Silent<\(T.name)>"
}
static var isMutuallyExclusive: Bool {
return T.isMutuallyExclusive
}
init(condition: T) {
self.condition = condition
}
func dependencyForOperation(operation: Operation) -> NSOperation? {
// Returning nil means we will never a dependency to be generated.
return nil
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
condition.evaluateForOperation(operation, completion: completion)
}
}

View file

@ -0,0 +1,56 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows how to implement the OperationObserver protocol.
*/
import Foundation
/**
`TimeoutObserver` is a way to make an `Operation` automatically time out and
cancel after a specified time interval.
*/
struct TimeoutObserver: OperationObserver {
// MARK: Properties
static let timeoutKey = "Timeout"
private let timeout: NSTimeInterval
// MARK: Initialization
init(timeout: NSTimeInterval) {
self.timeout = timeout
}
// MARK: OperationObserver
func operationDidStart(operation: Operation) {
// When the operation starts, queue up a block to cause it to time out.
let when = dispatch_time(DISPATCH_TIME_NOW, Int64(timeout * Double(NSEC_PER_SEC)))
dispatch_after(when, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)) {
/*
Cancel the operation if it hasn't finished and hasn't already
been cancelled.
*/
if !operation.finished && !operation.cancelled {
let error = NSError(code: .ExecutionFailed, userInfo: [
self.dynamicType.timeoutKey: self.timeout
])
operation.cancelWithError(error)
}
}
}
func operation(operation: Operation, didProduceOperation newOperation: NSOperation) {
// No op.
}
func operationDidFinish(operation: Operation, errors: [NSError]) {
// No op.
}
}

View file

@ -0,0 +1,49 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
A convenient extension to UIKit.UIUserNotificationSettings.
*/
#if os(iOS)
import UIKit
extension UIUserNotificationSettings {
/// Check to see if one Settings object is a superset of another Settings object.
func contains(settings: UIUserNotificationSettings) -> Bool {
// our types must contain all of the other types
if !types.contains(settings.types) {
return false
}
let otherCategories = settings.categories ?? []
let myCategories = categories ?? []
return myCategories.isSupersetOf(otherCategories)
}
/**
Merge two Settings objects together. `UIUserNotificationCategories` with
the same identifier are considered equal.
*/
func settingsByMerging(settings: UIUserNotificationSettings) -> UIUserNotificationSettings {
let mergedTypes = types.union(settings.types)
let myCategories = categories ?? []
var existingCategoriesByIdentifier = Dictionary(sequence: myCategories) { $0.identifier }
let newCategories = settings.categories ?? []
let newCategoriesByIdentifier = Dictionary(sequence: newCategories) { $0.identifier }
for (newIdentifier, newCategory) in newCategoriesByIdentifier {
existingCategoriesByIdentifier[newIdentifier] = newCategory
}
let mergedCategories = Set(existingCategoriesByIdentifier.values)
return UIUserNotificationSettings(forTypes: mergedTypes, categories: mergedCategories)
}
}
#endif

View file

@ -0,0 +1,54 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Shows how to lift operation-like objects in to the NSOperation world.
*/
import Foundation
private var URLSessionTaksOperationKVOContext = 0
/**
`URLSessionTaskOperation` is an `Operation` that lifts an `NSURLSessionTask`
into an operation.
Note that this operation does not participate in any of the delegate callbacks \
of an `NSURLSession`, but instead uses Key-Value-Observing to know when the
task has been completed. It also does not get notified about any errors that
occurred during execution of the task.
An example usage of `URLSessionTaskOperation` can be seen in the `DownloadEarthquakesOperation`.
*/
class URLSessionTaskOperation: Operation {
let task: NSURLSessionTask
init(task: NSURLSessionTask) {
assert(task.state == .Suspended, "Tasks must be suspended.")
self.task = task
super.init()
}
override func execute() {
assert(task.state == .Suspended, "Task was resumed by something other than \(self).")
task.addObserver(self, forKeyPath: "state", options: [], context: &URLSessionTaksOperationKVOContext)
task.resume()
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
guard context == &URLSessionTaksOperationKVOContext else { return }
if object === task && keyPath == "state" && task.state == .Completed {
task.removeObserver(self, forKeyPath: "state")
finish()
}
}
override func cancel() {
task.cancel()
super.cancel()
}
}

View file

@ -0,0 +1,122 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
This file shows an example of implementing the OperationCondition protocol.
*/
#if os(iOS)
import UIKit
/**
A condition for verifying that we can present alerts to the user via
`UILocalNotification` and/or remote notifications.
*/
struct UserNotificationCondition: OperationCondition {
enum Behavior {
/// Merge the new `UIUserNotificationSettings` with the `currentUserNotificationSettings`.
case Merge
/// Replace the `currentUserNotificationSettings` with the new `UIUserNotificationSettings`.
case Replace
}
static let name = "UserNotification"
static let currentSettings = "CurrentUserNotificationSettings"
static let desiredSettings = "DesiredUserNotificationSettigns"
static let isMutuallyExclusive = false
let settings: UIUserNotificationSettings
let application: UIApplication
let behavior: Behavior
/**
The designated initializer.
- parameter settings: The `UIUserNotificationSettings` you wish to be
registered.
- parameter application: The `UIApplication` on which the `settings` should
be registered.
- parameter behavior: The way in which the `settings` should be applied
to the `application`. By default, this value is `.Merge`, which means
that the `settings` will be combined with the existing settings on the
`application`. You may also specify `.Replace`, which means the `settings`
will overwrite the exisiting settings.
*/
init(settings: UIUserNotificationSettings, application: UIApplication, behavior: Behavior = .Merge) {
self.settings = settings
self.application = application
self.behavior = behavior
}
func dependencyForOperation(operation: Operation) -> NSOperation? {
return UserNotificationPermissionOperation(settings: settings, application: application, behavior: behavior)
}
func evaluateForOperation(operation: Operation, completion: OperationConditionResult -> Void) {
let result: OperationConditionResult
let current = application.currentUserNotificationSettings()
switch (current, settings) {
case (let current?, let settings) where current.contains(settings):
result = .Satisfied
default:
let error = NSError(code: .ConditionFailed, userInfo: [
OperationConditionKey: self.dynamicType.name,
self.dynamicType.currentSettings: current ?? NSNull(),
self.dynamicType.desiredSettings: settings
])
result = .Failed(error)
}
completion(result)
}
}
/**
A private `Operation` subclass to register a `UIUserNotificationSettings`
object with a `UIApplication`, prompting the user for permission if necessary.
*/
private class UserNotificationPermissionOperation: Operation {
let settings: UIUserNotificationSettings
let application: UIApplication
let behavior: UserNotificationCondition.Behavior
init(settings: UIUserNotificationSettings, application: UIApplication, behavior: UserNotificationCondition.Behavior) {
self.settings = settings
self.application = application
self.behavior = behavior
super.init()
addCondition(AlertPresentation())
}
override func execute() {
dispatch_async(dispatch_get_main_queue()) {
let current = self.application.currentUserNotificationSettings()
let settingsToRegister: UIUserNotificationSettings
switch (current, self.behavior) {
case (let currentSettings?, .Merge):
settingsToRegister = currentSettings.settingsByMerging(self.settings)
default:
settingsToRegister = self.settings
}
self.application.registerUserNotificationSettings(settingsToRegister)
}
}
}
#endif

View file

@ -0,0 +1,168 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
Contains the logic to parse a JSON file of earthquakes and insert them into an NSManagedObjectContext
*/
import Foundation
import CoreData
/// A struct to represent a parsed earthquake.
private struct ParsedEarthquake {
// MARK: Properties.
let date: NSDate
let identifier, name, link: String
let depth, latitude, longitude, magnitude: Double
// MARK: Initialization
init?(feature: [String: AnyObject]) {
guard let earthquakeID = feature["id"] as? String where !earthquakeID.isEmpty else { return nil }
identifier = earthquakeID
let properties = feature["properties"] as? [String: AnyObject] ?? [:]
name = properties["place"] as? String ?? ""
link = properties["url"] as? String ?? ""
magnitude = properties["mag"] as? Double ?? 0.0
if let offset = properties["time"] as? Double {
date = NSDate(timeIntervalSince1970: offset / 1000)
}
else {
date = NSDate.distantFuture()
}
let geometry = feature["geometry"] as? [String: AnyObject] ?? [:]
if let coordinates = geometry["coordinates"] as? [Double] where coordinates.count == 3 {
longitude = coordinates[0]
latitude = coordinates[1]
// `depth` is in km, but we want to store it in meters.
depth = coordinates[2] * 1000
}
else {
depth = 0
latitude = 0
longitude = 0
}
}
}
/// An `Operation` to parse earthquakes out of a downloaded feed from the USGS.
class ParseEarthquakesOperation: Operation {
let cacheFile: NSURL
let context: NSManagedObjectContext
/**
- parameter cacheFile: The file `NSURL` from which to load earthquake data.
- parameter context: The `NSManagedObjectContext` that will be used as the
basis for importing data. The operation will internally
construct a new `NSManagedObjectContext` that points
to the same `NSPersistentStoreCoordinator` as the
passed-in context.
*/
init(cacheFile: NSURL, context: NSManagedObjectContext) {
let importContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
importContext.persistentStoreCoordinator = context.persistentStoreCoordinator
/*
Use the overwrite merge policy, because we want any updated objects
to replace the ones in the store.
*/
importContext.mergePolicy = NSOverwriteMergePolicy
self.cacheFile = cacheFile
self.context = importContext
super.init()
name = "Parse Earthquakes"
}
override func execute() {
guard let stream = NSInputStream(URL: cacheFile) else {
finish()
return
}
stream.open()
defer {
stream.close()
}
do {
let json = try NSJSONSerialization.JSONObjectWithStream(stream, options: []) as? [String: AnyObject]
if let features = json?["features"] as? [[String: AnyObject]] {
parse(features)
}
else {
finish()
}
}
catch let jsonError as NSError {
finishWithError(jsonError)
}
}
private func parse(features: [[String: AnyObject]]) {
let parsedEarthquakes = features.flatMap { ParsedEarthquake(feature: $0) }
context.performBlock {
for newEarthquake in parsedEarthquakes {
self.insert(newEarthquake)
}
let error = self.saveContext()
self.finishWithError(error)
}
}
private func insert(parsed: ParsedEarthquake) {
let earthquake = NSEntityDescription.insertNewObjectForEntityForName(Earthquake.entityName, inManagedObjectContext: context) as! Earthquake
earthquake.identifier = parsed.identifier
earthquake.timestamp = parsed.date
earthquake.latitude = parsed.latitude
earthquake.longitude = parsed.longitude
earthquake.depth = parsed.depth
earthquake.webLink = parsed.link
earthquake.name = parsed.name
earthquake.magnitude = parsed.magnitude
}
/**
Save the context, if there are any changes.
- returns: An `NSError` if there was an problem saving the `NSManagedObjectContext`,
otherwise `nil`.
- note: This method returns an `NSError?` because it will be immediately
passed to the `finishWithError()` method, which accepts an `NSError?`.
*/
private func saveContext() -> NSError? {
var error: NSError?
if context.hasChanges {
do {
try context.save()
}
catch let saveError as NSError {
error = saveError
}
}
return error
}
}

View file

@ -0,0 +1,30 @@
/*
Copyright (C) 2015 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples licensing information
Abstract:
A UISplitViewController subclass that is its own delegate.
*/
import UIKit
class SplitViewController: UISplitViewController {
// MARK: Life Cycle
override func awakeFromNib() {
super.awakeFromNib()
preferredDisplayMode = .AllVisible
delegate = self
}
}
extension SplitViewController: UISplitViewControllerDelegate {
func splitViewController(splitViewController: UISplitViewController, collapseSecondaryViewController secondaryViewController: UIViewController, ontoPrimaryViewController primaryViewController: UIViewController) -> Bool {
guard let navigation = secondaryViewController as? UINavigationController else { return false }
guard let detail = navigation.viewControllers.first as? EarthquakeTableViewController else { return false }
return detail.earthquake == nil
}
}

42
LICENSE.txt Normal file
View file

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

14
README.md Normal file
View file

@ -0,0 +1,14 @@
# Advanced NSOperations
This shows how to use NSOperations to simplify app architecture. It includes several different kinds of ready-to-use NSOperation subclasses to guarantee that your code will only execute if certain conditions have been met. By composing and chaining these operations together, you can quickly construct complex behaviors with extremely little code.
## Requirements
### Build
* XCode 7.3
* Swift 2.2
### Runtime
* iOS 9