Nick Harris

UIAlertController + NSOperation

leave a comment »

I’ve been investigating NSOperation and dependency chaining lately including my posts about syncing data with CloudKit. While working on that code I had two things come up that I didn’t get around to investigating:

1. How can I pass data back after an operation completes in a more consistent way
2. How can I allow user input within an operation

I put together a little project tonight that I used to explore these issues. I like it so I thought I’d share it.

Sample App

This code is available on BitBucket

Its so much easier to explain ideas within the context of a sample app. This app is super simple:

– Tap the button
– Get prompted for what operation you want to run
– View the results

The code creates and executes an operation chain when you tap the button. The operations are executed or canceled depending on which option is chosen. The console documents everything that is happening and when. Simple.

The operation choice is encapsulated in an enum:

enum OperationChoice {
    case Unknown
    case OperationOne
    case OperationTwo
    case OperationThree
    
    func actionSheetOptionTitle() -> String {
        switch self {
        case .Unknown: return "Unknown"
        case .OperationOne: return "Operation One"
        case .OperationTwo: return "Operation Two"
        case .OperationThree: return "Operation Three"

        }

    }

}

Operation Return Values 

I really dislike how my operations in my CloudKit proof of concept return data. Its done with multiple properties on the operation class. For another developer to use those operations they would need to know more details of the operation then they really need to. Moving forward I think I’ll use a tuple instead:

var operationResult: (error: NSError?, operationChoice: OperationChoice)

 

I’ve used this approach in some other Swift class operations but I like how it fits with operations. The consumer of the operation knows the one property they need to interact with on completion. It’s much simpler then lines of documentation about what public properties will have the changes. It also reflects a more functional approach to programming with monad type behavior.

User Interaction in an NSOperation

Every NSOperation I’ve ever created has used main() to execute instead of start(). To be honest, the responsibilities of overriding functions and KVO calls using start() to be a good citizen made me avoid it.

That is until I read a post about combining NSURLSession and NSOperation by Marcus Zarra. Its a great approach that I plan on using. It also demystified some of my apprehensions with overriding the life-cycle of an NSOperation.

I had tried using UIAlertController’s in a main() override. The problem is that the operation reaches the end of main() before the user has a chance to respond, therefore releasing the operation before any dependencies could use the input. Controlling the life-cycle of the operation means that I can keep the operation executing until the user interaction is registered and other operations in the chain can then react appropriately on completion.

If you haven’t read Marcus’s post yet please do so now. I’ll wait…

Cool. Now you know that Swift is a little weird with overriding private properties which is essential to this approach with NSOperations. I also tried using the ivars directly and also found it didn’t work. I need this code in multiple operations. I initially tried using a protocol but ran into more issues about overriding private properties. I think the only solution in this situation is to subclass:

class ConcurrentOperation: NSOperation {
    
    override init() {
        
        override_executing = false
        override_finished = false
        
        super.init()
    }
    
    func completeOperation() {
        executing = false
        finished = true
    }
 
    private var override_executing : Bool
    override var executing : Bool {
        get { return override_executing }
        set {
            willChangeValueForKey("isExecuting")
            override_executing = newValue
            didChangeValueForKey("isExecuting")
        }
    }
    
    private var override_finished : Bool
    override var finished : Bool {
        get { return override_finished }
        set {
            willChangeValueForKey("isFinished")
            override_finished = newValue
            didChangeValueForKey("isFinished")
        }
    }
}

 

Sample Operation Code

With a way to keep my operation in memory until the user interaction is complete, I could create an operation that displays an Action Sheet to choose an operation and operations that use an Alert to display which option was chosen (I’ll only show OperationOne as the other two are identical):

class PromptUserOperation: ConcurrentOperation {
    
    var operationResult: (error: NSError?, operationChoice: OperationChoice)
    
    override init() {
        
        operationResult = (error: nil, operationChoice: .Unknown)
        
        super.init()
    }
    
    override func start() {
 
        print("PromptUserOperation.start()")
        promptUser()
    }
    
    func promptUser() {
        
        let alertController = UIAlertController(title: "Which operation should be performed?", message: nil, preferredStyle: .ActionSheet)
        
        alertController.addAction(createOperationAlertAction(OperationChoice.OperationOne))
        alertController.addAction(createOperationAlertAction(OperationChoice.OperationTwo))
        alertController.addAction(createOperationAlertAction(OperationChoice.OperationThree))
        
        dispatch_async(dispatch_get_main_queue(),{
            if let appDelegate = UIApplication.sharedApplication().delegate,
                let appWindow = appDelegate.window!,
                let rootViewController = appWindow.rootViewController {
                    rootViewController.presentViewController(alertController, animated: true, completion: nil)
            }
        })
    }
    
    func createOperationAlertAction(operationChoice: OperationChoice) -> UIAlertAction {
        
        return UIAlertAction(title: operationChoice.actionSheetOptionTitle(), style: .Default) {
            [unowned self]
            action in
            
            self.userSelection(operationChoice)
        }
    }
 
    func userSelection(selectedOperation: OperationChoice) {
        print("PromptUserOperation.userSelection( \(selectedOperation.actionSheetOptionTitle()) )")
        
        operationResult = (error: nil, operationChoice: selectedOperation)
        completeOperation()
    }
    
    deinit {
        print("PromptUserOperation.deinit")
    }
}
 
class OperationOne: ConcurrentOperation {
 
    override func start() {
        
        if cancelled {
            print("OperationOne - CANCELLED")
            completeOperation()
            return
        }
        
        print("OperationOne.start()")
        promptUser()
    }
    
    func promptUser() {
        
        let alertController = UIAlertController(title: "You chose Operation One!", message: nil, preferredStyle: .Alert)
        
        let alertAction = UIAlertAction(title: "Yay!", style: .Default) {
            [unowned self]
            action in
            
            self.completeOperation()
        }
        alertController.addAction(alertAction)
            
        dispatch_async(dispatch_get_main_queue(),{
            if let appDelegate = UIApplication.sharedApplication().delegate,
                let appWindow = appDelegate.window!,
                let rootViewController = appWindow.rootViewController {
                    rootViewController.presentViewController(alertController, animated: true, completion: nil)
            }
        })
    }
    
    deinit {
        print("OperationOne.deinit")
    }
}
 
@IBAction func runOperations() {
        
    // create an operation queue
    let operationQueue = NSOperationQueue()
        
    // create the operations
    let promptUserOperation = PromptUserOperation()
    let operationOne = OperationOne()
    let operationTwo = OperationTwo()
    let operationThree = OperationThree()
       
    // create the process operation
    let processChoiceOperation = NSBlockOperation {
        [unowned promptUserOperation, unowned operationOne, unowned operationTwo, unowned operationThree] in
            
        print("processChoiceOperation")
            
        let promptUserOperationResult = promptUserOperation.operationResult
            
        if let error = promptUserOperationResult.error {
            print("Error while prompting the user: \(error)")
        }
        else {
            switch promptUserOperationResult.operationChoice {
            case OperationChoice.OperationOne:
                operationTwo.cancel()
                operationThree.cancel()
            case OperationChoice.OperationTwo:
                operationOne.cancel()
                operationThree.cancel()
            case OperationChoice.OperationThree:
                operationOne.cancel()
                operationTwo.cancel()
            default:
                fatalError("Unknown operation choice: \(promptUserOperationResult.operationChoice)")
            }
        }
    }
        
    // set the dependencies
    processChoiceOperation.addDependency(promptUserOperation)
    operationOne.addDependency(processChoiceOperation)
    operationTwo.addDependency(processChoiceOperation)
    operationThree.addDependency(processChoiceOperation)
        
    // queue the operations
    operationQueue.addOperation(promptUserOperation)
    operationQueue.addOperation(processChoiceOperation)
    operationQueue.addOperation(operationOne)
    operationQueue.addOperation(operationTwo)
    operationQueue.addOperation(operationThree)
}

 

And here’s what the console looks like after the user interacts with the app state in the screen shot:

initial_launch

PromptUserOperation.start()


prompt_choice

 

PromptUserOperation.userSelection( Operation One )

processChoiceOperation

OperationThree – CANCELLED

OperationThree.deinit

OperationOne.start()

OperationTwo – CANCELLED

OperationTwo.deinit


show_choice

 

OperationOne.deinit

PromptUserOperation.deinit

 

I love NSOperation Chaining even more now!

Written by Nick Harris

February 10, 2016 at 6:34 am

Posted in Uncategorized

CloudKit + Core Data + NSOperations – Conclusions

leave a comment »

This is post four of four about CloudKit syncing with Core Data. The original post is here. Sample code is here. You’ll need to enable CloudKit and other entitlements in your own environment to get it to work correctly.

Conclusions

The previous posts includes a ton of code. You can see all of it by getting the project from BitBucket. In this post I’ll highlight what I liked and what I didn’t like. I learned a ton while writing it and would again urge you to heed my warning in the first post about it being unpolished and incomplete. 

CloudKit

CloudKit is incredibly powerful. Once I started really diving in I could tell it was thoughtfully created to support almost any type of app someone would want to create. Getting started was relatively easy and the more I learned I think the architectural decisions about how to get the most out of it became easier. Having pre-made NSOperations that handle all of the networking is very nice, even though I found myself subclassing them. Subscriptions are also incredibly powerful for getting any kind of push notifications and background fetching an app might need.

What I don’t like is that it is so generically built – its a strength and a weakness. From my time playing around and researching I didn’t find any real good best practices guidance which made all of the different ways of utilizing it harder to figure out (and I still think I’m wrong with some) as well as possible edge cases I never thought of.

You’re also betting a lot of usability of your app on your users having CloudKit enabled and having enough storage for your app to function appropriately. Using error codes you can build around that and appropriately inform your users of the problem, but they’re more then likely going to blame you instead of Apple. A casual user who paid $10, $20 maybe even $50 for a productivity app but was then prompted to pony up more money to Apple to make it work is a support nightmare.

I also don’t like that its unknown how Apple will handle changes they need to make to keep CloudKit going forward while handling backward compatibility. Its completely reasonable to think that the next version of CloudKit could deprecate all of the NSOperatoins I’m using in favor of new ones that handle problems they may be having server side. It may not be a big deal to refactor, depending how big the changes are, but that’s still a risk that is out of my hands.

The last thing that concerns me is how I would transfer a users private data to another backend system should I ever need to. The device would have to somehow do this at the great expense of the user experience. Imagine running an app for the first time after it updated in the background and it tells you it needs an hour of processing and all of your bandwidth to transfer the data somewhere else. This exact problem came up as my time on Glassboard was ending. The pricing of Azure SQL Server instances was dropping and the code we were using to support Table storage was beginning to show cracks. We didn’t do this but we could have spun up our new SQL instance and a background instance to slowly move our data from Tables to SQL. Our server code could make the process much more seamless to our users then what I would envision with CloudKit.

That being said, for me personally I would prefer a custom backend. I’m probably more biased then most iOS or Mac developers though since I’ve built a few on my own and worked on a handful of others throughout my career. I’ve built them in Java, Node.JS, C# and even straight JavaScript and classic asp pages years ago. I actually miss working on them. Performance tuning a database and server instances is more interesting to me then tweaking auto layout constraints for a day to get an animation to look nice. Not that I don’t love when a app looks sleek when I show it off, but I’d prefer showing off response time improvements more.

For my side project though? I’ll go with CloudKit. I’m building it for myself so I can easily keep up with any changes without worrying about dealing with the support emails.

NSOperation Chaining

I’ve said it before and I’ll say it again. I LOVE NSOperations! Once I found the pattern for passing data I love chaining them even more. CloudKit’s dependencies between zones and records as well as fetching before modifying make NSOperation dependencies close to a no brainer. I also like how I can separate concerns, encapsulate better and have reusable blocks of code I can chain together in many different ways.

It reminds me of one of my last final exams in college where we needed to make a data transformation diagram and detailed writeup showing how we would process a big chuck of data into its many pieces. I wish I could remember what the actual problem we had to solve was but after taking all 2 hours on the final to write it up I promptly empty my brain of it. Someone on twitter mentioned that the approach also resembles monads in functional programming. Just judging from the wikipedia page I’d have to agree but I haven’t explored functional programming very much yet.

Swift

Frankly my love of Swift just keeps growing. The proof of concept uses protocols and enums heavily. I haven’t taken the time to figure out how I would have accomplished the same things in Objective-C but I’m certain I would like my Swift code better. With swift though I learned:

– I love power of Swift Sets. Having intersect and subtract methods is fantastic. 

– Swift higher order functions are also great. I use flatMap a ton in this code. I probably could have used filter and reduce more as well.

– Finding out I could do inline predicates to find the index of an object was a very happy surprise:

// do we have this record locally? We need to know so we can remove it from the changedCloudKitManagedObjects
if let index = changedCloudKitManagedObjects.indexOf( { $0.recordName == deletedServerRecordID.recordName } ) {
    changedCloudKitManagedObjects.removeAtIndex(index)
}

 

– Casting in Swift is powerful in “if let” statements but even better in flatMap. 

– Guard statements continue to be good for me as well though I found I used them much less in this code then my first attempts at Swift.

– I think Swift is much more readable then Objective-C at this point.

– I still hate retain cycles. Seriously. I hate them. Capture lists are nice and all but I hate the fact that I still have to think about memory management.

Series Conclusion

I hope these posts are helpful. Don’t use them as a blueprint for how to use CloudKit but rather a long winded explanation of one persons adventures. Any feedback would be greatly appreciated!

Written by Nick Harris

February 9, 2016 at 6:54 am

Posted in Uncategorized

CloudKit + Core Data + NSOperations – Syncing

leave a comment »

This is post three of four about CloudKit syncing with Core Data. The original post is here. Sample code is here. You’ll need to enable CloudKit and other entitlements in your own environment to get it to work correctly.

Syncing Both CloudKit and Core Data Changes Together

So far I’ve talked about getting changes from Core Data and using operations to save them to CloudKit. I’ve also talked about using CKSubscriptions to get changes from CloudKit and save them to Core Data. But what happens if changes are made offline? Or if the a remote notification is never delivered (remember they’re not guaranteed)? These situations need to be handled as well and the process to do this is much more involved then anything else I’ve talked about.

I was pleasantly surprised to see that CloudKit operations will pause until the app comes back online. With the dependencies that also means that nothing in the chain will execute as well. Its not a full proof plan and I wouldn’t trust it. I’d be interested to hear what others think. Should the NSOperationQueue remove all pending operations when network connectivity changes are detected? How about when the app moves to the background? Let me know your thoughts.

Offline Deletes

Offline deletes are the most difficult. Once the Core Data persistent store saves the delete the object is gone forever. To handle this I added another entity to my Core Data model called DeletedCloutKitObject:

Deleted Cloud Kit Entity

extension DeletedCloudKitObject {
 
    @NSManaged var recordType: String?
    @NSManaged var recordID: NSData?
 
}
 

class DeletedCloudKitObject: NSManagedObjectCloudKitRecordIDObject {

 

}

 

