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])
}
}