01 February 2016

With Parse going away, the quest for alternatives begins. CloudKit is one option. No matter what backend one may choose, there are inherent frailties within any app that relies on a backend. Sometimes these weaknesses are only revealed under significant loads or unusual usage patterns and are often a result of a flawed handling of concurrency.

Given the full toolset of delegation, observation, completion handlers, Grand Central Dispatch (GCD) and NSOperations, just about any concurrency situation can be handled. However, the natural complexity involved in employing those tools can lead to subtle flaws that are difficult to reproduce.

This is where ReactiveCocoa and a signal chaining approach can offer improvement. RAC is not necessarily a replacement for the existing tools. Instead it offers an added value environment that allows implementations to be even more functional, in the functional programming sense, than otherwise possible. I’m going to use a typical set of backend operations to illustrate how a functional reactive approach can minimize points of failure within a typical set of linked backend operations.

Before I get into that, I want to make a few comments about Swift and ReactiveCocoa.

Swift has made ReactiveCocoa more accessible to me because it is free from the cumbersome syntax of Objective-C. Additionally, static typing has improved my ability to relate to everything within the language, perhaps that is just due to my personal preferences.

There are still some barriers such as the code not reading in a way that is natural to me. For example, signals are chained by returning other signals within Swift closures and that still seems strange but I’m starting to be more comfortable with it.

As long as I remember that the syntax is an ends to a mean, a vehicle to reach a destination, then it becomes more pleasant to work with. The other benefits that Swift offers make up for the slight awkward syntax that results from fitting Swift into a functional reactive approach.

The integration of RAC4 with Swift’s type system is a huge improvement over the previous Objective-C version and truly assists in areas such as propagating errors through a signal chain. Xcode’s handling of Swift types is also extremely helpful when it comes to writing code. Xcode’s documentation features for Swift types is great as I show in an example for the .on() function in Figure 1.

Figure 1: Xcode showing type hints for ReactiveCocoa 4.The function types shown here are an example of how RAC4 makes use of Swift's static type system. Having these hints is the greatest thing to me. The types make it possible to keep track of signals in a way that wasn’t possible before.
Figure 1: Xcode showing type hints for ReactiveCocoa 4. The function types shown here are an example of how RAC4 makes use of Swift's static type system. Having these hints is the greatest thing to me. The types make it possible to keep track of signals in a way that wasn’t possible before.

Example implementation of CloudKit functions

For my example, I’m going to cover a typical operation when working with any backend, deleting records. I’ll be using CloudKit as my backend.

As an initial, not functional reactive solution, we could retrieve the record IDs into an array and then iterate over that array to delete the matching records.

For my test scenario, I’ll need to first add records on every run.

These operations amount to three distinct functions.

  • Add test records.
  • Retrieve records matching some query.
  • Perform deletions of records.

I’ve provided sample code for these three functions here.

 1 /// Add a test photo record into the public DB.
 2 func addTestPhoto() {
 3     let recordID = CKRecordID(recordName: getUUID())
 4     let photo = CKRecord(recordType: TypeConstants.TestPhoto, recordID: recordID)
 5     publicDB.saveRecord(photo) { record, error in
 6         guard error == nil else {
 7             // Handle the error.
 8             return
 9         }
10     }
11 }
 1 /// Get all of the photos in the public DB and place the results in an Array.
 2 func getAllPhotos(completionWithPhotos: (photos: [CKRecord])->()) {
 3     var result = Array<CKRecord>()
 4     let photoPredicate = NSPredicate(value: true)
 5     let query = CKQuery(recordType: TypeConstants.TestPhoto, 
 6                         predicate: photoPredicate)
 7     publicDB.performQuery(query, inZoneWithID: nil) { records, error in
 8         guard let foundRecords = records where error == nil else {
 9             // Handle the error.
10             return
11         }
12         for record in foundRecords {
13             result.append(record)
14         }
15         completionWithPhotos(photos: result)
16     }
17 }
1 /// Delete a record from the public DB.
2 func deleteRecord(recordID: CKRecordID) {
3     publicDB.deleteRecordWithID(recordID, completionHandler: { recordID, error in
4         guard error == nil else {
5             // Handle the error.
6             return
7         }
8     })
9 }

