Setting up CloudKit with Core Data

This is something I keep forgetting how to do so I’m posting this as a reference for future me and anyone else that finds it helpful.

These instructions assume you have created a new Xcode project with these options.

  • Language: Swift
  • User Interface: SwiftUI
  • Check the box for “Use Core Data”
  • Check the box for “Use CloudKit

This will create some boiler plate Core Data code in the AppDelegate and SceneDelegate.

In the SceneDelete, add this line after creating the context.

context.automaticallyMergesChangesFromParent = true

In Project Settings, select the target and add some entitlements.

  1. iCloud – Add this entitlement and select the “CloudKit” checkbox. Use the default container for your project or select one from the list if you have already created one.
  2. Background Modes – Add this entitlement and select the checkbox for “Remote notifications

That’s it really. From here you can start to work with SwiftUI views by adding the context into the environment for the initial view.

let contentView = ContentView().environment(\.managedObjectContext, context)

Dynamic Sort Descriptors and Predicates

I based most of my core data code for Retrospective Timelines on an example project that you can check out here. The developer sent me the link to this in a comment on stack overflow a couple of months ago. Throughout the project I’ve made small changes to this to better suite my needs.

Today I made a huge set of changes. I wanted a way to build UI controls that can modify the sort descriptors and predicates that the fetch requests used to drive the FRC. In the sample project this is done by passing in some optional strings, then parsing them into the objects they need to be in the a function that prepares the fetch request. This approach did not scale for what I need, as some of the layouts need complex sorting and/or predicates. I made a new version of this that replaces the optional string properties with some alternatives.

First the sort descriptors. Core Data has a way to apply multiple sort descriptors by passing them as an array. Even if I only have one sort order (rare for this project) I can just pass a single descriptor in the array

private var sortDescriptors: [NSSortDescriptor]?

The predicates were a bit different. Core data accepts one predicate for a fetch request, not multiple… kinda. Predicates can be combined using compounds. I can make compound predicates on each layout as needed.

private var predicate: NSPredicate?

Then I needed a way to add these to the fetched request.

private func configureFetchRequest() -> NSFetchRequest<T> {
        let fetchRequest: NSFetchRequest<T> = T.fetchRequest() as! NSFetchRequest<T>
        fetchRequest.fetchBatchSize = 0
        
        if let sortDescriptors = self.sortDescriptors {
            fetchRequest.sortDescriptors = sortDescriptors
        }
        
        if let predicate = self.predicate {
            fetchRequest.predicate = predicate
        }
        
        return fetchRequest
    }

I need a way to call this publicly as well. This calls the private method after checking the optionals. If I no longer have a descriptor or predicate, I set the property back to nil so it’s no longer used in configureFetchRequest

public func loadDataSource(sort: [NSSortDescriptor]?, predicate: NSPredicate?) -> [T] {
        
        if let sort = sort {
            self.sortDescriptors = sort
        } else {
            self.sortDescriptors = nil
        }
        
        if let predicate = predicate {
            self.predicate = predicate
        } else {
            self.predicate = nil
        }
        
        self.fetchRequest = configureFetchRequest()
        self.frc = configureFetchedResultsController()
        
        return self.allInOrder
    }

Now for the cool part. I can make a View Model for each layout where I can place some properties to drive controls in the user interface. This View Model will also handle building the sort descriptors and predicates for the layout. They can then be used as a parameter when I call loadDataSource.

Here is a basic example of the View Model that drives the list of event dates. The sortToggle variable is bound to a UI toggle so when the user taps it the sort order changes. I’ll replace this with a better sort button, but the underlying concept will remain the same.

class EventListVM: ObservableObject {
    
    @Published var sortToggle = false

    public func getSort() -> [NSSortDescriptor] {
        return [NSSortDescriptor(key: "isOngoing", ascending: sortToggle), NSSortDescriptor(key: "date", ascending: sortToggle)]
    }
    
    public func getPredicate(timeline: Timeline?) -> NSPredicate? {
        if let timeline = timeline {
            
            let startString = String(format: "%@%@", "dateStartEvent.eventTimeline", " == %@")
            let startPredicate = NSPredicate(format: startString, timeline)
            
            let endString = String(format: "%@%@", "dateEndEvent.eventTimeline", " == %@")
            let endPredicate = NSPredicate(format: endString, timeline)
            
            let compoundPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [startPredicate, endPredicate])
            return compoundPredicate
        }
        return nil
    }
    
}

The only thing that is a little nuts is the way I get the records to use in the ForEach view. This calls a method on one ObservedObject while using return values from two functions on another ObservedObject. I feel like I’m getting away with something here.

@ObservedObject var dataSource = RADDataSource<RTDate>()
@ObservedObject var eventListVM = EventListVM()

...

ForEach(dataSource.loadDataSource(sort: self.eventListVM.getSort(), predicate: self.eventListVM.getPredicate(timeline: self.timeline))) { rtDate in
...
}

Togging the sort order.

This is just a simple example, but now that I have the foundation in place I can extend this to work on much more complex user interfaces.

Thoughts on sorting events

Objective: 

I need a way to sort events in a list view that makes them easy to find. 

Constrains: 

  • All events have a start date
  • Some events have an end date
  • Some people may look for an event based on its start date, other may want to look for the end date. It could even vary depending on the type of data tracked on the timeline.

Options:

  1. Show only a list of events sorted by start date. Not at all what I want.
  2. Add a timeline setting to control sorting all related events. In this option an event would appear once in a list, either at the start date or end date position.
    1. Add a Bool to the Timeline entity to indicate preference to use end date if available.
    2. If an event has an end date, use it for the sorting
    3. Else, sort by start date
  3. Generate a list of all events with all start dates AND a list of events with end dates.
    1. Start date list – all events for a timeline
    2. End date list – only events with an end date for a timeline
    3. Combine these lists together and sort them. I don’t know how to sort both lists together like this…

I’d like to proceed with option three from the list above. Using this approach will place an event in the list twice if it has a start and end date. I can format the rows in the list with some indication of type such as single date, start date range, end date range. I can find some symbols to indicate what type the are. I could also place a toggle switch on the list layout to quickly show and hide the end date rows. I might even be able to make a cool animation for this.

I’m running into problems with how to actually pull this off. I wrote a simplified version of this in a playground but I can’t figure out how to sort the final resulting array, as the elements in the array have different sorting rules depending on rather they are a starting or ending record.

var fetchResults = [Event] // this is populated with an array of events

var endDateRows = fetchResults.filter({$0.includeEndDate == true && $0.dateEnd != nil})

var rows = fetchResults + endDateRows // combine the original array with the new array
// Now what do I sort by???

I’m considering some possible solutions but I’m not sure if these are a good way to proceed.

Solution A. 

Make a View Model that can fetch all of the events from Core Data, then process them into a new array of a different type. I could make a new containing object like this.

class EventContainer {
    var dateToSort: Date
    var eventRecord: Event // a reference to the Core Data record
}

My View Model could make two arrays of EventContainer, using map and filter calls to write the start and end dates to the dateToSort field, and writing the entire event to the eventRecord field. Then it could combine these two arrays into one and sort by the dateToSort field.

This approach seems reasonable but it puts a lot of processing into the hands of the View Model. Every time I navigate to a list I’ll have to run these processes. For individual Timelines that may not be a problem but when it comes to making list views that show events from all Timelines this could run into performance issues.

Solution B.

Modify the schema. Currently I have a simple Event entity in Core Data. In simplified form it looks like this.

class Event {
    var name: String
    var dateStart: Date
    var includeDateEnd: Bool = false
    var dateEnd: Date?
}

I could modify this by moving the date fields into a new table and making one to one relationships.

class Event {
    var name: String
    var dateStart: EventDate // Core Data To One relationship
    var includeDateEnd: Bool = false
    var dateEnd: EventDate //Core Data To One relationship
}

class EventDate {
    var date: Date
}

If I set this up correctly I could modify my fetch requests to record a list of EventDate records, then use the relationships to access the rest of the event data. This option would probably perform far better. It’s also a ton of work to update the app as it stands now. This might also complicate some of the calculations involving dates because the will be several database records involved instead of one Event record.

Out of these two options I’m leaning towards the first. This is not really an approach I’ve taken before and I think it could be valuable to try it. Coming from 8 years of FileMaker development, my first thought is to try to solve this like a database normalization problem, but I’ve learned the hard way before that Core Data is not a database. Sure, it uses a data and fills the same role as a database, but it’s an Object Modeling tool. It may be good to force myself to use a new type of thinking to solve this problem. If I run into performance issues then I can also take another crack at the second option. Either way, I need to spend some time thinking about this more before I decide how to proceed. Perhaps this is a good topics for Project Update.