Hacking with iOS: SwiftUI Edition - 书虫项目(二)

Hacking with iOS: SwiftUI Edition - 书虫项目(二)_第1张图片
book

显示书籍详细内容

当用户在ContentView中点击一本书时,我们将显示一个详细视图,其中包含更多信息:书籍的类型,简短的评论等等。我们还将重用新的RatingView,甚至对其进行自定义,以便您可以看到SwiftUI的灵活性。

为了使该屏幕更有趣,我们将添加一些代表我们应用中每个类别的插图。我已经从 Unsplash 中挑选了一些艺术品,并将其放入这本书的 project11-files 文件夹中——如果您尚未下载它们,请立即下载,然后将其拖到资产目录中。

Unsplash 拥有许可,使我们可以在有或没有署名的情况下以商业或非商业方式使用图片,尽管对署名表示赞赏。我添加的照片是Ryan Wallace,Eugene Triguba,Jamie Street,Alvaro Serrano,Joao Silas,David Dilbert和Casey Horner的照片-如果需要,您可以从 https://unsplash.com 获取原始照片。

接下来,创建一个新的SwiftUI视图,称为“DetailView”。这仅需要一个属性,即它应该显示的书,因此请立即添加:

let book: Book

即使仅具有该属性也足以破坏 DetailView.swift 底部的预览代码。以前,这很容易解决,因为我们只是发送了一个示例对象,但是涉及到 Core Data,事情就变得更加混乱:创建一本新书还意味着要在内部创建一个托管对象上下文。

为了解决这个问题,我们可以更新预览代码以创建一个临时的受管对象上下文,然后使用它来创建我们的书。完成后,我们可以传递一些示例数据以使预览看起来不错,然后使用测试书来创建详细视图预览。

创建托管对象上下文意味着我们需要从导入Core Data开始。将此行添加到 DetailView.swift 顶部现有导入附近:

import CoreData

至于预览代码本身,请使用以下命令替换现有的代码:

struct DetailView_Previews: PreviewProvider {
    static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)

    static var previews: some View {
        let book = Book(context: moc)
        book.title = "Test book"
        book.author = "Test author"
        book.genre = "Fantasy"
        book.rating = 4
        book.review = "This was a great book; I really enjoyed it."

        return NavigationView {
            DetailView(book: book)
        }
    }
}

如您所见,创建托管对象上下文涉及告诉系统我们要使用哪种并发类型。这是另一种说法:“您打算使用哪个线程访问数据?”在我们的示例中,使用主队列(即应用启动时使用的队列)就非常好。

完成后,我们可以将注意力转移到更有趣的问题上,即设计视图本身。首先,我们将类别图像和类型放置在 ZStack中,这样我们就可以很好地将它们放在另一个之上。这又意味着要进入GeometryReader,因此我们可以确保图像不会占用太多空间。我挑选了一些看起来不错的样式,但是欢迎您尝试所有想要的样式。

以此替换当前的body属性:

GeometryReader { geometry in
    VStack {
        ZStack(alignment: .bottomTrailing) {
            Image(self.book.genre ?? "Fantasy")
                .frame(maxWidth: geometry.size.width)

            Text(self.book.genre?.uppercased() ?? "FANTASY")
                .font(.caption)
                .fontWeight(.black)
                .padding(8)
                .foregroundColor(.white)
                .background(Color.black.opacity(0.75))
                .clipShape(Capsule())
                .offset(x: -5, y: -5)
        }
    }
}
.navigationBarTitle(Text(book.title ?? "Unknown Book"), displayMode: .inline)

这会将流派名称放置在ZStack的右下角,并带有背景色,粗体和一些填充以使其突出。

在该堆栈下方,我们将添加作者,评论和评分,以及一个空格,以便将所有内容推送到视图顶部。我们不希望用户能够在此处调整评分,因此我们可以使用另一个常量绑定将其转换为简单的只读视图。更好的是,因为我们使用SF Symbols创建了评分图像,所以我们可以无缝地使用简单的font()修饰符将其放大,以更好地利用我们拥有的所有空间。

因此,将这些视图直接添加到以前的ZStack下:

Text(self.book.author ?? "Unknown author")
    .font(.title)
    .foregroundColor(.secondary)

Text(self.book.review ?? "No review")
    .padding()

