Hacking with iOS: SwiftUI Edition - Hot Prospects项目(一)

时间:2022-07-25
本文章向大家介绍Hacking with iOS: SwiftUI Edition - Hot Prospects项目(一),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

简介

在此项目中,我们将构建 Hot Prospects,该应用程序可跟踪您在会议上遇到的人。您可能以前曾经看过类似的应用程序:它将显示一个QR码,用于存储您的与会者信息,然后其他人可以扫描该代码,将您添加到他们的潜在客户列表中,以供日后跟进。

这听起来似乎很容易,但是在此过程中,我们将涵盖一堆非常重要的新技术:创建选项卡栏和上下文菜单,使用环境共享自定义数据,发送自定义更改通知等。最终的应用程序很棒,您在学习过程中会学到的内容也特别有用!

与往常一样,在开始实施项目之前,我们有很多技术要介绍,因此请首先使用Single View App模板创建一个新的iOS项目,并将其命名为 HotProspects。

让我们开始吧!

建立我们的 TabBar

此应用将在标签栏中显示四个SwiftUI视图:一个显示您遇到的所有人,一个显示您联系的人,另一个显示您未联系的人,最后一个显示您的个人信息供其他人扫描。

前三个视图是同一概念的变体,但最后一个则有很大不同。结果是,我们可以仅用三个视图来表示我们的所有UI:一个用于显示人员,一个用于显示我们的数据,另一个使用TabView将所有其他视图组合在一起。

因此,我们的第一步将是为我们的标签创建占位符视图,以便稍后返回并填写。按 Cmd + N 新建一个SwiftUI视图,并将其命名为“ ProspectsView”,然后创建另一个名为 “MeView” 的SwiftUI视图。您可以将它们都保留为默认的“ Hello,World!”文字视图;暂时不重要。

目前,重要的是ContentView,因为我们将在其中存储包含UI中所有其他视图的TabView。我们将在此处不久添加更多逻辑,但是现在这将是一个具有三个ProspectView实例和一个MeViewTabView。这些视图中的每个视图都有一个tabItem()修饰符,其中包含从SF Symbols中选取的图像和一些文本。

用以下内容替换当前ContentView的正文:

TabView {
    ProspectsView()
        .tabItem {
            Image(systemName: "person.3")
            Text("Everyone")
        }
    ProspectsView()
        .tabItem {
            Image(systemName: "checkmark.circle")
            Text("Contacted")
        }
    ProspectsView()
        .tabItem {
            Image(systemName: "questionmark.diamond")
            Text("Uncontacted")
        }
    MeView()
        .tabItem {
            Image(systemName: "person.crop.square")
            Text("Me")
        }
}

如果您现在运行该应用程序,您将在屏幕底部看到一个整洁的标签栏,我们可以通过它浏览四个视图。

现在,显然,在实践中创建三个ProspectView实例会很奇怪,因为它们只是相同的,但是我们可以通过自定义每个视图来解决。记住,我们希望第一个显示您遇到的每个人,第二个显示您曾经联系的人,第三个显示您尚未联系的人,我们可以在ProspectsView上用枚举和属性来表示。

因此,现在将这个枚举添加到ProspectsView中:

enum FilterType {
    case none, contacted, uncontacted
}

现在,我们可以使用它,通过为它提供一个新属性,使ProspectsView的每个实例略有不同:

let filter: FilterType

这将立即破坏ContentViewProspectsView_Previews,因为它们在创建ProspectsView时需要为该属性提供一个值,但首先让我们使用它通过给它们一个导航栏标题稍微自定义三个视图。

首先将此计算的属性添加到ProspectsView

var title: String {
    switch filter {
    case .none:
        return "Everyone"
    case .contacted:
        return "Contacted people"
    case .uncontacted:
        return "Uncontacted people"
    }
}

现在替换默认的“ Hello,World!”为如下内容:

NavigationView {
    Text("Hello, World!")
        .navigationBarTitle(title)
}

