Nick Harris

CloudKit + Core Data + NSOperations – Introduction

with 9 comments

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

Idea

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

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

Warning

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

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

Cars Trucks Buses

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

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

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

Core Data Model

The Core Data model is very simple:

Core Data 

All of the objects share these properties:

var added: NSDate?

var lastUpdate: NSDate?

var recordName: String?

var recordID: NSData?

The main three objects also have:

var name: String?

var notes: NSSet?

While Note has:

var text: String?

var truck: Truck?

var car: Car?

var bus: Bus?

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

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

@objc protocol CTBRootManagedObject {

    var name: String? { get set }

    var added: NSDate? { get set }

    var lastUpdate: NSDate? { get set }

    var notes: NSSet? { get set }

}

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

protocol CoreDataManagerViewController {

    var coreDataManager: CoreDataManager? { get set }

    var modelObjectType: ModelObjectType? { get set }

}

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

// MARK: Segue

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

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

    if var destinationCoreDataViewController = segue.destinationViewController asCoreDataManagerViewController {

        destinationCoreDataViewController.coreDataManager = coreDataManager

        destinationCoreDataViewController.modelObjectType = modelObjectType

    }

}

Enter CloudKit

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

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

Public or Private Database?

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

Default Zone or Custom Zones?

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

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

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

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

CKRecordID 

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

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

let uuid = NSUUID()

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

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

        

return CKRecord(recordType: recordType, recordID: recordID)

 

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

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

        

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

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

    }

        

    let entityName = recordName.substringToIndex(index)

        

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

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

    }

        

    return managedObjectType.rawValue

}

 

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

enum ModelObjectType: String {

    case Car = “Car”

    case Truck = “Truck”

    case Bus = “Bus”

    case Note = “Note”

    

    init?(storyboardRestorationID: String) {

        switch storyboardRestorationID {

        case “CarsListScene” : self = .Car

        case“TrucksListScene” : self = .Truck

        case “BusesListScene” : self = .Bus

        default : returnnil

        }

    }

    

    static let allCloudKitModelObjectTypes = [

        ModelObjectType.Car.rawValue,

        ModelObjectType.Truck.rawValue,

        ModelObjectType.Bus.rawValue,

        ModelObjectType.Note.rawValue

    ]

}

 

CKSubscriptions

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

CloudKitManager

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

CKDatabaseOperation, NSOperation and Dependencies

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

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

Initializing CKZones

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

The enum for the zones started out simple enough:

enum CloudKitZone: String {

    case CarZone = “CarZone”

    case TruckZone = “TruckZone”

    case BusZone = “BusZone”

}

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

Passing Data Between Operations

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

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

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

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

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

My new FetchAllRecordZonesOperation:

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

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

class ProcessServerRecordZonesOperation: NSOperation {

 

    var preProcessRecordZoneIDs: [CKRecordZoneID]

    var postProcessRecordZonesToCreate: [CKRecordZone]?

    var postProcessRecordZoneIDsToDelete: [CKRecordZoneID]?

    

    override init() {

        

        preProcessRecordZoneIDs = []

        postProcessRecordZonesToCreate = nil

        postProcessRecordZoneIDsToDelete = nil

    }

    

    override func main() {

        

        print(“ProcessServerRecordZonesOperation.main()”)

        setZonesToCreate()

        setZonesToDelete()

    }

    

    private func setZonesToCreate() {

        

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

        let expectedZoneNamesSet = Set(CloudKitZone.allCloudKitZoneNames)

        let missingZoneNamesSet = expectedZoneNamesSet.subtract(serverZoneNamesSet)

        

        if missingZoneNamesSet.count > 0 {

            postProcessRecordZonesToCreate = []

            for missingZoneName in missingZoneNamesSet {

                if let missingCloudKitZone = CloudKitZone(rawValue: missingZoneName) {

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

                    postProcessRecordZonesToCreate?.append(missingRecordZone)

                }

            }

        }

    }

    

    private func setZonesToDelete() {

        

        // its important to not inadvertently delete the default zone

        for recordZoneID in preProcessRecordZoneIDs {

            if (recordZoneID.zoneName != CKRecordZoneDefaultName) &&

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

                if postProcessRecordZoneIDsToDelete == nil {

                    postProcessRecordZoneIDsToDelete = []

                }

                postProcessRecordZoneIDsToDelete?.append(recordZoneID)

            }

        }

    }

}

 

enum CloudKitZone: String {

    case CarZone = “CarZone”

    case TruckZone = “TruckZone”

    case BusZone = “BusZone”


    func recordZoneID() -> CKRecordZoneID {

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

 

    }

    

