UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

CoreData and CloudKit syncing question

Forums > SwiftUI

Hi,

I have a SwiftUI project that is using CoreData and host in CloudKit. I find myself wanting to create some default items for a certain entity in my CoreData IF it is empty when I first run my app.

So I've set up an @FetchRequest and put the data into a variable .onAppear I check to see if the .count < 1 in that variable And if it is, then I generate my default items and save them to CoreData

That is great... on device 1 (iPod touch)

Then I switch to device 2 (iPad Pro) and open my project and... It runs the @FetchRequest and puts the data into a variable .onAppear it thinks the .count < 1 because it has not yet received the synced data from iCloud. And so it happily generates a complete second set of default items and saves them to CoreData

And now I have TWO sets of that data

Is there some way to check if the cloud syncing is done before I have it check the .count? Or is there some other way I should be handling this?

3      

Maybe—and this is purely thinking out loud—approach it from the other side.

Instead of creating default items in CoreData and then syncing them to CloudKit, use the dashboard to create items in CloudKit's public database to pull into CoreData on first launch.

See the WWDC20 video "Sync a Core Data store with the CloudKit public database" for more info. From the transcript:

The first [feature request received by Apple since 2019] being that all of the requests wanted to create a data set that everyone could use - that is, all users of your application could access. Now sometimes this was data that you, the developer, would create, such as an application template or an initial data set so the user has a rich experience with your application from day one.

4      

Thanks for the reply @roosterboy.