Previously I wrote about separating the CloudKitRecordIDObject protocol and its extension away from the CloudKitManagedObject protocol. This is the reason why. I still need the functionality of getting a recordID from NSData in Core Data but I don’t want this Core Data entity synced to CloudKit. Because I use the CloudKitManagedObject protocol to handle all of the Core Data to CloudKit changes, these delete entities get filtered out by using flatMap while performing the casting of NSManagedObjects to CloudKitManagedObjects. This is the kind of flexibility I really like with protocols over subclasses!

The idea behind this entity is to keep a temporary store of CloudKitManagedObjects when they get deleted, then clear them all when a save to CloudKit is completed. Any CloudKitManagedObject should be able to do this so I added another function to the CloudKitManagedObject extension:

func addDeletedCloudKitObject() {
        
    if let managedObject = self as? NSManagedObject,
        let managedObjectContext = managedObject.managedObjectContext,
        let recordID = recordID,
        let deletedCloudKitObject = NSEntityDescription.insertNewObjectForEntityForName("DeletedCloudKitObject", inManagedObjectContext: managedObjectContext) as? DeletedCloudKitObject {
            deletedCloudKitObject.recordID = recordID
            deletedCloudKitObject.recordType = recordType
    }
}

This made adding the objects on delete user actions really easy both in the TableViewControllers and DetailViewControllers. Here’s the code for the ObjectTableViewController as an example:

override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {

    if editingStyle == .Delete {

        if let managedObject = fetchedResultsController?.objectAtIndexPath(indexPath) asNSManagedObject {

                

            if let cloudKitManagedObject = managedObject asCloudKitManagedObject {

                cloudKitManagedObject.addDeletedCloudKitObject()

            }

                

            coreDataManager?.mainThreadManagedObjectContext.deleteObject(managedObject)

            coreDataManager?.save()

        }

    }

}

 

Deleting all these entities after a successful save to CloudKit gave me the opportunity to finally use a Core Data batch request. In the past you would need to fetch all of the entities then delete them one at a time. With a batch delete its much simpler. I added this to its own operation so that I could add it to the end of any operation chain:

class ClearDeletedCloudKitObjectsOperation: NSOperation {
    
    let coreDataManager: CoreDataManager
    
    init(coreDataManager: CoreDataManager) {
        
        self.coreDataManager = coreDataManager
        
        super.init()
    }
    
    override func main() {
        
        let managedObjectContext = coreDataManager.createBackgroundManagedContext()
        
        managedObjectContext.performBlockAndWait {
            [unowned self] in
            
            let fetchRequest = NSFetchRequest(entityName: ModelObjectType.DeletedCloudKitObject.rawValue)
            let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
            
            do {
                try managedObjectContext.executeRequest(deleteRequest)
            }
            catch let error as NSError {
                print("Error deleting from CoreData: \(error.localizedDescription)")
            }
            
            self.coreDataManager.saveBackgroundManagedObjectContext(managedObjectContext)
        }
    }
}

Offline Changes

Offline changes are a little easier. The main reason I added the lastUpdate property to my CloudKitManagedObjects was to know when something had changed after a certain point in time. There is a lastModified property on a CKRecord but I wanted something that I controlled. I also learned that you can have the same property in two different protocols. It makes sense but I was afraid the compiler might report some sort of conflict but its not an issue. Its why my CTBRootManagedObject protocol (used in the UI code) and my CloudKitManagedObject both have lastUpdate.

I decided the point in time to check against is after a successful modify CloudKit operation. I decided to also save this time stamp in NSUserDefaults but I added it to the CloudKitManager so that I could get to it from any operation by dependency injecting the CloudKitManager:

var lastCloudKitSyncTimestamp: NSDate {

        

    get {

        if let lastCloudKitSyncTimestamp = NSUserDefaults.standardUserDefaults().objectForKey(CloudKitUserDefaultKeys.LastCloudKitSyncTimestamp.rawValueasNSDate {

            return lastCloudKitSyncTimestamp

        }

        else {

            return NSDate.distantPast()

        }

    }

    set {

        NSUserDefaults.standardUserDefaults().setObject(newValue, forKey: CloudKitUserDefaultKeys.LastCloudKitSyncTimestamp.rawValue)

    }

}

 

The Process

Heres the full process now that I have a way of tracking offline changes and deletes:

1. Fetch changes from both Core Data and CloudKit
2. Process the changes from both Core Data and CloudKit
3. Fetch records from CloudKit that need to be modified with local changes
4. Modify records in CloudKit
5. Save changes to Core Data
6. Clear local cache of deleted objects 

Fetching Changes

Fetching changes from CloudKit is done using the FetchRecordChangesForZoneOperation I wrote about previously. Fetching local changes is done using a new FectchOfflineChangesFromCoreDataOperations:

class FetchOfflineChangesFromCoreDataOperation: NSOperation {
    
    var updatedManagedObjects: [NSManagedObjectID]
    var deletedRecordIDs: [CKRecordID]
    
    private let coreDataManager: CoreDataManager
    private let cloudKitManager: CloudKitManager
    private let entityNames: [String]
    
    init(coreDataManager: CoreDataManager, cloudKitManager: CloudKitManager, entityNames: [String]) {
        
        self.coreDataManager = coreDataManager
        self.cloudKitManager = cloudKitManager
        self.entityNames = entityNames
        
        self.updatedManagedObjects = []
        self.deletedRecordIDs = []
        
        super.init()
    }
 
    override func main() {
        
        let managedObjectContext = coreDataManager.createBackgroundManagedContext()
        
        managedObjectContext.performBlockAndWait {
            [unowned self] in
            
            let lastCloudKitSyncTimestamp = self.cloudKitManager.lastCloudKitSyncTimestamp
            
            for entityName in self.entityNames {
                self.fetchOfflineChangesForEntityName(entityName, lastCloudKitSyncTimestamp: lastCloudKitSyncTimestamp, managedObjectContext: managedObjectContext)
            }
            
            self.deletedRecordIDs = self.fetchDeletedRecordIDs(managedObjectContext)
        }
    }
    
    func fetchOfflineChangesForEntityName(entityName: String, lastCloudKitSyncTimestamp: NSDate, managedObjectContext: NSManagedObjectContext) {
        
        let fetchRequest = NSFetchRequest(entityName: entityName)
        fetchRequest.predicate = NSPredicate(format: "lastUpdate > %@", lastCloudKitSyncTimestamp)
        
        do {
            let fetchResults = try managedObjectContext.executeFetchRequest(fetchRequest)
            let managedObjectIDs = fetchResults.flatMap() { ($0 as? NSManagedObject)?.objectID
            
            updatedManagedObjects.appendContentsOf(managedObjectIDs)
        }
        catch let error as NSError {
            print("Error fetching from CoreData: \(error.localizedDescription)")
        }
    }
    
    func fetchDeletedRecordIDs(managedObjectContext: NSManagedObjectContext) -> [CKRecordID] {
        
        let fetchRequest = NSFetchRequest(entityName: ModelObjectType.DeletedCloudKitObject.rawValue)
        
        do {
            let fetchResults = try managedObjectContext.executeFetchRequest(fetchRequest)
            return fetchResults.flatMap() { ($0 as? DeletedCloudKitObject)?.cloudKitRecordID()  }
        }
        catch let error as NSError {
            print("Error fetching from CoreData: \(error.localizedDescription)")
        }
        
        return []
    }
}

 

The operation takes an array of entity names to compare lastUpdate properties on. To support this I added a static array of CloudKitManagedObject types to the ModelObjectType enum:

enum ModelObjectType: String {

    case Car = “Car”

    case Truck = “Truck”

    case Bus = “Bus”

    case Note = “Note”

    case DeletedCloudKitObject = “DeletedCloudKitObject”

    

    static let allCloudKitModelObjectTypes = [

        ModelObjectType.Car.rawValue,

        ModelObjectType.Truck.rawValue,

        ModelObjectType.Bus.rawValue,

        ModelObjectType.Note.rawValue

    ]

}

 

From there I can loop through all the entityNames and create fetch requests comparing lastUpdate to what I have stored in the lastCloudKitSyncTimestamp. Notice that I’m again saving off NSManagedObjectIDs instead of the objects themselves. In this case the managedObjectContext I’m using for the fetch will almost always be gone when the next operation executes.

Deletes with the DeletedCloudKitObject are easy. Just fetch them all and get their CKRecordID using the CloudKitRecordIDObject protocol extension.

Process Changes

I assumed that figuring out which changes need to go to CloudKit and which need to be saved in Core Data was going to be the most code intensive operation I was going to create. With the help of Sets in Swift it turned out to be less the 200 lines of code!

The main reason for that in this example though are the business rules around conflicts. Deletes always win. Last in always wins. More complex rules would mean more complex conflict resolution code.

class ProcessSyncChangesOperation: NSOperation {
 
    var preProcessLocalChangedObjectIDs: [NSManagedObjectID]
    var preProcessLocalDeletedRecordIDs: [CKRecordID]
    var preProcessServerChangedRecords: [CKRecord]
    var preProcessServerDeletedRecordIDs: [CKRecordID]
    
    var postProcessChangesToCoreData: [CKRecord]
    var postProcessChangesToServer: [CKRecord]
    var postProcessDeletesToCoreData: [CKRecordID]
    var postProcessDeletesToServer: [CKRecordID]
    
    private let coreDataManager: CoreDataManager
    private var changedCloudKitManagedObjects: [CloudKitManagedObject]
    
    init(coreDataManager: CoreDataManager) {
        
        self.coreDataManager = coreDataManager
        
        self.preProcessLocalChangedObjectIDs = []
        self.preProcessLocalDeletedRecordIDs = []
        self.preProcessServerChangedRecords = []
        self.preProcessServerDeletedRecordIDs = []
        
        self.postProcessChangesToCoreData = []
        self.postProcessChangesToServer = []
        self.postProcessDeletesToCoreData = []
        self.postProcessDeletesToServer = []
        
        self.changedCloudKitManagedObjects = []
        
        super.init()
    }
    
    override func main() {
        
        let managedObjectContext = coreDataManager.createBackgroundManagedContext()
        
        managedObjectContext.performBlockAndWait() {
            [unowned self] in
            
            print("------------------------------------------")
            print("preProcessLocalChangedObjectIDs: \(self.preProcessLocalChangedObjectIDs.count)")
            print("preProcessLocalDeletedRecordIDs: \(self.preProcessLocalDeletedRecordIDs.count)")
            print("preProcessServerChangedRecords: \(self.preProcessServerChangedRecords.count)")
            print("preProcessServerDeletedRecordIDs: \(self.preProcessServerDeletedRecordIDs.count)")
            print("------------------------------------------")
            
            // first we need CloudKitManagedObjects from NSManagedObjectIDs
            self.changedCloudKitManagedObjects = self.coreDataManager.fetchCloudKitManagedObjects(managedObjectContext, managedObjectIDs: self.preProcessLocalChangedObjectIDs)
            
            // deletes are the first thing we should process
            // anything deleted on the server should be removed from any local changes
            // anything deleted local should be removed from any server changes
            self.processServerDeletions(managedObjectContext)
            self.processLocalDeletions()
            
            // next process the conflicts
            self.processConflicts(managedObjectContext)
            
            // anything left in changedCloudKitManagedObjects needs to be added to postProcessChangesToServer
            let changedLocalRecords = self.changedCloudKitManagedObjects.flatMap { $0.managedObjectToRecord(nil) }
            self.postProcessChangesToServer.appendContentsOf(changedLocalRecords)
            
            // anything left in preProcessServerChangedRecords needs to be added to postProcessChangesToCoreData
            self.postProcessChangesToCoreData.appendContentsOf(self.preProcessServerChangedRecords)
            
            print("postProcessChangesToServer: \(self.postProcessChangesToServer.count)")
            print("postProcessDeletesToServer: \(self.postProcessDeletesToServer.count)")
            print("postProcessChangesToCoreData: \(self.postProcessChangesToCoreData.count)")
            print("postProcessDeletesToCoreData: \(self.postProcessDeletesToCoreData.count)")
            print("------------------------------------------")
            
            self.coreDataManager.saveBackgroundManagedObjectContext(managedObjectContext)
        }
    }
    
    // MARK: Process Deleted Objects
    func processServerDeletions(managedObjectContext: NSManagedObjectContext) {
 
        // anything deleted on the server needs to be removed from local change objects
        // and then added to the postProcessDeletesToCoreData array
        for deletedServerRecordID in preProcessServerDeletedRecordIDs {
            
            // do we have this record locally? We need to know so we can remove it from the changedCloudKitManagedObjects
            if let index = changedCloudKitManagedObjects.indexOf( { $0.recordName == deletedServerRecordID.recordName } ) {
                changedCloudKitManagedObjects.removeAtIndex(index)
            }
            
            // make sure to add it to the postProcessDeletesToCoreData array so we delete it from core data
            postProcessDeletesToCoreData.append(deletedServerRecordID)
        }
    }
    
    func processLocalDeletions() {
        
        // anything deleted locally needs to be removed from the server change objects
        // and also added to the postProcessDeletesToServer array
        
        for deletedLocalRecordID in preProcessLocalDeletedRecordIDs {
            
            if let index = preProcessServerChangedRecords.indexOf( { $0.recordID.recordName == deletedLocalRecordID.recordName} ) {
                preProcessServerChangedRecords.removeAtIndex(index)
            }
            
            // make sure to add it to the
            postProcessDeletesToServer.append(deletedLocalRecordID)
        }
    }
    
    // MARK: Process Conflicts
    func processConflicts(managedObjectContext: NSManagedObjectContext) {
        
        // make sets of the recordNames for both local and server changes
        let changedLocalRecordNamesArray = changedCloudKitManagedObjects.flatMap { $0.recordName }
        let changedServerRecordNamesArray = preProcessServerChangedRecords.flatMap { $0.recordID.recordName }
        let changedLocalRecordNamesSet = Set(changedLocalRecordNamesArray)
        let changedServerRecordNamesSet = Set(changedServerRecordNamesArray)
        
        // the interset of the sets are the recordNames we need to resolve conflicts with
        let conflictRecordNameSet = changedLocalRecordNamesSet.intersect(changedServerRecordNamesSet)
        
        for recordName in conflictRecordNameSet {
            resolveConflict(recordName, managedObjectContext: managedObjectContext)
        }
    }
    
    func resolveConflict(recordName: String, managedObjectContext: NSManagedObjectContext) {
        
        // only do the comparison if we have both objects. If we don't that's really bad
        guard let serverChangedRecordIndex = preProcessServerChangedRecords.indexOf( { $0.recordID.recordName == recordName } ),
            let localChangedObjectIndex = changedCloudKitManagedObjects.indexOf( { $0.recordName == recordName } ) else {
                fatalError("Could not find either the server record or local managed object to compare in conflict")
        }
        
        // get the objects from their respective arrays
        let serverChangedRecord = preProcessServerChangedRecords[serverChangedRecordIndex]
        let localChangedObject = changedCloudKitManagedObjects[localChangedObjectIndex]
        
        // also would be really bad if either of them don't have a lastUpdate property
        guard let serverChangedRecordLastUpdate = serverChangedRecord["lastUpdate"] as? NSDate,
              let localChangedObjectLastUpdate = localChangedObject.lastUpdate else {
            fatalError("Could not find either the server record or local managed object lastUpdate property to compare in conflict")
        }
    
        // we need to remove the change from their respective preProcess arrays so they don't end up there later in the process
        preProcessServerChangedRecords.removeAtIndex(serverChangedRecordIndex)
        changedCloudKitManagedObjects.removeAtIndex(localChangedObjectIndex)
        
        // finally we check which time stamp is newer
        if serverChangedRecordLastUpdate.compare(localChangedObjectLastUpdate) == NSComparisonResult.OrderedDescending {
            
            // server wins - add the record to those that will go to core data
            print("CONFLICT: \(recordName) - SERVER WINS. UPDATE COREDATA")
            postProcessChangesToCoreData.append(serverChangedRecord)
            
        } else if serverChangedRecordLastUpdate.compare(localChangedObjectLastUpdate) == NSComparisonResult.OrderedAscending {
            
            // local wins - add the NSManagedObjectID to those that will go to the server
            print("CONFLICT: \(recordName) - LOCAL WINS. UPDATE CLOUDKIT")
            postProcessChangesToServer.append(localChangedObject.managedObjectToRecord(serverChangedRecord))
            
        }
        else {
            // they're the same - we can just ignore these changes (curious how they would be the same ever though)
            print("CONFLICT: \(recordName) - SAME!! Will ignore")
        }
    }
}

 

The process in this operation:

1. Fetch all the NSManagedObjects I’ll need using my local managedObjectContext
2. Removed any object that was deleted either locally or in CloudKit from any changes made locally or in CloudKit. Deletes always wins.
3. Figure out which objects were updated both locally and in CloudKit.
4. Resolve those conflicts by using the lastUpdate timestamp. Last in always wins.
5. Move any local changes or CloudKit changes to their appropriate post process array for other operations in the chain. 

The first thing I’ll point out is that with Swift you can add a predicate for indexOf inline:

if let index = changedCloudKitManagedObjects.indexOf( { $0.recordName == deletedServerRecordID.recordName } ) {
    changedCloudKitManagedObjects.removeAtIndex(index)
}

 

The next is to again point out the power of Sets in swift compared to Objective-C. They’re much more powerful! The inclusion of the intersect function is fantastic.

// the interset of the sets are the recordNames we need to resolve conflicts with
let conflictRecordNameSet = changedLocalRecordNamesSet.intersect(changedServerRecordNamesSet)

 

Resolving any conflicts becomes pretty mundane once you have both objects. Whichever object has the newer lastUpdate timestamp wins. Obviously this assumes that the timestamp generated on different devices is correct instead of letting the server set it, but I think this is a fair assumption.

We now know which changes need to be saved where. The rest of the process relies on operations we already have but with some minor tweaks.

Fetch Modified Records from CloudKit

The FetchRecordsForModifiedObjectsOperation needs a little tweaking. First it needs a property for the CKRecords to fetch that can be set in a transfer operation. If that property is set then it should use it to set the recordIDs to fetch:

class FetchRecordsForModifiedObjectsOperation: CKFetchRecordsOperation {
 
    var fetchedRecords: [CKRecordID : CKRecord]?
    var preFetchModifiedRecords: [CKRecord]?
    private let coreDataManager: CoreDataManager
    private let modifiedManagedObjectIDs: [NSManagedObjectID]?
    
    init(coreDataManager: CoreDataManager, modifiedManagedObjectIDs: [NSManagedObjectID]) {
        
        self.coreDataManager = coreDataManager
        self.modifiedManagedObjectIDs = modifiedManagedObjectIDs
        
        super.init()
        
        // setup the CKFetchRecordsOperation blocks
        setOperationBlocks()
    }
    
    init (coreDataManager: CoreDataManager) {
        self.coreDataManager = coreDataManager
        self.modifiedManagedObjectIDs = nil
        super.init()
    }
    
    override func main() {
        
        print("FetchRecordsForModifiedObjectsOperation.main")
        
        let managedObjectContext = coreDataManager.createBackgroundManagedContext()
        
        managedObjectContext.performBlockAndWait {
            
            if let modifiedManagedObjectIDs = self.modifiedManagedObjectIDs {
                let modifiedCloudKitObjects = self.coreDataManager.fetchCloudKitManagedObjects(managedObjectContext, managedObjectIDs: modifiedManagedObjectIDs)
                self.recordIDs = modifiedCloudKitObjects.flatMap { $0.cloudKitRecordID() }
            }
            else if let preFetchModifiedRecords = self.preFetchModifiedRecords {
                self.recordIDs = preFetchModifiedRecords.flatMap { $0.recordID }
            }
            
            super.main()
        }
    }
    
    private func setOperationBlocks() {
        
        fetchRecordsCompletionBlock = {
            [unowned self]
            (fetchedRecords: [CKRecordID : CKRecord]?, error: NSError?) -> Void in
            
            self.fetchedRecords = fetchedRecords
            print("FetchRecordsForModifiedObjectsOperation.fetchRecordsCompletionBlock - fetched \(fetchedRecords?.count) records")
        }
    }
}
 

Send Modifications To Cloud Kit

The ModifyRecordsFromManagedObjects operation also needs a few simple changes. Initially it had NSManagedObjectIDs passed in based on changes to the mainThreadManagedObjectContext. The work for fetching and creating CKRecords was done in the ProcessSyncChangesOperation so I added a new CKRecord array that can be set in the operation chain. To be honest, I’m not happy with how this operation turned out and will probably refactor it a little differently in a production app.

class ModifyRecordsFromManagedObjectsOperation: CKModifyRecordsOperation {
    
    var fetchedRecordsToModify: [CKRecordID : CKRecord]?
    var preModifiedRecords: [CKRecord]?
    private let modifiedManagedObjectIDs: [NSManagedObjectID]?
    private let coreDataManager: CoreDataManager
    private let cloudKitManager: CloudKitManager
    
    init(coreDataManager: CoreDataManager, cloudKitManager: CloudKitManager) {
        
        self.coreDataManager = coreDataManager
        self.cloudKitManager = cloudKitManager
        self.modifiedManagedObjectIDs = nil
        self.fetchedRecordsToModify = nil
        self.preModifiedRecords = nil
        super.init()
    }
    
    init(coreDataManager: CoreDataManager, cloudKitManager: CloudKitManager, modifiedManagedObjectIDs: [NSManagedObjectID], deletedRecordIDs: [CKRecordID]) {
        
        // save off the modified objects and the fetch operation
        self.coreDataManager = coreDataManager
        self.cloudKitManager = cloudKitManager
        self.modifiedManagedObjectIDs = modifiedManagedObjectIDs
        self.fetchedRecordsToModify = nil
        
        super.init()
        
        // get the recordIDs for deleted objects
        recordIDsToDelete = deletedRecordIDs
    }
    
    override func main() {
        
        print("ModifyRecordsFromManagedObjectsOperation.main")
        
        // setup the CKFetchRecordsOperation blocks
        setOperationBlocks()
        
        let managedObjectContext = coreDataManager.createBackgroundManagedContext()
        
        managedObjectContext.performBlockAndWait {
            // before we run we need to map the records we fetched in the dependent operation into our records to save
            let modifiedRecords: [CKRecord]
            if let modifiedManagedObjectIDs = self.modifiedManagedObjectIDs {
                modifiedRecords = self.modifyFetchedRecordsIDs(managedObjectContext, modifiedManagedObjectIDs: modifiedManagedObjectIDs)
            } else if let preModifiedRecords = self.preModifiedRecords {
                modifiedRecords = preModifiedRecords
            }
            else {
                modifiedRecords = []
            }
            
            if modifiedRecords.count > 0 {
                if self.recordsToSave == nil {
                    self.recordsToSave = modifiedRecords
                }
                else {
                    self.recordsToSave?.appendContentsOf(modifiedRecords)
                }
            }
            
            print("ModifyRecordsFromManagedObjectsOperation.recordsToSave: \(self.recordsToSave)")
            print("ModifyRecordsFromManagedObjectsOperation.recordIDsToDelete: \(self.recordIDsToDelete)")
            
            super.main()
        }
    }
    
    private func modifyFetchedRecordsIDs(managedObjectContext: NSManagedObjectContext, modifiedManagedObjectIDs: [NSManagedObjectID]) -> [CKRecord] {
        
        guard let fetchedRecords = fetchedRecordsToModify else {
            return []
        }
        
        var modifiedRecords: [CKRecord] = []
        
        let modifiedManagedObjects = coreDataManager.fetchCloudKitManagedObjects(managedObjectContext, managedObjectIDs: modifiedManagedObjectIDs)
        for cloudKitManagedObject in modifiedManagedObjects {
            if let recordID = cloudKitManagedObject.cloudKitRecordID(),
               let record = fetchedRecords[recordID] {
                modifiedRecords.append(cloudKitManagedObject.managedObjectToRecord(record))
            }
        }
        
        return modifiedRecords
    }
 
    private func setOperationBlocks() {
        
        perRecordCompletionBlock = {
            (record:CKRecord?, error:NSError?) -> Void in
            
            if let error = error {
                print("!!! ERROR !!! - ModifyRecordsFromManagedObjectsOperation.perRecordCompletionBlock: \(error)")
            }
            else {
                print("Record modification successful for recordID: \(record?.recordID)")
            }
        }
        
        modifyRecordsCompletionBlock = {
            [unowned self]
            (savedRecords: [CKRecord]?, deletedRecords: [CKRecordID]?, error:NSError?) -> Void in
            
            if let error = error {
                print("!!! ERROR !!! - ModifyRecordsFromManagedObjectsOperation.modifyRecordsCompletionBlock: \(error)")
            }
            else if let deletedRecords = deletedRecords {
                for recordID in deletedRecords {
                    print("DELETED: \(recordID)")
                }
            }
            self.cloudKitManager.lastCloudKitSyncTimestamp = NSDate()
            print("ModifyRecordsFromManagedObjectsOperation modifyRecordsCompletionBlock")
        }
    }
}

The operation for saving changes to Core Data did not need any modifications and I already covered cleaning up the DeletedCloudKitObjects entities.

Chaining the Operations

Chaining the 13 sync operations is almost 100 lines of code but is pretty straight forward:

private func queueFullSyncOperations() {
        
    // 1. Fetch all the changes both locally and from each zone
    let fetchOfflineChangesFromCoreDataOperation = FetchOfflineChangesFromCoreDataOperation(coreDataManager: coreDataManager, cloudKitManager: self, entityNames: ModelObjectType.allCloudKitModelObjectTypes)
    let fetchCarZoneChangesOperation = FetchRecordChangesForCloudKitZoneOperation(cloudKitZone: CloudKitZone.CarZone)
    let fetchTruckZoneChangesOperation = FetchRecordChangesForCloudKitZoneOperation(cloudKitZone: CloudKitZone.TruckZone)
    let fetchBusZoneChangesOperation = FetchRecordChangesForCloudKitZoneOperation(cloudKitZone: CloudKitZone.BusZone)
        
    // 2. Process the changes after transfering
    let processSyncChangesOperation = ProcessSyncChangesOperation(coreDataManager: coreDataManager)
    let transferDataToProcessSyncChangesOperation = NSBlockOperation {
        [unowned processSyncChangesOperation, unowned fetchOfflineChangesFromCoreDataOperation, unowned fetchCarZoneChangesOperation, unowned fetchTruckZoneChangesOperation, unowned fetchBusZoneChangesOperation] in
            
        processSyncChangesOperation.preProcessLocalChangedObjectIDs.appendContentsOf(fetchOfflineChangesFromCoreDataOperation.updatedManagedObjects)
        processSyncChangesOperation.preProcessLocalDeletedRecordIDs.appendContentsOf(fetchOfflineChangesFromCoreDataOperation.deletedRecordIDs)
            
        processSyncChangesOperation.preProcessServerChangedRecords.appendContentsOf(fetchCarZoneChangesOperation.changedRecords)
        processSyncChangesOperation.preProcessServerChangedRecords.appendContentsOf(fetchTruckZoneChangesOperation.changedRecords)
        processSyncChangesOperation.preProcessServerChangedRecords.appendContentsOf(fetchBusZoneChangesOperation.changedRecords)
            
        processSyncChangesOperation.preProcessServerDeletedRecordIDs.appendContentsOf(fetchCarZoneChangesOperation.deletedRecordIDs)
        processSyncChangesOperation.preProcessServerDeletedRecordIDs.appendContentsOf(fetchTruckZoneChangesOperation.deletedRecordIDs)
        processSyncChangesOperation.preProcessServerDeletedRecordIDs.appendContentsOf(fetchBusZoneChangesOperation.deletedRecordIDs)
    }
        
    // 3. Fetch records from the server that we need to change
    let fetchRecordsForModifiedObjectsOperation = FetchRecordsForModifiedObjectsOperation(coreDataManager: coreDataManager)
    let transferDataToFetchRecordsOperation = NSBlockOperation {
        [unowned fetchRecordsForModifiedObjectsOperation, unowned processSyncChangesOperation] in
            
        fetchRecordsForModifiedObjectsOperation.preFetchModifiedRecords = processSyncChangesOperation.postProcessChangesToServer
    }
        
    // 4. Modify records in the cloud
    let modifyRecordsFromManagedObjectsOperation = ModifyRecordsFromManagedObjectsOperation(coreDataManager: coreDataManager, cloudKitManager: self)
    let transferDataToModifyRecordsOperation = NSBlockOperation {
        [unowned fetchRecordsForModifiedObjectsOperation, unowned modifyRecordsFromManagedObjectsOperation, unowned processSyncChangesOperation] in
            
        if let fetchedRecordsDictionary = fetchRecordsForModifiedObjectsOperation.fetchedRecords {
            modifyRecordsFromManagedObjectsOperation.fetchedRecordsToModify = fetchedRecordsDictionary
        }
        modifyRecordsFromManagedObjectsOperation.preModifiedRecords = processSyncChangesOperation.postProcessChangesToServer
            
        // also set the recordIDsToDelete from what we processed
        modifyRecordsFromManagedObjectsOperation.recordIDsToDelete = processSyncChangesOperation.postProcessDeletesToServer
    }
        
    // 5. Modify records locally
    let saveChangedRecordsToCoreDataOperation = SaveChangedRecordsToCoreDataOperation(coreDataManager: coreDataManager)
    let transferDataToSaveChangesToCoreDataOperation = NSBlockOperation {
        [unowned saveChangedRecordsToCoreDataOperation, unowned processSyncChangesOperation] in
            
        saveChangedRecordsToCoreDataOperation.changedRecords = processSyncChangesOperation.postProcessChangesToCoreData
        saveChangedRecordsToCoreDataOperation.deletedRecordIDs = processSyncChangesOperation.postProcessDeletesToCoreData
    }
        
    // 6. Delete all of the DeletedCloudKitObjects
    let clearDeletedCloudKitObjectsOperation = ClearDeletedCloudKitObjectsOperation(coreDataManager: coreDataManager)
        
    // set dependencies
    // 1. transfering all the fetched data to process for conflicts
    transferDataToProcessSyncChangesOperation.addDependency(fetchOfflineChangesFromCoreDataOperation)
    transferDataToProcessSyncChangesOperation.addDependency(fetchCarZoneChangesOperation)
    transferDataToProcessSyncChangesOperation.addDependency(fetchTruckZoneChangesOperation)
    transferDataToProcessSyncChangesOperation.addDependency(fetchBusZoneChangesOperation)
        
    // 2. processing the data onces its transferred
    processSyncChangesOperation.addDependency(transferDataToProcessSyncChangesOperation)
        
    // 3. fetching records changed local
    transferDataToFetchRecordsOperation.addDependency(processSyncChangesOperation)
    fetchRecordsForModifiedObjectsOperation.addDependency(transferDataToFetchRecordsOperation)
        
    // 4. modifying records in CloudKit
    transferDataToModifyRecordsOperation.addDependency(fetchRecordsForModifiedObjectsOperation)
    modifyRecordsFromManagedObjectsOperation.addDependency(transferDataToModifyRecordsOperation)
        
    // 5. modifying records in CoreData
    transferDataToSaveChangesToCoreDataOperation.addDependency(processSyncChangesOperation)
    saveChangedRecordsToCoreDataOperation.addDependency(transferDataToModifyRecordsOperation)
        
    // 6. clear the deleteCloudKitObjects
    clearDeletedCloudKitObjectsOperation.addDependency(saveChangedRecordsToCoreDataOperation)
        
    // add operations to the queue
    operationQueue.addOperation(fetchOfflineChangesFromCoreDataOperation)
    operationQueue.addOperation(fetchCarZoneChangesOperation)
    operationQueue.addOperation(fetchTruckZoneChangesOperation)
    operationQueue.addOperation(fetchBusZoneChangesOperation)
    operationQueue.addOperation(transferDataToProcessSyncChangesOperation)
    operationQueue.addOperation(processSyncChangesOperation)
    operationQueue.addOperation(transferDataToFetchRecordsOperation)
    operationQueue.addOperation(fetchRecordsForModifiedObjectsOperation)
    operationQueue.addOperation(transferDataToModifyRecordsOperation)
    operationQueue.addOperation(modifyRecordsFromManagedObjectsOperation)
    operationQueue.addOperation(transferDataToSaveChangesToCoreDataOperation)
    operationQueue.addOperation(saveChangedRecordsToCoreDataOperation)
    operationQueue.addOperation(clearDeletedCloudKitObjectsOperation)
}

 

The transferDataToProcessSyncChangesOperation is dependent on the 4 fetch operations. My NSOperationQueue is setup with maxConcurrentOperationCount set to 1 so its basically a serial queue but I can easily change that so that all the fetch operations could happen at the same time if the system has the resources to do it. Another example of the power of NSOperations!

Next Post – Conclusions > 

 

Written by Nick Harris

February 9, 2016 at 6:53 am

Posted in Uncategorized

CloudKit + Core Data + NSOperations – Saving Changes

with one comment

This is post two of four about CloudKit syncing with Core Data. The original post is here. Sample code is here. You’ll need to enable CloudKit and other entitlements in your own environment to get it to work correctly.

Saving Core Data Changes to CloudKit

Saving core data changes to CloudKit was the principle reason I did the proof of concept. The work is done in a series of either CloudKit operation subclasses, custom operations and data transfer block operations. The real power to how these work though are a couple of protocols I created. The first is the CloudKitRecordIDObject protocol:

@objc protocol CloudKitRecordIDObject {
    var recordID: NSData? { get set }
}

The other is the CloudKitManagedObject protocol:

@objc protocol CloudKitManagedObject: CloudKitRecordIDObject {
    var lastUpdate: NSDate? { get set }
    var recordName: String? { get set }
    var recordType: String { get }
    func managedObjectToRecord(record: CKRecord?) -> CKRecord
    func updateWithRecord(record: CKRecord)
}

The protocols include the properties I need to do things with CloudKit along with a couple of functions to create a CKRecord and update with a CKRecord. I’ll explain the reason why I split the CloudKitRecordIDObject protocol out from the CloudKitManagedObject one when I talk about syncing offline changes. 

One of the tricks I learned from this post by Natasha the Robot is that you can add extensions to Swift protocols. That’s really awesome. It means you can create a default function that all objects that implement the protocol can use. I struggled with that for a while. I had been duplicating the same function on all my objects that implemented a protocol even though it was copy/paste code. Now I have only one implementation everyone can use. I do this for both of these protocols.

The CloudKitRecordIDObject protocol extension will unarchive the CKRecordID if it exists in Core Data:

extension CloudKitRecordIDObject {
    func cloudKitRecordID() -> CKRecordID? {
        guard let recordID = recordID else {
            return nil
        }
        
        return NSKeyedUnarchiver.unarchiveObjectWithData(recordID) as? CKRecordID
    }
}

The CloudKitManagedObject protocol extension will create a CKRecord:

extension CloudKitManagedObject {
    func cloudKitRecord(record: CKRecord?, parentRecordZoneID: CKRecordZoneID?) -> CKRecord {
        
        if let record = record {
            return record
        }
 
        var recordZoneID: CKRecordZoneID
        if parentRecordZoneID != .None {
            recordZoneID = parentRecordZoneID!
        }
        else {
            guard let cloudKitZone = CloudKitZone(recordType: recordType) else {
                fatalError("Attempted to create a CKRecord with an unknown zone")
            }
            
            recordZoneID = cloudKitZone.recordZoneID()
        }
        
        let uuid = NSUUID()
        let recordName = recordType + "." + uuid.UUIDString
        let recordID = CKRecordID(recordName: recordName, zoneID: recordZoneID)
        
        return CKRecord(recordType: recordType, recordID: recordID)
    }
}

The reason for the optional CKRecord and CKRecordZoneID will be more apparent in a bit. You’ll notice these functions are not declared within the protocol. If they were the compiler would insist on them being included in any class that implements the protocol. With them just in the extension they’re still available but they don’t require an implementation in each class.

The CloudKitManagedObject protocol does includes a function called managedObjectToRecord. This needs to be implemented in any NSManagedObject subclass that implements the CloudKitManagedObject protocol since each object would theoretically have different properties. That’s not the case in this app but you get the point. The same is true with the updateWithRecord function that also exists in the protocol.

Generating NSManagedObject subclasses in Xcode now gives you two files. One that’s a <subclass>+CoreDataProperties.swift class that holds the property declarations and another <subclass>.swift file that you can add any custom code you want into. Its very similar to what mogenerator does. I’ve stopped using mogenerator in favor of the built in Xcode functionality. 

With that in mind, here’s what the Car.swift file looks like:

class Car: NSManagedObject, CTBRootManagedObject, CloudKitManagedObject {
    
    var recordType: String { return  ModelObjectType.Car.rawValue }
    
    func managedObjectToRecord(record: CKRecord?) -> CKRecord {
        guard let name = name,
              let added = added,
              let lastUpdate = lastUpdate else {
            fatalError("Required properties for record not set")
        }
        
        let carRecord = cloudKitRecord(record, parentRecordZoneID: nil)
        
        recordName = carRecord.recordID.recordName
        recordID = NSKeyedArchiver.archivedDataWithRootObject(carRecord.recordID)
        
        carRecord["name"] = name
        carRecord["added"] = added
        carRecord["lastUpdate"] = lastUpdate
        
        return carRecord
    }
 
    func updateWithRecord(record: CKRecord) {
        name = record["name"] as? String
        added = record["added"] as? NSDate
        lastUpdate = record["lastUpdate"] as? NSDate
        recordName = record.recordID.recordName
        recordID = NSKeyedArchiver.archivedDataWithRootObject(record.recordID)
    }
}

Bus and Truck in the proof of concept are basically the same. Note is different though:

class Note: NSManagedObject, CloudKitManagedObject {
    
    var recordType: String { return  ModelObjectType.Note.rawValue }
 
    func managedObjectToRecord(record: CKRecord?) -> CKRecord {
        guard let text = text,
            let added = added,
            let lastUpdate = lastUpdate else {
                fatalError("Required properties for record not set")
        }
        
        var noteRecord: CKRecord
        if let record = record {
            noteRecord = record
        }
        else {
            noteRecord = createNoteRecord()
        }
        
        noteRecord["text"] = text
        noteRecord["added"] = added
        noteRecord["lastUpdate"] = lastUpdate
        
        return noteRecord
    }
    
    func updateWithRecord(record: CKRecord) {
        text = record["text"] as? String
        added = record["added"] as? NSDate
        lastUpdate = record["lastUpdate"] as? NSDate
        recordName = record.recordID.recordName
        recordID = NSKeyedArchiver.archivedDataWithRootObject(record.recordID)
        
        if car == .None && truck == .None && bus == .None {
            // need to set the parent object based on the CKReference in the record
            // will include the code for this later in the post
            // setParentObjectFromCloudKitRecord(record)
        }
    }
    
    func createNoteRecord() -> CKRecord {
        // we need to figure out what type of object the parent is
        if let car = car as? CloudKitManagedObject {
            return createNoteRecordWithParent(car)
        }
        else if let truck = truck as? CloudKitManagedObject {
            return createNoteRecordWithParent(truck)
        }
        else if let bus = bus as? CloudKitManagedObject {
            return createNoteRecordWithParent(bus)
        }
        else {
            fatalError("ERROR Have a note without a parent")
        }
    }
    
    func createNoteRecordWithParent(parentObject: CloudKitManagedObject) -> CKRecord {
        guard let cloudKitZone = CloudKitZone(recordType: parentObject.recordType),
              let parentRecordID = parentObject.cloudKitRecordID() else {
            fatalError("ERROR - not enough information to create a CKReference for a Note")
        }
        
        let noteRecord = cloudKitRecord(nil, parentRecordZoneID: cloudKitZone.recordZoneID())
        let parentReference = CKReference(recordID: parentRecordID, action: .DeleteSelf)
        
        recordName = noteRecord.recordID.recordName
        recordID = NSKeyedArchiver.archivedDataWithRootObject(noteRecord.recordID)
        
        noteRecord["parent"] = parentReference
        
        return noteRecord
    }
}

 

Dealing with the Core Data relationship is the reason its so much bigger. A relationship in Core Data is analogous to a CKReference. As I stated previously, I created the Core Data relationship to cascade on delete meaning that if I delete a Car, any and all Notes associated with that Car will also be deleted. CKReference can do the same. If the reference is created with the CKReferenceAction of DeleteSelf, the record will be deleted if its parent is deleted.

The important things to know about CKReference:
1. The records in the reference must be in the same zone
2. The reference is stored as a property on the child record and not the parent (in this case the Note) 

The reason I needed to pass in an optional parentRecordZoneID in the cloudKitRecord method should be more clear now. The Note must be saved in the same zone as its parent record so the code in Note figures out which relationship is not nil and uses that zone as the parent.

That’s it for the object specific code that translates my NSManagedObjects to CKRecords. Relationships/References make them more difficult but its easier to do a little code up front and have the cascading deletes handled for you on both sides.

CoreDataManager Save

With the translation details out of the way, lets look at how the Core Data changes make it to CloudKit. It starts with the save() method on the CoreDataManager. Initially I was passing the actual NSManagedObject sets into the CloudKitManager. This works but its not a good idea. It assumes the mainThreadManagedObjectContext will always be around. If I ever refactored the code to move the calls in my CloudKitManager operations into a background task when the app moves to the background, I would run into problems with the NSManagedObjects having nil properties. Instead it needs to pass NSManagedObjectIDs and let the operations create their own child NSManagedObjectContext to do the work within the operation.

With that said, the code in the CoreDataManager looks like this:

func save() {
        
    let insertedObjects = mainThreadManagedObjectContext.insertedObjects
    let modifiedObjects = mainThreadManagedObjectContext.updatedObjects
    let deletedRecordIDs = mainThreadManagedObjectContext.deletedObjects.flatMap { ($0 as? CloudKitManagedObject)?.cloudKitRecordID() }
        
    if privateObjectContext.hasChanges || mainThreadManagedObjectContext.hasChanges {
            
        self.mainThreadManagedObjectContext.performBlockAndWait {
            [unowned self] in
                
            do {
                try self.mainThreadManagedObjectContext.save()
                self.savePrivateObjectContext()
            }
            catch let error as NSError {
                fatalError("CoreDataManager - SAVE MANAGEDOBJECTCONTEXT FAILED: \(error.localizedDescription)")
            }
                
            let insertedManagedObjectIDs = insertedObjects.flatMap { $0.objectID }
            let modifiedManagedObjectIDs = modifiedObjects.flatMap { $0.objectID }
              
            self.cloudKitManager?.saveChangesToCloudKit(insertedManagedObjectIDs, modifiedManagedObjectIDs: modifiedManagedObjectIDs, deletedRecordIDs: deletedRecordIDs)
        }
    }
}

 

The code first makes a copy of the insertedObjects. Inserted NSManagedObjects have a temporary objectID until they are saved into persistent storage. Using the temporary objectID in the operations will fail. Instead the code makes a copy then uses that after the savePrivateObjectContext() method is complete to get the real objectID of the new objects. ModifiedObjects are also copied off since the set on the mainThreadManagedObjectContext will be empty once the save is complete. The deletedRecordIDs array is created before hand as well since that set will also be empty after the save. After the save completes the code gets the objectID’s it needs and passes them to the saveChangesToCloudKit method.

Save to CloudKit Operations

Saving to CloudKit is done in three operations which perform the following tasks:

1. Create CKRecords for inserted objects
2. Fetch CKRecords from CloudKit for modified objects
3. Send the local changes along with deleted CKRecordIDs to CloudKit

Create CKRecords

The operation to create new CKRecords looks like this:

class CreateRecordsForNewObjectsOperation: NSOperation {
 
    var createdRecords: [CKRecord]?
    private let insertedManagedObjectIDs: [NSManagedObjectID]
    private let coreDataManager: CoreDataManager
    
    init(insertedManagedObjectIDs: [NSManagedObjectID], coreDataManager: CoreDataManager) {
        
        self.insertedManagedObjectIDs = insertedManagedObjectIDs
        self.coreDataManager = coreDataManager
        self.createdRecords = nil
        super.init()
    }
    
    override func main() {
        
        let managedObjectContext = coreDataManager.createBackgroundManagedContext()
        
        if insertedManagedObjectIDs.count > 0 {
            managedObjectContext.performBlockAndWait {
                [unowned self] in
                
                let insertedCloudKitObjects = self.coreDataManager.fetchCloudKitManagedObjects(managedObjectContext, managedObjectIDs: self.insertedManagedObjectIDs)
                self.createdRecords = insertedCloudKitObjects.flatMap() { $0.managedObjectToRecord(.None) }
                self.coreDataManager.saveBackgroundManagedObjectContext(managedObjectContext)
            }
        }
    }
}

 

It uses a helper method on the CoreDataManager which fetches NSManagedObjects on a specific context by their NSManagedObjectID and casts them to CloudKitManagedObjects if they conform to the protocol:

// MARK: Fetch CloudKitManagedObjects from Context by NSManagedObjectID
func fetchCloudKitManagedObjects(managedObjectContext: NSManagedObjectContext, managedObjectIDs: [NSManagedObjectID]) -> [CloudKitManagedObject] {
        
    var cloudKitManagedObjects: [CloudKitManagedObject] = []
    for managedObjectID in managedObjectIDs {
        do {
            let managedObject = try managedObjectContext.existingObjectWithID(managedObjectID)
                
            if let cloudKitManagedObject = managedObject as? CloudKitManagedObject {
                cloudKitManagedObjects.append(cloudKitManagedObject)
            }
        }
        catch let error as NSError {
            print("Error fetching from CoreData: \(error.localizedDescription)")
        }
    }
        
    return cloudKitManagedObjects
}

 

The operation itself is hopefully straight forward:

1. Takes in an array of NSManagedObjectID’s
2. Fetches all the conform to the CloudKitManagedObject protocol
3. Call managedObjectToRecord method on each of the fetched objects using flatMap and assign the resulting array of CKRecords to createdRecords
4. Save the child managedContext since the objects will now have a recordName and recordID

All of this work is based off the CloudKitManagedObject protocol. It doesn’t care that type of object it is as long as it conforms. Nice and generic!

Important Note: The managedObjectContext MUST be created in main() and not init(). This ensures its on the correct thread when the operation executes.

Fetch CKRecords

Modified records first need to be fetched from CloudKit before being updated. This drives me nuts but everything I read and everything I tried leads me to believe its true. I’d love to know if there’s a way to modify objects another way. The extra network call seems like such a waste just to get server specific meta data.

Anyway… the operation for fetching modified records:

class FetchRecordsForModifiedObjectsOperation: CKFetchRecordsOperation {
 
    var fetchedRecords: [CKRecordID : CKRecord]?
    private let coreDataManager: CoreDataManager
    private let modifiedManagedObjectIDs: [NSManagedObjectID]?
    
    init(coreDataManager: CoreDataManager, modifiedManagedObjectIDs: [NSManagedObjectID]) {
        
        self.coreDataManager = coreDataManager
        self.modifiedManagedObjectIDs = modifiedManagedObjectIDs
        
        super.init()
        
        // setup the CKFetchRecordsOperation blocks
        setOperationBlocks()
    }
    
    override func main() {
        
        print("FetchRecordsForModifiedObjectsOperation.main")
        
        let managedObjectContext = coreDataManager.createBackgroundManagedContext()
        
        managedObjectContext.performBlockAndWait {
            
            if let modifiedManagedObjectIDs = self.modifiedManagedObjectIDs {
                let modifiedCloudKitObjects = self.coreDataManager.fetchCloudKitManagedObjects(managedObjectContext, managedObjectIDs: modifiedManagedObjectIDs)
                self.recordIDs = modifiedCloudKitObjects.flatMap { $0.cloudKitRecordID() }
            }
            
            super.main()
        }
    }
    
    private func setOperationBlocks() {
        
        fetchRecordsCompletionBlock = {
            [unowned self]
            (fetchedRecords: [CKRecordID : CKRecord]?, error: NSError?) -> Void in
            
            self.fetchedRecords = fetchedRecords
            print("FetchRecordsForModifiedObjectsOperation.fetchRecordsCompletionBlock - fetched \(fetchedRecords?.count) records")
        }
    }
}

 

It’s a subclass of the CKFetchRecordsOperation for the same reason I mentioned before. It also uses the same CoreDataManager helper function to get the modified objects from their NSManagedObjectID’s.

Modify Records

Modify records is also a subclass of a CloudKit operation – CKModifyRecordsOperation:

class ModifyRecordsFromManagedObjectsOperation: CKModifyRecordsOperation {
    
    var fetchedRecordsToModify: [CKRecordID : CKRecord]?
    private let modifiedManagedObjectIDs: [NSManagedObjectID]?
    private let coreDataManager: CoreDataManager
    private let cloudKitManager: CloudKitManager
    
    init(coreDataManager: CoreDataManager, cloudKitManager: CloudKitManager, modifiedManagedObjectIDs: [NSManagedObjectID], deletedRecordIDs: [CKRecordID]) {
        
        // save off the modified objects and the fetch operation
        self.coreDataManager = coreDataManager
        self.cloudKitManager = cloudKitManager
        self.modifiedManagedObjectIDs = modifiedManagedObjectIDs
        self.fetchedRecordsToModify = nil
        
        super.init()
        
        // get the recordIDs for deleted objects
        recordIDsToDelete = deletedRecordIDs
    }
    
    override func main() {
        
        print("ModifyRecordsFromManagedObjectsOperation.main")
        
        // setup the CKFetchRecordsOperation blocks
        setOperationBlocks()
        
        let managedObjectContext = coreDataManager.createBackgroundManagedContext()
        
        managedObjectContext.performBlockAndWait {
 
            let modifiedRecords: [CKRecord]
 
            if let modifiedManagedObjectIDs = self.modifiedManagedObjectIDs {
                modifiedRecords = self.modifyFetchedRecordsIDs(managedObjectContext, modifiedManagedObjectIDs: modifiedManagedObjectIDs)
            }
            else {
                modifiedRecords = []
            }
            
            if modifiedRecords.count > 0 {
                if self.recordsToSave == nil {
                    self.recordsToSave = modifiedRecords
                }
                else {
                    self.recordsToSave?.appendContentsOf(modifiedRecords)
                }
            }
            
            print("ModifyRecordsFromManagedObjectsOperation.recordsToSave: \(self.recordsToSave)")
            print("ModifyRecordsFromManagedObjectsOperation.recordIDsToDelete: \(self.recordIDsToDelete)")
            
            super.main()
        }
    }
    
    private func modifyFetchedRecordsIDs(managedObjectContext: NSManagedObjectContext, modifiedManagedObjectIDs: [NSManagedObjectID]) -> [CKRecord] {
        
        guard let fetchedRecords = fetchedRecordsToModify else {
            return []
        }
        
        var modifiedRecords: [CKRecord] = []
        
        let modifiedManagedObjects = coreDataManager.fetchCloudKitManagedObjects(managedObjectContext, managedObjectIDs: modifiedManagedObjectIDs)
        for cloudKitManagedObject in modifiedManagedObjects {
            if let recordID = cloudKitManagedObject.cloudKitRecordID(),
               let record = fetchedRecords[recordID] {
                modifiedRecords.append(cloudKitManagedObject.managedObjectToRecord(record))
            }
        }
        
        return modifiedRecords
    }
 
   

private func setOperationBlocks() {

        

        perRecordCompletionBlock = {

            (record:CKRecord?, error:NSError?) -> Void in

            

            if let error = error {

                print(“ModifyRecordsFromManagedObjectsOperation.perRecordCompletionBlock error: \(error)”)

            }

            else {

                print(“Record modification successful for recordID: \(record?.recordID)”)

            }

        }

        

        modifyRecordsCompletionBlock = {

            [unowned self]

            (savedRecords: [CKRecord]?, deletedRecords: [CKRecordID]?, error:NSError?) -> Void in

            

            if let error = error {

                print(“ModifyRecordsFromManagedObjectsOperation.modifyRecordsCompletionBlock error: \(error)”)

            }

            else if let deletedRecords = deletedRecords {

                for recordID in deletedRecords {

                    print(“DELETED: \(recordID)”)

                }

            }

            self.cloudKitManager.lastCloudKitSyncTimestamp = NSDate()

            print(“ModifyRecordsFromManagedObjectsOperation modifyRecordsCompletionBlock”)

        }

    }

}

 

This operation follows along with the patterns used in the others so I won’t go into much detail expect to point out the call the managedObjectToRecord passes in the record that was fetched from CloudKit instead of .None which was used when creating new records.

Chaining the Operations

Finally here’s the saveChangesToCloudKit function which chains the operations together:

func saveChangesToCloudKit(insertedObjects: [NSManagedObjectID], modifiedManagedObjectIDs: [NSManagedObjectID], deletedRecordIDs: [CKRecordID]) {
 
    // create the operations
    let createRecordsForNewObjectsOperation = CreateRecordsForNewObjectsOperation(insertedManagedObjectIDs: insertedObjects, coreDataManager: coreDataManager)
    let fetchModifiedRecordsOperation = FetchRecordsForModifiedObjectsOperation(coreDataManager: coreDataManager, modifiedManagedObjectIDs: modifiedManagedObjectIDs)
    let modifyRecordsOperation = ModifyRecordsFromManagedObjectsOperation(coreDataManager: coreDataManager, cloudKitManager: self, modifiedManagedObjectIDs: modifiedManagedObjectIDs, deletedRecordIDs: deletedRecordIDs)
        
    let transferCreatedRecordsOperation = NSBlockOperation() {
        [unowned modifyRecordsOperation, unowned createRecordsForNewObjectsOperation] in
            
        modifyRecordsOperation.recordsToSave = createRecordsForNewObjectsOperation.createdRecords
    }
        
    let transferFetchedRecordsOperation = NSBlockOperation() {
        [unowned modifyRecordsOperation, unowned fetchModifiedRecordsOperation] in
            
        modifyRecordsOperation.fetchedRecordsToModify = fetchModifiedRecordsOperation.fetchedRecords
    }
        
    // setup dependencies
    transferCreatedRecordsOperation.addDependency(createRecordsForNewObjectsOperation)
    transferFetchedRecordsOperation.addDependency(fetchModifiedRecordsOperation)
    modifyRecordsOperation.addDependency(transferCreatedRecordsOperation)
    modifyRecordsOperation.addDependency(transferFetchedRecordsOperation)
        
    // add the operations to the queue
    operationQueue.addOperation(createRecordsForNewObjectsOperation)
    operationQueue.addOperation(transferCreatedRecordsOperation)
    operationQueue.addOperation(fetchModifiedRecordsOperation)
    operationQueue.addOperation(transferFetchedRecordsOperation)
    operationQueue.addOperation(modifyRecordsOperation)
}

 

This is the first example in the app of a single operation having multiple dependencies. ModifyRecordsOperation needs data from both the createRecordsForNewObjectsOperation and the fetchModifiedRecords operation. The good thing is that the way the dependencies are setup means that both the create and fetch can execute at the same time if the system allows it. That’s the power of NSOperation dependencies!

Saving from CloudKit to Core Data using CKSubscriptions

I mentioned earlier that I decided to create a CKSubscriptions per zone in order to get push notifications whenever something in a zone changes on a different device. I also described how I setup those subscriptions when initializing my CloudKitManager. The next thing to look at is the code in the AppDelegate that handles the push notification. I say push notification but technically its a remote notification in the documentation. You’ll need to include the remote notification background mode in your apps capabilities. If you want on screen notifications you’ll need to enable those too as well as prompt the user for the permission. I won’t get into all that but you can read the documentation here.

It’s important to note that the CKNotificationInfo object I created with each subscription sets the shouldSendContentAvailable flag to true. The operating system will only wake your app from the background if that flag is true:

func notificationInfo() -> CKNotificationInfo {
        
    let notificationInfo = CKNotificationInfo()
    notificationInfo.alertBody = "Subscription notification for \(self.rawValue)"
    notificationInfo.shouldSendContentAvailable = true
    notificationInfo.shouldBadge = false
    return notificationInfo
}

Here’s the code in my AppDelegate:

// MARK: remote notifications
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
    print("application.didReceiveRemoteNotification")
        
    if let stringObjectUserInfo = userInfo as? [String : NSObject] {
        let cloudKitZoneNotificationUserInfo = CKRecordZoneNotification(fromRemoteNotificationDictionary: stringObjectUserInfo)
            
        if let recordZoneID = cloudKitZoneNotificationUserInfo.recordZoneID {
                
            let completionBlockOperation = NSBlockOperation {
                completionHandler(UIBackgroundFetchResult.NewData)
            }
                
            self.cloudKitManager?.syncZone(recordZoneID.zoneName, completionBlockOperation: completionBlockOperation)
        }
    }
    else {
        completionHandler(UIBackgroundFetchResult.NoData)
    }
}
 

The first thing this code does is cast the userInfo into a [String : NSObject] dictionary. Next it passes that dictionary to the CKRecordZoneNotification init method which will do the hard work of reading all the values in the dictionary and set the appropriate properties on the CKRecordZoneNotification object. Using that object I can get the recordZoneID that triggered the notification. 

The next part creates an NSBlockOperation in order to call the completionHandler. Its very important to call the completionHandler. If you don’t the operating system will eventually stop processing remote notifications for your app. I’m doing this in an NSBlockOperation so that I can easily add it to the end of my operation chain. Finally it calls syncZone on the CloudKitManager.

Operations

I mentioned before the power of CKFetchRecordChangesOperation and serverChangeTokens. Unfortunately I ran into the same issue as other CloudKit operations where I didn’t have a good way to get the fetched changes once the operation completed and again had to subclass.

class FetchRecordChangesForCloudKitZoneOperation: CKFetchRecordChangesOperation {
    
    var changedRecords: [CKRecord]
    var deletedRecordIDs: [CKRecordID]
    var operationError: NSError?
    private let cloudKitZone: CloudKitZone
    
    init(cloudKitZone: CloudKitZone) {
        
        self.cloudKitZone = cloudKitZone
        
        self.changedRecords = []
        self.deletedRecordIDs = []
        
        super.init()
        self.recordZoneID = cloudKitZone.recordZoneID()
        self.previousServerChangeToken = getServerChangeToken(cloudKitZone)
    }
    
    override func main() {
        
        print("FetchCKRecordChangesForCloudKitZoneOperation.main() - \(previousServerChangeToken)")
        setBlocks()
        super.main()
    }
    
    // MARK: Set operation blocks
    func setBlocks() {
        
        recordChangedBlock = {
            [unowned self]
            (record: CKRecord) -> Void in
            
            print("Record changed: \(record)")
            self.changedRecords.append(record)
        }
        
        recordWithIDWasDeletedBlock = {
            [unowned self]
            (recordID: CKRecordID) -> Void in
            
            print("Record deleted: \(recordID)")
            self.deletedRecordIDs.append(recordID)
        }
        
        fetchRecordChangesCompletionBlock = {
            [unowned self]
            (serverChangeToken: CKServerChangeToken?, clientChangeToken: NSData?, error: NSError?) -> Void in
            
            if let operationError = error {
                print("SyncRecordChangesToCoreDataOperation resulted in an error: \(error)")
                self.operationError = operationError
            }
            else {
                self.setServerChangeToken(self.cloudKitZone, serverChangeToken: serverChangeToken)
            }
        }
    }
 
    // MARK: Change token user default methods
    func getServerChangeToken(cloudKitZone: CloudKitZone) -> CKServerChangeToken? {
        
        let encodedObjectData = NSUserDefaults.standardUserDefaults().objectForKey(cloudKitZone.serverTokenDefaultsKey()) as? NSData
        
        if let encodedObjectData = encodedObjectData {
            return NSKeyedUnarchiver.unarchiveObjectWithData(encodedObjectData) as? CKServerChangeToken
        }
        else {
            return nil
        }
    }
    
    func setServerChangeToken(cloudKitZone: CloudKitZone, serverChangeToken: CKServerChangeToken?) {
        
        if let serverChangeToken = serverChangeToken {
            NSUserDefaults.standardUserDefaults().setObject(NSKeyedArchiver.archivedDataWithRootObject(serverChangeToken), forKey:cloudKitZone.serverTokenDefaultsKey())
        }
        else {
            NSUserDefaults.standardUserDefaults().setObject(nil, forKey:cloudKitZone.serverTokenDefaultsKey())
        }
    }
}

 

The operation includes an array of CKRecords that changed as well as an array of CKRecordIDs that were deleted. As the blocks are called the information is appended to the appropriate arrays. These blocks are called serially so its safe to use the arrays without needing to lock them.

The power of this operation is the serverChangeToken. Each zone needs to keep its own serverChangeToken. I decided to stash these in NSUserDefatuls and added the methods to get and set them directly in this operation since its the only placed they’re used.

One thing my proof of concept app does not do is handle the moreComing property. I’ve noticed though that after deleting and reinstalling the app I’ll get deletes for the first few calls to each zone. It makes sense and in production I will pay attention to that flag and continue to call syncZone until its done. I think it would be better to go ahead and process the changes returned and save them to core data instead of waiting for all of them to be downloaded.

The only other operation is my custom SavedChangedRecordsToCoreDataOperation:

class SaveChangedRecordsToCoreDataOperation: NSOperation {
    
    var changedRecords: [CKRecord]
    var deletedRecordIDs: [CKRecordID]
    private var rootRecords: [CKRecord]
    private var noteRecords: [CKRecord]
    private let coreDataManager: CoreDataManager
    
 
    init(coreDataManager: CoreDataManager) {
        
        // set the coreDataManager here
        self.coreDataManager = coreDataManager
        
        // set the default values
        self.changedRecords = []
        self.deletedRecordIDs = []
        self.rootRecords = []
        self.noteRecords = []
    }
    
    override func main() {
        
        print("SaveChangedRecordsToCoreDataOperation()")
        
        // this is where we set the correct managedObjectContext
        let managedObjectContext = self.coreDataManager.createBackgroundManagedContext()
        
        managedObjectContext.performBlockAndWait {
            [unowned self] in
            
            // loop through changed records and filter our child records
            for record in self.changedRecords {
                if let modelObjecType = ModelObjectType(rawValue: record.recordType) {
                    
                    if modelObjecType == ModelObjectType.Note {
                        self.noteRecords.append(record)
                    }
                    else {
                        self.rootRecords.append(record)
                    }
                }
            }
            
            // loop through all the changed root records first and insert or update them in core data
            for record in self.rootRecords {
                self.saveRecordToCoreData(record, managedObjectContext: managedObjectContext)
            }
            
            // loop through all the changed child records next and insert or update them in core data
            for record in self.noteRecords {
                self.saveRecordToCoreData(record, managedObjectContext: managedObjectContext)
            }
            
            // loop through all the deleted recordIDs and delete the objects from core data
            for recordID in self.deletedRecordIDs {
                self.deleteRecordFromCoreData(recordID, managedObjectContext: managedObjectContext)
            }
            
            // save the context
            self.coreDataManager.saveBackgroundManagedObjectContext(managedObjectContext)
        }
    }
    
    private func saveRecordToCoreData(record: CKRecord, managedObjectContext: NSManagedObjectContext) {
        
        print("saveRecordToCoreData: \(record.recordType)")
        let fetchRequest = createFetchRequest(record.recordType, recordName: record.recordID.recordName)
        
        if let cloudKitManagedObject = fetchObject(fetchRequest, managedObjectContext: managedObjectContext) {
            print("UPDATE CORE DATA OBJECT")
            cloudKitManagedObject.updateWithRecord(record)
        }
        else {
            print("NEW CORE DATA OBJECT")
            let cloudKitManagedObject = createNewCloudKitManagedObject(record.recordType, managedObjectContext: managedObjectContext)
            cloudKitManagedObject.updateWithRecord(record)
        }
    }
    
    private func createFetchRequest(entityName: String, recordName: String) -> NSFetchRequest {
        
        let fetchRequest = NSFetchRequest(entityName: entityName)
        fetchRequest.predicate = NSPredicate(format: "recordName LIKE[c] %@", recordName)
        
        return fetchRequest
    }
    
    private func fetchObject(fetchRequest: NSFetchRequest, managedObjectContext: NSManagedObjectContext) -> CloudKitManagedObject? {
        
        do {
            let fetchResults = try managedObjectContext.executeFetchRequest(fetchRequest)
            
            guard fetchResults.count <= 1 else {
                fatalError("ERROR: Found more then one core data object with recordName")
            }
            
            if fetchResults.count == 1 {
                return fetchResults[0] as? CloudKitManagedObject
            }
        }
        catch let error as NSError {
            print("Error fetching from CoreData: \(error.localizedDescription)")
        }
        
        return nil
    }
    
    private func createNewCloudKitManagedObject(entityName: String, managedObjectContext: NSManagedObjectContext) -> CloudKitManagedObject {
        
        guard let newCloudKitManagedObject = NSEntityDescription.insertNewObjectForEntityForName(entityName, inManagedObjectContext: managedObjectContext) as? CloudKitManagedObject else {
            fatalError("CloudKitManager: could not create object")
        }
        
        return newCloudKitManagedObject
    }
    
    private func deleteRecordFromCoreData(recordID: CKRecordID, managedObjectContext: NSManagedObjectContext) {
        
        let entityName = entityNameFromRecordName(recordID.recordName)
        let fetchRequest = createFetchRequest(entityName, recordName: recordID.recordName)
        
        if let cloudKitManagedObject = fetchObject(fetchRequest, managedObjectContext: managedObjectContext) {
            print("DELETE CORE DATA OBJECT: \(cloudKitManagedObject)")
            managedObjectContext.deleteObject(cloudKitManagedObject as! NSManagedObject)
        }
    }
    
    private func entityNameFromRecordName(recordName: String) -> String {
        
        guard let index = recordName.characters.indexOf(".") else {
            fatalError("ERROR - RecordID.recordName does not contain an entity prefix")
        }
        
        let entityName = recordName.substringToIndex(index)
        
        guard let managedObjectType = ModelObjectType(rawValue: entityName) else {
            fatalError("ERROR - unknown managedObjectType: \(entityName)")
        }
        
        return managedObjectType.rawValue
    }
}
 

There are a couple of things to discuss with this operation.

First is that the array of CKRecords is not ordered in a way that allows the operation to be guaranteed that a parent object already exists in Core Data before its relationship object is saved. To get around this the code looks for any Note objects and separates them into their own array which is saved to Core Data once the rest of the objects are saved. I’d like to find a better approach to this problem. The proof of concept app doesn’t have a big relationship graph but I see this approach getting even more messy with each layer of relationships added to the data model. For now this works though.

The second is how the deletes work. As I stated before, I’m prepending the entityName of an object to a UUID when creating its recordName. When processing deleted CKRecordID’s, I can split out the entityName from the recordName and know how to build my NSFetchRequest. Again, this might not be the best approach but it works. I’d love to hear other suggestions!

Operation Chaining

I queue the operations for syncing a zone in two functions. The reason will be explained in the next section about doing a full sync.

The syncZone() method called from the AppDelegate:

func syncZone(zoneName: String, completionBlockOperation: NSBlockOperation) {
        
    if let cloudKitZone = CloudKitZone(rawValue: zoneName) {
            
        // suspend the queue so nothing finishes before all our dependencies are setup
        operationQueue.suspended = true
            
        // queue up the change operations for a zone
        let saveChangedRecordsToCoreDataOperation = queueChangeOperationsForZone(cloudKitZone)
            
        // add our completion block to the queue as well to handle background fetches
        completionBlockOperation.addDependency(saveChangedRecordsToCoreDataOperation)
        operationQueue.addOperation(completionBlockOperation)
            
        // let the queue begin firing again
        operationQueue.suspended = false
    }
}
 

It’s a pretty simple function. I suspend the NSOperationQueue first so that I can guarantee all of the operations have their dependencies set before any execute. I use a separate function to queue up the fetch and save changes operations. The function passes back the save changes operation. I do that so I can set it as a dependency on the completionBlockOperation passed in from the AppDelegate. Finally it lets the queue begin processing again.

The queueChangeOperationsForZone function which at this point should be straight forward:

private func queueChangeOperationsForZone(cloudKitZone: CloudKitZone) -> SaveChangedRecordsToCoreDataOperation {
        
    // there are two operations that need to be chained together for each zone
    // the first is to fetch record changes
    // the second is to save those changes to CoreData
    // we'll also need a block operation to transfer data between them
    let fetchRecordChangesOperation = FetchRecordChangesForCloudKitZoneOperation(cloudKitZone: cloudKitZone)
    let saveChangedRecordsToCoreDataOperation = SaveChangedRecordsToCoreDataOperation(coreDataManager: coreDataManager)
        
    let dataTransferOperation = NSBlockOperation() {
        [unowned saveChangedRecordsToCoreDataOperation, unowned fetchRecordChangesOperation] in
            
        print("addChangeOperationsForZone.dataTransferOperation")
        saveChangedRecordsToCoreDataOperation.changedRecords = fetchRecordChangesOperation.changedRecords
        saveChangedRecordsToCoreDataOperation.deletedRecordIDs = fetchRecordChangesOperation.deletedRecordIDs
    }
        
    // set the dependencies
    dataTransferOperation.addDependency(fetchRecordChangesOperation)
    saveChangedRecordsToCoreDataOperation.addDependency(dataTransferOperation)
        
    // add the operations to the queue
    operationQueue.addOperation(fetchRecordChangesOperation)
    operationQueue.addOperation(dataTransferOperation)
    operationQueue.addOperation(saveChangedRecordsToCoreDataOperation)
        
    return saveChangedRecordsToCoreDataOperation
}

 

Next Post – Syncing Data >

 

Written by Nick Harris

February 9, 2016 at 6:53 am

Posted in Uncategorized

CloudKit + Core Data + NSOperations – Introduction

with 3 comments

This is the first post about CloudKit syncing with Core Data. Sample code is here. You’ll need to enable CloudKit and other entitlements in your own environment to get it to work correctly.

Idea

I’ve been curious about CloudKit for a while now. I’ve had a side project idea that I finally had time to do a little work on in December and part of it is the ability to sync between my iPhone and iPad(s). Before adding CloudKit into my side project blindly, I decided to try a proof of concept app to learn the ins and outs of the API’s. In these next four posts I’ll share what I learned and what I wrote to accomplish writing an app with a Core Data foundation that syncs across devices via CloudKit using Swift. I also took the opportunity to try my hand at an NSOperation dependency chained architecture to network calls that was described in Advanced NSOperations Session from WWDC 2015 (or on ASCII WWDC).

If you’re new to CloudKit, I highly recommend reading the CloudKit Quick Start from Apple along with Tom Harrington’s great rundown (a bit dated but still spot on) as well as NSHipster.

Warning

The code shared in this post really is proof of concept code. Its not polished and I’m not even sure how much I would use in a production app. One glaring absence is the lack of CloudKit error handling. I’m sure there are bugs and I bet there are things I misunderstood with how CloudKit works. I view this post as a way of sharing what I learned and how I tackled some of the big ideas of CloudKit syncing. I’d love to hear other ideas and especially what I did wrong. Feel free to email me with your thoughts or even better write a follow-up post of your own (I’d love to see more people blogging). Twitter works too.

You can find the code here. You’ll need to enable CloudKit and other entitlements in your own environment to get it to work correctly.

Cars Trucks Buses

The sample app I came up with is called Cars Trucks Buses (aka CloudKitSyncProofOfConcept or CloudKitSyncPOC). Its based on the structure of my side project so I could get a real feel for what its needs are. I’m also a big fan of the band Phish. The app is very simple. There are three root model objects – Cars, Trucks and Buses – along with a fourth model object called Note. Notes can be attached to any of the three root objects.

App Screen Shot   App Screen Shot   App Screen Shot   App Screen Shot

The code for the UI is your typical master-details setup using table views and NSFetchedResultControllers. I wrote the code for the UI to be even more generic since all three of the root objects are basically the same thing. There are only four classes: ObjectTableViewController.swift, DetailsViewController.swift, NotesTableViewController.swift and NoteDetailsViewController.swift. The UI isn’t the point of this sample app but I did have some fun with using a swift enum and tying it to storyboard restoration ID’s to know which tab was which.

Core Data Model

The Core Data model is very simple:

Core Data 

All of the objects share these properties:

var added: NSDate?

var lastUpdate: NSDate?

var recordName: String?

var recordID: NSData?

The main three objects also have:

var name: String?

var notes: NSSet?

While Note has:

var text: String?

var truck: Truck?

var car: Car?

var bus: Bus?

The relationship between a Note and its parent object is one to many with cascading deletes.

I created a protocol called CTBRootManagedObject that encapsulates the properties that Car, Truck and Bus share to make my UI code simpler. This way I could manipulate the objects using the protocol as an object without caring what its entity type is. You can accomplish the same by subclassing from a base NSManagedObject but I prefer the power Swift gives me here. I can have a class conform to multiple protocols instead of inheriting from just a single class. This is convenient and comes in handy later.

@objc protocol CTBRootManagedObject {

    var name: String? { get set }

    var added: NSDate? { get set }

    var lastUpdate: NSDate? { get set }

    var notes: NSSet? { get set }

}

I also used the same CoreDataManager pattern I wrote about previously but with some changes that I’ll get to in this post. One to talk about upfront is that I created a protocol that I could use with view controllers.

protocol CoreDataManagerViewController {

    var coreDataManager: CoreDataManager? { get set }

    var modelObjectType: ModelObjectType? { get set }

}

This way I can look to see if a new view controller conforms to this protocol and inject the CoreDataManager during performSegue:

// MARK: Segue

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

    // set the coreDataManager either on the root of a UINavigationController or on the segue destination itself

    if var destinationCoreDataViewController = segue.destinationViewController asCoreDataManagerViewController {

        destinationCoreDataViewController.coreDataManager = coreDataManager

        destinationCoreDataViewController.modelObjectType = modelObjectType

    }

}

