A Swift framework inspired by WWDC 2015 Advanced NSOperations session. See the session video here: https://developer.apple.com/videos/wwdc/2015/?id=226
As of version 2.3, Operations is a multi-platform framework, with CocoaPods support in addition to framework targets for iOS Extensions, iOS Apps, OS X, watchOS and tvOS.
Current development focus is on improving test coverage (broke 60% for v2.3), and improving documentation coverage. Documentation is hosted here: docs.danthorpe.me/operations.
NSOperation
is a class which enables composition of discrete tasks or work for asynchronous execution on an operation queue. It is therefore an abstract class, and Operation
is a similar abstract class. Therefore, typical usage in your own codebase would be to subclass Operation
and override execute
.
For example, an operation to save a Contact
value in YapDatabase
might be:
class SaveContactOperation: Operation {
typealias CompletionBlockType = Contact -> Void
let connection: YapDatabaseConnection
let contact: Contact
let completion: CompletionBlockType?
init(connection: YapDatabaseConnection, contact: Contact, completion: CompletionBlockType? = .None) {
self.connection = connection
self.contact = contact
self.completion = completion
super.init()
name = “Save Contact: \(contact.displayName)”
}
override func execute() {
connection.asyncWrite(contact) { (returned: Contact) in
self.completion?(returned)
self.finish()
}
}
}
The power of the Operations
framework however, comes with attaching conditions and observer to operations. For example, perhaps before the user is allowed to delete a Contact
, we want them to confirm their intention. We can achieve this using the supplied UserConfirmationCondition
.
func deleteContact(contact: Contact) {
let delete = DeleteContactOperation(connection: readWriteConnection, contact: contact)
let confirmation = UserConfirmationCondition(
title: NSLocalizedString("Are you sure?", comment: "Are you sure?"),
message: NSLocalizedString("The contact will be removed from all your devices.", comment: "The contact will be removed from all your devices."),
action: NSLocalizedString("Delete", comment: "Delete"),
isDestructive: true,
cancelAction: NSLocalizedString("Cancel", comment: "Cancel"),
presentingController: self)
delete.addCondition(confirmation)
queue.addOperation(delete)
}
When this delete operation is added to the queue, the user will be presented with a standard system UIAlertController
asking if they're sure. Additionally, other AlertOperation
instances will be prevented from running.
The above “save contact” operation looks quite verbose for though for a such a simple task. Luckily in reality we can do this:
let save = ComposedOperation(connection.writeOperation(contact))
save.addObserver(BlockObserver { (_, errors) in
print(“Did save contact”)
})
queue.addOperation(save)
Because sometimes creating an Operation
subclass is a little heavy handed. Above we composed an existing NSOperation
but we can utilize a BlockOperation
. For example, let say we want to warn the user before they cancel a "Add New Contact" controller without saving the Contact.
@IBAction didTapCancelButton(button: UIButton) {
dismiss()
}
func dismiss() {
// Define a dispatch block for unwinding.
let dismiss = {
self.performSegueWithIdentifier(SegueIdentifier.UnwindToContacts.rawValue, sender: nil)
}
// Wrap this in a block operation
let operation = BlockOperation(mainQueueBlock: dismiss)
// Attach a condition to check if there are unsaved changes
// this is an imaginary conditon - doesn't exist in Operation framework
let condition = UnsavedChangesCondition(
connection: connection,
value: contact,
save: save(dismiss),
discard: BlockOperation(mainQueueBlock: dismiss),
presenter: self
)
operation.addCondition(condition)
// Attach an observer to see if the operation failed because
// there were no edits from a default Contact - in which case
// continue with dismissing the controller.
operation.addObserver(BlockObserver { [unowned queue] (_, errors) in
if let error = errors.first as? UnsavedChangesConditionError {
switch error {
case .NoChangesFromDefault:
queue.addOperation(BlockOperation(mainQueueBlock: dismiss))
case .HasUnsavedChanges:
break
}
}
})
queue.addOperation(operation)
}
In the above example, we're able to compose reusable (and testable!) units of work in order to express relatively complex control logic. Another way to achieve this kind of behaviour might be through FRP techniques, however those are unlikely to yield re-usable types like UnsavedChangesCondition
, or even DismissController
if the above was composed inside a custom GroupOperation
.
Requesting permissions from the user can often be a relatively complex task, which almost all apps have to perform at some point. Often developers put requests for these permissions in their AppDelegate, meaning that new users are bombarded with alerts. This isn't a great experience, and Apple expressly suggest only requesting permissions when you need them. However, this is easier said than done. The Operations framework can help however. Lets say we want to get the user's current location.
func getCurrentLocation(completion: CLLocation -> Void) {
queue.addOperation(UserLocationOperation(handler: completion))
}
This operation will automatically request the user's permission if the application doesn't already have the required authorization, the default is "when in use".
Perhaps also you want to just test to see if authorization has already been granted, but not ask for it if it hasn't. In Apple’s original sample code from WWDC 2015, there are a number of OperationCondition
s which express the authorization status for device or OS permissions. Things like, LocationCondition
, and HealthCondition
. However, in version 2.2 of Operations I moved away from this model to unify this functionality into the CapabilityType
protocol. Where previously there were bespoke conditions (and errors) to test the status, there is now a single condition, which is initialized with a CapabilityType
.
For example, where previously you would have written this:
operation.addCondition(LocationCondition(usage: .WhenInUse))
now you write this:
operation.addCondition(AuthorizedFor(Capability.Location(.WhenInUse)))
As of 2.2 the following capabilities are expressed:
-
Capability.Calendar
- includes support forEKEntityType
requirements, defaults to.Event
. -
Capability.Cloud
- includes support for defaultCKContainer
or with specific identifier, and.UserDiscoverable
cloud container permissions. -
Capability.Health
- improved support for exactly whichHKObjectType
s have read permissions. Please get in touch if you want to use HealthKit with Operations, as I currently don't, and would like some feedback or input on further improvements that can be made for HealthKit. -
Capability.Location
- includes support for required usage, either.WhenInUse
(the default) or.Always
. -
Capability.Passbook
- note here that there is void "status" type, just an availability boolean. -
Capability.Photos
In addition to a generic operation condition, which can be used as before with SilentCondition
and NegatedCondition
. There are also two capability generic operations. GetAuthorizationStatus
will retrieve the current status of the capability. Authorize
will explicity request the required permissions for the capability. Both of these operations accept the capability as their first initializer argument without a label, and have a completion block with the same type. Therefore, it is trivial to write a function which can update your controller or UI and use it for both operations.
For example here we can check the status of location services for the app:
func locationServicesEnabled(enabled: Bool, withAuthorization status: CLAuthorizationStatus) {
switch (enabled, status) {
case (false, _):
// Location services are not enabled
case (true, .NotDetermined):
// Location services are enabled, but not currently determined for the app.
}
}
func determineAuthorizationStatus() {
queue.addOperation(GetAuthorizationStatus(Capability.Location(), completion: locationServicesEnabled))
}
func requestPermission() {
queue.addOperation(Authorize(Capability.Location(), completion: locationServicesEnabled))
}
See the Permissions example project, where the above code is taken from.
Operations is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'Operations'
This is a brief summary of the current and planned functionality.
-
Operation
andOperationQueue
class definitions. -
OperationCondition
and evaluator functionality. -
OperationObserver
definition and integration.
-
MutuallyExclusive
condition e.g. can only oneAlertPresentation
at once. -
NegatedCondition
evaluates the reverse of the composed condition. -
SilentCondition
suppress any dependencies of the composed condition. -
NoCancelledDependencies
NoFailedDependenciesCondition
requires that all dependencies succeeded. -
BlockObserver
run blocks when the attached operation starts, produces another operation or finishes. -
BackgroundObserver
automatically start and stop background tasks if the application enters the background while the attached operation is running. -
NetworkObserver
automatically manage the device’s network indicator while the operation is running. -
TimeoutObserver
automatically cancel the attached operation if the timeout interval is reached. -
LoggingObserver
enable simple logging of the lifecycle of the operation and any of it’s produced operations. -
GroupOperation
encapsulate multiple operations into their own discrete unit, running on their own queue. Supports internally adding new operations, so can be used for batch processing or greedy operation tasks. -
DelayOperation
inserts a delay into the operation queue. -
BlockOperation
run a block inside anOperation
. Supports unsuccessful finishing. -
GatedOperation
only run the composed operation if the provided block evaluates true. -
ComposedOperation
run a composedNSOperation
. This is great for adding conditions or observers to bog-standardNSOperation
s without having to subclass them.
-
GetAuthorizationStatus
get the current authorization status for the givenCapabilityType
. Supports EventKit, CloudKit, HealthKit, CoreLocation, PassKit, Photos. -
Authorize
request the required permissions to access the requiredCapabilityType
. Supports EventKit, CloudKit, HealthKit, CoreLocation, PassKit, Photos. -
AuthorizedFor
express the required permissions to access the requiredCapabilityType
as anOperationCondition
. Meaning that if the status has not been determined yet, it will trigger authorization. Supports EventKit, CloudKit, HealthKit, CoreLocation, PassKit, Photos. -
ReachabilityCondition
requires that the supplied URL is reachable. -
ReachableOperation
compose an operation which must complete and requires network reachability. This uses an included system Reachability object and does not require any extra dependencies. However currently in Swift 1.2, as function pointers are not supported, this uses a polling mechanism withdispatch_source_timer
. I will probably replace this with more efficient Objective-C soon. -
CloudKitOperation
compose aCKDatabaseOperation
inside anOperation
with the appropriateCKDatabase
. -
UserLocationOperation
access the user’s current location with desired accuracy. -
ReverseGeocodeOperation
perform a reverse geocode lookup of the suppliedCLLocation
. -
ReverseGeocodeUserLocationOperation
perform a reverse geocode lookup of user’s current location. -
UserNotificationCondition
require that the user has granted permission to present notifications. -
RemoteNotificationCondition
require that the user has granted permissions to receive remote notifications. -
UserConfirmationCondition
requires that the user confirms an action presented to them using aUIAlertController
. The condition is configurable for title, message and button texts. -
WebpageOperation
given a URL, will present aSFSafariViewController
.
Available as a subspec (if using CocoaPods) is ABAddressBook.framework
related operations.
-
AddressBookCondition
require authorized access to ABAddressBook. Will automatically request access if status is not already determined. -
AddressBookOperation
is a base operation which creates the address book and requests access. -
AddressBookGetResource
is a subclass ofAddressBookOperation
and exposes methods to access resources from the address book. These can include person records and groups. All resources are wrapped inside Swift facades to the underlying opaque AddressBook types. -
AddressBookGetGroup
will get the group for a given name. -
AddressBookCreateGroup
will create the group for a given name, if it doesn’t already exist. -
AddressBookRemoveGroup
will remove the group for a given name. -
AddressBookAddPersonToGroup
will add the person with record id to the group with the provided name. -
AddressBookRemovePersonFromGroup
will remove the person with record id from the group with the provided name. -
AddressBookMapPeople<T>
takes an optional group name, and a mapping transform. It will map all the people in the address book (or in the group) via the transform. This is great if you have your own representation of a Person, and which to import the AddressBook. In such a case, create the following:
extension MyPerson {
init?(addressBookPerson: AddressBookPerson) {
// Create a person, return nil if not possible.
}
}
Then, import people
let getPeople = AddressBookMapPeople { MyPerson($0) }
queue.addOperation(getPeople)
Use an observer or GroupOperation
to access the results via the map operation’s results
property.
-
AddressBookDisplayPersonViewController
is an operation which will display a person (provided their record id), from a controller in your app. This operation will perform the necessary address book tasks. It can present the controller using 3 different styles, either.Present
(i.e. modal),.Show
(i.e. old style push) or.ShowDetail
. Here’s an example:
func displayPersonWithAddressBookRecordID(recordID: ABRecordID) {
let controller = ABPersonViewController()
controller.allowsActions = true
controller.allowsEditing = true
controller.shouldShowLinkedPeople = true
let show = AddressBookDisplayPersonViewController(
personViewController: controller,
personWithID: recordID,
displayControllerFrom: .ShowDetail(self),
delegate: self
)
queue.addOperation(show)
}
-
AddressBookDisplayNewPersonViewController
same as the above, but for showing the standard create new person controller. For example:
@IBAction func didTapAddNewPerson(sender: UIBarButtonItem) {
let show = AddressBookDisplayNewPersonViewController(
displayControllerFrom: .Present(self),
delegate: self,
addToGroupWithName: “Special People”
)
queue.addOperation(show)
}
I want to stress that this code is heavily influenced by Apple. In no way am I attempting to assume any sort of credit for this architecture - that goes to Dave DeLong and his team. My motivations are that I want to adopt this code in my own projects, and so require a solid well tested framework which I can integrate with.