Nick Gillett definitely is inspiring to listen to. In fact, it was the vision he shared of having apps using iCloud to sync data across macOS, iOS, and iPadOS in a previous WWDC video that really inspired me to try it out (Paul didn't cover CloudKit in 100 Days Of SwiftUI so I was really expecting the worse). Fortunately, most of that just works right out-of-the-box when you check that you want to use CoreData and host in CloudKit... and what is/was broken in the template code actually was pretty easy/obvious to fix (the missing buttons in the template to actually start playing with creating some new records and see this in action) and I was able to find a video to walk through a couple of the steps NOT handled by the template (the additional setup steps and a couple lines of code to help make sure things are syncing "quickly"... well... in about 15 seconds or so at least. It was also of great benefit that Paul covered CoreData in 100 Days Of SwiftUI so even though the template is not exactly the same, it is close enough that I could figure out what they were doing and how to use it.

But... Nick Gillett is sadly no Paul Hudson (and I'm not talking about the lack of a British accent or the complete lack of cute Samoyeds wanting treats throughout the presentation). Through most of this video Nick might as well be speaking some strange alien language because I've watched through it a few times now and I still have no clue what he is talking about or how to do what I would need to do to "use the dashboard to create items in CloudKit's public database to pull into CoreData on first launch". This is a shame because, it does actually sound like an ideal solution to the problem is right there... sadly... this is a problem I have in general with Apple's developer documentation and is the sort of thing that led to two disasterous attempts at learning Swift that crashed and burned and had me almost to the point of giving up completely prior to coming across this site and 100 Days Of SwiftUI.

In fact, I'm not even sure... did Nick even demonstrate what I would need to do in that presentation? Does anyone know of any video or tutorial out there that explains this better?

3      

hi Mike,

i worked with a another developer on this problem for one of his apps that's now on the App Store. our issue concerned pre-populating the Core Data store on each device with an initial catalog of about 350 records defined in JSON in the app bundle. the user could then add to or even modify some of the catalog records.

some comments relevant to what you're doing:

  • there is no way to really ask on device 2 if there's data in the cloud from device 1 and then skip pre-populating data on device 2. there are too many factors involving connection to the internet and even the timing of when cloud data is delevered to device 2 that you cannot make such a determination at startup.
  • there was no reason for us to put a moderately large collection of what, for the most part, was most likely going to be immutable data into the cloud. the cloud is really only for user-generated data.
  • @roosterboy makes a good point, about placing default data in the public database, but i think it's only sensible for read-only data.

the solution we came up with was a variant of Apple's suggestion, that you add a local configuration to your Core Data model to manage multiple stores -- whatever default data you want to load onto the device goes into a local store, whatever the user adds or modifies goes into the cloud store.

this is where i go into TL;DR; mode, so tread carefully ...

however, since our catalog data was relatively small in size, we opted to load local data directly into memory, and then, all user CRUD operations are reflected in Core Data records. a custom "data manager" object then coordinates access to the records, knowing how to find them as needed.

short story, trying not to get into the weeds on details:

PART 1: the user opens the app on device 1, loads the default data, but never edits it or creates new data.

  • when the app starts up on device 1, all catalog records are loaded into memory from the app bundle.
  • all records have a UUID (that must be specified in the JSON and is not dependent on the device on which they are loaded).
  • we have a "data manager" object that tracks all in-memory records by UUID (a dictionary is useful for quick lookup, since we often find records by UUID).
  • as we need to read data from one of the catalog records, the data manager returns the appropriate in-memory record.

PART 2: the user wants to edit the default data or create new data on device 1.

  • when the user updates one of the in-memory records, we create a new record in Core Data, copying all the data from the in-memory record ... including its UUID ... together with the updates into the new record. importantly, we have a property on each in-memory record that is a link to a Core Data record, and we set that link now to the new Core Data object (it is initially nil). thus, an edited in-memory record now becomes just a proxy for a Core Data record.
  • in the future, when we retrieve a property of a record, the value is located by looking at the core data link: if it is nil, use the in-memory value, but if non-nil, then use value in the Core Data record.
  • if the user adds a new record, we add both a new Core Data record and an in-memory record, setting the core data link of the in-memory object to the core data record, setting the in-memory UUID to be the same as the Core Data record, and have the data manager add the in-memory record to its dictionary.

PART 3: enter device 2.

to work with device 2, everything starts out the same way, except: device 2 does not yet know what, if anything, is on-device in Core Data; and in the futue, device 2 will be alerted to any user-generated data from the cloud created on device 1. so the data manager needs to know when something happens in Core Data that it wasn't aware of at startup -- i.e., objects on-device and in the cloud.

  • our data manager has an NSFetchedResultsController (FRC). when the FRC is created, and whenever the FRC kicks due to Core Data changes (it conforms to the NSFetchedResultsControllerDelegate protocol), the data manager can inspect all the Core Data objects on-device and examine their UUID values.
  • for any Core Data object unknown to the data manager, it creates a new in-memory object with the same Core Data UUID and sets the in-memory core data link to the Core Data object.

that should do it. whew! in short, our data manager tracks only in-memory objects, which hold either default data that are not stored in Core Data, or represents a proxy for data in Core Data.

hope that helps,

DMG

Edited after the fact: the data manager we use is an @ObservableObject, but not everything is a @Published or even public property since we have a little more going on in this particular app, so remember that it may be necessary to explicitly call objectWillChange.send() when processing updates that are handled by the manager.

4      

Wow... so much hassle to add (what would be for me)... 3 or 4 pre-defined choices (that are just Strings - one or two words at most) that the user might end up deleting and adding their own anyway... yikes... and on top of that, if they do use them, they need to have a relationship to another entity in the private database if they are selected. I'm not even sure from the way this is described if the user could delete them at all, and even if they did... would they just reappear like some U2 album? Somehow it feels like going through the process of mailing a $0.02 check to someone... I mean, the postage costs far more... the processing costs far more... it seems rather crazy.

3      

Hmm, just 3-4 choices of small strings. Seems like syncing with CloudKit would be overkill for that.

How about this...

Keep your 3-4 strings locally in Core Data and don't worry about syncing them across the user's devices. That miniscule amount of data is fine to keep around all the time.

Instead, use NSUbiquitousKeyValueStore (kind of a UserDefaults that stores in iCloud) to store a flag for each of your 3-4 strings to indicate whether they should be displayed or if the user has deleted them. I mean, they won't actually be deleted from your Core Data store but if you use this flag to determine whether or not to display each string in your UI then the effect for the user is the same as deleting them. You could even store the same flags in UserDefaults as a hedge against iCloud not being available due to lack of network connection or whatever.

Since these are all strings and you can store arrays in the key-value store, just store the data as an array of strings and then when you fetch all your data from Core Data, you can filter based on whether the array contains each item from Core Data. So, something like a key of "defaultChoices" with an array containing all the defaults that the user hasn't deleted.

Just a thought.

3      

I'm not sure I understand this @roosterboy so I guess I'm going to have to go with something specific here. The user can create an item in a Password entity. The Password entity has a "to one" relationship named "label" with a destination of "PasswordLabel" and an inverse of "passwords".

The PasswordLabel entity has just one attribute... "name" which is a String and has a "to many" relationship named "passwords" with a destination of "Password" and an inverse of "label".

Basically this is to help categorize the passwords into groups that make sense to the user (hopefully for filtering in a list once I get to trying to figure out that bit - my only experience here is 100 Days Of SwiftUI which I only just finished a few months ago). So what I'm trying to do is give the user 3-4 presets like "Shopping", "Social Media", "Streaming Services", etc. on the assumption they might store things like online shopping site passwords for places like Amazon, Walmart, etc. or social media sites such as Twitter, Facebook, etc. or streaming services such as "Netflix", "Hulu", etc. So when they are creating a new Password record and they tap to select a label there will be something in the list to start them off. But... that isn't it for labels... if it was I could probably have just used an array and stored values to match the position and set up my entity to just use an Int16. The thing is they might want to add other category labels... perhaps "Food" for restaurants they like to order from... or more general services like GrubHub or DoorDash, etc. Or maybe they want a "Games" category label for games with personalized user accounts... or... maybe a "Developer" label for useful sites like um... HackingWithSwift (for example). Maybe they don't subscribe to any streaming services and so want to get rid of that label, or maybe they want to change the name of a label to something that works better for them.

So it makes sense to have the PasswordLabel entity sync to iCloud. The Password entity will and there is that relationship going on between the Password and PasswordLabel that CoreData is maintaining. So I'm not quite sure what you mean by just storing strings locally in CoreData. I mean, how would that even work when it comes time for CoreData to sort out a relationship to a PasswordLabel that doesn't match on another device in the Password entity? Honestly, I'm just building this off of the template that Xcode created when I selected the CoreData and Host in CloudKit checkboxes. So on the CloudKit stuff I really don't know anything (sadly, Paul didn't cover CloudKit in 100 Days Of SwiftUI and there is no 100 Days Of CloudKit), so basically I'm just relying on the template to be handling the local cached data and sync it with the cloud and just using the CoreData stuff that Paul covered in 100 Days Of SwiftUI plus the CoreData portions in the Xcode template (which, while slightly different in places, are understandable thanks to what Paul covered).

I've been able to somewhat broaden my understanding of things a bit with useful videos, tutorials, or (sometimes) even some answers provided on some developer forums to questions others have had... and sometimes answers provided when I have asked questions. But sometimes my searches turn up empty or I'm left with only explainations of stuff that I know nothing about (such as that WWDC video you had suggested) or some Apple documentation that explains how something is written, but not how to actually use it. Is there some book that explains all of this clearly? Is there some video or tutorial that explains this all really well that I'm just not finding?

3      

use the dashboard to create items in CloudKit's public database to pull into CoreData on first launch

@roosterboy, do you know of any videos, tutorials, books, etc. that clearly demonstrate how to do this? Or can you walk me through this perhaps referencing the particular time stamps in the WWDC video that are relevant here (because I'm not seeing anything where Nick is demonstrating creating items in the public database to pull into CoreData on first launch)?

In my project I'm using the template that Xcode provides when you choose Core Data and Host in CloudKit when the project is created. My data model includes an entity called Password with a "To One" relationship named "label" with a destination of "PasswordLabel" and Inverse of "passwords". The relevant bit here is:

The entity called "PasswordLabel" which contains just a single attribute "name" of type String. And contains a "To Many" relationship of "passwords", with a destination of "Password", and Inverse of "label".

Picking this apart a bit... you said "create items in CloudKit's public database". I want to be sure I have this part correct to start:

  1. log into the dashboard
  2. select my container
  3. click on Data?
  4. Where it says Private Database click and select Public Database?
  5. Under type change to CD_PasswordLabel?
  6. Click the "New Record" button?
  7. This presents me with a Creating Record section with 2 Custom Fields... 2??? but my Core Data only has a single attribute "name"... so I'm a bit confused. What do I put in for CD_entityName? "PasswordLabel"?
  8. Presumably I would then enter my String that I want to use as the label in the CD_name field and then click Save.

And then repeat steps 6-8 for my other predefined defaults? I'd like to make sure this is correct before I start creating a bunch of stuff and find I did it wrong and can't delete it for some reason... I mean... I have an extra container in there that I made for an intial experiment before my current project... and there is no way I can see to remove that. So I'm a bit hesitant.

And then... well... I'm completely lost on the "pull the public database items into CoreData on first launch" part. In the video Nick zooms through looking at some invisible file "Core Data Stack"... I'm not really sure how he even brings that up. Then he shows changing a single line to work with the public database... does that stop it working with the private database? Then he shows copying and pasting a bunch of code to work with the public database again? A different one?

Edit: I'm still lost, but after digging around online I managed to find that what Nick was doing in "Core Data Stack" is now done in the Persistence file with something like the following... though I still don't really know what exactly that is doing? Such as if this code is then putting EVERYTHING into the public database instead of the private one? Or if it is how I would be setting things up to just use the public database to import the items there (assuming the steps above are correct) into the private database on first launch if nothing is there already. But at least I'm not looking around for a file that isn't there (so... yay... progress I guess):

let container: NSPersistentCloudKitContainer

init(inMemory: Bool = false) {
    container = NSPersistentCloudKitContainer(name: "iKeeper")

    // This is the part I found online that seems to be taking care of what Nick did in the video?
    guard let description = container.persistentStoreDescriptions.first else {
        fatalError("Unresolved error")
    }
    description.cloudKitContainerOptions?.databaseScope = .public
    // ---------------------------------------------------------------------------------------

    if inMemory {
        container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
    }
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    container.viewContext.automaticallyMergesChangesFromParent = true
}

And then back to the dashboard to add a couple things to the Schema section and looking at Indexes. Then he says something about adding some indexes to all five of the record types for recordName and modifiedAt... ok, presumably that would mean adding it to everything that shows up on the sidebar for my database too... or would it be just the sections where I want to actually pull into CoreData?

Then it sounds like it requires Polling to import (whatever that means?). Then Nick confuses me a bit when he talks about editing... but... I really want to pull this data into the private side (I think) and so I shouldn't have to worry about editing then because at that point it would be private and work just like any other data the user adds so the user could do whatever they want with it. Right? So is any of this part relevant to what I need to do?

It sounds like maybe this polling stuff has something to do with something called a CKQueryOperation? Then he says something about restricting the entitites that are using the public database to a specific configuration for that store (I'm not sure what exactly that means in the context of trying to simply import my pre-defined passwordLabel items. Then he says something about a configuration with a managed object model and talking about that in detail in some documentation (I'm sure that's about clear as mud).

So... yeah... How do I import the predefined items from the public database into the private one on first launch? That seems to be missing from the presentation entirely.

Then he starts talking about deleting from the public database... ok... I'm guessing we are back to something I shouldn't have to worry about?

3      

So WWWDC21 has come and gone, and little additional light has been cast on this topic :)

Nick's latest presentation on Sharding data using NSPersistentCloudKitContainer was exactly what I was waiting for, but...

1) The code supplied by Nick and the team doesn't seem to work for me in Xcode 13 (Beta 1 or Beta 2) 2) The code supplied doesn't seem to make use of the SwiftUI framework, which is where I thought Apple was pushing developers 3) The boilerplate code for a CloudKit hosted CoreData application doesn't seem to work either...

Maybe this is all too hard? Surely these things would have been the focus of initial beta release testing?

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.