Deleting the records that are retrieved using getAllPhotos() requires that the result is passed to deleteRecord(). This is where potential points of failure can be introduced using approaches typical to iOS and OS X development.

Due to the asynchronous nature of both retrieving records from the backend and the deletion of those records, getting the two functions to talk to each other can be accomplished in a couple of ways:

  • Through the completion handler of getAllPhotos().
  • Through a property that holds an array of record IDs.

In both of these cases, it will be found that we have to loop over some collection to perform the deletion.

Here is how it looks using a property, self.recordIDs, that is an array of record IDs.

 1 /// Delete all records matching recordIDs in self.recordIDs. 
 2 func deleteAllRecords() {
 3     for recordID in self.recordIDs {
 4         publicDB.deleteRecordWithID(recordID, completionHandler: { recordID, error in
 5             guard error == nil else {
 6                 // Handle the error
 7                 return
 8             }
 9         })
10     }
11 }

In this case, we would have had to have loaded the records from CloudKit into self.recordIDs ahead of time.

If we instead use the completion handler we could use a function like the one below.

1 /// Delete all records that are retrieved by getAllPhotos() using its 
2 /// completion handler.
3 func deleteAllPhotos() {
4     getAllPhotos { photos in
5         for photo in photos {
6             self.deleteRecord(photo.recordID)
7         }
8     }
9 }

A function here, a property there. This doesn’t seem that bad but over the course of development, all these little additions start to add up. In the worst case, your app’s complexity starts increasing exponentially due to needing to manage all of this additional code every time a change is made.

I’ve seen implementations like this fail under heavy usage even though it can be fine under lighter loads.

Enter ReactiveCocoa.

ReactiveCocoa 4 implementation of CloudKit record deleting

The immediate reason for my stepping into ReactiveCocoa is that it offers a more functional approach that implies greater reliability and elegance in concurrency implementations. The following benefits can be obtained as a result.

  • Reducing the lines of code that are actively involved in operations.
  • Removing intermediary variable allocations that can be points of failure during concurrent operations.
  • Gaining unified error handling and cancellations.

Instead of adding to the operational complexity of an app, using ReactiveCocoa can have the effect of reducing it.

Function modifications for ReactiveCocoa

Retrieve all photos modifications for RAC

Retrieving all photos in our app can be accomplished with a signal producer that calls our existing photo retrieval function.

In the process, the completion handler can be eliminated from that function thereby eliminating a potential source of error.

Here is the getAllPhotos() function when used with ReactiveCocoa. The observer is passed into the function so that the function can send events on the signal.

 1 func getAllPhotos(observer: Observer<CKRecordID, NSError>) {
 2     let photoPredicate = NSPredicate(value: true)
 3     let query = CKQuery(recordType: TypeConstants.TestPhoto, predicate: photoPredicate)
 4     publicDB.performQuery(query, inZoneWithID: nil) { records, error in
 5         guard let foundRecords = records where error == nil else {
 6             observer.sendFailed(error!)
 7             return
 8         }
 9         for record in foundRecords {
10             observer.sendNext(record.recordID)
11         }
12         observer.sendCompleted()
13     }
14 }
Giving errors the care they deserve

In addition to removing the completion handler, we have gained a result type that will propagate an error if it occurs through the rather elegant sendFailed() method. This is much better than handling the error in a completion handler because it will be sent up the entire signal chain. Too often, errors are left unhandled due to the extra work that is involved. ReactiveCocoa minimizes the effort required to setup error handling and this can mean that more errors will be treated properly.

Add photo and delete record modifications for RAC

I altered the photo adding and deleting functions, in a likewise manner, to include the observer argument to be able to send events on a signal.

 1 /// Photo add function modified for use with ReactiveCocoa.
 2 func addTestPhoto(observer: Observer<(), NSError>) {
 3     let recordID = CKRecordID(recordName: getUUID())
 4     let photo = CKRecord(recordType: TypeConstants.TestPhoto, recordID: recordID)
 5     publicDB.saveRecord(photo) { record, error in
 6         guard error == nil else {
 7             observer.sendFailed(error!)
 8             return
 9         }
10         observer.sendCompleted()
11     }
12 }
 1 /// Record delete function modified for use with ReactiveCocoa. 
 2 func deleteRecord(recordID: CKRecordID, observer: Observer<CKRecordID, NSError>) {
 3     publicDB.deleteRecordWithID(recordID, completionHandler: { recordID, error in
 4         guard error == nil else {
 5             observer.sendFailed(error!)
 6             return
 7         }
 8         observer.sendCompleted()
 9     })
10 }

