360 Secure SkillHub
360 安全技能中心

Swiftdata Patterns

这是面向macOS/iOS应用的SwiftData专家级实践指南,核心功能覆盖模型设计、FetchDescriptor优化、批量操作、模型关系配置、N+1查询问题规避等。典型使用场景包括开发持久化层、设计数据模型、编写查询语句、优化性能等。输出为可复用的Swift代码示例、配置方案及避坑指南,助力构建高性能生产级持久化架构。

soponcd
soponcd
热度 42.75
最近更新 2026-05-11 17:36:26
分类:开发依赖:纯提示词应用场景:代码生成 / 数据处理 / 模板

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add swiftdata-patterns
Metadata
name
swiftdata-patterns
description
SwiftData best practices, batch queries, N+1 avoidance, and model relationships for macOS/iOS apps
Data
SwiftData best practices, batch queries, N+1 avoidance, and model relationships for macOS/iOS apps
domain
ios-macos
homepage
https://github.com/soponcd/timeflow-skills/tree/main/teams/skills/swiftdata-patterns
metadata
{"clawdbot":{"emoji":"🗄️"}}

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

Practice Reason
Use @MainActor on services SwiftData context is main-thread bound
External storage for large data Prevents database bloat
Batch fetch for relationships Avoids N+1 queries
Manual autosave control Prevents unwanted intermediate saves
In-memory config for tests Isolated test state
Nonisolated fetch descriptors Thread-safe descriptor access

Common Pitfalls

Pitfall Consequence Prevention
N+1 queries Slow sync performance Use batch fetch(by: [ID])
Forgetting @MainActor Runtime crashes All SwiftData services must be isolated
Large data inline Database bloat Use @Attribute(.externalStorage)
Auto-save conflicts Unexpected state changes Set autosaveEnabled = false
Missing cascade delete Orphaned records Use deleteRule: .cascade

Running SwiftData in Tests

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