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:

  1. DataStoreConfiguration: Describes the store.
  2. DataStoreSnapshot: Holds a codable representation of model values.
  3. 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

  1. Open Xcode and create a new project using the App template.
  2. 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

  1. Build and run the app in the simulator.
  2. 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
Fareeth John

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

Leave a Reply

Your email address will not be published. Required fields are marked *