译自 www.hackingwithswift.com/books/ios-s…
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀
项目中的第一个任务是为我们的图书设计 Core Data 模型,然后创建一个把书添加到数据库的新视图。
首先是模型:打开 Bookworm.xcdatamodeld ,然后添加一个新的实体,取名 “Book” —— 我们将为用户读过的每本书创建一个新的对象。以构成书的要素,我需要添加下面这样属性:
所有这些属性看起来都挺合理,但最后一项 “integer 16” 是什么意思?16 代表什么?Integer 32 和 Integer 64 又是什么? 是这样的,正如Float
和Double
的区别: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
,@Environment
,TextField
,Picker
,sheet()
,等等,加上你刚学的 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 的常规动作:
@State
属性跟踪 sheet 是否应该显示AddBookView
的sheet()
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 请求 (以便我们测试一切工作正常),以及一个跟踪是否应当展示添加图书界面的布尔值。
对于ContentView
的body
,我们将用到一个导航视图,在上面添加一个标题,以及在右上角添加一个按钮,这个按钮是我们触发添加图书 sheet 的地方。
你已经知道我们如何利用@Environment
属性包装器从环境中读取值,这里我们还需要学习如何往环境中写入值。这用到一个叫environment()
的 modifier,它接收两个参数:要写入的 keyPath,要写入的值。对于我们所使用的值,直接传递即可。
把ContentView
的body
属性替换成下面这样:
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 能很快得到解决 —— 可能你看到这篇教程的时候已经解决了!