[SwiftUI 100 天] Bookworm-part4 用 Core Data 创建图书

641 阅读4分钟
译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀

用 Core Data 创建图书

项目中的第一个任务是为我们的图书设计 Core Data 模型,然后创建一个把书添加到数据库的新视图。

首先是模型:打开 Bookworm.xcdatamodeld ,然后添加一个新的实体,取名 “Book” —— 我们将为用户读过的每本书创建一个新的对象。以构成书的要素,我需要添加下面这样属性:

  • id, UUID —— 一个可以确保唯一的用于区分不同图书的标识符
  • title, String —— 书名
  • author, String —— 书的作者
  • genre, String —— 流派
  • review, String —— 用户对于书的评价
  • rating, Integer 16 —— 用户给这本书的打分

所有这些属性看起来都挺合理,但最后一项 “integer 16” 是什么意思?16 代表什么?Integer 32 和 Integer 64 又是什么? 是这样的,正如FloatDouble的区别:Integer 16 使用 16 个二进制位 (“bits”) 来存储数字,所以它可以保存从 -32,768 到 32,767 的数字,而 Integer 32 使用 32 位来存储数字,所以能保存从 -2,147,483,648 到 2,147,483,647 的数字。至于 Integer 64… 那是相当大的数。

要点在于这些类型之间不是可互换的:你不能接收一个 64 位的数字,尝试存放在 16 位的数字中,这样会丢失精度。另一方面,用 64 位整数来存放一个我们已知很小的数字也是很浪费的。因此,Core Data 提供了不同的选项,让我们选取想要使用的存储空间。

下一步是写一个用来创建新实体的表单。这个过程结合了许多你已经学习过的技能:Form@State@EnvironmentTextFieldPickersheet(),等等,加上你刚学的 Core Data 知识。

我们创建一个新的叫 “AddBookView” 的 SwiftUI 视图。对于它的属性,我们需要用到一个环境属性,存储 managed object context:

@Environment(\.managedObjectContext) var moc

为了构成一本书,除了 id 之外,我们需要为书的每个字段使用@State属性,而 id 我们可以自动生成:

@State private var title = ""
@State private var author = ""
@State private var rating = 3
@State private var genre = ""
@State private var review = ""

最后,我们还需要一个额外的属性存储所有可能的流派,以便结合ForEach实现一个选择器。把下面这个属性添加到AddBookView

let genres = ["Fantasy", "Horror", "Kids", "Mystery", "Poetry", "Romance", "Thriller"]

对于表单的实现暂且到这里 —— 我们稍后还会改进它,不过目前为止已经够用了。把body替换成下面这样:

NavigationView {
    Form {
        Section {
            TextField("Name of book", text: $title)
            TextField("Author's name", text: $author)

            Picker("Genre", selection: $genre) {
                ForEach(genres, id: \.self) {
                    Text($0)
                }
            }
        }

        Section {
            Picker("Rating", selection: $rating) {
                ForEach(0..<6) {
                    Text("\($0)")
                }
            }

            TextField("Write a review", text: $review)
        }

        Section {
            Button("Save") {
                // add the book
            }
        }
    }
    .navigationBarTitle("Add Book")
}

对于按钮的 action,我们要实现创建一个Book类实现的动作,用到我们的 managed object context,从表单中拷贝所有的字段(包括把rating转换成Int16以适应 Core Data),然后保存 managed object context。

大部分操作只是简单的拷贝,稍微有点模糊的地方在于我们如何把一个Int的 rating 转换成Int16。很容易猜到:Int16(someInt)可以实现我们的需求。

用下面的代码替换// 添加图书注释:

let newBook = Book(context: self.moc)
newBook.title = self.title
newBook.author = self.author
newBook.rating = Int16(self.rating)
newBook.genre = self.genre
newBook.review = self.review

try? self.moc.save()

这样我们就完成了表单,但我们还需要一个在图书被添加后显示和隐藏它的方法。

显示新增的 AddBookView 主要涉及回到 ContentView.swift ,实现下面几个针对 sheet 的常规动作:

  1. 添加一个 @State 属性跟踪 sheet 是否应该显示
  2. 添加某个按钮 —— 可以是一个导航栏菜单 —— 以便触发该属性。
  3. 一个用来显示AddBookViewsheet()modifier。

不过,这里还有一个额外的任务,跟 SwiftUI 的 environment 工作机制相关。你看,当我们把一个对象放进视图的环境中时,它对于视图是可访问的,并且所有以这个视图为祖先的视图也可以访问这个对象。具体来说,如果我们的视图 A 包含视图 B,那么视图 A 环境里的任何东西也将处于视图 B 的环境中。

现在,让我们来思考一下 sheet —— 这些 iOS 上以全屏方式呈现的弹出式窗口。是的,某一个屏可以触发它们显示,但这是否意味着被呈现的 sheet 可以称这些触发它们的视图为祖先呢?SwiftUI 的答案是否定的。因此,如果我们以 sheet 的方式呈现一个新视图,我们需要显式地传递 managed object context。对于在ContentView以 sheet 形式呈现的AddBookView,我们需要添加一个 managed object context 属性到ContentView,以便可以传递给AddBookView

把下面三个属性添加到ContentView

@Environment(\.managedObjectContext) var moc
@FetchRequest(entity: Book.entity(), sortDescriptors: []) var books: FetchedResults<Book>

@State private var showingAddScreen = false

这样我们就得到了一个可以传给AddBookView的 managed object context,一个读取所有图书的 fetch 请求 (以便我们测试一切工作正常),以及一个跟踪是否应当展示添加图书界面的布尔值。

对于ContentViewbody,我们将用到一个导航视图,在上面添加一个标题,以及在右上角添加一个按钮,这个按钮是我们触发添加图书 sheet 的地方。

你已经知道我们如何利用@Environment属性包装器从环境中读取值,这里我们还需要学习如何往环境中写入值。这用到一个叫environment()的 modifier,它接收两个参数:要写入的 keyPath,要写入的值。对于我们所使用的值,直接传递即可。

ContentViewbody属性替换成下面这样:

 NavigationView {
    Text("Count: \(books.count)")
        .navigationBarTitle("Bookworm")
        .navigationBarItems(trailing: Button(action: {
            self.showingAddScreen.toggle()
        }) {
            Image(systemName: "plus")
        })
        .sheet(isPresented: $showingAddScreen) {
            AddBookView().environment(\.managedObjectContext, self.moc)
        }
}

最后一步是确保用户完成添加之后关闭表单。

我们需要用到另一个环境属性,跟踪当前的 presentation mode:

@Environment(\.presentationMode) var presentationMode

然后在按钮的的 action 闭包最后调用dismiss,像这样:

self.presentationMode.wrappedValue.dismiss()

现在,你可以运行应用,尝试添加一个新书,当AddBookView消失时,你应该会发现计数标签更新为 1 。

提示:取决于你的 Xcode 版本,有两个 SwiftUI 的小毛病可能会影响到你。第一个是你可能会发现 + 按钮难以点击,你需要点的非常准确。这是因为 UIKit 扩展了点击热区以方便交互,而 SwiftUI 没有。第二点是你可能发现点击按钮只响应一次。这肯定是一个 bug,因为我们如果用文本视图的onTapGesture()来触发布尔值,一切工作正常 —— 只有导航栏上的按钮才会这样。希望这两个 bug 能很快得到解决 —— 可能你看到这篇教程的时候已经解决了!


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~