Introducing (v2.6 of) Operations
Back in June 2015, after watching the excellent WWDC session Advanced NSOperations I started writing my own version of the core code proposition. I called this project Operations.
I stuck to the main architecture introduced by Dave De Long, but added backwards compatibility for Swift 1.2 and unit tests.
Version 1.0 of this framework targets Swift 1.2 and Version 2.0 targets Swift 2.*, and both were released simultaneously. Then I got to work…
An Improved Architecture
Through versions 2.0 to 2.5 I have evolved the architecture and added new Operation types. Below introduces the largest single departure from the original architecture: capabilities.
Capabilities
The original sample code included OperationCondition
types to represent the application’s ability to access protected resources. For example LocationCondition
would manage asking the user for access to the device’s location services. However, after implementing this for a number of conditions, its clear that this approach has some issues. It conflates the action of determining whether or not the application is authorized to access location services (or the calendar, HealthKit etc) with the concept of a requirement for the attached Operation
. This means a lot of repeated boiler plate code, with reduced discovery, readability, maintainability and testability.
Instead, I introduced the concept of a capability with the CapabilityType
protocol. Location, Calendar and HealthKit are all examples of a capability of the device or the user’s account. The CapabilityType
protocol expresses asking for the current authorization status and requesting authorization for a specific requirement. The status of a CapabilityType
is a typealias, meaning that the user can expect to work with CLAuthoriationStatus
when using a location capability type. Similarly, because the type of the requirements of an operation will be CapabilityType
dependent, this is a typealias of the status type. For example, the location capability will support working with a requirement type which is an enum
with cases for .WhenInUse
and .Always
which matches how location services are used.
Formalizing device and user account capabilities in this manner allows generic Operation
and OperationCondition
types. For example:
class MyGeoOperation: Operation {
super.init()
name = "MyGeoOperation"
addCondition(AuthorizedFor(Capability.Location(.Always)))
}
We'll look at the above example, an Operation
subclass, a bit more closely. During initialization after calling super
is a good opportunity to do any configuration such as setting the name. We also automatically add an OperationCondition
to this operation. The condition is AuthorizedFor<T>
where the T
is a CapabilityType
. In this case, Capability.Location
is the type. There are many subtypes to Capability
it is only used to help with discovering what is available. The first argument all CapabilityType
s is the requirement which always has a default, so is often not necessary.
When an instance of the MyGeoOperation
is added to a queue, the dependencies of the condition will be scheduled first. In this case, the framework will attempt to get authorization for the app to access the device's location services. This will result in the familiar alert dialog when it happens for the first time. The condition will wait until the user dismissing the dialog, and then evaluate the app's authorization. The location capability will determine if the authorized access meets its requirements, for example, perhaps the app is only authorized for .WhenInUse
but the requirement is for .Always
. In which cases, the attached operation will fail with an error. An advantage of this approach is that this error is always the same type regardless of the capability. This makes it much easier to write reusable code which is CapabilityType
aware.
One of the example apps, Permissions, has examples of using CapabilityType
to provide information to the user around authentication challenges. If your app presents a bunch of alerts when it first runs without any warning, Operations can help you ask for authorization in the most user friendly way.
Project Maturation
The changes for v2.5 have been quite extensive, and the framework has somewhat matured. At this time, I wrote a programming guide to help framework consumers use Operations more effectively. This includes some direction on how to combine SilentCondition
and NegatedCondition
to great effect.
Version 2.6
With v2.6 in addition to new Operation types, there have also been refinements to how dependencies are scheduled. These are some subtle behaviors which further separates Operations from the original Apple sample code.
Dependencies vs Condition Dependencies
Prior to v2.6 there was no recognition between regular dependent operations added via addDependency
(which I call a direct dependency) and operations which are dependencies of conditions (which I call an indirect dependency). This creates some unexpected behavior where an indirect dependency may be executed before a direct dependency finishes.
Now in v2.6, this has been fixed so that all indirect dependencies wait until all direct dependencies have finished.
Mutually Exclusive Condition with a Dependency
Another odd behavior of operation conditions with a dependency, is that if the condition is mutually exclusive, prior to v2.6, it would always be the operation which the condition is attached to which is made exclusive. Again, this is a subtle behavior which is a little unexpected. i.e. a bug :)
With v2.6, for mutually exclusive conditions, if there is a dependency, we ensure that this is the operation which is made exclusive, otherwise it is the operation which the condition is attached to.
A classic scenario where this might happen would be an Operation which requires the user is logged into a WebService. There should be a condition to check the logged in state, which should be mutually exclusive. This condition would return an operation to perform the login task as it’s optional dependency.
Putting this into practice on multiple operations, prior to v2.6 would result in multiple login operations from being enqueued. After v2.6, they are mutually exclusive, so they would be enqueued serially, and clearly should check the log in status prior to executing anything.
RepeatedOperation
These are two new operation types added in version 2.6. RepeatedOperation
is initialized with a generator which vends new instances of an NSOperation
. It is actually generic over this type.
This can be used to repeatedly execute the same kind of operation. Among its features is support for consistently configuring each operation instance in the same way before it is added to the queue. Additionally, a DelayOperation
can be inserted into the queue before the operation.
Alongside this class, is a strategy for creating different kinds of delays, called WaitStrategy
. For example, to wait a random time or use exponential back-off.
RetryOperation
RetryOperation
extends RepeatedOperation
, but instead of always adding another instance of the operation, this only happens if there are errors received. Additionally, it accepts an error handler. In the event of an error, the handler is called with the error received. The handler can inspect the error and adjust the responding operation accordingly.
CloudKitOperation
This is a pretty huge improvement over what is available in version 2.5.
Rather than just composing a CKOperation
instance, CloudKitOperation
is a RetryOperation
subclass which enqueues the instance wrapped in a ReachableOperation
. ReachableOperation
ensures that the device is on a viable network before executing its composed operation.
This is incredibly powerful, as it means that a whole class of connectivity errors can be handled automatically.
Additionally, CloudKitOperation
has its own error recovery to provide some default error handlers where possible. Many CKError
values are non-recoverable, and indicate a compile time or configuration issue. For these cases, the error is logged, and no retry will occur.
Another class of errors simple require a retry after a provided time interval. These are all handled automatically using the mechanisms of RepeatedOperation
.
The framework consumer can provide custom error handlers for particular CKError
values. This means that where appropriate error handling can be re-used.
Finally, CloudKitOperation
exposes properties and APIs which are suited to the type of the composed CKOperation
. This allows configuration to be done once at the top level CloudKitOperation
but is applied to the underlying CKOperation
instances, including the retried instances.
BatchedCloudKitOperation
Some CKOperation
types have the concept of more results are available and should be batched. BatchedCloudKitOperation
is a RepeatedOperation
subclass which enqueues CloudKitOperation
instances. Mind. Blown!
What's Next?
As we look forward to Swift 2.2 arriving, I think that work on Operations v3.0 for Swift 3.0 will begin. My stretch goal for v3 is "Pure Swift". This means, an OperationType
protocol with Operation
and OperationQueue
as base classes. We will still have interopability with NSOperation
instances, but avoiding subclasses allows us to re-engineer Operation
without key-value observing and to fully embrace protocol orientated design.