# SwiftUI应用CoreData小纸条（二）

在[SwiftUI应用CoreData小纸条（一）](https://blog.hdcola.one/swiftuicoredata1)里简单review了模板，赞赏之余不要被它的表面所迷惑。虽然通过模板，可以快速的完成了一个可以增、删、查的App，但是要想把想要的功能完成，光鲜亮丽表面的下面就是各种秘籍，实话说，几千工程师根本就没写出点像样的文档。在这里记录一下简单的数据结构定义和使用。在我写英语小助手时，我们会需要存储Chapter、Topic、Picture、Word这样的数据。我们这里先把Chapter完成。

### 定义Entity和Attribute

每一个Chapter都有一个name Attribute，它是一个String，所以我们做好这样的设置

![Attribute Setup](https://cdn.hashnode.com/res/hashnode/image/upload/v1646338982638/Chxom5vIO.png)

对于这个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"

![Constraints Setup](https://cdn.hashnode.com/res/hashnode/image/upload/v1646339242925/xfCJDxoGs.png)

### 将原有文件中的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工作非常正常

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646341212238/N-jf8Bb62.png)

如果在Preview中点运行后，会发现连点两次"+"后会触发闪退：

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646341363897/HB75I0fkk.png)

如果我们看report，可以发现是我们设置的Constraints生效了，在数据中不能存在两个name为new chapter的数据。

##### Simulator

在Simulator中运行会发现没有了初始的10条数据（对，它是我们在preview里生成的），按两次+，我们也会发现它停止运行了：

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646342825299/kyc96OZDB.png)

停止运行的点就是我们的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：

![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1646349604198/YGQlAMAy3.png)

这样的情况其实就是在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，从而不会产生错误。这显然是一个更为友好的处理措施。不过，带来的不好则是按+后会感觉没有反应，这个就看你自己想怎么处理更为友好了 :)

