Skip to content

Instantly share code, notes, and snippets.

@azamsharpschool
Created May 25, 2026 14:03
Show Gist options
  • Select an option

  • Save azamsharpschool/c50a0a6e2b4870102fef62c234a91b42 to your computer and use it in GitHub Desktop.

Select an option

Save azamsharpschool/c50a0a6e2b4870102fef62c234a91b42 to your computer and use it in GitHub Desktop.
import SwiftUI
import SwiftData
// MARK: - Model
@Model
class Book {
// Custom stable identifier for the Book.
// We use this as the primary key in the text file.
var bookId: String
// Book title.
var name: String
// Book author.
var author: String
init(name: String, author: String) {
// Generate unique identifier when a new book is created.
self.bookId = UUID().uuidString
self.name = name
self.author = author
}
}
// MARK: - Store Configuration
final class TextStoreConfiguration: DataStoreConfiguration {
// Tell SwiftData which store this configuration belongs to.
typealias Store = TextStore
// Human readable store name.
var name: String
// Schema describing supported models.
var schema: Schema?
// Physical location of the text file.
var fileURL: URL
init(
name: String,
schema: Schema? = nil,
fileURL: URL
) {
self.name = name
self.schema = schema
self.fileURL = fileURL
}
// Used to compare configurations.
static func == (
lhs: TextStoreConfiguration,
rhs: TextStoreConfiguration
) -> Bool {
lhs.name == rhs.name
}
// Used for hashing configuration.
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
// MARK: - Snapshot
// Snapshot is SwiftData’s raw persistence representation.
// SwiftData communicates with custom stores using snapshots.
struct BookSnapshot: DataStoreSnapshot {
// Unique identifier for the persisted object.
let persistentIdentifier: PersistentIdentifier
// Stored properties.
let bookId: String
let name: String
let author: String
// Manual initializer.
init(
persistentIdentifier: PersistentIdentifier,
bookId: String,
name: String,
author: String
) {
self.persistentIdentifier = persistentIdentifier
self.bookId = bookId
self.name = name
self.author = author
}
// SwiftData calls this initializer when converting
// a model into a snapshot during save().
init(
from backingData: any BackingData,
relatedBackingDatas: inout [PersistentIdentifier : any BackingData]
) {
// Ensure persistent identifier exists.
guard let persistentIdentifier = backingData.persistentModelID else {
fatalError("Missing persistent identifier.")
}
// Cast generic backing data into Book backing data.
guard let bookBackingData = backingData as? any BackingData<Book> else {
fatalError("Expected Book backing data.")
}
self.persistentIdentifier = persistentIdentifier
// Extract values from the model.
self.bookId = bookBackingData.getValue(forKey: \Book.bookId)
self.name = bookBackingData.getValue(forKey: \Book.name)
self.author = bookBackingData.getValue(forKey: \Book.author)
}
// Creates a copy of the snapshot with a new identifier.
// Usually used when converting temporary IDs into permanent IDs.
func copy(
persistentIdentifier: PersistentIdentifier,
remappedIdentifiers: [PersistentIdentifier : PersistentIdentifier]?
) -> BookSnapshot {
BookSnapshot(
persistentIdentifier: persistentIdentifier,
bookId: bookId,
name: name,
author: author
)
}
}
// MARK: - Custom Text Store
final class TextStore: DataStore {
typealias Configuration = TextStoreConfiguration
// Snapshot type used by the store.
typealias Snapshot = BookSnapshot
// Store configuration.
var configuration: TextStoreConfiguration
// Human readable name.
var name: String
// Schema describing supported models.
var schema: Schema
// Unique store identifier.
var identifier: String
init(
_ configuration: TextStoreConfiguration,
migrationPlan: (any SchemaMigrationPlan.Type)?
) throws {
self.configuration = configuration
self.name = configuration.name
self.schema = configuration.schema!
// Use filename as unique store identifier.
self.identifier = configuration.fileURL.lastPathComponent
}
// MARK: - Read Snapshots
// Reads the text file and converts rows into snapshots.
private func getSnapshots() throws -> [PersistentIdentifier: Snapshot] {
let fileURL = configuration.fileURL
// Return empty dictionary if file does not exist.
guard FileManager.default.fileExists(
atPath: fileURL.path(percentEncoded: false)
) else {
return [:]
}
// Read entire text file.
let content = try String(
contentsOf: fileURL,
encoding: .utf8
)
// Split file into lines.
let rows = content.components(separatedBy: .newlines)
var result: [PersistentIdentifier: Snapshot] = [:]
for row in rows where !row.isEmpty {
// Split line using pipe separator.
let columns = row.components(separatedBy: "|")
// Ensure row has expected number of columns.
guard columns.count == 3 else {
continue
}
// Parse values.
let bookId = columns[0]
let name = columns[1]
let author = columns[2]
// Recreate persistent identifier from stored primary key.
let persistentIdentifier = try PersistentIdentifier.identifier(
for: identifier,
entityName: String(describing: Book.self),
primaryKey: bookId
)
// Create snapshot from parsed values.
let snapshot = BookSnapshot(
persistentIdentifier: persistentIdentifier,
bookId: bookId,
name: name,
author: author
)
result[persistentIdentifier] = snapshot
}
return result
}
// MARK: - Fetch
// SwiftData calls fetch when @Query executes.
func fetch<T>(
_ request: DataStoreFetchRequest<T>
) throws -> DataStoreFetchResult<T, Snapshot> where T : PersistentModel {
// Read stored snapshots.
let snapshots = try getSnapshots()
// Filter snapshots matching requested entity type.
let fetchedSnapshots = snapshots.values.filter {
$0.persistentIdentifier.entityName == String(describing: T.self)
}
// Return snapshots back to SwiftData.
return DataStoreFetchResult(
descriptor: request.descriptor,
fetchedSnapshots: fetchedSnapshots
)
}
// MARK: - Persist Snapshots
// Converts snapshots into pipe-delimited text file.
private func persistSnapshots(
_ snapshots: [PersistentIdentifier: Snapshot]
) throws {
var csv = ""
for (_, snapshot) in snapshots {
// Convert snapshot into text row.
csv.append(
"\(snapshot.bookId)|\(snapshot.name)|\(snapshot.author)\n"
)
}
// Save text file to disk.
try csv.write(
to: configuration.fileURL,
atomically: true,
encoding: .utf8
)
}
// MARK: - Save
// SwiftData calls save() whenever context.save() executes.
func save(
_ request: DataStoreSaveChangesRequest<Snapshot>
) throws -> DataStoreSaveChangesResult<Snapshot> {
// Load existing snapshots from file.
var storedSnapshots = try getSnapshots()
// Tracks temporary ID -> permanent ID mappings.
var remappedIdentifiers: [
PersistentIdentifier: PersistentIdentifier
] = [:]
// MARK: INSERT
for snapshot in request.inserted {
// Create permanent identifier using bookId.
let permanentIdentifier = try PersistentIdentifier.identifier(
for: identifier,
entityName: snapshot.persistentIdentifier.entityName,
primaryKey: snapshot.bookId
)
// Create snapshot using permanent identifier.
let permanentSnapshot = snapshot.copy(
persistentIdentifier: permanentIdentifier,
remappedIdentifiers: remappedIdentifiers
)
// Add snapshot to in-memory collection.
storedSnapshots[permanentIdentifier] = permanentSnapshot
// Store temporary -> permanent mapping.
remappedIdentifiers[
snapshot.persistentIdentifier
] = permanentIdentifier
}
// MARK: UPDATE
for snapshot in request.updated {
// Replace existing snapshot.
storedSnapshots[
snapshot.persistentIdentifier
] = snapshot
}
// MARK: DELETE
for snapshot in request.deleted {
// Remove snapshot from collection.
storedSnapshots[
snapshot.persistentIdentifier
] = nil
}
// Persist updated snapshots back to text file.
try persistSnapshots(storedSnapshots)
// Return save result to SwiftData.
return DataStoreSaveChangesResult(
for: identifier,
remappedIdentifiers: remappedIdentifiers
)
}
}
// MARK: - UI
struct ContentView: View {
@Environment(\.modelContext)
private var context
// Automatically fetch books from custom store.
@Query
private var books: [Book]
@State private var name = ""
@State private var author = ""
private var canSave: Bool {
!name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
!author.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
private func saveBook() {
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedAuthor = author.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedName.isEmpty, !trimmedAuthor.isEmpty else { return }
let book = Book(
name: trimmedName,
author: trimmedAuthor
)
context.insert(book)
do {
try context.save()
name = ""
author = ""
} catch {
print(error.localizedDescription)
}
}
// Delete helper.
private func deleteBook(at indexSet: IndexSet) {
indexSet.forEach { index in
let book = books[index]
context.delete(book)
}
}
var body: some View {
List {
Section("Add Book") {
TextField("Book name", text: $name)
.textInputAutocapitalization(.words)
TextField("Author", text: $author)
.textInputAutocapitalization(.words)
Button("Save", action: saveBook)
.disabled(!canSave)
}
Section("Books") {
ForEach(books) { book in
VStack(alignment: .leading) {
Text(book.name)
Text(book.author)
.font(.caption)
.foregroundStyle(.secondary)
}
}
.onDelete(perform: deleteBook)
}
}
}
}
//
// LearnApp.swift
// Learn
//
// Created by Mohammad Azam on 5/24/26.
//
import SwiftUI
import SwiftData
@main
struct LearnApp: App {
let container: ModelContainer
init() {
let fileURL = URL.documentsDirectory.appending(
path: "books.txt",
directoryHint: .notDirectory
)
print(fileURL)
let configuration = TextStoreConfiguration(
name: "TextStore",
fileURL: fileURL
)
self.container = try! ModelContainer(
for: Book.self,
configurations: configuration
)
}
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(container)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment