SwiftData Techniques Instruction

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Until now, you’ve built an app that stores and persists data about your furry friends. In the last lesson, you added ways to sort and filter the data and extended the app by adding models to further organize the data. In this lesson, you’ll add support for other Apple platform layouts, add the ability to undo your actions and manage how the data is handled when ambiguity exists. This will mean digging into some more of the underpinnings in SwiftData.

Teaching an Old Dog New Tricks

Out of the box, a SwiftUI app supports iPhone portrait and landscape modes, and you navigate into a detail view or have a sheet slide up from the bottom. That’s a pretty convenient user experience. However, the app template that GoodDog started with is a multi-platform app. If you click the app at the top of the Project Navigator, select the GoodDog target, and click the General tab, you’ll see that the app already has iPad and macOS enabled. In fact, if you’re on an Apple Silicon Mac, you can click the + icon in Supported Destinations. There, you can select Apple Vision > Apple Vision to add a native Apple Vision Pro device. It’s really that simple to convert this app to run natively on visionOS.

// example NavigationSplitView setup
var body: some View {
  NavigationSplitView {
    List {
      ForEach(dogs) { dog in
        Text(dog.name)
      }
    }
  } detail: {
    EditDogView(dag: selectedDog)
  }
}

Adding a Unique Attribute

One of the advantages of grouping repeated data into a model is that it aids people in choosing the correct data. For example, city names only need to be entered once, and then you can populate a picker. Having uniform shared data can also prevent user input errors. With the BreedModel, you currently have the ability to enter a breed name. However, it can lead to errors and mismatches. Is the rule “i before e” in spelling the name Retriever? What if someone enters a second or third name with misspellings. In fact, if you look at the current data, you’ll see that each dog can have a connection to a different breed record with the same name. Currently, nothing is preventing multiple entries like this:

@Attribute(.unique) var name: String

Explicit Saves and FetchDescriptor

Similar to SortDescriptor, which specifies how to sort numerics and strings in SwiftData’s sort, there is FetchDescriptor type which specifies the objects collected when performing a fetch(), which like @Query gets the data from the store. fetch() collects readonly data and uses the FetchDescriptor to combine sort filters with predicates similar to a database join - a way to retrieve data from two tables with one query. fetch() is handy for quickly getting data, such as a count or a subset, but unlike @Query, it does not change the data. If you want to save with fetch(), SwiftData objects have an .id modifier, so you can use the .id(refresh) to trigger an update in your view. fetch() has .sortBy and .fetchLimit to get a small set of records. @Query does not have limit capability.

Undoing Changes

SwiftData has the ability to add undo and redo to your changes. However you’ll need to enable it. Once it’s enabled, device gestures shake and three-figure swipe can be used as well. You can enable the UndoManager as a property in the app’s .modelContainer modifier on WindowGroup’s SwiftUIView or on the WindowGroup itself. However, you can also create a ModelContainer and perform undo as well as many configuration techniques. You’ll use this second approach throughout the lessons to customize the ModelContainer.

// again just sample code
var container: ModelContainer {
  let container = try! ModelContainer(for: [DogModel.self]
  container.mainContext.undoManager = UndoManager()

  return container
}

Adding an Unknown Breed

Many dogs you meet at the park will be of mixed breeds. Some owners have no idea what type of dog they have without a DNA test. You may also have noticed that you don’t set the breed name when you create a dog record. This is a common problem with data entry. You may not know all the values. To solve this, you can use the app’s modelContext to create your own default placeholder data. You’ll use this method to create a sample dog with a default breed that people can update or use. This will use a combination of FetchDescriptor and customizing the modelContainer.

Bringing Your Own Schema

There are even more ways to customize the ModelContainer. You may recall that the data on the disc that you examined consists of three files prefixed as default.store. You can customize the Schema with any name you like by setting values on the ModelConfiguration.

let schema = Schema([DogModel.self])
let config = ModelConfiguration("GoodDogs", schema: schema)
let container = try! ModelContainer(for: schema)

 /* specify a custom path */
let storeURL = URL.libraryDirectory.appending(path: "Private Documents")
let config = ModelConfiguration(schema: schema, url: storeURL.appending(path: "GoodDogs.sqlite"))
See forum comments
Download course materials from Github
Previous: SwiftData Techniques Next: SwiftData Techniques Demo