SwiftData introduces Custom DataStores, a powerful feature that allows developers to persist data in any backend or file format while leveraging the simplicity of SwiftData. In this tutorial, I’ll explore the concept of Custom DataStores and implement one using JSON as the persistence backend. Additionally, I’ll walk through a sample project, SampleTrips, to demonstrate how to put this into practice.
What Are Custom DataStores?
Custom DataStores allow you to replace SwiftData’s default persistence backend with a custom implementation. This flexibility enables you to:
- Use formats like JSON, databases, or cloud storage.
- Maintain compatibility with SwiftData’s features like filtering, sorting, and more.
At the core of this functionality are three key components:
- DataStoreConfiguration: Describes the store.
- DataStoreSnapshot: Holds a codable representation of model values.
- DataStore Protocol: Defines how data is fetched, saved, and managed.
Building the SampleTrips App with a JSON DataStore
To help you understand how to create and use a Custom DataStore, let’s build the SampleTrips app, which allows users to manage a list of trips.
Step 1: Set Up Your Project
- Open Xcode and create a new project using the App template.
- Name the project
SampleTrips
, and choose SwiftUI as the interface.
Step 2: Define the Trip Model
The Trip
model represents a travel destination and conforms to PersistentModel
and Codable
.
import SwiftData @Model struct Trip: Codable, Identifiable { @Attribute(.unique) var id: UUID var destination: String var date: Date init(destination: String, date: Date) { self.id = UUID() self.destination = destination self.date = date } }
Step 3: Implement the Custom JSON DataStore
Create the Configuration
import SwiftData struct JSONStoreConfiguration: DataStoreConfiguration { typealias Store = JSONStore let fileURL: URL }
Define the Store
import SwiftData struct JSONStore: DataStore { typealias Configuration = JSONStoreConfiguration typealias Snapshot = DefaultSnapshot let configuration: Configuration func fetch(request: DataStoreFetchRequest) throws -> DataStoreFetchResult<DefaultSnapshot> { let data = try Data(contentsOf: configuration.fileURL) let snapshots = try JSONDecoder().decode([DefaultSnapshot].self, from: data) return DataStoreFetchResult(snapshots: snapshots) } func save(changes: DataStoreSaveChangesRequest<DefaultSnapshot>) throws -> DataStoreSaveChangesResult { var snapshotsByIdentifier = try readSnapshots() for snapshot in changes.inserted { let newIdentifier = PersistentIdentifier(UUID().uuidString) let newSnapshot = snapshot.settingPersistentIdentifier(newIdentifier) snapshotsByIdentifier[newIdentifier] = newSnapshot } for snapshot in changes.updated { snapshotsByIdentifier[snapshot.persistentIdentifier] = snapshot } for identifier in changes.deletedIdentifiers { snapshotsByIdentifier.removeValue(forKey: identifier) } let updatedSnapshots = Array(snapshotsByIdentifier.values) let data = try JSONEncoder().encode(updatedSnapshots) try data.write(to: configuration.fileURL) return DataStoreSaveChangesResult(remappedIdentifiers: changes.inserted.map { ($0.persistentIdentifier, PersistentIdentifier(UUID().uuidString)) }) } private func readSnapshots() throws -> [PersistentIdentifier: DefaultSnapshot] { guard FileManager.default.fileExists(atPath: configuration.fileURL.path) else { return [:] } let data = try Data(contentsOf: configuration.fileURL) let snapshots = try JSONDecoder().decode([DefaultSnapshot].self, from: data) return Dictionary(uniqueKeysWithValues: snapshots.map { ($0.persistentIdentifier, $0) }) } }
Step 4: Configure the ModelContainer
Replace the default store configuration in your app definition.
import SwiftUI import SwiftData @main struct SampleTripsApp: App { let container: ModelContainer init() { let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("trips.json") container = try! ModelContainer(for: [Trip.self], configurations: [JSONStoreConfiguration(fileURL: fileURL)]) } var body: some Scene { WindowGroup { ContentView() .modelContainer(container) } } }
Step 5: Build the SwiftUI Interface
Create a user-friendly interface for managing trips.
import SwiftUI import SwiftData struct ContentView: View { @Query private var trips: [Trip] @Environment(\.modelContext) private var modelContext @State private var destination = "" @State private var date = Date() var body: some View { NavigationView { VStack { List(trips) { trip in VStack(alignment: .leading) { Text(trip.destination) .font(.headline) Text(trip.date, style: .date) .font(.subheadline) } } HStack { TextField("Destination", text: $destination) .textFieldStyle(RoundedBorderTextFieldStyle()) DatePicker("Date", selection: $date, displayedComponents: .date) .labelsHidden() Button("Add Trip") { let newTrip = Trip(destination: destination, date: date) modelContext.insert(newTrip) try? modelContext.save() destination = "" } .buttonStyle(.borderedProminent) } .padding() } .navigationTitle("Sample Trips") } } }
Step 6: Run the App
- Build and run the app in the simulator.
- Add trips and verify that the data is persisted in the
trips.json
file in the app’s document directory.
Conclusion
Custom DataStores in SwiftData provide incredible flexibility, allowing you to persist data in any format while leveraging the simplicity of SwiftUI. By following the steps outlined in this tutorial, you’ve implemented a custom JSON-based DataStore and integrated it into a working app.
Written By
I’m working as a Sr. Solution Architect in Akamai Technologies. I have more than 13 years of experience in the Mobile app development industry. Worked on different technologies like VR, Augmented reality, OTT, and IoT in iOS, Android, flutter, and other cross-platform apps. Have worked on 45+ apps from scratch which are in the AppStore and PlayStore. My knowledge of mobile development including design/architecting solutions, app development, knowledge around backend systems, cloud computing, CDN, Test Automation, CI/CD, Frida Pentesting, and finding mobile app vulnerabilities