Nick Harris

The Making of an Icon (by a developer with Flying Meat’s Acorn)

leave a comment »

I spent a year working for the Ohio University College of Visual Communication. I still love that Ohio U gave me the opportunity to make some money while letting me audit classes outside of my Major (Computer Science) and Minor (English).

I learned a ton about Adobe Photoshop that year.

—-

I‘ve been working on a side project called “Suntracker”. Its a simple app that tracks sunrise / sunset times and how much sunlight there is per day based on Core Location. 

Last weekend I download Flying Meats Acorn 5.3 to make an icon. I’ve been an Acorn user for years so I was pretty excited to see the new version.

My review: Fantastic.

Here’s the icon I created in an evening:

Icon 180

CbttyXQUUAA8MEb jpg large

—-

I’m by no means a graphic designer, nor would I ever hire myself out as one… but all of the personal apps that I’ve published on the app store have icons I created. I’m familiar with the process and how many icon sizes you need to make.

This one is my favorite.

I like how it looks on my iPhone 4S, 5, 6, 6S Plus, iPad Pro and Watch.

Its actually a really simple image. A circle shape with a yellow > orange gradient on top of a light blue > midnight blue gradient background.

The genius of Acorn is how I can scale it any way to fulfill all the sizes needed in todays iOS development environment.

Acorn wasn’t always that way. This is the first icon I’ve ever created with it for iOS. In the past I’ve used vector based image editors – particularly Gimp which is powerful but not fun to use.

—-

Now I’ll admit that Gus Mueller is a friend. A few years ago, as I was introducing him to new friends at WWDC, my friend Rebecca asked me if he was one of my heroes. I paused for a second then answered with an easy and simple “yep”.

—-

Thanks Gus for Acorn! Keep up the amazing work!

Written by Nick Harris

February 26, 2016 at 7:35 am

Posted in Uncategorized

Swift Array with a Default Value

with 2 comments

I discovered this nifty little Swift array initializer tonight:

Screen Shot 2016 02 17 at 10 45 11 PM

Sweet! Set me up with a bunch of default objects!

Screen Shot 2016 02 17 at 10 48 29 PM

What the… Ugh.

Screen Shot 2016 02 17 at 10 49 09 PM

I get why but I really wanted 10 different NSObjects. 

Oh well.

Written by Nick Harris

February 18, 2016 at 5:56 am

Posted in Uncategorized

Swift vs. JavaScript

leave a comment »

Every year around this time I start watching the sunrise and sunset times. Two years ago I even spent a few days looking into the math that powers the NOAA sunrise/sunset calculator. It was before Swift but I spent a little time porting the JavaScript to Objective-C. I had it somewhat working but never finished it off. It was more an exercise for myself to port JavaScript to Objective-C.

I hated JavaScript at the time. I still prefer other languages to JavaScript. I like strongly typed languages with powerful compilers. Its just my preference though. I like the color green more then red too.

In 2003 I wrote a backend service in JavaScript that used classic ASP pages and XMLHTTPRequests to communicate with a Java Applet client as well as Delphi written Windows Services to control teleconferences. It ended up being my intro to C# and eventually Azure. Since then I’ve written some throw-away Node.JS web services including a Passport project and talk I gave about a year ago.

Those experiences really opened my eyes to the power of JavaScript. Tonight I spent a few hours porting the same NOAA JavaScript code to Swift.

A recurring theme I’ve heard in the developer community at large is that knowing JavaScript means you can now write iOS apps in Swift.

HA HA HA HA HA HA HA HA HA HA HA HA HA HA

I know all of my long time iOS developer colleagues know that this train of thought is ridiculous. 

The syntax between Swift and JavaScript is similar. Higher lever functions like map and filter are in both. Beyond that I fail to see the comparison.

I’m not going to dive into the differences. This post is more of a wake up call to anyone looking to hire an iOS dev and thinking that JavaScript experience = Swift.

It doesn’t.
At all.

If you are hiring – show them my NSOperation / NSOperationBlock retain cycle post and ask them to solve it. 

Written by Nick Harris

February 16, 2016 at 8:06 am

Posted in Uncategorized

UIAlertController + NSOperation

with one 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

with one 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 2 comments

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

Follow

Get every new post delivered to your Inbox.