Enter CloudKit

I’m going to assume you already have a good idea about the different parts of CloudKit. Check out the links at the beginning of the post if you don’t.

CloudKit on the surface is pretty simple. When you start to dig in you begin to realize how flexible it is for virtually any app you could imagine. That also means there’s no one (or even two or three) architecture patterns to use. Its going to be different from app to app based on what your app is trying to accomplish. I’ll admit that I changed my mind virtually every other day about what approach I should use. I won’t walk you through all of the decisions and changes I made while making this proof of concept app. Instead I’ll just describe where I ended up and a bit about why.

Public or Private Database?

This decision was the easiest. My proof of concept app along with my side project are completely private to the user so I used the private database. I’d imagine if I was building something that used the public database I’d have made many other decisions differently. By assuming a single user I can also assume that a record probably isn’t being updated by multiple people at the same time which greatly minimizes how I deal with any conflicts.

Default Zone or Custom Zones?

I decided to create custom zones for each of my three core objects (CarZone, TruckZone, BusZone). Even as I’m writing this I’m not sure if I made the right decision here.

CKFetchRecordChangesOperation does not work with the default zone but is probably the most powerful part of CloudKit. It gives you the ability to only get changes from the server from some previous point in time. I can’t over emphasize how important that is when creating an app the syncs data. It works by returning a serverChangeToken which you then pass back the next time you talk to the server.

