Skip to content

Latest commit

Β 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SwiftDataManager

Swift Platforms SPM License: MIT

A production-ready, thread-safe SwiftData singleton and framework for iOS 17+, macOS 14+, watchOS 10+, tvOS 17+, and visionOS 1+.

Zero third-party dependencies. Full CRUD surface with type inference, atomic transactions, dedicated @ModelActor background tasks with cancellation propagation, store-level batch operations, and real-time AsyncStream / Combine observation.


✨ Features

  • πŸš€ Zero Dependencies β€” Pure Swift standard library and Apple's native SwiftData framework.
  • 🧡 Swift 6 Strict Concurrency Ready β€” Thread-safe @MainActor singleton paired with a dedicated @ModelActor (SwiftDataBackgroundActor) constructed and executed off the main thread.
  • πŸ“¦ Effortless CRUD with Type Inference β€” Concise APIs like let items: [Item] = try manager.fetch() with predicate and sort descriptor support.
  • πŸ” PersistentIdentifier Lookup β€” Safely resolve models across actor boundaries by identifier (manager.model(for: id)).
  • ⚑ Store-Level Batch Deletes β€” High-performance deleteAll executed directly at the store layer without materializing objects in memory.
  • πŸ”„ Atomic Transactions & Safe Rollbacks β€” Multi-step mutations with automatic context rollback on error and dirty-context protection.
  • πŸ“‘ Reactive Observation β€” Subscribe to manager-performed saves via bounded AsyncStream<Void> (.bufferingNewest(1)) or Combine AnyPublisher<Void, Never>.
  • πŸ§ͺ Previews & Testing First β€” Seamless in-memory configurations for XCTest and SwiftUI #Preview.

πŸ“‹ Requirements

Requirement Minimum Version
iOS / iPadOS 17.0+
macOS 14.0+
watchOS 10.0+
tvOS 17.0+
visionOS 1.0+
Xcode 15.0+
Swift 5.9+

πŸ“¦ Installation

Swift Package Manager (SPM)

Via Xcode

  1. Open your Xcode project.
  2. Go to File > Add Package Dependencies...
  3. Paste the repository URL:
    https://github.com/bhargavkukadiya/SwiftDataManager.git
    
  4. Select the version rule and add SwiftDataManager to your target.

Via Package.swift

Add the dependency to your Package.swift:

dependencies: [
    .package(url: "https://github.com/bhargavkukadiya/SwiftDataManager.git", from: "1.0.0")
]

Manual Installation

Simply drag and drop Sources/SwiftDataManager/SwiftDataManager.swift into your Xcode project.


πŸš€ Quick Start

1. Define your SwiftData Model

import SwiftData
import Foundation

@Model
final class Note {
    var id: UUID
    var title: String
    var body: String
    var createdAt: Date

    init(title: String, body: String) {
        self.id = UUID()
        self.title = title
        self.body = body
        self.createdAt = Date()
    }
}

2. Configure at App Launch

Configure SwiftDataManager once at startup (e.g. in your @main App init()) and pass manager.container! to your SwiftUI view hierarchy:

import SwiftUI
import SwiftData
import SwiftDataManager

@main
struct MyApp: App {
    init() {
        let schema = Schema([Note.self])
        try! SwiftDataManager.shared.configure(schema: schema)
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(SwiftDataManager.shared.container!)
    }
}

3. Use Anywhere in Your App

let manager = SwiftDataManager.shared

// Create
let note = Note(title: "SwiftData", body: "Made simple.")
try manager.insert(note)

// Read (type inferred)
let notes: [Note] = try manager.fetch()

// Filtered & Sorted
let recent: [Note] = try manager.fetch(
    predicate: #Predicate { $0.title.contains("SwiftData") },
    sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)

// Lookup by Identifier
if let existing: Note = try manager.model(for: note.persistentModelID) {
    print("Found:", existing.title)
}

// Update (throwing closure with automatic rollback on failure)
try manager.update(note) {
    $0.title = "Updated Title"
}

// Delete
try manager.delete(note)

πŸ“– API Reference

Configuration

// Default Persistent Store
try manager.configure(schema: Schema([Note.self]))

// Custom Schema Migration Plan & Multiple Store Configurations
try manager.configure(
    schema: Schema([Note.self]),
    migrationPlan: AppMigrationPlan.self,
    configurations: [ModelConfiguration(isStoredInMemoryOnly: false)]
)

// In-Memory Store (Unit Tests & SwiftUI Previews)
try manager.configure(
    schema: Schema([Note.self]),
    configuration: ModelConfiguration(isStoredInMemoryOnly: true)
)

Create Operations

// Single Insert (saves automatically)
try manager.insert(note)

// Batch Insert (single round-trip save)
try manager.insertBatch([note1, note2, note3])

Read Operations

// Fetch all with optional filter and sort
let notes: [Note] = try manager.fetch(
    predicate: #Predicate { $0.title != "" },
    sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)

// Fetch by PersistentIdentifier
let note: Note? = try manager.model(for: persistentId)

// Paged Fetch (LIMIT and OFFSET)
let page: [Note] = try manager.fetchPaged(
    sortBy: [SortDescriptor(\.createdAt)],
    offset: 20,
    limit: 10
)