至少这会使每个ProspectView实例看起来略有不同,因此我们可以确保选项卡工作正常。

为了使我们的代码再次编译,我们需要确保每个ProspectsView初始化程序都使用过滤器进行调用。因此,在FilteredView_Previews中将主体更改为:

ProspectsView(filter: .none)

然后在ContentView中更改三个ProspectsView实例,以便它们分别具有:filter: .none, filter: .contacted, 和 filter: .uncontacted

如果您现在运行该应用程序,将会发现它看起来更好。现在是真正的挑战:前三个视图需要使用相同的数据,那么我们如何才能顺利地共享它们呢?为此,我们需要转向SwiftUI的环境...

使用 @EnvironmentObject 在 TabBar 之间共享数据

SwiftUI的环境使我们能够以一种非常漂亮的方式共享数据:任何视图都可以将对象发送到环境中,然后任何子视图都可以稍后从环境中读取这些对象。更好的是,如果一个视图更改了对象,则所有其他视图都会自动更新——这是在大型应用程序中共享数据的一种非常聪明的方法。

在我们的应用程序中,我们有一个TabView,其中包含三个ProspectView实例,我们希望所有三个实例在同一共享数据上充当不同的视图。这是SwiftUI环境有意义的一个很好的例子:我们可以定义一个存储一个潜在客户的类,然后将这些潜在客户的数组放入环境中,以便我们所有的视图都可以在需要时读取它。

因此,首先制作一个名为 Prospect.swift 的新Swift文件,用SwiftUI替换其Foundation导入,然后为其提供以下代码:

class Prospect: Identifiable, Codable {
    let id = UUID()
    var name = "Anonymous"
    var emailAddress = ""
    var isContacted = false
}

是的,那是一个类而不是结构体。这是有意的,因为它允许我们直接更改类的实例,并同时在所有其他视图中对其进行更新。请记住,SwiftUI会自动将更改传播到我们的视图中,因此不存在视图过时的风险。

在多个视图之间共享时,SwiftUI环境的最佳优势之一是它使用了与@ObservedObject属性包装器相同的ObservableObject协议。这意味着我们可以使用@Published属性包装器标记应发布的属性—— SwiftUI为我们完成了大部分工作。

因此,在Prospect.swift 中添加这个类:

class Prospects: ObservableObject {
    @Published var people: [Prospect]

    init() {
        self.people = []
    }
}

稍后我们将再次讨论,使初始化程序做更多的工作,而不仅仅是创建一个空数组,但对现在来说这已经足够了。

现在,我们希望所有ProspectView实例共享一个Prospects类的实例,因此它们都指向相同的数据。如果我们在这里编写UIKit代码,那么我将详细解释如何做到这一点有多困难,以及我们需要多么谨慎才能确保所有更改都能清晰地传播,但是使用SwiftUI只需三步。

  1. 首先,我们需要向ContentView添加一个属性,该属性创建和存储Prospects类的单个实例:
var prospects = Prospects()
  1. 其次,我们需要将该属性发布到SwiftUI环境中,以便所有子视图都可以访问它。因为 Tabs 被视为TabView的子级,所以它们在其中,因此,如果将其添加到TabView的环境中,则我们所有的ProspectsView实例都将获得该对象。因此,将此修饰符添加到ContentView中的TabView中:
.environmentObject(prospects)
  1. 现在,我们希望ProspectsView的所有实例在创建对象时都会从环境中读取该对象。这使用了一个新的@EnvironmentObject属性包装器,该包装器完成查找对象,将其附加到属性并随时间推移保持最新状态的所有工作。因此,最后一步就是将此属性添加到ProspectsView中:
@EnvironmentObject var prospects: Prospects

这确实就是全部——我认为SwiftUI没有办法可以使这一切变得更容易。