RatingView(rating: .constant(Int(self.book.rating)))
    .font(.largeTitle)

Spacer()

这样就完成了DetailView,因此我们可以回到 ContentView.swift 来更改导航链接,使其指向正确的内容:

NavigationLink(destination: DetailView(book: book)) {

现在,再次运行该应用程序,因为您应该可以点击输入的任何图书,以在我们的新详细信息视图中显示它们。

PS:我增加了一个混合模式,开始变得好玩起来

Hacking with iOS: SwiftUI Edition - 书虫项目(二)_第2张图片
我增加了一个 .blendMode(.difference) 的效果

使用 NSSortDescriptor 对请求的数据进行排序

当您使用SwiftUI的@FetchRequest属性包装器将对象从 Core Data 中拉出时,您可以指定要如何对数据进行排序——应该按字段之一的字母顺序排列吗?还是数字上数字最高的?我们指定了一个空数组,该数组可能对少数项目有效,但在20个左右后只会使用户烦恼。

在此项目中,我们有多个字段可能对排序有用:书的标题,作者或等级都是明智的,并且是不错的选择,但我怀疑标题可能是最常见的,因此让我们使用它。

提取请求排序是使用称为NSSortDescriptor的新类执行的,我们可以从两个值创建它们:我们要对其进行排序的属性,以及是否应升序。例如,我们可以按字母顺序对title属性进行排序,如下所示:

@FetchRequest(entity: Book.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Book.title, ascending: true)]) var books: FetchedResults

您可以指定多个排序描述符,它们将按照您提供的顺序应用。例如,如果用户添加了Pete Hamill的书“Forever”,然后添加了Judy Blume的“Forever”(一本完全不同的书,恰好具有相同的标题),则指定第二个排序字段将很有帮助。

因此,我们可能要求将书名排在首位,然后是书作者排在第二位,如下所示:

@FetchRequest(entity: Book.entity(), sortDescriptors: [
    NSSortDescriptor(keyPath: \Book.title, ascending: true),
    NSSortDescriptor(keyPath: \Book.author, ascending: true)
]) var books: FetchedResults

除非您有大量具有相似值的数据,否则拥有第二个甚至第三个排序字段对性能几乎没有影响。例如,使用我们的图书数据,几乎每本书都会有一个唯一的书名,因此就性能而言,拥有次要排序字段或多或少是无关紧要的。

从 Core Data 中删除数据

我们已经使用@FetchRequest将核心数据对象放置到SwiftUI列表中,并且只需做一点点的工作,就可以启用滑动删除和专用的“Edit/Done”按钮。

就像常规数据数组一样,大多数工作都是通过在ForEach上附加onDelete(perform:)修饰符来完成的,而不仅仅是从数组中删除项目,我们需要在获取请求中找到请求的对象然后使用它在我们的托管对象上下文上调用delete()。一旦所有对象被删除,我们就可以触发上下文的另一次保存。没有这些更改,实际上不会写到磁盘上。

因此,首先将此方法添加到ContentView中:

func deleteBooks(at offsets: IndexSet) {
    for offset in offsets {
        // 在我们的获取请求中找到这本书
        let book = books[offset]

        // 从上下文中删除它
        moc.delete(book)
    }

    // 保存上下文
    try? moc.save()
}

PS:这个删除代码可能会让你有点困惑,此处参数IndexPath是一个只包含一个范围空间range的索引,比如删除第一行,此时的offsets为:

(lldb) po offsets
▿ 1 indexes
  ▿ ranges : 1 element
    ▿ 0 : Range(0..<1)
      - lowerBound : 0
      - upperBound : 1

所以我们取得的offset值为: 0,当我们使用数组的remove(at:)时,参数就是IndexPath,数组内部能够解析出具体的值,而此处我们需要获取确定的值,所以使用这种方法取出来。

我们可以通过在ContentViewForEach上添加onDelete(perform:)修饰符来触发该操作,但是请记住:它需要放在ForEach而不是List上。

立即添加此修饰符:

.onDelete(perform: deleteBooks)

这使我们滑动即可删除,并且可以通过添加“Edit/Done”按钮来做得更好。在内容视图中找到navigationBarItems()修改器,并将其第一行更改为:

