commit 8b89ba572e5991fb5e5dee5ae02936d67415065e Author: Sami Samhuri Date: Wed Feb 16 22:16:09 2022 -0800 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4f83d9 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Advanced NSOperations.xcodeproj/project.pbxproj b/Advanced NSOperations.xcodeproj/project.pbxproj new file mode 100644 index 0000000..5860e80 --- /dev/null +++ b/Advanced NSOperations.xcodeproj/project.pbxproj @@ -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 = ""; }; + 551344B81B016CA7004A1569 /* Earthquakes.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Earthquakes.entitlements; sourceTree = ""; }; + 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 = ""; }; + 551344C11B02D34A004A1569 /* UIUserNotifications+Operations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIUserNotifications+Operations.swift"; sourceTree = ""; }; + 551344C31B02D7BA004A1569 /* Dictionary+Operations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Operations.swift"; sourceTree = ""; }; + 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 = ""; }; + 551B9BE21AEB1C9700302388 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 551B9BE71AEB1C9700302388 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 551B9BE91AEB1C9700302388 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 551B9BEC1AEB1C9700302388 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 551B9C021AEB1CA900302388 /* Operation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = ""; }; + 551B9C041AEB1CC800302388 /* OperationCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationCondition.swift; sourceTree = ""; }; + 551B9C061AEB2D7800302388 /* OperationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationQueue.swift; sourceTree = ""; }; + 551B9C0B1AEBE4F300302388 /* CloudCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudCondition.swift; sourceTree = ""; }; + 551B9C0D1AEBE52800302388 /* ReachabilityCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityCondition.swift; sourceTree = ""; }; + 551B9C101AEBE54D00302388 /* PassbookCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PassbookCondition.swift; sourceTree = ""; }; + 5521B4811B700C87007089CE /* NSLock+Operations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLock+Operations.swift"; sourceTree = ""; }; + 553C5AC41AF6C774002FC47A /* PhotosCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosCondition.swift; sourceTree = ""; }; + 553C5AC61AF6D2EB002FC47A /* HealthCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HealthCondition.swift; sourceTree = ""; }; + 553F2D651B00041100BF4093 /* OperationErrors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationErrors.swift; sourceTree = ""; }; + 553F50031B07FB5E005F991E /* LocationOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationOperation.swift; sourceTree = ""; }; + 553F50051B081760005F991E /* OperationObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationObserver.swift; sourceTree = ""; }; + 553F50081B0817D2005F991E /* BlockObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockObserver.swift; sourceTree = ""; }; + 553F500E1B081A9D005F991E /* NetworkObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = ""; }; + 553F50101B082BCF005F991E /* BackgroundObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundObserver.swift; sourceTree = ""; }; + 553F50131B0832D2005F991E /* DelayOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DelayOperation.swift; sourceTree = ""; }; + 553F50151B08E98A005F991E /* LoadModelOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadModelOperation.swift; sourceTree = ""; }; + 55727FAB1AF216C800EC6916 /* CalendarCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalendarCondition.swift; sourceTree = ""; }; + 55727FAF1AF276A900EC6916 /* RemoteNotificationCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteNotificationCondition.swift; sourceTree = ""; }; + 55727FB11AF2798C00EC6916 /* GroupOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupOperation.swift; sourceTree = ""; }; + 55727FB31AF27BDB00EC6916 /* NSOperation+Operations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSOperation+Operations.swift"; sourceTree = ""; }; + 55727FB91AF2849E00EC6916 /* URLSessionTaskOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionTaskOperation.swift; sourceTree = ""; }; + 55857B311AF203C100219D5A /* LocationCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationCondition.swift; sourceTree = ""; }; + 55857B331AF2055600219D5A /* SilentCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SilentCondition.swift; sourceTree = ""; }; + 55857B351AF2077700219D5A /* CKContainer+Operations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CKContainer+Operations.swift"; sourceTree = ""; }; + 55857B381AF20CC700219D5A /* MutuallyExclusive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutuallyExclusive.swift; sourceTree = ""; }; + 55857B3A1AF20DE800219D5A /* BlockOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockOperation.swift; sourceTree = ""; }; + 55857B3E1AF2116600219D5A /* NoCancelledDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoCancelledDependencies.swift; sourceTree = ""; }; + 55AD643C1B128CC3000EF5CB /* TimeoutObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeoutObserver.swift; sourceTree = ""; }; + 55CD4D201AF5C05300E3A9E3 /* ExclusivityController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExclusivityController.swift; sourceTree = ""; }; + 55CD4D261AF6709400E3A9E3 /* UserNotificationCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotificationCondition.swift; sourceTree = ""; }; + 55E7021C1AFC38920032742F /* EarthquakeTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EarthquakeTableViewController.swift; sourceTree = ""; }; + 55E7021E1AFD15C80032742F /* SplitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplitViewController.swift; sourceTree = ""; }; + 55E702211AFE587C0032742F /* Earthquakes.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Earthquakes.xcdatamodel; sourceTree = ""; }; + 55E702231AFE58EF0032742F /* GetEarthquakesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetEarthquakesOperation.swift; sourceTree = ""; }; + 55E702251AFE59610032742F /* EarthquakesTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EarthquakesTableViewController.swift; sourceTree = ""; }; + 55E702641AFE5E590032742F /* EarthquakeTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EarthquakeTableViewCell.swift; sourceTree = ""; }; + 55E702671AFE61700032742F /* Earthquake.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Earthquake.swift; sourceTree = ""; }; + 55E7026A1AFE63E70032742F /* DownloadEarthquakesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadEarthquakesOperation.swift; sourceTree = ""; }; + 55E7026C1AFE64D40032742F /* ParseEarthquakesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseEarthquakesOperation.swift; sourceTree = ""; }; + 55E702721AFE95C00032742F /* NegatedCondition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NegatedCondition.swift; sourceTree = ""; }; +/* 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 = ""; + }; + 551B9BDE1AEB1C9700302388 /* Products */ = { + isa = PBXGroup; + children = ( + 551B9BDD1AEB1C9700302388 /* Earthquakes.app */, + ); + name = Products; + sourceTree = ""; + }; + 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 = ""; + }; + 551B9BE01AEB1C9700302388 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 551B9BEB1AEB1C9700302388 /* LaunchScreen.xib */, + 551B9BE61AEB1C9700302388 /* Main.storyboard */, + 551B9BE91AEB1C9700302388 /* Images.xcassets */, + 551B9BE11AEB1C9700302388 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 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 = ""; + }; + 553F2D621AFFED5300BF4093 /* Operation Queue */ = { + isa = PBXGroup; + children = ( + 551B9C061AEB2D7800302388 /* OperationQueue.swift */, + 55CD4D201AF5C05300E3A9E3 /* ExclusivityController.swift */, + ); + name = "Operation Queue"; + sourceTree = ""; + }; + 553F2D631B00025200BF4093 /* Model */ = { + isa = PBXGroup; + children = ( + 55E702201AFE587C0032742F /* Earthquakes.xcdatamodeld */, + 55E702671AFE61700032742F /* Earthquake.swift */, + ); + name = Model; + sourceTree = ""; + }; + 553F50071B0817BB005F991E /* Observers */ = { + isa = PBXGroup; + children = ( + 553F50051B081760005F991E /* OperationObserver.swift */, + 553F50081B0817D2005F991E /* BlockObserver.swift */, + 55AD643C1B128CC3000EF5CB /* TimeoutObserver.swift */, + ); + name = Observers; + sourceTree = ""; + }; + 553F50121B082D9E005F991E /* Frameworks */ = { + isa = PBXGroup; + children = ( + 551344B91B016CAF004A1569 /* CloudKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 553F50171B08EBBC005F991E /* Earthquake List */ = { + isa = PBXGroup; + children = ( + 55E702251AFE59610032742F /* EarthquakesTableViewController.swift */, + 55E702641AFE5E590032742F /* EarthquakeTableViewCell.swift */, + ); + name = "Earthquake List"; + sourceTree = ""; + }; + 553F50181B08EBC3005F991E /* Earthquake Detail */ = { + isa = PBXGroup; + children = ( + 55E7021C1AFC38920032742F /* EarthquakeTableViewController.swift */, + ); + name = "Earthquake Detail"; + sourceTree = ""; + }; + 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 = ""; + }; + 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 = ""; + }; + 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 = ""; + }; + 55E702691AFE63C50032742F /* Operations */ = { + isa = PBXGroup; + children = ( + 553F2D621AFFED5300BF4093 /* Operation Queue */, + 55857B301AF1E59900219D5A /* Operations */, + 553F50071B0817BB005F991E /* Observers */, + 551B9C0F1AEBE53400302388 /* Conditions */, + 55857B371AF2077A00219D5A /* Convenience Extensions */, + ); + path = Operations; + sourceTree = ""; + }; +/* 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 = ""; + }; + 551B9BEB1AEB1C9700302388 /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + 551B9BEC1AEB1C9700302388 /* Base */, + ); + name = LaunchScreen.xib; + sourceTree = ""; + }; +/* 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 = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 551B9BD51AEB1C9700302388 /* Project object */; +} diff --git a/Advanced NSOperations.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Advanced NSOperations.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Advanced NSOperations.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Advanced NSOperations.xcodeproj/xcshareddata/xcschemes/Earthquakes.xcscheme b/Advanced NSOperations.xcodeproj/xcshareddata/xcschemes/Earthquakes.xcscheme new file mode 100644 index 0000000..99bfee7 --- /dev/null +++ b/Advanced NSOperations.xcodeproj/xcshareddata/xcschemes/Earthquakes.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Earthquakes/AlertOperation.swift b/Earthquakes/AlertOperation.swift new file mode 100644 index 0000000..6932fcd --- /dev/null +++ b/Earthquakes/AlertOperation.swift @@ -0,0 +1,82 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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()) + } + + 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) + } + } +} diff --git a/Earthquakes/AppDelegate.swift b/Earthquakes/AppDelegate.swift new file mode 100644 index 0000000..4ae0e0f --- /dev/null +++ b/Earthquakes/AppDelegate.swift @@ -0,0 +1,26 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) + } +} diff --git a/Earthquakes/BackgroundObserver.swift b/Earthquakes/BackgroundObserver.swift new file mode 100644 index 0000000..c2d9093 --- /dev/null +++ b/Earthquakes/BackgroundObserver.swift @@ -0,0 +1,82 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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() + } +} diff --git a/Earthquakes/Base.lproj/LaunchScreen.xib b/Earthquakes/Base.lproj/LaunchScreen.xib new file mode 100644 index 0000000..a0e045b --- /dev/null +++ b/Earthquakes/Base.lproj/LaunchScreen.xib @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Earthquakes/Base.lproj/Main.storyboard b/Earthquakes/Base.lproj/Main.storyboard new file mode 100644 index 0000000..a5e6462 --- /dev/null +++ b/Earthquakes/Base.lproj/Main.storyboard @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Earthquakes/DownloadEarthquakesOperation.swift b/Earthquakes/DownloadEarthquakesOperation.swift new file mode 100644 index 0000000..cadd532 --- /dev/null +++ b/Earthquakes/DownloadEarthquakesOperation.swift @@ -0,0 +1,74 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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. + } + } +} diff --git a/Earthquakes/Earthquake.swift b/Earthquakes/Earthquake.swift new file mode 100644 index 0000000..ed56520 --- /dev/null +++ b/Earthquakes/Earthquake.swift @@ -0,0 +1,78 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) + } +} diff --git a/Earthquakes/EarthquakeTableViewCell.swift b/Earthquakes/EarthquakeTableViewCell.swift new file mode 100644 index 0000000..77316ee --- /dev/null +++ b/Earthquakes/EarthquakeTableViewCell.swift @@ -0,0 +1,41 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) + } + +} diff --git a/Earthquakes/EarthquakeTableViewController.swift b/Earthquakes/EarthquakeTableViewController.swift new file mode 100644 index 0000000..eb91c02 --- /dev/null +++ b/Earthquakes/EarthquakeTableViewController.swift @@ -0,0 +1,162 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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()) + + 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 + } +} diff --git a/Earthquakes/Earthquakes.entitlements b/Earthquakes/Earthquakes.entitlements new file mode 100644 index 0000000..acd9801 --- /dev/null +++ b/Earthquakes/Earthquakes.entitlements @@ -0,0 +1,16 @@ + + + + + com.apple.developer.icloud-container-identifiers + + iCloud.$(CFBundleIdentifier) + + com.apple.developer.icloud-services + + CloudKit + + com.apple.developer.ubiquity-kvstore-identifier + $(TeamIdentifierPrefix)$(CFBundleIdentifier) + + diff --git a/Earthquakes/Earthquakes.xcdatamodeld/Earthquakes.xcdatamodel/contents b/Earthquakes/Earthquakes.xcdatamodeld/Earthquakes.xcdatamodel/contents new file mode 100644 index 0000000..bb05dd1 --- /dev/null +++ b/Earthquakes/Earthquakes.xcdatamodeld/Earthquakes.xcdatamodel/contents @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Earthquakes/EarthquakesTableViewController.swift b/Earthquakes/EarthquakesTableViewController.swift new file mode 100644 index 0000000..99c0030 --- /dev/null +++ b/Earthquakes/EarthquakesTableViewController.swift @@ -0,0 +1,158 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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()) + + 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() + } + +} diff --git a/Earthquakes/GetEarthquakesOperation.swift b/Earthquakes/GetEarthquakesOperation.swift new file mode 100644 index 0000000..ba56c77 --- /dev/null +++ b/Earthquakes/GetEarthquakesOperation.swift @@ -0,0 +1,104 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 +} diff --git a/Earthquakes/Images.xcassets/2.0.imageset/2.0.png b/Earthquakes/Images.xcassets/2.0.imageset/2.0.png new file mode 100644 index 0000000..844d0eb Binary files /dev/null and b/Earthquakes/Images.xcassets/2.0.imageset/2.0.png differ diff --git a/Earthquakes/Images.xcassets/2.0.imageset/2.0@2x.png b/Earthquakes/Images.xcassets/2.0.imageset/2.0@2x.png new file mode 100644 index 0000000..c5f181b Binary files /dev/null and b/Earthquakes/Images.xcassets/2.0.imageset/2.0@2x.png differ diff --git a/Earthquakes/Images.xcassets/2.0.imageset/2.0@3x.png b/Earthquakes/Images.xcassets/2.0.imageset/2.0@3x.png new file mode 100644 index 0000000..feed35f Binary files /dev/null and b/Earthquakes/Images.xcassets/2.0.imageset/2.0@3x.png differ diff --git a/Earthquakes/Images.xcassets/2.0.imageset/Contents.json b/Earthquakes/Images.xcassets/2.0.imageset/Contents.json new file mode 100644 index 0000000..c24e1fd --- /dev/null +++ b/Earthquakes/Images.xcassets/2.0.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Earthquakes/Images.xcassets/3.0.imageset/3.0.png b/Earthquakes/Images.xcassets/3.0.imageset/3.0.png new file mode 100644 index 0000000..9607993 Binary files /dev/null and b/Earthquakes/Images.xcassets/3.0.imageset/3.0.png differ diff --git a/Earthquakes/Images.xcassets/3.0.imageset/3.0@2x.png b/Earthquakes/Images.xcassets/3.0.imageset/3.0@2x.png new file mode 100644 index 0000000..da312a3 Binary files /dev/null and b/Earthquakes/Images.xcassets/3.0.imageset/3.0@2x.png differ diff --git a/Earthquakes/Images.xcassets/3.0.imageset/3.0@3x.png b/Earthquakes/Images.xcassets/3.0.imageset/3.0@3x.png new file mode 100644 index 0000000..adcc90d Binary files /dev/null and b/Earthquakes/Images.xcassets/3.0.imageset/3.0@3x.png differ diff --git a/Earthquakes/Images.xcassets/3.0.imageset/Contents.json b/Earthquakes/Images.xcassets/3.0.imageset/Contents.json new file mode 100644 index 0000000..d157ec9 --- /dev/null +++ b/Earthquakes/Images.xcassets/3.0.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Earthquakes/Images.xcassets/4.0.imageset/4.0.png b/Earthquakes/Images.xcassets/4.0.imageset/4.0.png new file mode 100644 index 0000000..7e78851 Binary files /dev/null and b/Earthquakes/Images.xcassets/4.0.imageset/4.0.png differ diff --git a/Earthquakes/Images.xcassets/4.0.imageset/4.0@2x.png b/Earthquakes/Images.xcassets/4.0.imageset/4.0@2x.png new file mode 100644 index 0000000..49e259c Binary files /dev/null and b/Earthquakes/Images.xcassets/4.0.imageset/4.0@2x.png differ diff --git a/Earthquakes/Images.xcassets/4.0.imageset/4.0@3x.png b/Earthquakes/Images.xcassets/4.0.imageset/4.0@3x.png new file mode 100644 index 0000000..62e8f91 Binary files /dev/null and b/Earthquakes/Images.xcassets/4.0.imageset/4.0@3x.png differ diff --git a/Earthquakes/Images.xcassets/4.0.imageset/Contents.json b/Earthquakes/Images.xcassets/4.0.imageset/Contents.json new file mode 100644 index 0000000..8b731a3 --- /dev/null +++ b/Earthquakes/Images.xcassets/4.0.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Earthquakes/Images.xcassets/5.0.imageset/5.0.png b/Earthquakes/Images.xcassets/5.0.imageset/5.0.png new file mode 100644 index 0000000..c4af8e9 Binary files /dev/null and b/Earthquakes/Images.xcassets/5.0.imageset/5.0.png differ diff --git a/Earthquakes/Images.xcassets/5.0.imageset/5.0@2x.png b/Earthquakes/Images.xcassets/5.0.imageset/5.0@2x.png new file mode 100644 index 0000000..27be65c Binary files /dev/null and b/Earthquakes/Images.xcassets/5.0.imageset/5.0@2x.png differ diff --git a/Earthquakes/Images.xcassets/5.0.imageset/5.0@3x.png b/Earthquakes/Images.xcassets/5.0.imageset/5.0@3x.png new file mode 100644 index 0000000..78ef2d3 Binary files /dev/null and b/Earthquakes/Images.xcassets/5.0.imageset/5.0@3x.png differ diff --git a/Earthquakes/Images.xcassets/5.0.imageset/Contents.json b/Earthquakes/Images.xcassets/5.0.imageset/Contents.json new file mode 100644 index 0000000..e8bfd32 --- /dev/null +++ b/Earthquakes/Images.xcassets/5.0.imageset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/29.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..9610fb2 Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/29.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/29@2x-1.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/29@2x-1.png new file mode 100644 index 0000000..400283e Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/29@2x-1.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/29@2x-2.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/29@2x-2.png new file mode 100644 index 0000000..400283e Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/29@2x-2.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/29@3x.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/29@3x.png new file mode 100644 index 0000000..382b57f Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/29@3x.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/40.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..538c881 Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/40.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/40@2x-1.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/40@2x-1.png new file mode 100644 index 0000000..0b8cb2d Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/40@2x-1.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/40@2x-2.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/40@2x-2.png new file mode 100644 index 0000000..0b8cb2d Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/40@2x-2.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/40@3x.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/40@3x.png new file mode 100644 index 0000000..893d9bb Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/40@3x.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/60@2x.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/60@2x.png new file mode 100644 index 0000000..893d9bb Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/60@2x.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/60@3x.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/60@3x.png new file mode 100644 index 0000000..17ff4fd Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/60@3x.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/76.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000..74cfb9b Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/76.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/76@2x.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/76@2x.png new file mode 100644 index 0000000..aefd6a1 Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/76@2x.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/83.5@2x.png b/Earthquakes/Images.xcassets/AppIcon.appiconset/83.5@2x.png new file mode 100644 index 0000000..02d7e4d Binary files /dev/null and b/Earthquakes/Images.xcassets/AppIcon.appiconset/83.5@2x.png differ diff --git a/Earthquakes/Images.xcassets/AppIcon.appiconset/Contents.json b/Earthquakes/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..e44f4ce --- /dev/null +++ b/Earthquakes/Images.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} \ No newline at end of file diff --git a/Earthquakes/Info.plist b/Earthquakes/Info.plist new file mode 100644 index 0000000..feb50e9 --- /dev/null +++ b/Earthquakes/Info.plist @@ -0,0 +1,55 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocationAlwaysUsageDescription + Earthquakes uses your location to show how close you are to the epicenter of an earthquake. + NSLocationWhenInUseUsageDescription + Earthquakes uses your location to show how close you are to the epicenter of an earthquake. + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSAppTransportSecurity + + NSExceptionDomains + + earthquake.usgs.gov + + NSTemporaryExceptionAllowsInsecureHTTPLoads + + + + + + diff --git a/Earthquakes/LoadModelOperation.swift b/Earthquakes/LoadModelOperation.swift new file mode 100644 index 0000000..b81c8c0 --- /dev/null +++ b/Earthquakes/LoadModelOperation.swift @@ -0,0 +1,136 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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()) + } + + 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) + } +} diff --git a/Earthquakes/MoreInformationOperation.swift b/Earthquakes/MoreInformationOperation.swift new file mode 100644 index 0000000..265250b --- /dev/null +++ b/Earthquakes/MoreInformationOperation.swift @@ -0,0 +1,54 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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()) + } + + // 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() + } + } +} diff --git a/Earthquakes/NetworkObserver.swift b/Earthquakes/NetworkObserver.swift new file mode 100644 index 0000000..fbd2f19 --- /dev/null +++ b/Earthquakes/NetworkObserver.swift @@ -0,0 +1,116 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 + } +} \ No newline at end of file diff --git a/Earthquakes/Operations/BlockObserver.swift b/Earthquakes/Operations/BlockObserver.swift new file mode 100644 index 0000000..f09b4b3 --- /dev/null +++ b/Earthquakes/Operations/BlockObserver.swift @@ -0,0 +1,41 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) + } +} diff --git a/Earthquakes/Operations/BlockOperation.swift b/Earthquakes/Operations/BlockOperation.swift new file mode 100644 index 0000000..4a7f015 --- /dev/null +++ b/Earthquakes/Operations/BlockOperation.swift @@ -0,0 +1,59 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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() + } + } +} diff --git a/Earthquakes/Operations/CKContainer+Operations.swift b/Earthquakes/Operations/CKContainer+Operations.swift new file mode 100644 index 0000000..52079c7 --- /dev/null +++ b/Earthquakes/Operations/CKContainer+Operations.swift @@ -0,0 +1,80 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) + } + } + } +} diff --git a/Earthquakes/Operations/CalendarCondition.swift b/Earthquakes/Operations/CalendarCondition.swift new file mode 100644 index 0000000..ee3df1a --- /dev/null +++ b/Earthquakes/Operations/CalendarCondition.swift @@ -0,0 +1,81 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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() + } + } + +} diff --git a/Earthquakes/Operations/CloudCondition.swift b/Earthquakes/Operations/CloudCondition.swift new file mode 100644 index 0000000..5c303cc --- /dev/null +++ b/Earthquakes/Operations/CloudCondition.swift @@ -0,0 +1,90 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) + } + } + +} diff --git a/Earthquakes/Operations/DelayOperation.swift b/Earthquakes/Operations/DelayOperation.swift new file mode 100644 index 0000000..4c85cfe --- /dev/null +++ b/Earthquakes/Operations/DelayOperation.swift @@ -0,0 +1,78 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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() + } +} diff --git a/Earthquakes/Operations/Dictionary+Operations.swift b/Earthquakes/Operations/Dictionary+Operations.swift new file mode 100644 index 0000000..c092727 --- /dev/null +++ b/Earthquakes/Operations/Dictionary+Operations.swift @@ -0,0 +1,31 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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: Sequence, @noescape keyMapper: Value -> Key?) { + self.init() + + for item in sequence { + if let key = keyMapper(item) { + self[key] = item + } + } + } +} diff --git a/Earthquakes/Operations/ExclusivityController.swift b/Earthquakes/Operations/ExclusivityController.swift new file mode 100644 index 0000000..cf96a90 --- /dev/null +++ b/Earthquakes/Operations/ExclusivityController.swift @@ -0,0 +1,79 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 + } + } + +} diff --git a/Earthquakes/Operations/GroupOperation.swift b/Earthquakes/Operations/GroupOperation.swift new file mode 100644 index 0000000..a304fc6 --- /dev/null +++ b/Earthquakes/Operations/GroupOperation.swift @@ -0,0 +1,111 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) + } + } +} diff --git a/Earthquakes/Operations/HealthCondition.swift b/Earthquakes/Operations/HealthCondition.swift new file mode 100644 index 0000000..4d29ecb --- /dev/null +++ b/Earthquakes/Operations/HealthCondition.swift @@ -0,0 +1,129 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 + let readTypes: Set + + /** + 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, typesToRead: Set) { + 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, 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 + let readTypes: Set + + init(shareTypes: Set, readTypes: Set) { + self.shareTypes = shareTypes + self.readTypes = readTypes + + super.init() + + addCondition(MutuallyExclusive()) + addCondition(MutuallyExclusive()) + 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 diff --git a/Earthquakes/Operations/LocationCondition.swift b/Earthquakes/Operations/LocationCondition.swift new file mode 100644 index 0000000..97ddbfd --- /dev/null +++ b/Earthquakes/Operations/LocationCondition.swift @@ -0,0 +1,143 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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() + } + } +} diff --git a/Earthquakes/Operations/LocationOperation.swift b/Earthquakes/Operations/LocationOperation.swift new file mode 100644 index 0000000..41d9807 --- /dev/null +++ b/Earthquakes/Operations/LocationOperation.swift @@ -0,0 +1,78 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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()) + } + + 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) + } +} diff --git a/Earthquakes/Operations/MutuallyExclusive.swift b/Earthquakes/Operations/MutuallyExclusive.swift new file mode 100644 index 0000000..3300eab --- /dev/null +++ b/Earthquakes/Operations/MutuallyExclusive.swift @@ -0,0 +1,39 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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: 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`. +*/ +enum Alert { } + +/// A condition describing that the targeted operation may present an alert. +typealias AlertPresentation = MutuallyExclusive diff --git a/Earthquakes/Operations/NSLock+Operations.swift b/Earthquakes/Operations/NSLock+Operations.swift new file mode 100644 index 0000000..ef7146f --- /dev/null +++ b/Earthquakes/Operations/NSLock+Operations.swift @@ -0,0 +1,18 @@ +/* + Copyright (C) 2015 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + An extension to NSLock to simplify executing critical code. +*/ + +import Foundation + +extension NSLock { + func withCriticalScope(@noescape block: Void -> T) -> T { + lock() + let value = block() + unlock() + return value + } +} \ No newline at end of file diff --git a/Earthquakes/Operations/NSOperation+Operations.swift b/Earthquakes/Operations/NSOperation+Operations.swift new file mode 100644 index 0000000..33e661b --- /dev/null +++ b/Earthquakes/Operations/NSOperation+Operations.swift @@ -0,0 +1,38 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) + } + } +} diff --git a/Earthquakes/Operations/NegatedCondition.swift b/Earthquakes/Operations/NegatedCondition.swift new file mode 100644 index 0000000..e805b46 --- /dev/null +++ b/Earthquakes/Operations/NegatedCondition.swift @@ -0,0 +1,56 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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: 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) + } + } + } +} diff --git a/Earthquakes/Operations/NoCancelledDependencies.swift b/Earthquakes/Operations/NoCancelledDependencies.swift new file mode 100644 index 0000000..92d1ce0 --- /dev/null +++ b/Earthquakes/Operations/NoCancelledDependencies.swift @@ -0,0 +1,46 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) + } + } +} diff --git a/Earthquakes/Operations/Operation.swift b/Earthquakes/Operations/Operation.swift new file mode 100644 index 0000000..6782fe0 --- /dev/null +++ b/Earthquakes/Operations/Operation.swift @@ -0,0 +1,348 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 { + return ["state"] + } + + class func keyPathsForValuesAffectingIsExecuting() -> Set { + return ["state"] + } + + class func keyPathsForValuesAffectingIsFinished() -> Set { + 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 +} diff --git a/Earthquakes/Operations/OperationCondition.swift b/Earthquakes/Operations/OperationCondition.swift new file mode 100644 index 0000000..e400f17 --- /dev/null +++ b/Earthquakes/Operations/OperationCondition.swift @@ -0,0 +1,110 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) + } + } +} diff --git a/Earthquakes/Operations/OperationErrors.swift b/Earthquakes/Operations/OperationErrors.swift new file mode 100644 index 0000000..a1cd6e2 --- /dev/null +++ b/Earthquakes/Operations/OperationErrors.swift @@ -0,0 +1,31 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 +} diff --git a/Earthquakes/Operations/OperationObserver.swift b/Earthquakes/Operations/OperationObserver.swift new file mode 100644 index 0000000..ee2b994 --- /dev/null +++ b/Earthquakes/Operations/OperationObserver.swift @@ -0,0 +1,29 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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]) + +} diff --git a/Earthquakes/Operations/OperationQueue.swift b/Earthquakes/Operations/OperationQueue.swift new file mode 100644 index 0000000..371b8d9 --- /dev/null +++ b/Earthquakes/Operations/OperationQueue.swift @@ -0,0 +1,124 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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() + } + } + } +} diff --git a/Earthquakes/Operations/PassbookCondition.swift b/Earthquakes/Operations/PassbookCondition.swift new file mode 100644 index 0000000..30d7e31 --- /dev/null +++ b/Earthquakes/Operations/PassbookCondition.swift @@ -0,0 +1,43 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 diff --git a/Earthquakes/Operations/PhotosCondition.swift b/Earthquakes/Operations/PhotosCondition.swift new file mode 100644 index 0000000..d606996 --- /dev/null +++ b/Earthquakes/Operations/PhotosCondition.swift @@ -0,0 +1,67 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 diff --git a/Earthquakes/Operations/ReachabilityCondition.swift b/Earthquakes/Operations/ReachabilityCondition.swift new file mode 100644 index 0000000..85646a9 --- /dev/null +++ b/Earthquakes/Operations/ReachabilityCondition.swift @@ -0,0 +1,92 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) + } + } +} diff --git a/Earthquakes/Operations/RemoteNotificationCondition.swift b/Earthquakes/Operations/RemoteNotificationCondition.swift new file mode 100644 index 0000000..707b05a --- /dev/null +++ b/Earthquakes/Operations/RemoteNotificationCondition.swift @@ -0,0 +1,127 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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()) + } + + 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 diff --git a/Earthquakes/Operations/SilentCondition.swift b/Earthquakes/Operations/SilentCondition.swift new file mode 100644 index 0000000..6f8a12a --- /dev/null +++ b/Earthquakes/Operations/SilentCondition.swift @@ -0,0 +1,40 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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: 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) + } +} diff --git a/Earthquakes/Operations/TimeoutObserver.swift b/Earthquakes/Operations/TimeoutObserver.swift new file mode 100644 index 0000000..e9f8752 --- /dev/null +++ b/Earthquakes/Operations/TimeoutObserver.swift @@ -0,0 +1,56 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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. + } +} diff --git a/Earthquakes/Operations/UIUserNotifications+Operations.swift b/Earthquakes/Operations/UIUserNotifications+Operations.swift new file mode 100644 index 0000000..530fe82 --- /dev/null +++ b/Earthquakes/Operations/UIUserNotifications+Operations.swift @@ -0,0 +1,49 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 diff --git a/Earthquakes/Operations/URLSessionTaskOperation.swift b/Earthquakes/Operations/URLSessionTaskOperation.swift new file mode 100644 index 0000000..b743b95 --- /dev/null +++ b/Earthquakes/Operations/URLSessionTaskOperation.swift @@ -0,0 +1,54 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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) { + 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() + } +} diff --git a/Earthquakes/Operations/UserNotificationCondition.swift b/Earthquakes/Operations/UserNotificationCondition.swift new file mode 100644 index 0000000..4b2aa78 --- /dev/null +++ b/Earthquakes/Operations/UserNotificationCondition.swift @@ -0,0 +1,122 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 diff --git a/Earthquakes/ParseEarthquakesOperation.swift b/Earthquakes/ParseEarthquakesOperation.swift new file mode 100644 index 0000000..7968a46 --- /dev/null +++ b/Earthquakes/ParseEarthquakesOperation.swift @@ -0,0 +1,168 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 + } +} diff --git a/Earthquakes/SplitViewController.swift b/Earthquakes/SplitViewController.swift new file mode 100644 index 0000000..ab633b0 --- /dev/null +++ b/Earthquakes/SplitViewController.swift @@ -0,0 +1,30 @@ +/* +Copyright (C) 2015 Apple Inc. All Rights Reserved. +See LICENSE.txt for this sample’s 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 + } +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..063901e --- /dev/null +++ b/LICENSE.txt @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4dd301b --- /dev/null +++ b/README.md @@ -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