重要:使用@EnvironmentObject时,您明确告诉SwiftUI在创建视图时,对象将在环境中存在。如果不存在,您的应用将立即崩溃——请注意,并将其视为隐式解包。

很快,我们将添加代码以通过扫描二维码来添加潜在客户,但现在,我们将添加一个导航栏项,该项只会添加测试数据并在屏幕上显示。

ProspectsViewbody属性更改为此:

NavigationView {
    Text("People: (prospects.people.count)")
        .navigationBarTitle(title)
        .navigationBarItems(trailing: Button(action: {
            let prospect = Prospect()
            prospect.name = "韦弦zhy"
            prospect.emailAddress = "zhy@weixian.com"
            self.prospects.people.append(prospect)
        }) {
            Image(systemName: "qrcode.viewfinder")
            Text("扫码")
        })
}

现在,您将在标签页视图的前三个视图中看到一个“扫码"”按钮,轻按它会同时在这三个视图中同时添加一个人——无论您轻按哪个按钮,您都将看到计数增量。

动态过滤SwiftUI列表

SwiftUI的List视图喜欢使用符合Identifiable协议的对象数组,或者至少可以提供某种保证唯一的id参数。但是,这不是需要将这些属性存储为视图的属性的理由,实际上,如果我们使用计算属性,则可以根据需要调整过滤条件。

在我们的应用程序中,我们有三个ProspectsView实例,它们仅根据从选项卡视图传递的FilterType属性而有所不同。我们已经使用它来设置每个视图的标题,但是我们也可以使用它来设置列表的内容。

最简单的方法是使用Swiftfilter()方法。这将通过您为闭包提供的测试运行序列中的每个元素,并且从测试返回 true 的所有元素将作为新数组的一部分返回。我们的ProspectsView已经有一个prospecs属性,其中传入了一系列人,因此我们可以返回所有人,所有联系的人或所有未联系的人。

将此属性添加到ProspectsView中:

var filteredProspects: [Prospect] {
    switch filter {
    case .none:
        return prospects.people
    case .contacted:
        return prospects.people.filter { $0.isContacted }
    case .uncontacted:
        return prospects.people.filter { !$0.isContacted }
    }
}

当filter()运行时,它将通过我们的测试传递people数组中的每个元素。因此,0.isContacted表示“当前元素的isContacted属性是否设置为 true ?”数组中所有通过该测试的项目——isContacted设置为 true ——将被添加到新数组中并从filterResults返回。而当我们使用!0.isContacted时,我们得到相反的结果:仅包括尚未联系的潜在客户。

有了计算的属性后,我们现在可以创建一个List来遍历该数组。这将使用VStack同时显示每个潜在客户的标题和电子邮件地址,我们还将使用ForEach,以便稍后添加删除。

将此替换为ProspectsView中的现有文本视图:

List {
    ForEach(filteredProspects) { prospect in
        VStack(alignment: .leading) {
            Text(prospect.name)
                .font(.headline)
            Text(prospect.emailAddress)
                .foregroundColor(.secondary)
        }
    }
}

如果您再次运行该应用程序,将会发现看起来已经好多了。

在继续之前,我想让您考虑一下:既然我们使用的是计算属性,当属性更改时,SwiftUI如何知道刷新视图?答案实际上很简单:它不知道。

当我们向ProspectsView添加@EnvironmentObject属性时,我们还要求SwiftUI每当属性更改时重新调用body属性。因此,每当我们在people数组中插入一个新人员时,它的@Published属性包装器就会向正在监测它的所有视图发布更新,而SwiftUI将重新调用ProspectsViewbody属性。反过来,它将再次计算我们的计算属性,因此列表将更改。

我喜欢SwiftUI在这里为我们透明地承担大量工作的方式,这意味着我们可以专注于过滤和呈现数据的方式,而不是如何连接所有管道以确保事物保持最新状态。

译自 Hot Prospects: Introduction Building our tab bar Sharing data across tabs using @EnvironmentObject Dynamically filtering a SwiftUI List