Back at NewsGator we introduced sync tokens to our RSS sync API around a decade ago. Instead of a client getting a response with every blog post to an RSS feed they would get only the posts that were new or altered. This made writing a client so much more efficient from a processing stand point. If I had 1000 RSS subscriptions and hit the refresh button in my client with the old way of doing things, I could potentially get 10,000 posts back if not more that would then need to be evaluated for duplicates, updates or new posts. Using sync tokens only gives the client what they need to deal with. From a server stand point the code only needed to get and return the new or updated posts from either cache or from disk cutting response time and computational cost for each request and greatly reducing bandwidth costs. Its an easy win-win from both a server and client perspective. Minimizing data transfer is always the name of the game when dealing with web services (and really all parts of programming). The NewsGator sync token idea worked so well we used it again with Glassboard. Seeing serverChangeToken in the CloudKit documentation made my day knowing that the smart folks at Apple came to the same conclusion about their necessity.

Using multiple custom zones initially came from the wrong conclusion that I would need to use the zoneName to know the Core Data entityName of a deleted record. I worked that out another in the end, but I decided to stick with it because of another lesson I learned while working on the NewsGator platform. We eventually had so much data that we refactored to partition the data over multiple “buckets” in order to make our response times faster. Zones in CloudKit seem very similar to me in that all your data isn’t in one block so access becomes faster. It wasn’t needed for the proof of concept but I decided to keep it in just to see what it would take to get it working.