// Count (store-level aggregation without memory allocation)
let total = try manager.count(Note.self, predicate: #Predicate { $0.title != "" })

// Fetch First Match
let first: Note? = try manager.fetchFirst(
    sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)

Update Operations

// Mutate properties inside a throwing closure and save automatically.
// Requires a clean context to guarantee that rollback is strictly scoped to this mutation.
try manager.update(note) { target in
    target.title = "New Title"
    target.body = "Updated Body"
}

Delete Operations

// Delete single instance
try manager.delete(note)

// Store-Level Batch Delete (returns pre-delete snapshot count, discardable)
let deletedCount = try manager.deleteAll(
    Note.self,
    predicate: #Predicate { $0.title.contains("Draft") }
)

Background Tasks

SwiftDataManager is isolated to @MainActor. For heavy workloads (such as JSON syncing or large imports), performBackgroundTask constructs and executes a detached @ModelActor on a private background executor:

// Executes off the main thread, inherits cooperative task cancellation,
// saves changes automatically if dirty, and returns a Sendable result
let totalImported = try await manager.performBackgroundTask { context in
    for item in remoteFeed {
        try Task.checkCancellation()
        context.insert(Note(title: item.title, body: item.body))
    }
    return remoteFeed.count
}

Rules for Background Tasks:

  • The closure is @Sendable. Do not capture PersistentModel instances from other threads/contexts.
  • Work directly with the provided ModelContext.
  • If you need to access models on the main actor after saving, re-fetch them by PersistentIdentifier or via manager.fetch().

Atomic Transactions & Rollback

Execute multiple operations atomically. If any error is thrown within the transaction block, all pending changes are discarded via context rollback:

let result = try manager.transaction { ctx -> Int in
    ctx.insert(Note(title: "Step 1", body: "..."))
    ctx.insert(Note(title: "Step 2", body: "..."))
    return 2
}

Change Observation

Observe write operations and saves performed through SwiftDataManager in real-time:

1. Swift Concurrency (AsyncStream)

Task {
    for await _ in manager.changes {
        await reloadData()
    }
}

2. Combine (AnyPublisher)

manager.changePublisher
    .receive(on: DispatchQueue.main)
    .sink { [weak self] in
        self?.reloadData()
    }
    .store(in: &cancellables)

Observation Scope Note: changes and changePublisher emit notifications whenever a write operation is saved through SwiftDataManager (e.g. insert, update, delete, deleteAll, transaction, or a mutating performBackgroundTask). Saves performed directly on independent scratch contexts (newScratchContext()) or custom external ModelContext instances are not emitted through this stream. changes uses a .bufferingNewest(1) buffering policy to prevent unbounded memory growth.


Scratch Context

Create an independent, isolated scratch context for short-lived UI drafts or manual transaction flows:

let scratchContext: ModelContext = try manager.newScratchContext()

πŸ§ͺ Testing & Previews

Configure an in-memory store in your setUp() or SwiftUI #Preview to prevent disk writes:

// SwiftUI #Preview
#Preview {
    let schema = Schema([Note.self])
    try! SwiftDataManager.shared.configure(
        schema: schema,
        configuration: ModelConfiguration(isStoredInMemoryOnly: true)
    )
    try! SwiftDataManager.shared.insert(Note(title: "Preview Note", body: "Preview Body"))
    return NoteListView()
}

// XCTest
override func setUp() async throws {
    try await super.setUp()
    let schema = Schema([Note.self])
    try SwiftDataManager.shared.configure(
        schema: schema,
        configuration: ModelConfiguration(isStoredInMemoryOnly: true)
    )
}

πŸ›‘ Error Handling

Failures originating within SwiftDataManager (configuration, fetch, save, or dirty-context collisions) are thrown as typed, Sendable SwiftDataManagerError instances. Errors originating inside caller closures passed to update, transaction, and performBackgroundTask (such as CancellationError or domain validation errors) are preserved and propagated unchanged.

Error Case Reason
.notConfigured Attempted an operation before calling configure(schema:)
.containerCreationFailed(Error) Failed to initialize underlying ModelContainer
.saveFailed(Error) Context save failure (insert, delete, deleteAll, or transaction save)
.fetchFailed(Error) Fetch or count query failure
.unsavedChangesPending Attempted an update while mainContext already has pending unsaved changes
do {
    try manager.insert(note)
} catch SwiftDataManagerError.notConfigured {
    print("Please call SwiftDataManager.shared.configure(...) at app launch.")
} catch {
    print("Database error: \(error.localizedDescription)")
}

πŸ›  Architecture & Design Decisions

Feature Design Strategy
Main Actor Isolation All primary manager APIs are @MainActor bound for safe, direct interaction with SwiftUI views.
Off-Main-Thread Execution performBackgroundTask constructs and runs SwiftDataBackgroundActor off the main actor with cancellation bridging.
Memory Preservation deleteAll executes store-level deletions without materializing object graphs into memory.
Strict Concurrency Fully compliant with Swift 6 Concurrency and Sendable checking.
Scoped Observation Stream and publisher notify exclusively on manager-committed saves with bounded .bufferingNewest(1) buffering.

🀝 Contributing

Contributions are always welcome!

  1. Fork the repository.
  2. Create a new branch (git checkout -b feature/amazing-feature).
  3. Commit your changes (git commit -m 'Add amazing feature').
  4. Push to the branch (git push origin feature/amazing-feature).
  5. Open a Pull Request.

Please ensure all unit tests pass before submitting a PR:

swift test

πŸ“„ License

Distributed under the MIT License. See LICENSE for details.

About

A lightweight, production-ready SwiftData framework for iOS, macOS, watchOS, tvOS, and visionOS. Features type-inferred CRUD, dedicated @Modelactor background tasks, atomic transactions, and Swift 6 AsyncStream / Combine observation.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages