← 返回
未分类 中文

Swiftdata Patterns

SwiftData best practices, batch queries, N+1 avoidance, and model relationships for macOS/iOS apps
SwiftData 最佳实践、批量查询、避免 N+1 问题及模型关系管理,适用于 macOS/iOS 应用
soponcd soponcd 来源
未分类 clawhub v1.0.0 1 版本 100000 Key: 无需
★ 0
Stars
📥 363
下载
💾 4
安装
1
版本
#latest

概述

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'

版本历史

共 1 个版本

  • v1.0.0 当前
    2026-03-31 10:15 安全 安全

安全检测

腾讯云安全 (Keen)

安全,无风险
查看报告

腾讯云安全 (Sanbu)

安全,无风险
查看报告

🔗 相关推荐

dev-programming

Mcporter

steipete
使用 mcporter CLI 直接列出、配置、认证及调用 MCP 服务器/工具(支持 HTTP 或 stdio),涵盖临时服务器、配置编辑及 CLI/类型生成功能。
★ 198 📥 68,347
design-media

Web Design Guidelines

soponcd
审查 UI 代码以确保符合 Web 界面规范,适用于“审查我的 UI”“检查可访问性”“审计设计”“审查 UX”“检查我的网站”等请求。
★ 3 📥 1,393
dev-programming

YouTube

byungkyu
使用托管OAuth集成YouTube Data API,支持搜索视频、管理播放列表、获取频道数据及评论互动,适用于用户需要时使用此技能。
★ 142 📥 42,203