axiom-core-data-diag by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-core-data-diagCore Data 问题表现为因架构不匹配导致的生产环境崩溃、神秘的并发错误、负载下的性能下降以及不安全迁移导致的数据损坏。核心原则:85% 的 Core Data 问题源于对线程限制、架构迁移要求和关系查询模式的误解——而非 Core Data 本身的缺陷。
如果你看到以下任何情况,请怀疑是 Core Data 理解有误,而非框架损坏:
关键区别:模拟器在每次重建时都会删除数据库,从而隐藏架构不匹配问题。真实设备保留持久化数据库,并在架构不匹配时立即崩溃。强制要求:在发布前,使用真实设备和真实数据测试迁移。
始终首先运行这些步骤(在更改代码之前):
// 1. 识别崩溃/问题类型
// 截取崩溃消息截图并注意:
// - "无法解析的故障" = 架构不匹配
// - "不同线程" = 线程限制
// - 性能缓慢 = N+1 查询或获取大小问题
// - 数据损坏 = 不安全迁移
// 记录:"崩溃类型:[确切消息]"
// 2. 检查是否为架构不匹配
// 比较以下内容:
let coordinator = persistentStoreCoordinator
let model = coordinator.managedObjectModel
let store = coordinator.persistentStores.first
// 获取实际存储架构版本:
do {
let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(
ofType: NSSQLiteStoreType,
at: storeURL,
options: nil
)
print("存储版本标识符:\(metadata[NSStoreModelVersionIdentifiersKey] ?? "未知")")
// 获取应用的当前模型版本:
print("应用模型版本:\(model.versionIdentifiers)")
// 如果不同 = 架构不匹配
} catch {
print("架构检查错误:\(error)")
}
// 记录:"存储版本 vs. 应用模型:匹配或不匹配?"
// 3. 检查并发错误的线程限制
// 对于任何 NSManagedObject 访问:
print("主线程?\(Thread.isMainThread)")
print("上下文并发类型:\(context.concurrencyType.rawValue)")
print("从以下位置访问:\(Thread.current)")
// 记录:"线程不匹配?是/否"
// 4. 分析关系访问以查找 N+1 问题
// 在 Xcode 中,使用参数运行:
// -com.apple.CoreData.SQLDebug 1
// 在控制台中检查 SQL 查询:
// SELECT * FROM USERS; (1 次查询)
// SELECT * FROM POSTS WHERE user_id = 1; (每个用户 1 次查询 = N+1!)
// 记录:"发现 N+1?是/否,额外查询次数"
// 5. 检查 SwiftData 与 Core Data 混淆
if #available(iOS 17.0, *) {
// 如果同时使用 SwiftData @Model + Core Data:
// 错误:"存储被锁定" 或 "EXC_BAD_ACCESS"
// = 尝试从两个层访问同一数据库
print("在同一存储上同时使用 SwiftData 和 Core Data?")
}
// 记录:"混合使用 SwiftData + Core Data?是/否"
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
在更改任何代码之前,识别以下情况之一:
-com.apple.CoreData.SQLDebug 1 并统计 SQL 查询次数怀疑 Core Data 问题?
├─ 崩溃:"无法解析的故障"?
│ └─ 是 → 架构不匹配(存储 ≠ 应用模型)
│ ├─ 添加新的必需字段? → 模式 1a(轻量级迁移)
│ ├─ 删除字段、重命名或更改类型? → 模式 1b(重量级迁移)
│ └─ 不知道如何修复? → 模式 1c(测试安全性)
│
├─ 崩溃:"不同线程"?
│ └─ 是 → 违反线程限制
│ ├─ 使用 DispatchQueue 进行后台工作? → 模式 2a(异步上下文)
│ ├─ 混合 Core Data 与 async/await? → 模式 2b(结构化并发)
│ └─ SwiftUI @FetchRequest 导致问题? → 模式 2c(@FetchRequest 安全性)
│
├─ 性能:应用变慢?
│ └─ 是 → 可能是 N+1 查询
│ ├─ 在循环中访问 user.posts? → 模式 3a(预取)
│ ├─ 结果集很大? → 模式 3b(批处理大小)
│ └─ 刚添加了关系? → 模式 3c(关系调优)
│
├─ 同时使用 SwiftData 和 Core Data?
│ └─ 是 → 数据层冲突
│ ├─ 需要 SwiftData 缺乏的 Core Data 功能? → 模式 4a(降级到 Core Data)
│ ├─ 已承诺使用 SwiftData? → 模式 4b(留在 SwiftData)
│ └─ 不确定使用哪个? → 模式 4c(决策框架)
│
└─ 迁移在本地有效但在生产环境崩溃?
└─ 是 → 测试缺口
├─ 未使用真实数据测试? → 模式 5a(生产环境测试)
├─ 架构变更影响大数据集? → 模式 5b(迁移安全性)
└─ 发布前需要验证? → 模式 5c(部署前检查清单)
原则:如果操作正确,Core Data 可以自动迁移简单架构(增量变更)而不会丢失数据。
@NSManaged var nickname: String?// 错误:添加必需字段但未迁移
@NSManaged var userID: String // 必需,无默认值
// 错误:假设模拟器 = 生产环境
// 在模拟器上有效(删除数据库),在真实设备上崩溃
// 错误:修改字段类型
@NSManaged var createdAt: Date // 原为 String,现为 Date
// Core Data 无法自动转换
// 1. 在 Xcode 中:Editor → Add Model Version
// 创建新的 .xcdatamodel 版本文件
// 2. 在新版本中,添加带默认值的必需字段:
@NSManaged var userID: String = UUID().uuidString
// 3. 标记为当前模型版本:
// File Inspector → Versioned Core Data Model
// 勾选 "Current Model Version"
// 4. 测试:
// 模拟旧版本:删除应用,复制旧数据库,用新代码运行
// 真实应用加载 → 迁移成功
// 5. 有信心时部署
时间成本:轻量级迁移设置 5-10 分钟
原则:当轻量级迁移无效时,使用 NSEntityMigrationPolicy 进行自定义转换逻辑。
// 1. 创建映射模型
// File → New → Mapping Model
// Source:旧版本,Destination:新版本
// 2. 创建自定义迁移策略
class DateMigrationPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(
forSource sInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
let destination = NSEntityDescription.insertNewObject(
forEntityName: mapping.destinationEntityName ?? "",
into: manager.destinationContext
)
for key in sInstance.entity.attributesByName.keys {
destination.setValue(sInstance.value(forKey: key), forKey: key)
}
// 自定义转换:String → Date
if let dateString = sInstance.value(forKey: "createdAt") as? String,
let date = ISO8601DateFormatter().date(from: dateString) {
destination.setValue(date, forKey: "createdAt")
} else {
destination.setValue(Date(), forKey: "createdAt")
}
manager.associate(source: sInstance, withDestinationInstance: destination, for: mapping)
}
}
// 3. 在映射模型 Inspector 中:
// 设置 Custom Policy Class:DateMigrationPolicy
// 4. 发布前使用真实数据广泛测试
时间成本:每次迁移 30-60 分钟 + 测试
原则:Core Data 对象受线程限制。在后台线程上获取,转换为轻量级表示形式以供主线程使用。
DispatchQueue.global().async {
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let results = try! context.fetch(request)
DispatchQueue.main.async {
self.objects = results // ❌ 崩溃:对象在后台线程上故障
}
}
// 创建后台上下文
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
backgroundContext.parent = viewContext
// 在后台线程上获取
backgroundContext.perform {
do {
let results = try backgroundContext.fetch(userRequest)
// 在主线程之前转换为轻量级表示形式
let userIDs = results.map { $0.id } // 仅 ID,非完整对象
DispatchQueue.main.async {
// 在主线程上,从主上下文获取完整对象
let mainResults = try self.viewContext.fetch(request)
self.objects = mainResults
}
} catch {
print("获取错误:\(error)")
}
}
时间成本:重构 10 分钟
原则:使用 NSPersistentContainer 或 NSManagedObjectContext 的异步方法以实现 Swift 并发兼容性。
// iOS 13+:使用异步 perform
let users = try await viewContext.perform {
try viewContext.fetch(userRequest)
}
// 在正确的线程上执行获取,返回给调用者
// iOS 17+:直接使用 Swift 并发 async/await
let users = try await container.mainContext.fetch(userRequest)
// 用于后台工作:
let backgroundUsers = try await backgroundContext.perform {
try backgroundContext.fetch(userRequest)
}
// 获取发生在后台队列上,线程安全
async {
DispatchQueue.global().async {
try context.fetch(request) // ❌ 错误线程!
}
}
时间成本:从 DispatchQueue 转换为 async/await 5 分钟
原则:告诉 Core Data 预先获取关系,而不是在访问时延迟加载。
let users = try context.fetch(userRequest)
for user in users {
let posts = user.posts // ❌ 为每个用户触发获取!
// 1 次用户获取 + N 次关系获取 = 总计 N+1 次
}
var request = NSFetchRequest<User>(entityName: "User")
// 告诉 Core Data 预先获取关系
request.relationshipKeyPathsForPrefetching = ["posts", "comments"]
// 现在每个关系都在一次查询中获取
let users = try context.fetch(request)
for user in users {
let posts = user.posts // ✅ 即时:已获取
// 总计:1 次用户获取 + 1 次所有帖子获取 = 2 次查询
}
// 批处理大小:分块获取大型结果集
request.fetchBatchSize = 100
// 故障行为:将故障转换为轻量级快照
request.returnsObjectsAsFaults = false // 将对象保留在内存中
// 谨慎使用——大型结果可能导致内存压力
// 去重:从关系获取中移除重复项
request.returnsDistinctResults = true
时间成本:添加预取 2-5 分钟
原则:对于大型结果集,分批获取以管理内存。
var request = NSFetchRequest<User>(entityName: "User")
request.fetchBatchSize = 100 // 每次获取 100 个
// 为稳定分页设置排序描述符
request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
let results = try context.fetch(request)
// 内存占用:每次约 100 个用户,而非全部 100,000 个
for user in results {
// 访问用户 0-99:在内存中
// 访问用户 100:批处理重新获取(用户 100-199)
// 自动分页,内存使用最小化
}
时间成本:调整批处理大小 3 分钟
场景:你选择了 SwiftData,但需要它缺乏的功能。
// 对简单实体保留 SwiftData
@Model final class Note {
var id: String
var title: String
}
// 对复杂操作降级到 Core Data
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
backgroundContext.parent = container.viewContext
// 使用 Core Data 获取,转换为 SwiftData 模型
let results = try backgroundContext.perform {
try backgroundContext.fetch(coreDataRequest)
}
关键:不要同时从 SwiftData 和 Core Data 访问同一实体。二选一,不要同时使用。
时间成本:创建桥接层 30-60 分钟
场景:你正在使用 SwiftData,并想知道是否需要 Core Data。
时间成本:0 分钟(仅决策)
原则:永远不要在不使用真实数据测试的情况下部署迁移。
// 步骤 1:导出生产环境数据库
// 从模拟器或真实设备中运行的应用:
// ~/Library/Developer/CoreData/[AppName]/
// 复制整个 [AppName].sqlite 数据库
// 步骤 2:创建迁移测试
@Test func testProductionDataMigration() throws {
// 将生产环境数据库复制到测试位置
let testDB = tempDirectory.appendingPathComponent("test.sqlite")
try FileManager.default.copyItem(from: prodDatabase, to: testDB)
// 尝试迁移
var config = ModelConfiguration(url: testDB, isStoredInMemory: false)
let container = try ModelContainer(for: User.self, configurations: [config])
// 验证数据完整性
let context = container.mainContext
let allUsers = try context.fetch(FetchDescriptor<User>())
// 抽查:验证特定记录是否正确迁移
guard let user1 = allUsers.first(where: { $0.id == "test-id-1" }) else {
throw MigrationError.missingUser
}
// 检查派生数据是否正确
XCTAssertEqual(user1.name, "预期名称")
XCTAssertNotNil(user1.createdAt)
// 检查关系
XCTAssertEqual(user1.posts.count, 预期帖子数量)
}
// 步骤 3:针对真实生产环境数据运行测试
// 通过 ✓ 后再发布
时间成本:创建迁移测试 15-30 分钟
时间成本:5 分钟检查清单
| 问题 | 检查 | 修复 |
|---|---|---|
| "无法解析的故障"崩溃 | 存储/模型版本是否匹配? | 创建 .xcdatamodel 版本 + 映射模型 |
| "不同线程"崩溃 | 获取是否发生在主线程上? | 使用私有队列上下文进行后台工作 |
| 应用变慢 | 关系是否被预取? | 添加 relationshipKeyPathsForPrefetching |
| N+1 查询性能 | 检查 -com.apple.CoreData.SQLDebug 1 日志 | 添加预取或转换为轻量级表示形式 |
| SwiftData 需要 Core Data 功能 | 你需要自定义迁移吗? | 使用 Core Data NSEntityMigrationPolicy |
| 不确定 SwiftData 与 Core Data | 你需要 iOS 16 支持吗? | 对 iOS 16 使用 Core Data,对 iOS 17+ 使用 SwiftData |
| 迁移测试有效,生产环境失败 | 你是否使用真实数据测试过? | 使用生产环境数据库副本创建迁移测试 |
如果你花费了 >30 分钟且 Core Data 问题仍然存在:
-com.apple.CoreData.SQLDebug 1)❌ 仅在模拟器中测试迁移
❌ 假设默认值能防止数据丢失
❌ 未经转换跨线程访问 Core Data 对象
❌ 未意识到关系访问 = 数据库查询
user.posts 为每个用户触发一次获取(N+1)❌ 在同一存储上混合 SwiftData 和 Core Data
❌ 未经部署前测试部署迁移
❌ 合理化:"我就删除数据吧"
在生产环境危机压力下,你会面临以下要求:
这些听起来像是务实的危机应对。但它们会导致数据丢失和永久性的用户信任损害。 你的工作:使用数据安全原则和客户影响来捍卫,而非对压力的恐惧。
如果你在生产环境危机期间听到任何这些,停止并参考此技能:
"我想尽快解决这个崩溃,但让我告诉你删除存储意味着什么:
当前情况:
- 10,000 名活跃用户及其数据
- 平均每个用户 50 个项目(总计 500,000 条记录)
- 用户积累了 1 周到 2 年的数据
如果我们删除存储:
- 10,000 名用户在下次应用启动时丢失**所有**数据
- 卸载率:60-80%(数据丢失后的行业标准)
- App Store 评价:预计会出现提及数据丢失的 1 星评价
- 客户支持:预计会有大量数据丢失投诉
- 恢复:不可能 - 删除的数据无法恢复
安全替代方案:
- 在真实设备上使用生产环境数据副本测试迁移(2-4 小时)
- 部署保留用户数据的迁移
- 卸载率:<5%(标准更新流失率)"
向 PM/经理展示会发生什么:
"我可以让我们度过这次危机,同时保护用户数据:
#### 快速通道(总计 4 小时)
1. 从 TestFlight 用户复制生产环境数据库(30 分钟)
2. 在真实设备副本上编写和测试迁移(2 小时)
3. 提交经过测试的迁移构建(30 分钟)
4. 监控前 100 次更新的崩溃情况(1 小时)
#### 如果迁移失败的回退方案
- 准备好"删除存储"构建作为计划 B
- 仅当迁移显示 100% 失败率时部署
- 主动向用户沟通数据丢失
此方法:
- 首先尝试安全路径(保护用户数据)
- 有紧急回退方案(如果迁移不可能)
- 诚实的时间线(4 小时 vs. "直接删除" 30 分钟)"
如果被否决(PM 坚持删除存储):
Slack 消息给 PM + 团队:
"生产环境危机:架构不匹配导致现有用户崩溃。
PM 决策:删除持久化存储以立即解决。
影响评估:
- 10,000 名用户在下次应用启动时永久丢失**所有**数据
- 预期卸载率:60-80%(基于数据丢失模式)
- App Store 评价损害:1 星评价的高风险
- 客户支持:预计会有大量数据丢失投诉
- 恢复:不可能 - 删除的数据无法恢复
提议的替代方案(4 小时安全迁移)因紧急情况被拒绝。
我主动标记此决策,以便我们可以:
1. 为数据丢失投诉准备支持团队
2. 起草针对预期负面评价的 App Store 回应
3. 考虑在发布前向用户沟通数据丢失"
// ❌ 错误 - 删除所有用户数据(CTO 的要求)
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
let storeURL = /* 持久化存储 URL */
try? FileManager.default.removeItem(at: storeURL) // 500K 用户丢失数据
try! coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: storeURL,
options: nil)
// ✅ 正确 - 安全轻量级迁移(4 小时时间线)
let options = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: storeURL,
options: options)
// 迁移成功 - 用户数据保留
} catch {
// 迁移失败 — **现在**考虑删除并与用户沟通
print("迁移错误:\(error)")
}
时间估计:总计 4 小时(2 小时迁移测试,2 小时构建/部署)
有时数据丢失是唯一选择。如果以下情况,请接受:
"生产环境危机:经过 4 小时测试,迁移在生产环境数据副本上失败。
技术细节:
- 尝试轻量级迁移:
Core Data issues manifest as production crashes from schema mismatches, mysterious concurrency errors, performance degradation under load, and data corruption from unsafe migrations. Core principle 85% of Core Data problems stem from misunderstanding thread-confinement, schema migration requirements, and relationship query patterns—not Core Data defects.
If you see ANY of these, suspect a Core Data misunderstanding, not framework breakage:
Critical distinction Simulator deletes the database on each rebuild, hiding schema mismatch issues. Real devices keep persistent databases and crash immediately on schema mismatch. MANDATORY: Test migrations on real device with real data before shipping.
ALWAYS run these FIRST (before changing code):
// 1. Identify the crash/issue type
// Screenshot the crash message and note:
// - "Unresolvable fault" = schema mismatch
// - "different thread" = thread-confinement
// - Slow performance = N+1 queries or fetch size issues
// - Data corruption = unsafe migration
// Record: "Crash type: [exact message]"
// 2. Check if it's schema mismatch
// Compare these:
let coordinator = persistentStoreCoordinator
let model = coordinator.managedObjectModel
let store = coordinator.persistentStores.first
// Get actual store schema version:
do {
let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore(
ofType: NSSQLiteStoreType,
at: storeURL,
options: nil
)
print("Store version identifier: \(metadata[NSStoreModelVersionIdentifiersKey] ?? "unknown")")
// Get app's current model version:
print("App model version: \(model.versionIdentifiers)")
// If different = schema mismatch
} catch {
print("Schema check error: \(error)")
}
// Record: "Store version vs. app model: match or mismatch?"
// 3. Check thread-confinement for concurrency errors
// For any NSManagedObject access:
print("Main thread? \(Thread.isMainThread)")
print("Context concurrency type: \(context.concurrencyType.rawValue)")
print("Accessing from: \(Thread.current)")
// Record: "Thread mismatch? Yes/no"
// 4. Profile relationship access for N+1 problems
// In Xcode, run with arguments:
// -com.apple.CoreData.SQLDebug 1
// Check Console for SQL queries:
// SELECT * FROM USERS; (1 query)
// SELECT * FROM POSTS WHERE user_id = 1; (1 query per user = N+1!)
// Record: "N+1 found? Yes/no, how many extra queries"
// 5. Check SwiftData vs. Core Data confusion
if #available(iOS 17.0, *) {
// If using SwiftData @Model + Core Data simultaneously:
// Error: "Store is locked" or "EXC_BAD_ACCESS"
// = trying to access same database from both layers
print("Using both SwiftData and Core Data on same store?")
}
// Record: "Mixing SwiftData + Core Data? Yes/no"
Before changing ANY code, identify ONE of these:
-com.apple.CoreData.SQLDebug 1 and count SQL queriesCore Data problem suspected?
├─ Crash: "Unresolvable fault"?
│ └─ YES → Schema mismatch (store ≠ app model)
│ ├─ Add new required field? → Pattern 1a (lightweight migration)
│ ├─ Remove field, rename, or change type? → Pattern 1b (heavy migration)
│ └─ Don't know how to fix? → Pattern 1c (testing safety)
│
├─ Crash: "different thread"?
│ └─ YES → Thread-confinement violated
│ ├─ Using DispatchQueue for background work? → Pattern 2a (async context)
│ ├─ Mixing Core Data with async/await? → Pattern 2b (structured concurrency)
│ └─ SwiftUI @FetchRequest causing issues? → Pattern 2c (@FetchRequest safety)
│
├─ Performance: App became slow?
│ └─ YES → Likely N+1 queries
│ ├─ Accessing user.posts in loop? → Pattern 3a (prefetching)
│ ├─ Large result set? → Pattern 3b (batch sizing)
│ └─ Just added relationships? → Pattern 3c (relationship tuning)
│
├─ Using both SwiftData and Core Data?
│ └─ YES → Data layer conflict
│ ├─ Need Core Data features SwiftData lacks? → Pattern 4a (drop to Core Data)
│ ├─ Already committed to SwiftData? → Pattern 4b (stay in SwiftData)
│ └─ Unsure which to use? → Pattern 4c (decision framework)
│
└─ Migration works locally but crashes in production?
└─ YES → Testing gap
├─ Didn't test with real data? → Pattern 5a (production testing)
├─ Schema change affects large dataset? → Pattern 5b (migration safety)
└─ Need verification before shipping? → Pattern 5c (pre-deployment checklist)
PRINCIPLE Core Data can automatically migrate simple schemas (additive changes) without data loss if done correctly.
@NSManaged var nickname: String?// BAD: Adding required field without migration
@NSManaged var userID: String // Required, no default
// BAD: Assuming simulator = production
// Works in simulator (deletes DB), crashes on real device
// BAD: Modifying field type
@NSManaged var createdAt: Date // Was String, now Date
// Core Data can't automatically convert
// 1. In Xcode: Editor → Add Model Version
// Creates new .xcdatamodel version file
// 2. In new version, add required field WITH default:
@NSManaged var userID: String = UUID().uuidString
// 3. Mark as current model version:
// File Inspector → Versioned Core Data Model
// Check "Current Model Version"
// 4. Test:
// Simulate old version: delete app, copy old database, run with new code
// Real app loads → migration succeeded
// 5. Deploy when confident
Time cost 5-10 minutes for lightweight migration setup
PRINCIPLE When lightweight migration won't work, use NSEntityMigrationPolicy for custom transformation logic.
// 1. Create mapping model
// File → New → Mapping Model
// Source: old version, Destination: new version
// 2. Create custom migration policy
class DateMigrationPolicy: NSEntityMigrationPolicy {
override func createDestinationInstances(
forSource sInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
let destination = NSEntityDescription.insertNewObject(
forEntityName: mapping.destinationEntityName ?? "",
into: manager.destinationContext
)
for key in sInstance.entity.attributesByName.keys {
destination.setValue(sInstance.value(forKey: key), forKey: key)
}
// Custom transformation: String → Date
if let dateString = sInstance.value(forKey: "createdAt") as? String,
let date = ISO8601DateFormatter().date(from: dateString) {
destination.setValue(date, forKey: "createdAt")
} else {
destination.setValue(Date(), forKey: "createdAt")
}
manager.associate(source: sInstance, withDestinationInstance: destination, for: mapping)
}
}
// 3. In mapping model Inspector:
// Set Custom Policy Class: DateMigrationPolicy
// 4. Test extensively with real data before shipping
Time cost 30-60 minutes per migration + testing
PRINCIPLE Core Data objects are thread-confined. Fetch on background thread, convert to lightweight representations for main thread.
DispatchQueue.global().async {
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let results = try! context.fetch(request)
DispatchQueue.main.async {
self.objects = results // ❌ CRASH: objects faulted on background thread
}
}
// Create background context
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
backgroundContext.parent = viewContext
// Fetch on background thread
backgroundContext.perform {
do {
let results = try backgroundContext.fetch(userRequest)
// Convert to lightweight representation BEFORE main thread
let userIDs = results.map { $0.id } // Just the IDs, not full objects
DispatchQueue.main.async {
// On main thread, fetch full objects from main context
let mainResults = try self.viewContext.fetch(request)
self.objects = mainResults
}
} catch {
print("Fetch error: \(error)")
}
}
Time cost 10 minutes to restructure
PRINCIPLE Use NSPersistentContainer or NSManagedObjectContext async methods for Swift Concurrency compatibility.
// iOS 13+: Use async perform
let users = try await viewContext.perform {
try viewContext.fetch(userRequest)
}
// Executes fetch on correct thread, returns to caller
// iOS 17+: Use Swift Concurrency async/await directly
let users = try await container.mainContext.fetch(userRequest)
// For background work:
let backgroundUsers = try await backgroundContext.perform {
try backgroundContext.fetch(userRequest)
}
// Fetch happens on background queue, thread-safe
async {
DispatchQueue.global().async {
try context.fetch(request) // ❌ Wrong thread!
}
}
Time cost 5 minutes to convert from DispatchQueue to async/await
PRINCIPLE Tell Core Data to fetch relationships eagerly instead of lazy-loading on access.
let users = try context.fetch(userRequest)
for user in users {
let posts = user.posts // ❌ Triggers fetch for EACH user!
// 1 fetch for users + N fetches for relationships = N+1 total
}
var request = NSFetchRequest<User>(entityName: "User")
// Tell Core Data to fetch relationships eagerly
request.relationshipKeyPathsForPrefetching = ["posts", "comments"]
// Now relationships are fetched in a single query per relationship
let users = try context.fetch(request)
for user in users {
let posts = user.posts // ✅ INSTANT: Already fetched
// Total: 1 fetch for users + 1 fetch for all posts = 2 queries
}
// Batch size: fetch in chunks for large result sets
request.fetchBatchSize = 100
// Faulting behavior: convert faults to lightweight snapshots
request.returnsObjectsAsFaults = false // Keep objects in memory
// Use carefully—can cause memory pressure with large results
// Distinct: remove duplicates from relationship fetches
request.returnsDistinctResults = true
Time cost 2-5 minutes to add prefetching
PRINCIPLE For large result sets, fetch in batches to manage memory.
var request = NSFetchRequest<User>(entityName: "User")
request.fetchBatchSize = 100 // Fetch 100 at a time
// Set sort descriptor for stable pagination
request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
let results = try context.fetch(request)
// Memory footprint: ~100 users at a time, not all 100,000
for user in results {
// Accessing user 0-99: in memory
// Accessing user 100: batch refetch (user 100-199)
// Auto-pagination, minimal memory usage
}
Time cost 3 minutes to tune batch size
Scenario You chose SwiftData, but need features it lacks.
// Keep SwiftData for simple entities
@Model final class Note {
var id: String
var title: String
}
// Drop to Core Data for complex operations
let backgroundContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
backgroundContext.parent = container.viewContext
// Fetch with Core Data, convert to SwiftData models
let results = try backgroundContext.perform {
try backgroundContext.fetch(coreDataRequest)
}
CRITICAL Do NOT access the same entity from both SwiftData and Core Data simultaneously. One or the other, not both.
Time cost 30-60 minutes to create bridging layer
Scenario You're in SwiftData and wondering if you need Core Data.
Time cost 0 minutes (decision only)
PRINCIPLE Never deploy a migration without testing against real data.
// Step 1: Export production database
// From running app in simulator or real device:
// ~/Library/Developer/CoreData/[AppName]/
// Copy entire [AppName].sqlite database
// Step 2: Create migration test
@Test func testProductionDataMigration() throws {
// Copy production database to test location
let testDB = tempDirectory.appendingPathComponent("test.sqlite")
try FileManager.default.copyItem(from: prodDatabase, to: testDB)
// Attempt migration
var config = ModelConfiguration(url: testDB, isStoredInMemory: false)
let container = try ModelContainer(for: User.self, configurations: [config])
// Verify data integrity
let context = container.mainContext
let allUsers = try context.fetch(FetchDescriptor<User>())
// Spot checks: verify specific records migrated correctly
guard let user1 = allUsers.first(where: { $0.id == "test-id-1" }) else {
throw MigrationError.missingUser
}
// Check derived data is correct
XCTAssertEqual(user1.name, "Expected Name")
XCTAssertNotNil(user1.createdAt)
// Check relationships
XCTAssertEqual(user1.posts.count, expectedPostCount)
}
// Step 3: Run test against real production data
// Pass ✓ before shipping
Time cost 15-30 minutes to create migration test
Time cost 5 minutes checklist
| Issue | Check | Fix |
|---|---|---|
| "Unresolvable fault" crash | Do store/model versions match? | Create .xcdatamodel version + mapping model |
| "Different thread" crash | Is fetch happening on main thread? | Use private queue context for background work |
| App became slow | Are relationships being prefetched? | Add relationshipKeyPathsForPrefetching |
| N+1 query performance | Check -com.apple.CoreData.SQLDebug 1 logs | Add prefetching or convert to lightweight representation |
| SwiftData needs Core Data features | Do you need custom migrations? | Use Core Data NSEntityMigrationPolicy |
| Not sure about SwiftData vs. Core Data | Do you need iOS 16 support? | Use Core Data for iOS 16, SwiftData for iOS 17+ |
| Migration test works, production fails | Did you test with real data? | Create migration test with production database copy |
If you've spent >30 minutes and the Core Data issue persists:
-com.apple.CoreData.SQLDebug 1)❌ Testing migration in simulator only
❌ Assuming default values protect against data loss
❌ Accessing Core Data objects across threads without conversion
❌ Not realizing relationship access = database query
user.posts triggers a fetch for EACH user (N+1)❌ Mixing SwiftData and Core Data on same store
❌ Deploying migrations without pre-deployment testing
❌ Rationalizing: "I'll just delete the data"
Under production crisis pressure, you'll face requests to:
These sound like pragmatic crisis responses. But they cause data loss and permanent user trust damage. Your job: defend using data safety principles and customer impact, not fear of pressure.
If you hear ANY of these during a production crisis, STOP and reference this skill :
"I want to resolve this crash ASAP, but let me show you what deleting the store means:
Current situation:
- 10,000 active users with data
- Average 50 items per user (500,000 total records)
- Users have 1 week to 2 years of accumulated data
If we delete the store:
- 10,000 users lose ALL their data on next app launch
- Uninstall rate: 60-80% (industry standard after data loss)
- App Store reviews: Expect 1-star reviews citing data loss
- Recovery: Impossible - data is gone permanently
Safe alternative:
- Test migration on real device with production data copy (2-4 hours)
- Deploy migration that preserves user data
- Uninstall rate: <5% (standard update churn)"
Show the PM/manager what happens:
"I can get us through this crisis while protecting user data:
#### Fast track (4 hours total)
1. Copy production database from TestFlight user (30 min)
2. Write and test migration on real device copy (2 hours)
3. Submit build with tested migration (30 min)
4. Monitor first 100 updates for crashes (1 hour)
#### Fallback if migration fails
- Have "delete store" build ready as Plan B
- Only deploy if migration shows 100% failure rate
- Communicate data loss to users proactively
This approach:
- Tries safe path first (protects user data)
- Has emergency fallback (if migration impossible)
- Honest timeline (4 hours vs. "just delete it" 30 min)"
If overruled (PM insists on deleting store):
Slack message to PM + team:
"Production crisis: Schema mismatch causing crashes for existing users.
PM decision: Delete persistent store to resolve immediately.
Impact assessment:
- 10,000 users lose ALL data permanently on next app launch
- Expected uninstall rate: 60-80% based on data loss patterns
- App Store review damage: High risk of 1-star reviews
- Customer support: Expect high volume of data loss complaints
- Recovery: Impossible - deleted data cannot be recovered
Alternative proposed (4-hour safe migration) was declined due to urgency.
I'm flagging this decision proactively so we can:
1. Prepare support team for data loss complaints
2. Draft App Store response to expected negative reviews
3. Consider user communication about data loss before launch"
// ❌ WRONG - Deletes all user data (CTO's request)
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
let storeURL = /* persistent store URL */
try? FileManager.default.removeItem(at: storeURL) // 500K users lose data
try! coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: storeURL,
options: nil)
// ✅ CORRECT - Safe lightweight migration (4-hour timeline)
let options = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: storeURL,
options: options)
// Migration succeeded - user data preserved
} catch {
// Migration failed — NOW consider deleting with user communication
print("Migration error: \(error)")
}
Time estimate 4 hours total (2 hours migration testing, 2 hours build/deploy)
Sometimes data loss is the only option. Accept if:
"Production crisis: Migration failed on production data copy after 4-hour testing.
Technical details:
- Attempted lightweight migration: Failed with [error]
- Attempted heavy migration with mapping model: Failed with [error]
- Root cause: [specific schema incompatibility]
Data loss decision:
- No safe migration path exists
- PM approved delete persistent store approach
- Expected impact: 60-80% uninstall rate (500K → 100-200K users)
Mitigation plan:
- Add data export feature before next schema change
- Communicate data loss to users via in-app message
- Prepare support team for complaints
- Monitor uninstall rates post-launch"
This protects you and shows you exhausted safe options first.
Before Core Data debugging 3-8 hours per issue
After 30 minutes to 2 hours with systematic diagnosis
Key insight Core Data has well-established patterns for every common issue. The problem is developers don't know which pattern applies to their symptom.
Last Updated : 2025-11-30 Status : TDD-tested with pressure scenarios Framework : Core Data (Foundation framework) Complements : SwiftData skill (understanding relationship to Core Data)
Weekly Installs
96
Repository
GitHub Stars
606
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode82
claude-code77
gemini-cli75
codex75
cursor74
github-copilot71
SQL查询优化指南:PostgreSQL、Snowflake、BigQuery高性能SQL编写技巧与方言参考
1,000 周安装
如何创建 llms.txt 文件:为大型语言模型优化仓库导航的完整指南
7,800 周安装
AI自动化实施计划更新工具 - 结构化、机器可读的更新实施计划生成
7,800 周安装
技术栈蓝图生成器 - 自动分析代码库,生成.NET/Java/JS/Python等技术栈文档
7,800 周安装
Solidity 智能合约安全指南:防范重入攻击、溢出漏洞与访问控制
7,900 周安装
Terraform AzureRM Set Diff Analyzer - 解决AzureRM Provider误报差异的Terraform工具
7,800 周安装
Finnish Humanizer - AI文本芬兰语自然化工具,移除AI痕迹,让写作更地道
7,800 周安装