老房东
老房东的纸条箱

老房东的纸条箱

SwiftUI应用CoreData小纸条(二)

SwiftUI应用CoreData小纸条(二)

自定义一个Entity并优化addItem

老房东's photo
老房东
·Mar 3, 2022·

2 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

  • 定义Entity和Attribute
  • 将原有文件中的Item更换为Chapter
  • 测试一下
  • 简单修改addItem
  • 完善存盘策略

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

定义Entity和Attribute

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

Attribute Setup

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

将原有文件中的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

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

image.png

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

Simulator

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

image.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

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

Did you find this article valuable?

Support 老房东 by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this