swiftdata-patterns

SwiftData best practices, batch queries, N+1 avoidance, and model relationships for macOS/iOS apps

Safety Notice

This listing is from the official public ClawHub registry. Review SKILL.md and referenced scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "swiftdata-patterns" with this command: npx skills add soponcd/swiftdata-patterns

SwiftData Patterns

Expert-level SwiftData patterns for macOS/iOS applications. Optimized for performance, relationships, and production readiness.

When to Use

Use this skill when:

  • Designing SwiftData models
  • Writing SwiftData queries
  • Optimizing batch operations
  • Setting up model relationships
  • Handling persistence layer architecture
  • Avoiding N+1 query problems

Core Principles

1. Model Design

@Model
final class YourModel {
    @Attribute(.unique) var id: UUID

    // Use external storage for large data
    @Attribute(.externalStorage) var largeData: Data?

    // Relationships with cascade delete
    @Relationship(deleteRule: .cascade)
    var children: [ChildModel]?

    init(id: UUID = UUID()) {
        self.id = id
    }
}

2. FetchDescriptor Best Practices

// Use predicate for filtering
let descriptor = FetchDescriptor<YourModel>(
    predicate: #Predicate { $0.isActive && $0.createdAt >= startDate },
    sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)

// Batch fetch by IDs (N+1 avoidance)
func fetchModels(by ids: [UUID]) -> [YourModel] {
    guard !ids.isEmpty else { return [] }

    let descriptor = FetchDescriptor<YourModel>(
        predicate: #Predicate { ids.contains($0.id) }
    )
    return (try? context.fetch(descriptor)) ?? []
}

3. In-Memory Testing Pattern

@MainActor
final class ModelTests: XCTestCase {
    var container: ModelContainer!
    var context: ModelContext!

    override func setUp() async throws {
        try await super.setUp()
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        container = try ModelContainer(for: YourModel.self, configurations: config)
        context = container.mainContext
    }

    override func tearDown() async throws {
        try await super.tearDown()
        container = nil
        context = nil
    }
}

4. Service Layer Pattern

@MainActor
final class DataService {
    nonisolated let container: ModelContainer
    let context: ModelContext

    init(inMemory: Bool = false) throws {
        let configuration = ModelConfiguration(isStoredInMemoryOnly: inMemory)
        container = try ModelContainer(for: YourModel.self, configurations: configuration)
        context = ModelContext(container)
        context.autosaveEnabled = false  // Manual save control
    }

    func save() throws {
        try context.save()
    }
}

Performance Patterns

Batch Insert with Chunking

extension ModelContext {
    func safeBatchInsert<T: PersistentModel>(
        _ objects: [T],
        batchSize: Int = 100
    ) throws {
        for (index, object) in objects.enumerated() {
            insert(object)
            if index % batchSize == 0 {
                try save()
            }
        }
        try save()
    }
}

Avoid N+1 Queries

Bad - N+1 problem:

for reminder in reminders {
    let task = service.findIdentityMap(by: reminder.id)  // N queries!
    process(task)
}

Good - Batch fetch:

let ids = reminders.map { $0.id }
let tasks = service.fetchIdentityMaps(by: ids)  // 1 query!

for (index, reminder) in reminders.enumerated() {
    let task = tasks.first { $0.ekIdentifier == reminder.id }
    process(task)
}

Shared Fetch Descriptors

@MainActor
final class DataService {
    // Nonisolated for thread-safe descriptor access
    nonisolated func descriptorForActiveItems() -> FetchDescriptor<YourModel> {
        FetchDescriptor<YourModel>(
            predicate: #Predicate { $0.isActive },
            sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
        )
    }

    // Use in @Observable ViewModels
    func fetchActiveItems() -> [YourModel] {
        try? context.fetch(descriptorForActiveItems()) ?? []
    }
}

Model Relationships

Bidirectional Links

@Model
final class Note {
    @Attribute(.unique) var id: UUID

    // Forward links
    @Relationship(inverse: \Note.backlinks)
    var forwardLinks: [Note]?

    // Backward links (auto-maintained)
    var backlinks: [Note]?

    init(id: UUID = UUID()) {
        self.id = id
    }
}

