Introducing (v2.6 of) Operations

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 CapabilityTypes 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.