CKRecordID 

This was easily the CloudKit entity I struggled with the most. The reason, as I just stated, was knowing what type of model object was deleted. When the server reports deletes to the client it only has the CKRecordID. In order to create an NSFetchRequest you need to know the entityName. Initially I thought I could use the zoneName of the CKRecordID to figure it out. That became problematic when I introduced CKReferences for Notes. Both records have to be in the same zone for a CKReference. Finally I figured out how I could stash the entityName within the CKRecordID.

CKRecordID has a property called recordName. You have total control over this property if you want. I decided that I would prepend the recordType to a UUID separated by a period:

let uuid = NSUUID()

let recordName = recordType + “.” + uuid.UUIDString

let recordID = CKRecordID(recordName: recordName, zoneID: recordZoneID)

        

return CKRecord(recordType: recordType, recordID: recordID)

 

That will make more sense when I dive deeper into the code, but now I could get an entity name from a recordID easily.

private func entityNameFromRecordName(recordName: String) -> String {

        

    guard let index = recordName.characters.indexOf(“.”else {

        fatalError(“ERROR – RecordID.recordName does not contain an entity prefix”)

    }

        

    let entityName = recordName.substringToIndex(index)

        

    guard let managedObjectType = ModelObjectType(rawValue: entityName) else {

        fatalError(“ERROR – unknown managedObjectType: \(entityName)”)

    }

        

    return managedObjectType.rawValue

}

 

You’ve probably noticed that there are three terms I’m throwing around interchangeably: recordType, entityName and ModelObjectType. That’s because they’re all interrelated. In Core Data an entity has and entityName. In CloudKit a record has a recordType. To save myself from going crazy I created the ModelObjectType enum that drives them both. (You can also see how I used it to map Storyboard Restoration ID’s to the ModelObjectType they’re displaying):

enum ModelObjectType: String {

    case Car = “Car”

    case Truck = “Truck”

    case Bus = “Bus”

    case Note = “Note”

    

    init?(storyboardRestorationID: String) {

        switch storyboardRestorationID {

        case “CarsListScene” : self = .Car

        case“TrucksListScene” : self = .Truck

        case “BusesListScene” : self = .Bus

        default : returnnil

        }

    }

    

    static let allCloudKitModelObjectTypes = [

        ModelObjectType.Car.rawValue,

        ModelObjectType.Truck.rawValue,

        ModelObjectType.Bus.rawValue,

        ModelObjectType.Note.rawValue

    ]

}

 

CKSubscriptions

With custom zones setup there was really no excuse to not setup CKSubscriptions for each. Subscriptions are another big win for CloudKit. With a zone subscription, any changes to that zone will cause a push notification to be sent. With just a few lines of code in your app delegate (which I’ll detail in the next post) you can process that data in the background so that your app has the latest and greatest data even though its not open. It also gets you in-app change notifications so you can adjust your UI without the user touching a thing. Its almost magical if you don’t understand all the work that goes into how it actually works!

CloudKitManager

I decided to go with the same kind of approach for CloudKit that I did with Core Data and the CoreDataManager. This was practically the first decision I made on this proof of concept. The reason being is that I wanted a way to automatically fire off changes to CloudKit when anything changed with my Core Data model. My idea was to have the CoreDataManager create the CloudKitManager then pass any changes to it when the main thread context is saved. It worked great from the start. As the app progressed the CloudKitManager became the keeper of all CloudKit interactions. Its the biggest class in the app at just over 500 lines, but its really more of an NSOperation coordinator.

CKDatabaseOperation, NSOperation and Dependencies

The CloudKit API comes with a handful of NSOperation subclasses. There is a base class called CKDatabaseOperation which holds a reference to which database should be interacted with. Other operations are built on top for any other interaction you need with CloudKit. I’m a big fan of NSOperations and networking code so it was an easy decision to use these instead of the methods on CKDatabase. The documentation pushes you toward using the operations as well. The biggest gain you get is dependencies. For instance, before you can create a CKSubscription for a CKZone, the CKZone needs to already exist. Any records created also need to have the zone available for the save to complete.

Because of all these dependencies, I used the init operation of my CloudKitManager to make all the necessary calls to CloudKit to verify that all my zones are created and ready to go.

Initializing CKZones

I probably went a little overboard with how my zone initialization works but it was fun to experiment with the operations and some of the power of Swift enumerations. I decided to use CKFetchRecordZonesOperation to get all the zones that exist in CloudKit and compare them to an enumeration in code. I could then use CKModifyRecordZonesOperation to add any zones that were missing or delete any zones that shouldn’t be there. Deleting zones was really more of me fooling around with different zone names and ways to use them but it gave me a way to cleanup CloudKit without needing to write code outside the app to do it. I’m not sure I would do this in a production app.

The enum for the zones started out simple enough:

enum CloudKitZone: String {

    case CarZone = “CarZone”

    case TruckZone = “TruckZone”

    case BusZone = “BusZone”

}

The next step was figuring out how to use CKFetchRecordZonesOperation. You can either pass in the recordZoneIDs you want returned or you can use the convenience class method FetchAllRecordZonesOperation to fetch them all. I decided to use the fetch all approach in order for my code to delete zones I was no longer using. 

Passing Data Between Operations

Once I had my CloudKit zones fetched, I ran into an issue with how to pass data to the CKModifyRecordZonesOperation. I spent a good amount of time investigating this. If you look at the sample code from the Advanced NSOperations WWDC session you’ll see it uses a framework which all the operations are then built on top of. I didn’t spend a whole lot of time looking through that code. Its an interesting approach but it wasn’t something I was comfortable using. What I eventually settled on came from this fantastic answer from Quinn “The Eskimo!” on the Apple Developer Forum.

Side Note: Years ago I had the pleasure of working directly with Quinn on an issue through the developer relations program. Incredibly helpful guy and longtime Apple employee. In fact, what he helped me with ended up becoming a session I gave at 360iDev 2012 on using Client Certificates and Keychain for two factor authentication in iOS apps.

I recommend reading the entire answer as I agree with pretty much everything stated. But the basic idea is to use NSBlockOperations to pass data between operations and just make sure they have their dependencies setup correctly so that the transfer doesn’t happen until operation A is done and operation B doesn’t start until the NSBlockOperation is done.

Important Note: The code in the forum has a memory leak. You can read more about how I found the leak and how I eventually fixed it in my previous posts. Basically it should have used a capture list to make sure the operations in the NSBlockOperation were unowned to avoid a retain cycle.

This lead to a new issue though. All of the CloudKit operations use blocks to pass back the retrieved data, but they don’t have anyway of accessing that data once the operation is complete. So I decided to subclass the CloudKit operations. I’m not happy with this decision, but after days of deliberating different approaches I went with it to see how it would pan out.

My new FetchAllRecordZonesOperation:

class FetchAllRecordZonesOperation: CKFetchRecordZonesOperation {
 
    var fetchedRecordZones: [CKRecordZoneID : CKRecordZone]? = nil
    
    override func main() {
        
        print("FetchAllRecordZonesOperation.main()")
        self.setBlock()
        super.main()
    }
    
    func setBlock() {
        
        self.fetchRecordZonesCompletionBlock = {
            (recordZones: [CKRecordZoneID : CKRecordZone]?, error: NSError?) -> Void in
            
            print("FetchAllRecordZonesOperation.fetchRecordZonesCompletionBlock")
            
            if let error = error {
                print("FetchAllRecordZonesOperation error: \(error)")
            }
            
 if let recordZones = recordZones {
                self.fetchedRecordZones = recordZones
                for recordID in recordZones.keys {
                    print(recordID)
                }
            }
        }
    }
}

Using this subclass I could now pass the fetched CKRecordZoneIDs to another NSOperation I created to process them and figure out which zones I needed to create and which I needed to delete.

class ProcessServerRecordZonesOperation: NSOperation {

 

    var preProcessRecordZoneIDs: [CKRecordZoneID]

    var postProcessRecordZonesToCreate: [CKRecordZone]?

    var postProcessRecordZoneIDsToDelete: [CKRecordZoneID]?

    

    override init() {

        

        preProcessRecordZoneIDs = []

        postProcessRecordZonesToCreate = nil

        postProcessRecordZoneIDsToDelete = nil

    }

    

    override func main() {

        

        print(“ProcessServerRecordZonesOperation.main()”)

        setZonesToCreate()

        setZonesToDelete()

    }

    

    private func setZonesToCreate() {

        

        let serverZoneNamesSet = Set(preProcessRecordZoneIDs.map { $0.zoneName })

        let expectedZoneNamesSet = Set(CloudKitZone.allCloudKitZoneNames)

        let missingZoneNamesSet = expectedZoneNamesSet.subtract(serverZoneNamesSet)

        

        if missingZoneNamesSet.count > 0 {

            postProcessRecordZonesToCreate = []

            for missingZoneName in missingZoneNamesSet {

                if let missingCloudKitZone = CloudKitZone(rawValue: missingZoneName) {

                    let missingRecordZone = CKRecordZone(zoneID: missingCloudKitZone.recordZoneID())

                    postProcessRecordZonesToCreate?.append(missingRecordZone)

                }

            }

        }

    }

    

    private func setZonesToDelete() {

        

        // its important to not inadvertently delete the default zone

        for recordZoneID in preProcessRecordZoneIDs {

            if (recordZoneID.zoneName != CKRecordZoneDefaultName) &&

                (CloudKitZone(rawValue: recordZoneID.zoneName) == nil) {

                if postProcessRecordZoneIDsToDelete == nil {

                    postProcessRecordZoneIDsToDelete = []

                }

                postProcessRecordZoneIDsToDelete?.append(recordZoneID)

            }

        }

    }

}

 

enum CloudKitZone: String {

    case CarZone = “CarZone”

    case TruckZone = “TruckZone”

    case BusZone = “BusZone”


    func recordZoneID() -> CKRecordZoneID {

        return CKRecordZoneID(zoneName: self.rawValue, ownerName: CKOwnerDefaultName)

 

    }

    

    static let allCloudKitZoneNames = [

        CloudKitZone.CarZone.rawValue,

        CloudKitZone.TruckZone.rawValue,

        CloudKitZone.BusZone.rawValue

    ]

}

This code uses Swift Sets in a way that you couldn’t with NSSet. Swift Sets are more powerful. To figure out which zones I needed to create, I make a Set of zoneName’s using map to get them from the CKRecordZoneID’s passed in. I also create another Set using a static array I added to the CloudKitZone enum. I can then “subtract” from the expectedZoneNameSet all of the zoneNames in the serverZoneNameSet. The remainder are the zoneNames that need to be created.

Next I use a really handy feature of Swift enums to make sure the zoneName is one I expect. You can initiate an enum using its rawValue. If the rawValue doesn’t exist in the enum then you get back nil. The reason why I’m creating the CloudKitZone enum instance again is so that I can use a function I added to the enum which creates a CKRecordZoneID. 

When you hear that enums in Swift are first class, this is what people mean. I can use this CloudKitZone enum to not only keep a grouping of related things but can also validate enum values as well as create functions that are specific to the enum. Very powerful. I can’t overstate how much I love this in Swift. Maybe I love it too much!?

I find zones to delete a little differently. I bet there’s a fancier way to do it but this worked and I can read it so I was happy. I learned pretty fast that if you delete the defaultZone you’ll get some unexpected behavior (at least I didn’t expect it but for the life of me I can’t remember what happened), so I check to make sure I don’t send the defaultZone to be deleted. 

I didn’t see a need to subclass the CKModifyRecordZonesOperation. Instead I added a helper function to setup the modifyRecordZonesCompletionBlock:

private func createModifyRecordZoneOperation(recordZonesToSave: [CKRecordZone]?, recordZoneIDsToDelete: [CKRecordZoneID]?) -> CKModifyRecordZonesOperation {

        

    let modifyRecordZonesOperation = CKModifyRecordZonesOperation(recordZonesToSave: recordZonesToSave, recordZoneIDsToDelete: recordZoneIDsToDelete)

   

    modifyRecordZonesOperation.modifyRecordZonesCompletionBlock = {

        (modifiedRecordZones: [CKRecordZone]?, deletedRecordZoneIDs: [CKRecordZoneID]?, error: NSError?) -> Void in

            

        print(“— CKModifyRecordZonesOperation.modifyRecordZonesOperation”)

            

        if let error = error {

            print(“createModifyRecordZoneOperation ERROR: \(error)”)

            return

        }

            

        if let modifiedRecordZones = modifiedRecordZones {

            for recordZone in modifiedRecordZones {

                print(“Modified recordZone: \(recordZone)”)

            }

        }

            

        if let deletedRecordZoneIDs = deletedRecordZoneIDs {

            for zoneID in deletedRecordZoneIDs {

                print(“Deleted zoneID: \(zoneID)”)

            }

        }

    }

        

    return modifyRecordZonesOperation

}

Now I was ready to setup all my operations, data transfer block operations and dependencies:

// 1. Fetch all the zones

// 2. Process the returned zones and create arrays for zones that need creating and those that need deleting
// 3. Modify the zones in cloudkit
        
let fetchAllRecordZonesOperation = FetchAllRecordZonesOperation.fetchAllRecordZonesOperation()
let processServerRecordZonesOperation = ProcessServerRecordZonesOperation()
let modifyRecordZonesOperation = self.createModifyRecordZoneOperation(nil, recordZoneIDsToDelete: nil)
        

let transferFetchedZonesOperation = NSBlockOperation() {

    [unowned fetchAllRecordZonesOperation, unowned processServerRecordZonesOperation] in

            

    if let fetchedRecordZones = fetchAllRecordZonesOperation.fetchedRecordZones {

        processServerRecordZonesOperation.preProcessRecordZoneIDs = Array(fetchedRecordZones.keys)

    }

}

        

let transferProcessedZonesOperation = NSBlockOperation() {

    [unowned modifyRecordZonesOperation, unowned processServerRecordZonesOperation] in

            

    modifyRecordZonesOperation.recordZonesToSave = processServerRecordZonesOperation.postProcessRecordZonesToCreate

    modifyRecordZonesOperation.recordZoneIDsToDelete = processServerRecordZonesOperation.postProcessRecordZoneIDsToDelete

}

        
transferFetchedZonesOperation.addDependency(fetchAllRecordZonesOperation)
processServerRecordZonesOperation.addDependency(transferFetchedZonesOperation)
transferProcessedZonesOperation.addDependency(processServerRecordZonesOperation)
modifyRecordZonesOperation.addDependency(transferProcessedZonesOperation)
        
self.operationQueue.addOperation(fetchAllRecordZonesOperation)
self.operationQueue.addOperation(transferFetchedZonesOperation)
self.operationQueue.addOperation(processServerRecordZonesOperation)
self.operationQueue.addOperation(transferProcessedZonesOperation)
 

You may know that a CKDatabase has its own operation queue. I’m not using it. I’m creating my own NSOperationQueue that my CloudKitManager owns. The reason is that the operation queue on CKDatabase only excepts CKDatabaseOperations. Owning your own NSOperationQueue is no big deal at all. The queue off of a CKDatabase is handy in that it automatically sets which database to use (public or private) but a CKDatabaseOperation will default to the private database unless you set it otherwise. That means all my subclassed CKDatabaseOperations use the database I want without me doing anything. But its good to know how all that works.

Initializing CKSubscriptions

The subscriptions for each zone is very similar to what I did with the zones themselves. I updated the CloudKitZone enum to create a CKSubscription based on the zone. I also added code to create a CKNotificationInfo for each zone. I only added these to my subscriptions so that I got an on screen notification when my changes arrived. I’ll skip this in my side project and just let the notification serve as a silent background update.

I also subclassed CKFetchSubscriptionsOperation in order to get to the fetched subscription after the operation completed and made a new ProcessServerSubscriptionsOperation to figure out which needed to be created and deleted driven by the CloudKitZone enum. Finally I used the CKModifySubscriptionsOperation to add or delete the zone subscriptions. There are some subtle differences but I’ll skip them. Let me know if anything doesn’t make sense.

enum CloudKitZone: String {
    case CarZone = "CarZone"
    case TruckZone = "TruckZone"
    case BusZone = "BusZone"
    
    init?(recordType: String) {
        switch recordType {
        case ModelObjectType.Car.rawValue : self = .CarZone
        case ModelObjectType.Truck.rawValue : self = .TruckZone
        case ModelObjectType.Bus.rawValue : self = .BusZone
        default : returnnil
        }
    }
    
    func recordZoneID() -> CKRecordZoneID {
        return CKRecordZoneID(zoneName: self.rawValue, ownerName: CKOwnerDefaultName)
    }
    
    func cloudKitSubscription() -> CKSubscription {
        
        // options must be set to 0 per current documentation
        // https://developer.apple.com/library/ios/documentation/CloudKit/Reference/CKSubscription_class/index.html#//apple_ref/occ/instm/CKSubscription/initWithZoneID:options:
        let subscription = CKSubscription(zoneID: self.recordZoneID(), options: CKSubscriptionOptions(rawValue: 0))
        subscription.notificationInfo = self.notificationInfo()
        return subscription
    }
    
    func notificationInfo() -> CKNotificationInfo {
        
        let notificationInfo = CKNotificationInfo()
        notificationInfo.alertBody = "Subscription notification for \(self.rawValue)"
        notificationInfo.shouldSendContentAvailable = true
        notificationInfo.shouldBadge = false
        return notificationInfo
    }
    
    static let allCloudKitZoneNames = [
        CloudKitZone.CarZone.rawValue,
        CloudKitZone.TruckZone.rawValue,
        CloudKitZone.BusZone.rawValue
    ]
}
 
class FetchAllSubscriptionsOperation: CKFetchSubscriptionsOperation {
    
    var fetchedSubscriptions: [String : CKSubscription]
    
    override init() {
        
        fetchedSubscriptions = [:]
        super.init()
    }
    
    override func main() {
        
        print("FetchAllSubscriptionsOperation.main()")
        self.setBlock()
        super.main()
    }
    
    func setBlock() {
        
        self.fetchSubscriptionCompletionBlock = {
            (subscriptions: [String : CKSubscription]?, error: NSError?) -> Void in
            
            print("FetchAllSubscriptionsOperation.fetchRecordZonesCompletionBlock")
            
            if let error = error {
                print("FetchAllRecordZonesOperation error: \(error)")
            }
            
            if let subscriptions = subscriptions {
                self.fetchedSubscriptions = subscriptions
                for subscriptionID in subscriptions.keys {
                    print("Fetched CKSubscription: \(subscriptionID)")
                }
            }
        }
    }
}
 
class ProcessServerSubscriptionsOperation: NSOperation {
 
    var preProcessFetchedSubscriptions: [String : CKSubscription]
    var postProcessSubscriptionsToCreate: [CKSubscription]?
    var postProcessSubscriptionIDsToDelete: [String]?
    
    override init() {
        
        self.preProcessFetchedSubscriptions = [:]
        self.postProcessSubscriptionsToCreate = nil
        self.postProcessSubscriptionIDsToDelete = nil
        
        super.init()
    }
    
    override func main() {
        
        print("ProcessServerSubscriptionsOperation.main()")
        setSubscriptionsToCreate()
        setSubscriptionsToDelete()
    }
    
    private func setSubscriptionsToCreate() {
        
        let serverSubscriptionZoneNamesSet = self.createServerSubscriptionZoneNameSet()
        let expectedZoneNamesWithSubscriptionsSet = Set(CloudKitZone.allCloudKitZoneNames)
        let missingSubscriptionZoneNames = expectedZoneNamesWithSubscriptionsSet.subtract(serverSubscriptionZoneNamesSet)
        
        if missingSubscriptionZoneNames.count > 0 {
            postProcessSubscriptionsToCreate = []
            for missingSubscriptionZoneName in missingSubscriptionZoneNames {
                if let cloudKitSubscription = CloudKitZone(rawValue: missingSubscriptionZoneName) {
                    postProcessSubscriptionsToCreate?.append(cloudKitSubscription.cloudKitSubscription())
                }
            }
        }
    }
    
    private func setSubscriptionsToDelete() {
        
        let serverSubscriptionZoneNamesSet = self.createServerSubscriptionZoneNameSet()
        let expectedZoneNamesWithSubscriptionsSet = Set(CloudKitZone.allCloudKitZoneNames)
        let unexpectedSubscriptionZoneNamesSet = serverSubscriptionZoneNamesSet.subtract(expectedZoneNamesWithSubscriptionsSet)
        
        if unexpectedSubscriptionZoneNamesSet.count > 0 {
            postProcessSubscriptionIDsToDelete = []
            
            var subscriptionZoneNameDictionary: [String : CKSubscription] = [:]
            for subscription in preProcessFetchedSubscriptions.values {
                if let zoneID = subscription.zoneID {
                    subscriptionZoneNameDictionary[zoneID.zoneName] = subscription
                }
            }
            
            for subscriptionZoneName in unexpectedSubscriptionZoneNamesSet {
                if let subscription = subscriptionZoneNameDictionary[subscriptionZoneName] {
                    postProcessSubscriptionIDsToDelete?.append(subscription.subscriptionID)
                }
            }
        }
    }
    
    private func createServerSubscriptionZoneNameSet() -> Set<String> {
        
        let serverSubscriptions = Array(preProcessFetchedSubscriptions.values)
        let serverSubscriptionZoneIDs = serverSubscriptions.flatMap { $0.zoneID }
        let serverSubscriptionZoneNamesSet = Set(serverSubscriptionZoneIDs.map { $0.zoneName })
        
        return serverSubscriptionZoneNamesSet
    }
}
 
private func createModifySubscriptionOperation() -> CKModifySubscriptionsOperation {
        
    let modifySubscriptionsOperation = CKModifySubscriptionsOperation()
    modifySubscriptionsOperation.modifySubscriptionsCompletionBlock = {
        (modifiedSubscriptions: [CKSubscription]?, deletedSubscriptionIDs: [String]?, error: NSError?) -> Void in
            
        print("--- CKModifySubscriptionsOperation.modifySubscriptionsCompletionBlock")
            
        if let error = error {
            print("createModifySubscriptionOperation ERROR: \(error)")
            return
        }
            
        if let modifiedSubscriptions = modifiedSubscriptions {
            for subscription in modifiedSubscriptions {
                print("Modified subscription: \(subscription)")
            }
        }
            
        if let deletedSubscriptionIDs = deletedSubscriptionIDs {
            for subscriptionID in deletedSubscriptionIDs {
                print("Deleted subscriptionID: \(subscriptionID)")
            }
        }
    }
        
    return modifySubscriptionsOperation
}

 

// 1. Fetch all subscriptions
// 2. Process which need to be created and which need to be deleted
// 3. Make the adjustments in iCloud
        
let fetchAllSubscriptionsOperation = FetchAllSubscriptionsOperation.fetchAllSubscriptionsOperation()
let processServerSubscriptionsOperation = ProcessServerSubscriptionsOperation()
let modifySubscriptionsOperation = self.createModifySubscriptionOperation()
        

let transferFetchedSubscriptionsOperation = NSBlockOperation() {

    [unowned processServerSubscriptionsOperation, unowned fetchAllSubscriptionsOperation] in

            

    processServerSubscriptionsOperation.preProcessFetchedSubscriptions = fetchAllSubscriptionsOperation.fetchedSubscriptions

}

        

let transferProcessedSubscriptionsOperation = NSBlockOperation() {

    [unowned modifySubscriptionsOperation, unowned processServerSubscriptionsOperation] in

            

    modifySubscriptionsOperation.subscriptionsToSave = processServerSubscriptionsOperation.postProcessSubscriptionsToCreate

    modifySubscriptionsOperation.subscriptionIDsToDelete = processServerSubscriptionsOperation.postProcessSubscriptionIDsToDelete

}

        
transferFetchedSubscriptionsOperation.addDependency(fetchAllSubscriptionsOperation)
processServerSubscriptionsOperation.addDependency(transferFetchedSubscriptionsOperation)
transferProcessedSubscriptionsOperation.addDependency(processServerSubscriptionsOperation)
modifySubscriptionsOperation.addDependency(transferProcessedSubscriptionsOperation)
        
self.operationQueue.addOperation(fetchAllSubscriptionsOperation)
self.operationQueue.addOperation(transferFetchedSubscriptionsOperation)
self.operationQueue.addOperation(processServerSubscriptionsOperation)
self.operationQueue.addOperation(transferProcessedSubscriptionsOperation)
self.operationQueue.addOperation(modifySubscriptionsOperation)

 

Next Post – Saving Data >

 

Written by Nick Harris

February 9, 2016 at 6:47 am

Posted in Uncategorized

Weak vs Unowned When Passing Data Between NSOperations

leave a comment »

I posted the fix for the NSOperation retain cycle earlier this afternoon right before heading out to run some errands. While driving around town it dawned on me that the reason my attempt at using a capture list didn’t work is because I had mistakenly assumed that the first weak would be carried on to other classes in the list until another memory management keyword was encountered. When I got home I reread the documentation and that was indeed the case and updated the post.

This evening I started fixing my proof of concept app using weak but continuously unwrapping my operations in each NSBlockOperation seemed wrong. Perhaps unowned would be more correct. Here’s the key part in the documentation about which to use:

Like weak references, an unowned reference does not keep a strong hold on the instance it refers to. Unlike a weak reference, however, an unowned reference is assumed to always have a value. Because of this, an unowned reference is always defined as a nonoptional type. You indicate an unowned reference by placing the unowned keyword before a property or variable declaration.

I know for a fact that both operations in the data transfer will always have a value. That’s exactly what the dependencies do! Its why adding a dependency between two operations creates a strong relationship between them.

So the correct fixed code for the retain cycle looks like:

let operationOne = OperationOne()

let operationTwo = OperationTwo()

        

let transferOperation = NSBlockOperation() {

    [unowned operationTwo, unowned operationOne] in

            

    print(“transferOperation”)

    if let createdStrings = operationOne.createdStrings {

        operationTwo.passedInStrings = createdStrings

    }

}

        

        

transferOperation.addDependency(operationOne)

operationTwo.addDependency(transferOperation)

        

operationQueue.addOperation(operationOne)

operationQueue.addOperation(transferOperation)

operationQueue.addOperation(operationTwo)

 

Fun afternoon! Glad I finally have a good grasp on what all is going on in this setup.

Written by Nick Harris

February 4, 2016 at 2:13 am

Posted in Uncategorized

Retain Cycle with NSOperation Dependencies – FIXED

leave a comment »

I knew it was going to be something simple. 

Even though this doesn’t work:

let transferOperation = NSBlockOperation() {
    [weak operationOne, operationTwo] in
            
    print("transferOperation")
    if let createdObject = operationOne?.createdObject {
        operationTwo.passedInObject = createdObject
    }
}

 

this does:

let transferOperation = NSBlockOperation() {

    [weak operationTwo, weak operationOne] in

            

    print(“transferOperation”)

    if let createdStrings = operationOne?.createdStrings {

        operationTwo?.passedInStrings = createdStrings

    }

}

 

Why? I’m not sure yet. But glad I asked and got some more eyes on the problem.

Thanks Josh!

UPDATE

I should have read the docs closer. The first doesn’t work because operationTwo is still a strong reference. I mistakenly thought that it would still take on the weak declared with operationOne but after re-reading the documentation:

Defining a Capture List

Each item in a capture list is a pairing of the weak or unowned keyword with a reference to a class instance (such as self) or a variable initialized with some value (such as delegate = self.delegate!). These pairings are written within a pair of square braces, separated by commas.

I wish the compiler would have warned me. At least I know better now.

Written by Nick Harris

February 3, 2016 at 10:55 pm

Posted in Uncategorized

Follow

Get every new post delivered to your Inbox.