Skip to main content

Command Palette

Search for a command to run...

SwiftUI应用CoreData小纸条(二)

自定义一个Entity并优化addItem

Published
2 min read
SwiftUI应用CoreData小纸条(二)

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

SwiftUI小纸条

Part 24 of 29

使用和书写SwiftUI过程中留下的各种小纸条

Up next

SwiftUI应用CoreData小纸条(一)

解析Xcode13的CoreData项目模板

More from this blog

Https 本地服务器小纸条

最近想要尝试一下Telegram mini app,在调试时需要使用https的服务。试来试去发现在Mac下完成一个简单的HTTPS服务器还是需要点奇奇怪怪的工具的。但是整体来讲非常简单。 准备证书 主要会使用mkcert来创建证书。首先安装mkcert。 brew install mkcert brew install nss # if use Firefox 将mkcert加入到本地root CA。 mkcert -install 生成证书 本地调试可以使用localhost或127.0...

Feb 21, 20241 min read
Https 本地服务器小纸条

macOS中使用Docker发布一个python项目的小纸条

最近写了一个Telegram Bot,它可以使用语音和文字与GPT进行交互,成为了我日常重度使用的工具。从练习英语的听说读,到日常的搜索使用上都让我有了不少收获。终于,日常跑在我笔记本上的日子就要过去了,我需要它能日常跑在我的服务器上,所以准备使用Docker整个image,使得我日常的更新和服务器的迁移更为简单些。所以写下这个小纸条,方便以后自己回来查看。 安装Docker 使用Homebrew安装简单方便: brew install --cask docker Homebrew会视你的机器...

Mar 10, 20233 min read
macOS中使用Docker发布一个python项目的小纸条

老房东的纸条箱

39 posts