mcpField
S
swiftdata-coredata-persistence

swiftdata-coredata-persistence

github.com/dagba开发444 次安装40 周前更新
代码仓库

关于 swiftdata-coredata-persistence

swiftdata-coredata-persistence 是面向 iOS 开发者的持久化开发参考技能,帮助在 SwiftData 与 CoreData 之间选择并处理迁移和性能问题。内容涵盖版本化、iCloud 同步约束、批量读取及大型数据存储建议。

主要功能

  1. 01比较 SwiftData 与 CoreData 的适用场景
  2. 02说明版本化和复杂迁移风险
  3. 03梳理 iCloud 同步下的模式约束
  4. 04提供批量读取与关系加载模式
  5. 05给出大型数据的文件存储建议
  6. 06附迁移检查清单与常见问题

适用场景

  1. 01为新 iOS 应用选择持久化框架
  2. 02规划带 iCloud 的数据模型迁移
  3. 03排查更新后启动卡顿或迁移失败
  4. 04优化大量记录或媒体数据的读取

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dagba/ios-mcp --skill swiftdata-coredata-persistence

安装后在 Claude Code 中输入/swiftdata-coredata-persistence确认已加载。

详细介绍

SwiftData 和 CoreData 持久化

概述

SwiftData 和 CoreData 不可互换。 两者各有特定优势、迁移陷阱和性能模式。选择错误的框架或迁移策略会导致生产环境崩溃。

核心原则: 根据应用约束选择框架,从第一天起就对模型进行版本管理,永远不要在数据库中存储 BLOB。

框架选择

digraph framework_choice {
    "New iOS app?" [shape=diamond];
    "iOS 17+ only?" [shape=diamond];
    "Simple data model?" [shape=diamond];
    "iCloud sync?" [shape=diamond];
    "Existing CoreData?" [shape=diamond];
    "Use SwiftData" [shape=box, style=filled, fillcolor=lightgreen];
    "Use CoreData" [shape=box, style=filled, fillcolor=lightblue];
    "Stay with CoreData" [shape=box, style=filled, fillcolor=lightblue];

    "New iOS app?" -> "iOS 17+ only?" [label="yes"];
    "New iOS app?" -> "Existing CoreData?" [label="no"];
    "Existing CoreData?" -> "Stay with CoreData" [label="yes"];
    "Existing CoreData?" -> "iOS 17+ only?" [label="no"];
    "iOS 17+ only?" -> "Simple data model?" [label="yes"];
    "iOS 17+ only?" -> "Use CoreData" [label="no"];
    "Simple data model?" -> "iCloud sync?" [label="yes"];
    "Simple data model?" -> "Use CoreData" [label="no"];
    "iCloud sync?" -> "Use SwiftData" [label="yes"];
    "iCloud sync?" -> "Use SwiftData" [label="no"];
}
选择 SwiftData 的情况
  • 新应用,最低部署目标为 iOS 17+
  • 关系简单(主要是 1:1 或 1:N,复杂查询较少)
  • 以 SwiftUI 为先的架构
  • 需要 iCloud 同步,且希望尽量减少配置
选择 CoreData 的情况
  • 需要支持 iOS 16 或更早版本
  • 有 复杂查询,涉及 NSCompoundPredicate、子查询
  • 以 UIKit 为主的应用,使用 NSFetchedResultsController
  • 已有 CoreData 代码库(迁移复杂,应继续使用 CoreData)
  • 性能要求极高(截至 2025 年,CoreData 仍然更快)
  • 抽象实体或继承层次结构

迁移陷阱

陷阱 1:未版本化 → 版本化并启用 iCloud

严重: 如果你发布了启用 iCloud 同步的未版本化 SwiftData,用户会在首次迁移时遇到崩溃。

错误: “Cannot use staged migration with an unknown model version”

原因: CloudKit 要求使用版本化架构以确保同步一致性。

解决方案:

// ❌ WRONG: Adding versioning after shipping unversioned + iCloud
// This will crash for all existing users

// ✅ CORRECT: Two-phase migration
// Version 1.1: Add versioning WITHOUT schema changes
enum SchemaV1: VersionedSchema {
    static var models: [any PersistentModel.Type] { [User.self] }

    @Model
    final class User {
        var name: String
        var email: String
        // EXACT same schema as shipped v1.0
    }
}

// Version 1.2: Now safe to add new properties
enum SchemaV2: VersionedSchema {
    static var models: [any PersistentModel.Type] { [User.self] }

    @Model
    final class User {
        var name: String
        var email: String
        var createdAt: Date? // New property
    }
}

规则: 从第一天起就使用版本化架构,或先发布版本控制封装层。

陷阱 2:唯一约束与重复数据

场景: 数据中存在重复项时添加 @Attribute(.unique)。

结果: 轻量级迁移失败,应用无法正常使用。

解决方案: 在应用约束之前,先通过复杂迁移去重。

struct DeduplicateThenUnique: MigrationStage {
    func migrate(context: ModelContext) throws {
        let items = try context.fetch(FetchDescriptor<OldSchema.Product>())
        let grouped = Dictionary(grouping: items, by: { $0.sku })

        // Keep first, delete duplicates
        for (_, duplicates) in grouped where duplicates.count > 1 {
            for item in duplicates.dropFirst() {
                context.delete(item)
            }
        }
        try context.save()
    }
}

规则: 应用约束之前先清理数据。使用生产数据备份进行测试。

陷阱 3:iCloud 同步阻止迁移

严重: 启用 iCloud 同步后,SwiftData 会阻止破坏轻量级兼容性的迁移。

这意味着:

  • 不能重命名属性(会破坏同步)
  • 不能更改关系删除规则(会破坏同步)
  • 不能移除必需属性(会破坏同步)

变通方案:

// Instead of renaming (blocked):
// @Model class User {
//     var userName: String // Renamed from 'name'
// }

// Add new, deprecate old (allowed):
@Model class User {
    var name: String // Keep for backward compatibility
    var userName: String? // New preferred field

    var displayName: String {
        userName ?? name // Compute from both
    }
}

规则: 启用 iCloud 时,只允许添加式更改。提前仔细设计架构。

陷阱 4:主线程迁移阻塞

症状: 应用更新后启动时出现 3-5 秒白屏。

原因: 大型数据集迁移在主线程上运行。

解决方案: 在后台进行初始化。

// ✅ Initialize container on background thread
Task.detached {
    let container = try await ModelContainer(
        for: User.self,
        migrationPlan: MigrationPlan.self
    )
    await MainActor.run {
        self.container = container
    }
}

规则: 始终使用符合实际情况的数据量(1000 条以上记录)测试迁移。

性能模式

模式 1:批量提取

问题: 一次加载 1000 个以上对象会占用内存并阻塞界面。

解决方案: 使用 fetchBatchSize 实现延迟加载。

// ❌ WRONG: Loads all 1000 posts into memory
let request: NSFetchRequest<Post> = Post.fetchRequest()
let posts = try context.fetch(request)

// ✅ CORRECT: Loads 20 at a time as needed
let request: NSFetchRequest<Post> = Post.fetchRequest()
request.fetchBatchSize = 20
let posts = try context.fetch(request)

规则: 对信息流、列表和分页视图,始终设置 fetchBatchSize。

模式 2:BLOB 存储

问题: 在 CoreData/SwiftData 中存储图像或视频会严重影响性能。

规则:

  • < 100KB:内联存储(安全)
  • 100KB - 1MB:使用独立实体,并通过关系关联
  • > 1MB:存储在文件系统中,并将路径保存到数据库
// ❌ WRONG: 2MB images in Post entity
@Model
class Post {
    var title: String
    @Attribute(.externalStorage) var image: Data // Still loads eagerly!
}

// ✅ CORRECT: File system storage
@Model
class Post {
    var title: String
    var imagePath: String? // "images/post-123.jpg"
}

// Load image on-demand
func loadImage(for post: Post) -> UIImage? {
    guard let path = post.imagePath else { return nil }
    let url = FileManager.documentsDirectory.appendingPathComponent(path)
    return UIImage(contentsOfFile: url.path)
}

原因: 数据库提取会加载完整对象。即使不显示,BLOB 也会增加内存用量。

模式 3:关系故障化

问题: 提取帖子时,会急切加载所有关联用户和图像。

解决方案: 返回故障对象,在访问时再加载。

// ✅ Relationships default to faults (good)
let request: NSFetchRequest<Post> = Post.fetchRequest()
request.relationshipKeyPathsForPrefetching = [] // Don't prefetch
let posts = try context.fetch(request)

// Access triggers individual fault:
let authorName = posts[0].author.name // Only now loads User

// ✅ Prefetch when you know you'll need it:
request.relationshipKeyPathsForPrefetching = ["author"]

规则: 仅预取所有已提取对象都会访问的关系。

模式 4:NSFetchedResultsController(CoreData)

适用场景: 需要实时更新的表格视图或集合视图。

原因: 延迟加载、自动更新界面,并内置分区功能。

let request: NSFetchRequest<Post> = Post.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
request.fetchBatchSize = 20

let frc = NSFetchedResultsController(
    fetchRequest: request,
    managedObjectContext: context,
    sectionNameKeyPath: nil,
    cacheName: "PostsCache"
)

frc.delegate = self
try frc.performFetch()

// UITableView uses frc.fetchedObjects

规则: 对所有由 CoreData 支持的表格视图和集合视图使用 FRC。

SwiftData 限制(截至 iOS 18)

特性CoreDataSwiftData解决方法
NSCompoundPredicate✅❌多次查询
NSFetchedResultsController✅❌在 SwiftUI 中使用 @Query
抽象实体✅❌使用协议
子上下文✅❌使用 ModelContext
模型子类化✅❌不要创建子类
性能更快更慢等待更新

常见错误

错误实际情况修正方法
“迁移是自动的”仅支持轻量级迁移。唯一约束和重命名需要复杂迁移。使用生产数据进行测试
“以后再做版本管理”无版本管理 + iCloud = 首次迁移时崩溃从第一天起就进行版本管理
“SwiftData = 简化版 CoreData”SwiftData 的规则不同(不支持子类化,删除规则也不同)了解 SwiftData 特有的限制
“externalStorage 属性能解决 BLOB 问题”获取数据时仍会将其加载到内存中将数据存储在文件系统中
“我可以随意重命名属性”使用 iCloud 时,重命名会受阻添加新属性,保留旧属性
“迁移不会阻塞 UI”大型数据集会阻塞主线程在后台初始化

迁移检查清单

发布架构变更前:

  • 已定义有版本的架构(V1、V2)
  • 如迁移复杂(去重、重命名),已编写迁移阶段
  • 已使用生产数据备份进行测试(1000 条以上记录)
  • 已在后台线程测试迁移(不会出现白屏)
  • 已验证迁移后 iCloud 同步正常
  • 已确认新属性有默认值或为可选属性
  • 已确认唯一约束仅应用于干净的数据

快速参考

SwiftData:

// Basic model
@Model
class User {
    @Attribute(.unique) var email: String
    var name: String
    @Relationship(deleteRule: .cascade) var posts: [Post]
}

// Versioned schemas
enum SchemaV1: VersionedSchema {
    static var models: [any PersistentModel.Type] { [User.self] }
    @Model final class User { /* ... */ }
}

// Query (SwiftUI)
@Query(sort: \.createdAt, order: .reverse) var users: [User]

CoreData:

// Fetch with batching
let request: NSFetchRequest<User> = User.fetchRequest()
request.fetchBatchSize = 20
request.predicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText)

// Background context
let bgContext = container.newBackgroundContext()
bgContext.perform {
    // Heavy work here
    try? bgContext.save()
}

危险信号——立即停止并重新考虑

  • 发布了未进行版本管理且启用 iCloud 的架构 → 先发布版本管理包装层
  • 将唯一约束添加到不干净的数据 → 运行去重迁移
  • 在数据库中存储数 MB 的文件 → 移至文件系统
  • 更新后启动时出现白屏 → 在后台迁移
  • 获取数据耗时超过 500ms → 添加 fetchBatchSize,检查是否有 BLOB
  • “等用户投诉了再处理迁移” → 现在就使用生产数据进行测试

实际影响

之前: 首次迁移后,所有启用了 iCloud 的用户(100%)都会遇到应用崩溃(未进行版本管理后再添加版本管理的陷阱)。

之后: 发布 v1.1,添加版本管理包装层(不更改架构),然后在 v1.2 中进行实际变更。零崩溃。


之前: 加载包含 1000 篇帖子、每篇配有 2MB 图片的动态流需要 4 秒。

之后: 将图片移至文件系统,设置 fetchBatchSize = 20。动态流在 300ms 内加载,并延迟加载图片。

评论

支持 Markdown