.navigationBarItems(leading: EditButton(), trailing: Button(action: {

这样就完成了ContentView,因此请尝试运行该应用程序——您现在应该可以自由添加和删除图书,并且可以通过滑动删除或使用编辑按钮进行删除。

以编程方式 pop NavigationLink

您已经了解了NavigationLink如何让我们推送到详细信息屏幕,该屏幕可能是自定义视图,也可能是SwiftUI的内置类型之一,例如“文本”或“图像”。由于我们位于NavigationView中,因此iOS会自动提供“返回”按钮,以使用户返回上一屏幕,并且他们还可以从左侧边缘滑动以返回。但是,有时以编程方式返回是很有用的,即,在我们需要时(而不是在用户滑动时)返回到上一个屏幕。

为了说明这一点,我们将向我们的应用程序添加最后一个功能,该功能会删除用户当前正在看的书。为此,我们需要显示 alert,询问用户是否真的要删除该书,然后如果需要的话,从当前的受管对象上下文中删除该书。完成后,由于其关联的书已不存在,因此没有任何内容停留在当前屏幕上,因此我们将弹出当前视图——从NavigationView堆栈的顶部删除它,因此我们返回到前一个屏幕。

首先,我们在DetailView结构体中需要三个新属性:一个用于保存我们的Core Data受管对象上下文(以便我们可以删除内容),一个用于保存我们的演示模式(以便我们可以将视图从导航堆栈中弹出),以及一个控制我们是否显示删除确认警报。

因此,首先将这三个新属性添加到DetailView

@Environment(\.managedObjectContext) var moc
@Environment(\.presentationMode) var presentationMode
@State private var showingDeleteAlert = false

第二步是编写一种方法,该方法从我们的托管对象上下文中删除当前书籍,并关闭当前视图。使用 NavigationLink 而不是工作表显示此视图没关系——我们仍然使用相同的presentationMode.wrappedValue.dismiss()代码。

现在将此方法添加到DetailView中:

func deleteBook() {
    moc.delete(book)

    // 如果要永久删除,请取消注释此行
    // try? self.moc.save()
    presentationMode.wrappedValue.dismiss()
}

第三步是添加一个alert()修饰符,该修饰符监视showingDeleteAlert以及其中的Alert视图,要求用户确认操作。到目前为止,我们一直在使用带有“关闭”按钮的简单警报,但是这里我们需要两个按钮:一个按钮用于删除图书,另一个按钮用于取消。

SwiftUI为此提供了两种特定的按钮类型:.destructive具有标题和操作结束符,并以红色显示,以警告用户它将破坏数据,而.cancel()只会使警报消失。苹果公司提供了关于如何标记警报文本的非常明确的指导,但是归结为:如果是简单的“我理解”接受,那么“确定”是好的,但是如果您希望用户做出选择,则应该避免标题例如“是”和“否”,而是使用动词,例如“忽略”,“回复”和“确认”。

在这种情况下,我们将对破坏性按钮使用“删除”,然后在其旁边提供一个.cancel()按钮,以便用户可以根据需要退出删除操作。因此,将此修饰符添加到DetailView中的GeometryReader中:

.alert(isPresented: $showingDeleteAlert) {
    Alert(title: Text("Delete book"), message: Text("Are you sure?"), primaryButton: .destructive(Text("Delete")) {
            self.deleteBook()
        }, secondaryButton: .cancel()
    )
}

最后一步是添加一个导航栏项目以启动删除过程——这只需要翻转showingDeleteAlert布尔值,因为我们的alert()修饰符已经在监视它了。因此,将这最后一个修改器添加到DetailView中的GeometryReader中:

.navigationBarItems(trailing: Button(action: {
    self.showingDeleteAlert = true
}) {
    Image(systemName: "trash")
})

您现在可以使用滑动删除或编辑按钮删除ContentView中的书籍,或导航到DetailView然后点击其中的专用删除按钮——它应删除书籍,更新ContentView中的列表,然后自动关闭细节视图。

这是另一款完整的应用程序——Good job!

译自:
Showing book details
Sorting fetch requests with NSSortDescriptor
Deleting from a Core Data fetch request
Using an alert to pop a NavigationLink programmatically

书虫项目(一) Hacking with iOS: SwiftUI Edition 书虫项目——挑战

赏我一个赞吧~~~

你可能感兴趣的:(Hacking with iOS: SwiftUI Edition - 书虫项目(二))