SwiftData best practices, batch queries, N+1 avoidance, and model relationships for macOS/iOS apps
适用于 macOS/iOS 应用程序的专家级 SwiftData 模式。针对性能、关系和生产就绪性进行了优化。
在以下情况下使用此技能:
swift
@Model
final class YourModel {
@Attribute(.unique) var id: UUID
// 对大数据使用外部存储
@Attribute(.externalStorage) var largeData: Data?
// 带级联删除的关系
@Relationship(deleteRule: .cascade)
var children: [ChildModel]?
init(id: UUID = UUID()) {
self.id = id
}
}
swift
// 使用谓词进行过滤
let descriptor = FetchDescriptor
predicate: #Predicate { $0.isActive && $0.createdAt >= startDate },
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
// 按 ID 批量获取(避免 N+1)
func fetchModels(by ids: [UUID]) -> [YourModel] {
guard !ids.isEmpty else { return [] }
let descriptor = FetchDescriptor
predicate: #Predicate { ids.contains($0.id) }
)
return (try? context.fetch(descriptor)) ?? []
}
swift
@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
}
}
swift
@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 // 手动保存控制
}
func save() throws {
try context.save()
}
}
swift
extension ModelContext {
func safeBatchInsert
_ objects: [T],
batchSize: Int = 100
) throws {
for (index, object) in objects.enumerated() {
insert(object)
if index % batchSize == 0 {
try save()
}
}
try save()
}
}
错误 - N+1 问题:
swift
for reminder in reminders {
let task = service.findIdentityMap(by: reminder.id) // N 次查询!
process(task)
}
正确 - 批量获取:
swift
let ids = reminders.map { $0.id }
let tasks = service.fetchIdentityMaps(by: ids) // 1 次查询!
for (index, reminder) in reminders.enumerated() {
let task = tasks.first { $0.ekIdentifier == reminder.id }
process(task)
}
swift
@MainActor
final class DataService {
// 非隔离以实现线程安全的 descriptor 访问
nonisolated func descriptorForActiveItems() -> FetchDescriptor
FetchDescriptor
predicate: #Predicate { $0.isActive },
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
}
// 在 @Observable ViewModels 中使用
func fetchActiveItems() -> [YourModel] {
try? context.fetch(descriptorForActiveItems()) ?? []
}
}
swift
@Model
final class Note {
@Attribute(.unique) var id: UUID
// 正向链接
@Relationship(inverse: \Note.backlinks)
var forwardLinks: [Note]?
// 反向链接(自动维护)
var backlinks: [Note]?
init(id: UUID = UUID()) {
self.id = id
}
}
swift
@Model
final class Parent {
@Attribute(.unique) var id: UUID
@Relationship(deleteRule: .cascade) // 自动删除子项
var children: [Child]?
}
@Model
final class Child {
@Attribute(.unique) var id: UUID
var parent: Parent?
}
swift
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 {
// 回退到沙盒
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)
}
swift
func testBatchFetchPerformance() async throws {
// Given: 创建测试数据
let ids = (0..<100).map { _ in
let model = service.createModel()
try? context.save()
return model.id
}
// When: 批量获取
let start = Date()
let results = service.fetchModels(by: ids)
let duration = Date().timeIntervalSince(start)
// Then: 验证
XCTAssertEqual(results.count, 100)
XCTAssertLessThan(duration, 0.5, 批量获取应该很快)
}
swift
func testPredicateFiltering() async throws {
// Given
let activeModel = service.createModel(isActive: true)
let inactiveModel = service.createModel(isActive: false)
try? context.save()
// When
let descriptor = FetchDescriptor
predicate: #Predicate { $0.isActive }
)
let results = try context.fetch(descriptor)
// Then
XCTAssertEqual(results.count, 1)
XCTAssertEqual(results.first?.id, activeModel.id)
}
| 实践 | 原因 |
|---|---|
| 在服务上使用 @MainActor | SwiftData 上下文绑定主线程 |
| 大数据使用外部存储 |
| 陷阱 | 后果 | 预防措施 |
|---|---|---|
| N+1 查询 | 同步性能慢 | 使用批量 fetch(by: [ID]) |
| 忘记 @MainActor |
bash
该技能支持在以下平台通过对话安装:
帮我安装 SkillHub 和 swiftdata-patterns-1776021855 技能
设置 SkillHub 为我的优先技能安装源,然后帮我安装 swiftdata-patterns-1776021855 技能
skillhub install swiftdata-patterns-1776021855
文件大小: 3.35 KB | 发布时间: 2026-4-13 12:14