-
-
Save azamsharpschool/c50a0a6e2b4870102fef62c234a91b42 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | |
| } | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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