Chords, chordsets and CoreData

In this module we create a couple of Image Performer’s basic structures as well as the beginnings of the data model we will be using to make the structures persistent. We will get started by setting up Core Data.

Setting up Core Data

Core Data is Apple’s widely used persistence framework. If you are not familiar with it and want to see an example of how data entries and the relationships among them get established, I have provided a short tutorial on how to set-up core date entities and relationship. The entities and relationships used there are the ones that I created for this module. If you choose to actually go through the steps of the tutorial, it would be best if you do it in an Xcode project separate from ImagerProject. Here’s another small tutorial you can complete first if you need to create an Xcode project.

In any case when you are ready to look at the contents of the file Imager.xcdatamodeld, you will find two entities defined there, Chord and Chordset. Each has several attributes and one relationship. The relationships are inverse between the two entities. Chordsets have chords and each chord belongs to a chordset; so you will see that the first is a “To Many” relationship and the other is a “To One” relationship. If you select Chordset and look at its delete rule in the Data Model Inspector, you will see that it is Cascade. That means that when you delete a chordset, its chords are deleted as well. Near the end of this page, you will find a little exercise you can use to explore the implications of getting the Delete Rule wrong. But you won’t be able to do that until we have generated some actual chordsets and chords.

Next we need a place in the program where we will do the actual work of managing the data we have defined; where we can add and delete objects, save them to the store and so forth. The DataModelController class is defined in the file DataModelController.swift. The imports at the top of the file provide us with access to two important frameworks, CordData and SwiftUI.

We made the DataModelController class an ObservableObject so that when changes occur in it, other parts of our interface can take advantage of them. In its init function, we create a persistent store container. I have included a note suggesting that we eventually replace the NSPersistentContainer with an NSPersistentCloudKitContainer so that the same data can be used across a user’s devices. But that capability would likely confound testing at this point. The init allows for an inMemory store which always starts out empty. The inMemory creates a store that will exist only in the computer’s memory, never being actually written to disk. This will be useful later in conducting tests. More often, and by default, we will load the persistent store from a file maintained on disk.

The addChordset, delete and save functions are pretty straightforward. I added a printProfile option to the save routine. It prints a simple message to the console that may be useful during debugging as well as for the forthcoming exercise I have already mentioned. DeleteAll is a convenience function; again, primarily of use during development and testing.

Having established a data model and a DataModelController, we need to instantiate them. We do that in the ImagerProjectApp.swift file where we create a viewContext attached to Swift’s @EnvironmentObject, which functions as a global. The code that does this is in ImagerProjectApp.swift.

import SwiftUI

@main
struct ImagerProjectApp: App {
    @StateObject var dataModelController: DataModelController
    
    init() {
        let dataModelController = DataModelController()
        _dataModelController = StateObject(wrappedValue: dataModelController)
        
        //dataModelController.deleteAll()
   }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, dataModelController.container.viewContext)
                .environmentObject(dataModelController)
                .statusBar(hidden:true)
                .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in
                    dataModelController.save(printProfile:true)
                 }
        }
    }
}

The init() section establishes the dataModelController. The commented out call to the dataModelController’s deleteAll function can be used to clear out the data store while you are exploring this code.

Modifiers on the ContentView attach the dataModelController to our ContentView, hide the iPad’s status bar, and invoke the dataModelController’s save function when the notification center tells our application that it is about to move into a background state.

This next section draw relates a solution to a common SwiftUI-Core Data issue, that part of Paul Hudson’s Ultimate Portfolio App. His Cleaning up Core Data tutorial may be of interest if you want more background on this issue.

One of the places where CoreData (an old technology) and Swift (a much newer one) rub against each other is in their use of optionals. Another is in the way they handle collections (sets versus arrays). In the CoreDataExtensions.swift file, we are locating our null coalescing code in extensions to our data objects. We are also taking advantage of the extension to address other issues, such as returning sorted arrays where Core Data uses sets.

The chordset list view

A chordset, which contains chords, defines Imager’s “voice” at any particular time. In this implementation there will be eight chords in a cordset. Each chord contains a background color and a collection of lumis, the objects that are used to create Images. The file where we do this is the ChordsetListView.swift file.

The init() function at the top of the ChordsetListView uses SwiftUI’s fetchRequest wrapper version of an NSFetchRequest. The chordsets variable which is returned conforms to the requirement that the information be a collection of identifiable entities.

Each chordset is represented in the table as a ChordsetRow View and each chord is represented as a ChordCell View. For now, RoundedRectangles are being used to stand in for what will become Metal views.

Running and testing the code

We are finally ready to compile and run some code. With your iPad selected as the target, press Xcode’s build and run button. When the program has compiled you will see a white display with a blue + centered near the top. Press that button several times to create several empty chordsets. When you select a title field, the keyboard will appear enabling you to give it your own choice of titles. Your screen should begin to look something like this. Your colors will, of course, be different since we set each background color to a random value when we initialized the chords.

If you want to save this data, force the program to resign (by pressing the iPad’s home button, for example). When you do that, you will see a console message indicating how many chordsets and chords are in the Core Data store.

The number of chords should be eight times the number of chordsets. This relationship is maintained because we set the delete rule to “cascade”. If you want to see the effect of leaving it set to the default “nullify”, you can change it back and then add and delete chordsets. Chordsets can be deleted by swiping left and pressing the delete button. Save the data as above and check the console messages. There will be more chords than we expect. When you are all done, be sure to return the delete rule to “cascade”. Then go to the ImagerProjectApp.swift file and comment out the dataModelController.deleteAll() line and run the compiler again. This will clear out the store. Return the line to comment status and compile once more. Now you are back to where we were when you created a display like the one above and you can once again add and rename some chordsets. Check once again that the number of chords is three times the number of chordsets.