    static let allCloudKitZoneNames = [

        CloudKitZone.CarZone.rawValue,

        CloudKitZone.TruckZone.rawValue,

        CloudKitZone.BusZone.rawValue

    ]

}

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

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

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

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

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

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

        

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

   

    modifyRecordZonesOperation.modifyRecordZonesCompletionBlock = {

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

            

        print(“— CKModifyRecordZonesOperation.modifyRecordZonesOperation”)

            

        if let error = error {

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

            return

        }

            

        if let modifiedRecordZones = modifiedRecordZones {

            for recordZone in modifiedRecordZones {

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

            }

        }

            

        if let deletedRecordZoneIDs = deletedRecordZoneIDs {

            for zoneID in deletedRecordZoneIDs {

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

            }

        }

    }

        

    return modifyRecordZonesOperation

}

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

// 1. Fetch all the zones

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

let transferFetchedZonesOperation = NSBlockOperation() {

    [unowned fetchAllRecordZonesOperation, unowned processServerRecordZonesOperation] in

            

    if let fetchedRecordZones = fetchAllRecordZonesOperation.fetchedRecordZones {

        processServerRecordZonesOperation.preProcessRecordZoneIDs = Array(fetchedRecordZones.keys)

    }

}

        

let transferProcessedZonesOperation = NSBlockOperation() {

    [unowned modifyRecordZonesOperation, unowned processServerRecordZonesOperation] in

            

    modifyRecordZonesOperation.recordZonesToSave = processServerRecordZonesOperation.postProcessRecordZonesToCreate

    modifyRecordZonesOperation.recordZoneIDsToDelete = processServerRecordZonesOperation.postProcessRecordZoneIDsToDelete

}

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

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

Initializing CKSubscriptions

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

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

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

 

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

let transferFetchedSubscriptionsOperation = NSBlockOperation() {

    [unowned processServerSubscriptionsOperation, unowned fetchAllSubscriptionsOperation] in

            

    processServerSubscriptionsOperation.preProcessFetchedSubscriptions = fetchAllSubscriptionsOperation.fetchedSubscriptions

}

        

let transferProcessedSubscriptionsOperation = NSBlockOperation() {

    [unowned modifySubscriptionsOperation, unowned processServerSubscriptionsOperation] in

            

    modifySubscriptionsOperation.subscriptionsToSave = processServerSubscriptionsOperation.postProcessSubscriptionsToCreate

    modifySubscriptionsOperation.subscriptionIDsToDelete = processServerSubscriptionsOperation.postProcessSubscriptionIDsToDelete

}

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

 

Next Post – Saving Data >

 

Advertisements

Written by Nick Harris

February 9, 2016 at 6:47 am

Posted in Uncategorized

9 Responses

Subscribe to comments with RSS.

  1. […] 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 […]

  2. […] 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 […]

  3. […] 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 […]

  4. In FetchAllRecordZonesOperation, getting below error

    Hiren Gujarati

    July 2, 2016 at 10:24 pm

  5. In FetchAllRecordZonesOperation, getting below error

    CKError 0x1571af4e0: “Invalid Arguments” (12); “property recordZoneIDs must not be nil for FetchAllRecordZonesOperation”

    Hiren Gujarati

    July 2, 2016 at 10:25 pm

    • Be sure you’re using “class func fetchAllRecordZonesOperation() -> Self” to initialize the operation.

      Nick Harris

      July 2, 2016 at 11:26 pm

      • Thanks for your quick reply, yes this was the mistake, corrected and now working. Code works seamlessly to sync data with cloud kit. I love the code it works. i have few questions/suggestions.

        1. it shows notification whenever any change from cloud kit. Can it be silent (content available)?

        2. Regarding permission dialog, For iOS 10, the WWDC video shows changes for the notification – one property available so it also not ask for the permission. I think below iOS 10, there is no way for permission dialog.

        3. How about handling server token with the class – https://github.com/nofelmahmood/Seam/blob/master/Seam/Seam/SMServerTokenHandler.swift
        Reason: the token should be only saved permanently after successful execution of SaveChangedRecordsToCoreDataOperation

        4. if full sync is ongoing (queueFullSyncOperations), and user changes any record, will it cancel full sync process(queueFullSyncOperations)? or it will just add other operations in queue? Not sure, but as per the code it seems it will cancel all current operations and then continue with “saveChangesToCloudKit” function operations.

        5. Not sure, but class “FetchRecordChangesForCloudKitZoneOperation” required implementation of “moreComing” property?

        Thanks for all three blogs, it seems WWDC cloud kit best practices derived from this all tutorials 🙂

        Hiren Gujarati

        July 3, 2016 at 6:39 pm

  6. Hi,

    I don’t get answers for my last 2 questions (4 and 5), can you please help me to sort out those questions?

    Hiren Gujarati

    July 6, 2016 at 6:23 pm

    • I haven’t looked at this code in 5 months and in light of Swift 3 and the latest changes from WWDC I’m afraid I cannot answer your questions in a satisfactory way. The project is only a proof of concept and not complete. I’d encourage you to try things in your own project and find what works best for your needs.

      Nick Harris

      July 6, 2016 at 8:03 pm


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: