在SwiftUI应用CoreData小纸条(一)里简单review了模板,赞赏之余不要被它的表面所迷惑。虽然通过模板,可以快速的完成了一个可以增、删、查的App,但是要想把想要的功能完成,光鲜亮丽表面的下面就是各种秘籍,实话说,几千工程师根本就没写出点像样的文档。在这里记录一下简单的数据结构定义和使用。在我写英语小助手时,我们会需要存储Chapter、Topic、Picture、Word这样的数据。我们这里先把Chapter完成。
定义Entity和Attribute
每一个Chapter都有一个name Attribute,它是一个String,所以我们做好这样的设置
对于这个name,我不希望它为空(不是Optional的),所以再点Attribute里的name,再在Attribute的Inspector中去除Optional勾选Default String,设置为"New Chapter":
对于这个name,我不希望它为空(不是Optional的),所以再点Attribute里的name,再在Attribute的Inspector中去除Optional勾选Default String,设置为"New Chapter":
对于这个name,我希望它是唯一不发生重复的,哪么先点ENTITIES中的Chapter,再在Entity的Inspector中的Constraints里按+增加一个"name"
将原有文件中的Item更换为Chapter
更新Persistence.swift
这里主要是生成preview的部分需要更新一下
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for i in 0..<10 {
let newItem = Chapter(context: viewContext)
newItem.name = "Chapter \(i)"
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
print("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
更新ContentView.swift
ContentView中涉及的比较多,整个文件都放在这里吧
import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Chapter.name, ascending: true)],
animation: .default)
private var items: FetchedResults<Chapter>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.name ?? "UNKNOW ERROR")")
} label: {
Text(item.name ?? "UNKNOW ERROR")
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Chapter(context: viewContext)
newItem.name = "new chapter"
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
测试一下
Preview
Preview工作非常正常
如果在Preview中点运行后,会发现连点两次"+"后会触发闪退:
如果我们看report,可以发现是我们设置的Constraints生效了,在数据中不能存在两个name为new chapter的数据。
Simulator
在Simulator中运行会发现没有了初始的10条数据(对,它是我们在preview里生成的),按两次+,我们也会发现它停止运行了:
停止运行的点就是我们的addItem中的catch部分。
简单修改addItem
粗暴的将addItem里每次加入的Chapter name产生一个自增长:
private func addItem() {
withAnimation {
let count = items.count
let newItem = Chapter(context: viewContext)
newItem.name = "new chapter \(count)"
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
这样就不会再出错了。
完善存盘策略
制止闪退
让我们先把fatalError替换,让应用程序不再闪退:
private func addItem() {
withAnimation {
let newItem = Chapter(context: viewContext)
newItem.name = "new chapter"
do {
try viewContext.save()
} catch {
print(error.localizedDescription)
}
}
}
这里会发现在模拟器中运行时会在控制台上打印出错误,但是在界面上还在不停的加入新的Item:
这样的情况其实就是在ViewContext内存中的数据与存储中的数据不相符造成的。所以我们做一个优雅的处理,让它们同步。
使用NSMergePolicy
CoreData提供一个上下文合并策略可以让我们运用,这是修改后PersistenceController的init:
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "DictionariesManager")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
print("Unresolved error \(error.localizedDescription), \(error.userInfo)")
return
}
})
container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
}
这里在发现loadPersistentStores出错后会return。如果load成功,设置了一个mergePolicy为mergeByPropertyObjectTrump,如果新增的item产生了重复,它就会自动merge,从而不会产生错误。这显然是一个更为友好的处理措施。不过,带来的不好则是按+后会感觉没有反应,这个就看你自己想怎么处理更为友好了 :)