Wrapping our existing functions into signal producers

The Signal Producer for this method is then implemented by the following function.

1 /// Make a signal for adding test photos to CloudKit.
2 func testPhotoAdder() -> SignalProducer<(), NSError> {
3     return SignalProducer { observer, disposable in
4         addTestPhoto(observer)
5     }
6 }

A Signal Producer for deleting photos can be similarly implemented.

1 /// Make a signal for deleting records from CloudKit.
2 func photoDeleter(recordID: CKRecordID) -> SignalProducer<CKRecordID, NSError> {
3     return SignalProducer { observer, disposable in
4         deleteRecord(recordID)
5     }
6 }

Final form of the record deleting signal

The real magic occurs when the results of the photo retrieval signal are passed to the record deleting signal without the use of intermediary storage and without need for a completion handler.

1 /// A signal producer to retrieve records and delete them.
2 let deletedPhotos
3     = self.photosGetter().flatMap(FlattenStrategy.Merge) {
4         recordID -> SignalProducer<CKRecordID, NSError> in
5             return self.photoDeleter(recordID)
6 }

The flatMap() here converts the events of the photo retrieval signal into the signal producer that deletes individual records in CloudKit. The two signals are talking to each other directly without any intermediaries. Code friction is reduced to a minimum making for a smooth ride.

The last thing that remains is to call start() on deletedPhotos after test photos have been added to CloudKit. The following code accomplishes this where I’ve used a comparatively inelegant form of ordering the operations in sequence through the RAC-based completion handler for signals.

1 /// The photo deleting signal is started after the test photos are added to the 
2 /// backend.
3 self.testPhotoAdder().startWithCompleted {
4     self.testPhotoAdder().startWithCompleted {
5         deletedPhotos.start()
6     }
7 }

Summary of ReactiveCocoa signal chaining benefits

Chaining signals by returning a signal has been a slippery concept to grasp for me. I understand it is done that way so that it fits into Swift. Instead of getting hung up on the syntax, I am focused on obtaining results according to the theory that is well documented within the ReactiveCocoa repository.

If you can wrap your head around it, the end result can be thoroughly worth it.

Eliminating unnecessary completion handlers

Eliminating the need for defining a completion handler is extraordinary considering how many such handlers might be needed without the use of signals in ReactiveCocoa.

Here are a few reasons to reduce use of completion handlers:

  • Unexpected results can occur when a completion occurs at an unexpected point in time.
  • Propagating errors through multiple handlers is an exercise in patience.
  • Code is made more readable by removing extraneous usages.

Improving error handling

Regarding error propagation, the unified error handling that is gained through ReactiveCocoa is a benefit that can be immediately realized.

Reducing intermediary allocations

Using signals has the added benefit of eliminating the need for intermediate memory allocations for variables such as arrays used for processing values. This eliminates yet another potential source of failure during concurrent operations.

Reducing overall code complexity

The overall result of using ReactiveCocoa signals is that the overall operational complexity of an app can be reduced. This ends up giving the following advantages to an app.

  • Maintainability is made easier.
  • Failures are reduced.
  • User experience is improved.

Final opinion

It requires fairly comprehensive experience with concurrency in iOS and OS X to appreciate the advantages that ReactiveCocoa offers. ReactiveCocoa 4 makes sense to me in Swift even if the syntax is slightly awkward. Due to Swift’s type safety, RAC4 coding is significantly aided by Xcode (minus the SourceKit crashes). I’m not able to say the same about the Objective-C version.

My purpose in this post was to demonstrate how ReactiveCocoa can improve a typical backend implementation in a way that wouldn’t otherwise be practical using the conventional concurrency toolset in iOS and OS X. I used CloudKit for my example, but other backends would be handled similarly. There is an upfront cost to adapting existing code to use ReactiveCocoa signals but the long-term benefits that I have covered can justify that initial investment.

blog comments powered by Disqus