Introduction to Data Persistence in SwiftUI with SwiftData

In the constantly evolving world of SwiftUI app development, having a user-friendly and efficient way to handle data persistence is crucial. In this blog post, I introduce you to SwiftData – a new, Swift-native persistence framework. Designed to work seamlessly with SwiftUI, SwiftData can make your data management tasks much easier and faster than ever before.

We’ll explore how SwiftData, with its use of native Swift types and modern features, can simplify your interactions with persistence compared to older frameworks like CoreData. You’ll learn how to define models, use SwiftData in a SwiftUI project, perform CRUD operations, and navigate some of the limitations and bugs in the current version. SwiftData is using the new Observation feature for a simpler and more effective data flow.

Requirement: Xcode 15

I’ll use a demo project called ‘SnippetBox’ – a tool that helps programmers save and organize frequently used code snippets – to bring the concepts to life.

snippet sample project for swiftdata
SwiftData demo project with folders and snippet collections

⬇️ Download the project from GitHub https://github.com/gahntpo/SnippetBox-SwiftData

What is SwiftData?

SwiftData is a new Swift-native persistence framework. It is built with the Swift language. SwiftData is made to work well with SwiftUI.

Therefore, it makes it much easier and faster to persist the user’s data in your SwiftUI apps.

What is the difference between SwiftData and CoreData?

SwiftData is a new Swift-native persistence framework. CoreData is around for quite a while and was built with Objective-C. Therefore you have to deal with older types in CoreData. SwiftData is built with Swift and allows you to use Swift native types.

Additionally, SwiftData uses a lot of newer features like Swift concurrency and Swift macros.

SwiftData uses a lot of the same technologies as CoreData for managing the underlying database. But the way you write your code with SwiftData is much easier and Swift-like than CoreData.

If you have an existing project in CoreData, you can migrate it to SwiftData relatively easily and keep the same database. Have a look here for a full walk-through.

I see SwiftData as the new and easier version of CoreData. Apple took the technologies that work well from CoreData and added all the newer tools to work best with SwiftUI.

Creating Models for SwiftData

In SwiftData models are defined as classes. To add a model definition, you can decorate your class with the @Model macro:

import Foundation
import SwiftData

@Model final public class Folder {
    var creationDate: Date
    var name: String
    @Attribute(.unique) var uuid: UUID

    @Relationship(.cascade, inverse: \Snippet.folder) var snippets: [Snippet]?

    init(name: String = "",
         snippets: [Snippet] = []) {
        self.creationDate = Date()
        self.uuid = UUID()

        self.name = name
        self.snippets = snippets
    }
}

SwiftData will automatically create a Schema from your model file. Your model will also have ´PersistentModel´ conformance:

protocol PersistentModel : AnyObject, Observable, Hashable, Identifiable

In the above example, I also used other macros. @Attribute is a macro that allows you to finetune the attribute storage. For example, you can make a unique property like so:

@Attribute(.unique) var uuid: UUID

The @Relationship macro allows me to customize the delete rules.

Read more: Modeling Data in SwiftData

Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

Using SwiftData together with SwiftUI

SwiftData works well in a SwiftUI project. I will show you how quickly you can get data persistence in your app.

Setting up a new project with Xcode 15

You can now set up the modelContainer for your SwiftData project in the main app file. The modelContainer is a modifier that ensures all windows in the group are configured to access the same persistent container.

import SwiftUI
import SwiftData

@main
struct SnippetBoxApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(for: [Snippet.self, Folder.self, Tag.self])
        }
    }
}

This not only sets up your container but also creates and sets a default ModelContext in the environment. The ModelContext is used to track changes to instances of your app’s types and can be accessed from within any scene or view using the environment property. You can access the model context in any of your views from the environment:

import SwiftUI
import SwiftData
struct ContentView: View {

    @Environment(\.modelContext) private var context
    var body: some View {
      ...
   }
}

Saving changing in SwiftData

SwiftData automatically saves changes to the underlying database. This happens when SwiftUI views update, which allows you to keep your user’s data consistent. You don’t need to manually call save.

Working with SwiftData: Create, Read, Update, and Delete your Data

All-access to your data in SwiftData happens with the model context. For example to fetch all folders in the database, fetch the data from the context:

var folders = try context.fetch(FetchDescriptor<Folder>())

In SwiftUI you can fetch all folders with the `@Query` property wrapper:

import SwiftUI
import SwiftData

struct FolderListView: View {

    @Environment(\.modelContext) private var context
    @Query(sort: \Folder.creationDate, order: .forward)
    private var folders: [Folder]
    @Binding var selectedFolder: Folder?

    var body: some View {
        List(selection: $selectedFolder) {
            ForEach(folders) { folder in
                NavigationLink(value: folder) {
                    FolderRow(folder: folder, selectedFolder: selectedFolder)
                }
            }
            .onDelete(perform: deleteItems)
        }
        .navigationTitle("Folders")
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                EditButton()
            }
        }
    }

    private func addFolder() {
      ...
    }
    private func deleteItems(offsets: IndexSet) {
      ...
    }
}
example for swiftdata in swiftui

You can filter your query with the new predicate macro. For example, the Snippet type has a `isFavorite` boolean property. We can filter the query and only show the favorite snippets:

@Query(filter: #Predicate<Snippet> { $0.isFavorite },
       sort: [SortDescriptor(\.creationDate)] )
var favoriteSnippets: [Snippet]

See more complex and advanced predicates: How to fetch and filter data in SwiftData with Predicates

You can then create new objects and insert them into the model context to persist it like so:

let folder = Folder(name: "new folder")
context.insert(folder)

For example, I added a `New Folder` button in the above folder list view.

Additionally, you use the context to delete objects in SwiftData:

@Environment(\.modelContext) private var context

private func delete(folder: Folder) {
    context.delete(folder)
}

How to update SwiftData objects

Updating model objets is very simple. You set the property of the object to the new value. For example, if you want to change the ´isFavorite´ property of a snippet object, you can set it to true like so:

import SwiftUI

struct SnippetDetailView: View {

    var snippet: Snippet

    var body: some View {
     Button {
        snippet.isFavorite = true
     } label: {
        Label("Mark as Favorite", systemImage: "heart")
     }
     ...
   }
}

SwiftData uses the new Observation API from SwiftUI. When you add the ´@Model´ macro to your classes, they will conform to the new Observable protocol. This API is much better at updating SwiftUI views when model properties changes. Thus, you should benefit from a better performance when using SwiftData.

I you want to work with a view like `Toggle `in SwiftUI that requires a binding, you can use the `@Bindable` property wrapper like so:

import SwiftUI

struct SnippetDetailView: View {
    @Bindable var snippet: Snippet

    var body: some View {
      Toggle(snippet.isFavorite ? "favorite" : "not", 
             isOn: $snippet.isFavorite)
      ...
   }
}

SwiftData and Xcode Preview

Using SwiftData with Xcode previews is a struggle. Some of Apple’s demo projects gave up on previews completely and removed them from all files. However, here is a working solution. It is more complex than I would prefer.

First define a preview container that is in memory and repopulate it with your data:

let previewContainer: ModelContainer = {
    do {
        let container = try ModelContainer(for: Snippet.self,
                                           configurations: ModelConfiguration(isStoredInMemoryOnly: true))
        
        Task { @MainActor in
            
            let context = container.mainContext
            
            let snip = Snippet.example2()

            
            let folder = Folder(name: "folder with favorite snippet")
            context.insert(folder)
            folder.snippets.append(Snippet(isFavorite: true, 
                     title: "favorite snippet"))

            // add test data here
        }
        
        return container
    } catch {
        fatalError("Failed to create container: \(error.localizedDescription)")
    }
}()
example that you can work with swift data and Xcode previews

Advanced SwiftData

Can I sync SwiftData across devices?

You can use iCloud to sync your SwiftData across devices. This works for a private database but not for shared or public database, for which you would relay again on CoreData. You can learn about SwiftData container configurations in  this post SwiftData Stack: Understanding Schema, Container & Context

Can I use SwiftData together with CoreData in the same project?

SwiftData and CoreData are compatible. You can configure your storage so that both SwiftData and CoreData references the same SQLight database file.

How to migrate from CoreData to SwiftData and keep the existing database file?

You can migrate an existing CoreData file to SwiftData. Xcode has same handy tools that easily generate SwiftData model classes from CoreData schemas. For a full walk-through have a look at this post: How to convert a CoreData project to SwiftData

Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

Missing Features

SwiftData is in its first version and is thus missing quite a few features. Here is a list of what you might miss out on:

  • sectioned queries are not supported
  • limited support of predicates with relationships
  • predicate support for case insensitive text filtering
  • predicates with enum properties
  • to-many relationships do not keep their order 
  • iCloud sync works only with a private database

If you absolutely need some of these missing features, I would recommend working with CoreData instead. CoreData is much more mature and is integrated well with SwiftUI.

Limitations and Problems for SwiftData with Xcode 15

I encountered quite a few bugs and problems with SwiftData. Here are some of the most prominent ones:

  • app crashes when you use redo enabled container and custom types or enums as properties
  • to-many relationships with iCloud sync need to be optional. You have to work with optional arrays
  • to-many relationships are defined as arrays to other model classes. This makes the impression that they keep their sort order, but this is not the case, and the array is returned in seemingly random order.
  • app crashes after data migrations

Conclusion

SwiftData introduces an intuitive, Swift-native approach to handling data persistence in SwiftUI apps. It utilizes Swift’s native types, along with features like concurrency and macros, simplifying your coding experience compared to CoreData.

With SwiftData, models are easily created as classes and decorated with various macros, allowing for detailed attribute and relationship management. Implementing SwiftData in SwiftUI is straightforward, facilitating seamless data persistence in your apps.

However, SwiftData is a new framework and comes with certain limitations and bugs that need to be ironed out. Yet, its core concept of providing a Swift-native and user-friendly persistence framework holds potential.

If you’re developing a SwiftUI app or considering migrating from CoreData, SwiftData might be worth exploring. It can streamline your coding experience, though keep in mind the specific needs of your project before making a decision. Happy coding!

Further Reading:

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form