Cascade Delete

@Model
final class Parent {
    @Attribute(.unique) var id: UUID

    @Relationship(deleteRule: .cascade)  // Auto-delete children
    var children: [Child]?
}

@Model
final class Child {
    @Attribute(.unique) var id: UUID
    var parent: Parent?
}

Configuration Best Practices

App Group Support

private static func createConfiguration(inMemory: Bool) throws -> ModelConfiguration {
    if inMemory {
        return ModelConfiguration(isStoredInMemoryOnly: true)
    }

    let appGroupID = "group.your.app.id"
    guard let containerURL = FileManager.default.containerURL(
        forSecurityApplicationGroupIdentifier: appGroupID
    ) else {
        // Fallback to sandbox
        return createSandboxConfiguration()
    }

    let dataURL = containerURL.appendingPathComponent("App_Data")
    try? FileManager.default.createDirectory(at: dataURL, withIntermediateDirectories: true)
    let storeURL = dataURL.appendingPathComponent("App.sqlite")

    return ModelConfiguration(url: storeURL, cloudKitDatabase: .automatic)
}

Testing Guidelines

Given-When-Then Pattern

func testBatchFetchPerformance() async throws {
    // Given: Create test data
    let ids = (0..<100).map { _ in
        let model = service.createModel()
        try? context.save()
        return model.id
    }

    // When: Batch fetch
    let start = Date()
    let results = service.fetchModels(by: ids)
    let duration = Date().timeIntervalSince(start)

    // Then: Verify
    XCTAssertEqual(results.count, 100)
    XCTAssertLessThan(duration, 0.5, "Batch fetch should be fast")
}

Predicate Testing

func testPredicateFiltering() async throws {
    // Given
    let activeModel = service.createModel(isActive: true)
    let inactiveModel = service.createModel(isActive: false)
    try? context.save()

    // When
    let descriptor = FetchDescriptor<YourModel>(
        predicate: #Predicate { $0.isActive }
    )
    let results = try context.fetch(descriptor)

    // Then
    XCTAssertEqual(results.count, 1)
    XCTAssertEqual(results.first?.id, activeModel.id)
}

Best Practices

PracticeReason
Use @MainActor on servicesSwiftData context is main-thread bound
External storage for large dataPrevents database bloat
Batch fetch for relationshipsAvoids N+1 queries
Manual autosave controlPrevents unwanted intermediate saves
In-memory config for testsIsolated test state
Nonisolated fetch descriptorsThread-safe descriptor access

Common Pitfalls

PitfallConsequencePrevention
N+1 queriesSlow sync performanceUse batch fetch(by: [ID])
Forgetting @MainActorRuntime crashesAll SwiftData services must be isolated
Large data inlineDatabase bloatUse @Attribute(.externalStorage)
Auto-save conflictsUnexpected state changesSet autosaveEnabled = false
Missing cascade deleteOrphaned recordsUse deleteRule: .cascade

Running SwiftData in Tests

# Test with SwiftData
xcodebuild test -scheme YourApp \
  -destination 'platform=macOS' \
  -only-testing:'YourAppTests/ModelTests/testBatchFetch'

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

GigaChat (Sber AI) Proxy

Integrate GigaChat (Sber AI) with OpenClaw via gpt2giga proxy

Registry SourceRecently Updated
3600smvlx
General

TencentCloud Video Face Fusion

通过提取两张人脸核心特征并实现自然融合,支持多种风格适配,提升创意互动性和内容传播力,广泛应用于创意营销、娱乐互动和社交分享场景。

Registry SourceRecently Updated
General

TencentCloud Image Face Fusion

图片人脸融合(专业版)为同步接口,支持自定义美颜、人脸增强、牙齿增强、拉脸等参数,最高支持8K分辨率,有多个模型类型供选择。

Registry SourceRecently Updated
General

YoudaoNote News

有道云笔记资讯推送:基于收藏笔记分析关注话题,推送最新相关资讯。支持对话触发与每日定时推送(如早上9点)。触发词:资讯推送、设置资讯推送、生成资讯推送。

Registry SourceRecently Updated
1.5K1lephix