SwiftData was announced at WWDC in June 2023. SwiftData simplifies data persistence tasks by using declarative code. Built on top of Core Data, SwiftData is actually a framework designed to help developers manage and interact with data on a persistent store.
Main parts of of SwiftData are
@Model
macro.modelContainer()
view modifier@Environment(\.modelContext)
property wrapper@Query
property wrapperTo get started, make sure you have Xcode 15.0 or a later version installed and iOS 17 or later.
Simple app
To create a model you need to mark the class with the macro @Model
.
@Model final class Movie: Identifiable { @Attribute(.unique) var id: String = UUID().uuidString var name: String var created: Date init(id: String = UUID().uuidString, name: String, created: Date) { self.id = id self.name = name self.created = created } }
@Model
macro is an essential item in SwiftData’s functionality. Using it you can define data model’s schema directly from your Swift code and can annotate your properties with additional metadata when necessary.
Properties in the @Model
can have macros attached to them that will defined their behaviour. Currently there are 3 types of macros:
@Attribute
. To specify an uniqueness constraint to a property. @Relationship
. To specify a relationship between two model classes.@Transient
. By default, SwiftData persists all non-computed attributes. In case we want to exclude properties from persisting, we can use the @Transient
macro.The ModelContainer
serves as the persistent backend for your model types. It can operate with default settings, or be customized with configurations and migration options. To create a model container, simply specify the model types you want to store.
The .modelContainer
modifer allows you to specify some options: inMemory
, isAutosaveEnabled
, isUndoEnabled
, onSetup
.
Once your container is set up, you’re ready to fetch and save data using model contexts.
import SwiftUI import SwiftData @main struct SwiftDataApp: App { let modelContainer: ModelContainer init() { do { modelContainer = try ModelContainer(for: Movie.self) } catch { fatalError("Could not initialize ModelContainer") } } var body: some Scene { WindowGroup { MoviesView() } .modelContainer(modelContainer) } }
ModelContext
observes changes to your models and provides actions (CRUD) to do on them. It’s your main access for tracking updates, fetching data, saving changes. In SwiftUI you’ll get the ModelContext
from your view’s environment after creating your model container.
import SwiftUI struct MoviesView: View { @Environment(\.modelContext) private var modelContext @Query private var items: [Movie] //@Query(sort: \.created, order: .reverse) var items: [Movie] var body: some View { VStack { Button(action: { let name = "Movie \(Int.random(in: 1..<100))" modelContext.insert(Movie(name: name, created: Date())) }) { Label("Add movie", systemImage: "plus") } List { ForEach(items) { item in HStack { Text(item.name) Spacer() Text(item.created, format: Date.FormatStyle(date: .numeric, time: .standard)) } } .onDelete(perform: { offsets in for index in offsets { modelContext.delete(items[index]) } }) } } } }
MVVM & SwiftData
MovieDataSource
will contain the ModelContainer
final class MovieDataSource { private let modelContainer: ModelContainer private let modelContext: ModelContext @MainActor static let shared = MovieDataSource() @MainActor private init() { self.modelContainer = try! ModelContainer(for: Movie.self) self.modelContext = modelContainer.mainContext } func append(_ item: Movie) { modelContext.insert(item) do { try modelContext.save() } catch { fatalError(error.localizedDescription) } } func fetchAll() -> [Movie] { do { return try modelContext.fetch(FetchDescriptor<Movie>()) } catch { fatalError(error.localizedDescription) } } func remove(_ item: Movie) { modelContext.delete(item) } }
Here are the App and the main view
@main struct SwiftDataApp: App { var body: some Scene { WindowGroup { MoviesView() } } } struct MoviesView: View { @State private var vm = MovieViewModel() var body: some View { VStack { Button(action: { vm.append() }) { Label("Add movie", systemImage: "plus") } List { ForEach(vm.items) { item in HStack { Text(item.name) Spacer() Text(item.created, format: Date.FormatStyle(date: .numeric, time: .standard)) } } .onDelete(perform: { offsets in for index in offsets { vm.remove(at: index) } }) } } } }
Finally, the ViewModel
@Observable class MovieViewModel { @ObservationIgnored private let dataSource: MovieDataSource var items: [Movie] = [] init(dataSource: MovieDataSource = MovieDataSource.shared) { self.dataSource = dataSource items = dataSource.fetchAll() } func append() { let name = "Movie \(Int.random(in: 1..<100))" dataSource.append(Movie(name: name, created: Date())) items = dataSource.fetchAll() } func remove(at index: Int) { dataSource.remove(items[index]) } }