In this module we create several of Image Performer’s basic structures as well as the beginnings of the data model we will use to organize them and make them persistent. Let’s start with Core Data.
Setting up Core Data
Core Data is Apple’s widely-used data management framework. If you are going to be using Core Data in other projects, you probably should develop a deeper understanding of it than I will give here. I relied heavily upon Donny Wals book Practical Core Data and am continuing to learn from it. If you are at the other extreme, and have no interest in how we manage data and its persistence, you can safely skip this section completely. How we use the data structures will become clear in later sections and doesn’t really depend too much on how they were set up or are persisted.
The files for managing Image Performer’s data model are contained in the Storage folder. The Image Performer.xcdatamodeld file contains the data definitions. This file is edited using the Core Data model editor in Xcode. If you are unfamiliar with editing data entries and their relationships, you can refer to a short tutorial on how to set-up core date entities and relationship. The entities and relationships used there are similar to those that we use in this module. If you choose to actually go through the steps of that tutorial, you should do so in an Xcode project separate from Image Performer.
When you look at the contents of the file Image Performer.xcdatamodeld, you will find three entities defined there, Chord, Chordset and RLColor. Each has several attributes and one or two relationships. In general the relationships have inverses. For example, chordsets have chords and almost every 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 look at Chordset’s 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. Similarly when a chord is deleted, so is it’s bgColor (which is of type RLColor).
Following a suggestion from Paul Hudson in his Cleaning Up Core Data post, we are using the manual Codegen option when defining entities. One of the places where CoreData (an old technology) and SwiftUI (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). The RLColor.swift, Chord.swift and Chordset.swift files provide support for the entities. We are locating some of the null coalescing code in these extensions and using them to address issues such as returning sorted arrays where Core Data produces sets. Here, for example, is Chordset.swift.
@objc(Chordset)
public class Chordset: NSManagedObject {
}
extension Chordset {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Chordset> {
return NSFetchRequest<Chordset>(entityName: "Chordset")
}
@NSManaged public var id: UUID?
@NSManaged public var title: String?
@NSManaged public var creationDate: Date?
@NSManaged public var index: Int16
@NSManaged public var chords: NSSet?
}
extension Chordset : Identifiable {
var chordsetTitle: String {
get{ title ?? "" }
set{ title = newValue }
}
var chordArray: [Chord] {
let set = chords as? Set<Chord> ?? []
return set.sorted {
$0.position < $1.position
}
}
}
StorageProvider is an ObservableObject whose Published properties will be used to keep Image Performer’s various controls synchronized. This is where most of the data management activity, such as adding and deleting chordsets, will take place.
The StorageProvider is created at that app’s launch and the managedObjectContext of the environment is used to inject it into views where needed. StorageProvider functions are called when the app is initialized and whenever the scenePhase shifts.
@main
struct Image_PerformerApp: App {
@State var storageProvider: StorageProvider
@StateObject var globals = Globals()
@StateObject var userSettings = UserSettings()
@Environment(\.scenePhase) var scenePhase
init() {
let storageProvider = StorageProvider()
self._storageProvider = State(wrappedValue: storageProvider)
storageProvider.getControlsChord()
storageProvider.getAllChordsets()
storageProvider.currentChordsetNumber = UserDefaults.standard.integer(forKey: "currentChordsetNumber")
storageProvider.getCurrentChordset()
}
var body: some Scene {
WindowGroup {
ContentView()
.statusBar(hidden:true)
.environmentObject(globals)
.environmentObject(userSettings)
.environmentObject(storageProvider)
.environment(\.managedObjectContext, storageProvider.persistentContainer.viewContext)
.background(Color .black)
}
.onChange(of: scenePhase) { newScenePhase in
if newScenePhase == .background {
storageProvider.save()
storageProvider.profile()
UserDefaults.standard.set(storageProvider.currentChordsetNumber, forKey: "currentChordsetNumber")
}
if newScenePhase == .active {
storageProvider.getControlsChord()
storageProvider.getAllChordsets()
storageProvider.currentChordsetNumber = UserDefaults.standard.integer(forKey: "currentChordsetNumber")
storageProvider.getCurrentChordset()
}
}
}
}
The chordset panel
The ChordsetPanel view presents eight chord selectors with a ShiftButton to their left and a pair of StepButtons to the right.
We will eventually use the ShiftButton to extend the capabilities of lots of other controls, much as physical DJ consoles do. At the moment, it lets us store the values of the color controllers in the chord selectors. If you hold Shift down while touching one of the eight chord buttons, that chord’s background color will be changed to the current settings of the hue and purity ribbons. The other color controllers will be remembered as well.
Once you have set some of the chord buttons, you can recall the associated values by tapping them.
The stepper buttons on the right allow you to move through the chordsets.
Chord selectors will get more sophisticated, but this should already be giving you some ideas about Image Performer’s structure. A chordset contains chords. A chord contains information about color, form and motion—some of it related to the background and some to objects that the chord maintains and updates.
The chordset list
The chordset showing in the ChordsetPanel, defines Imager’s “voice” at any particular time. In this implementation there are eight chords to a cordset. For now, each chord contains a background color and some identifying information. Soon it will also house panes that describe the areas of the canvas its background colors and objects affect, and a collection of lumis, the objects that are used to create Images.Tapping the edge control at the middle of the right edge will toggle to ChordsetList panel’s visibility.
At the top of chordsetList panel’s view is a Plus sign, used for adding chordsets to the managed object collection. Beneath it is a scrolling list of the chordsets. Their titles can be edited by tapping on them. A chordset can be deleted using SwiftUI’s standard left swipe to expose the delete option and then pressing on the red delete button at its right.
The code is located in ChordsetListPanel.swift.
struct ChordsetListPanel: View {
@EnvironmentObject var storageProvider: StorageProvider
func chordRect(width:CGFloat, height:CGFloat) -> CGRect {
var theRect: CGRect = CGRect()
let unitWidth = width / 9
theRect.size.width = unitWidth
theRect.size.height = unitWidth * 9/16
return theRect
}
var body: some View {
VStack {
Button(action: { storageProvider.addChordset() } ) {
Image(systemName: "plus")
.padding(.top, 20)
.padding(.bottom, 10)
.font(.system(size:36, weight: .bold))
.foregroundColor(.blue)
}
GeometryReader { geo in
List {
let theRect: CGRect = chordRect(width:geo.size.width, height:geo.size.height)
ForEach(storageProvider.chordsets, id: \.self) { chordset in
ChordsetRow(chordset: chordset,
width:theRect.size.width,
height:theRect.size.height)
}
.onDelete(perform: removeChordset)
}
}
.padding(.bottom, 20)
}
}
struct ChordsetRow: View {
@State var chordset: Chordset
var width: CGFloat
var height: CGFloat
var body: some View {
VStack {
HStack {
ForEach(chordset.chordArray) { chord in
MetalView(chord:chord, noteNumber:0)
.frame(width:width, height: height)
.border(Color.black, width:0.25)
}
}
TextField("Chordset title", text: $chordset.chordsetTitle)
.foregroundColor(.blue)
.font(Font.headline.weight(.bold))
.multilineTextAlignment(.center)
}
}
}
func removeChordset(at offsets: IndexSet) {
for index in offsets {
let theChordset = storageProvider.chordsets[index]
storageProvider.deleteChordset(theChordset)
}
}
}
Status
A lot of the infrastructure of the program is now in place. We have connected to both Metal and Core Data. We have working versions of a few of Image Performer’s key structures. We have explored a fair swath of SwiftUI’s capabilities. And, you should be developing a feel for Image Performer’s architecture.
While our data can now persist, it will still be subject to loss for a while. As we change the data structures, new versions will likely become incompatible with earlier ones, so that we will need to delete the application and all of its data to move on. We’ll address that before very long, but for now want to be aware of the issue.
There are a couple of other known issues, that I have not resolved. Both produce console messages but seem to do no additional damage. The first, which occurs only when the program launches on a device, indicates a file open failure (no such file or directory). The second, which happens when editing the title of a chordset in the chordsetList view, produces a console message indicating that layout constraints are violated. The system recovers in both cases, but these represent code debt that we are currently carrying forward.
As of April 30, 2021