Hacking with iOS: SwiftUI Edition - 里程碑:项目1 - 3

Hacking with iOS: SwiftUI Edition - 里程碑:项目1 - 3_第1张图片
Milestone

What you learned - 你学到了什么

你现在已经完成了前两个SwiftUI项目,并且也完成了一个技术项目——两个应用程序和一个技术项目的节奏将持续到课程结束,这将帮助你快速提升知识,同时花时间回顾和完善你所学的内容。

虽然我们在SwiftUI中只有三个项目,但您已经学到了一些最重要的概念:视图、修饰符、状态、堆栈布局等等——这些都是您在SwiftUI中一次又一次使用的技能,这就是为什么我想早点把它们拿出来。

当然,您还构建了一些真实的项目,完成了许多编码挑战,以帮助巩固您的学习,所以希望您感受一下您到底掌握了哪些内容。

到目前为止,我们已经涵盖:

  • 构建将文本与Picker等控件混合在一起的滚动表单,SwiftUI将其转变为一个漂亮的基于表单的布局,滑动时新的列表项将会进入屏幕。
  • 创建一个NavigationView并给它一个标题。这不仅允许我们将新的视图推到屏幕上,还允许我们设置标题,避免内容在时钟下出现问题。
  • 如何使用@State来存储不断变化的数据,以及为什么需要它。记住,我们所有的SwiftUI视图都是结构体,这意味着没有@State之类的东西就无法更改它们。
  • 为用户界面控件(如TextField和Picker)创建双向绑定,学习如何使用$variable让我们同时读取和写入值。
  • 使用ForEach在循环中创建视图,这允许我们一次创建许多视图。
  • 使用VStackHStackZStack构建复杂的布局,并将它们组合在一起形成网格。
  • 如何使用颜色和渐变作为视图,包括如何为它们提供特定的Frame,以便您可以控制它们的大小。
  • 如何通过提供一些文本或图片以及在点击时应执行的闭包来创建按钮
  • 通过定义显示警报Alert的条件,然后从其他位置切换该状态来创建警报
  • SwiftUI如何(以及为什么)广泛使用不透明的结果类型(some View),以及为什么这与修饰符顺序如此非常重要密切相关。
  • 如何使用三元运算符创建条件修饰符,根据程序状态应用不同的结果。
  • 如何使用视图组合自定义视图修饰符将代码分解成小部分,从而使我们能够在不丢失代码的情况下构建更复杂的程序。

有一件事我想让你想想,视图 view 在SwiftUI是什么意思。在你开始这门课之前,你可能已经想到了“红色Color.red”不可能是一个视图,但,它是。您还看到了LinearGradient是一个视图,这意味着它很容易在我们的布局中使用。

但是VStack呢?或者Group?或者ForEach?还有其他的那些View呢?

对!这些绝对都是SwiftUI中的视图,正是这些视图使得这个框架非常容易组合——我们可以将ForEach放在VStack中的一个Group中的ForEach中,然后它们都是有效的。

请记住,为了符合View协议,需要做的所有事情就是拥有一个名为body的计算属性,该属性返回some View

以前,我们在Swift中非常仔细地研究了协议、协议扩展和面向协议的编程,您可能想知道为什么这一切如此重要。好吧,现在我希望您可以看到:View协议是SwiftUI的核心——任何东西都可以遵循它,并开始参与布局,只需几行简单的代码。

在其他用户界面框架(包括苹果自己的UIKit)中,类被用于这项工作。这意味着,如果您有一些现有的类型,并且希望将它们用于布局,则需要使它们从UIView继承——这反过来意味着获得200多个您可能不需要的属性和方法,以及大量在幕后使用的其他功能。

在SwiftUI中,这一切都没有发生:我们只是添加了一个View来适应。这就是协议和协议扩展的强大能力,这也是为什么面向协议编程如此重要的原因——如果我们向我们自己的类型中添加一个单独的body属性,那么SwiftUI知道如何使用它来进行布局和渲染。

现在,如果你注意的话,你可能会注意到一个奇怪的地方:当我们制作任何一种SwiftUI视图时,我们需要让它返回some View——我们制作一个视图,它返回一个或多个其他视图。这些视图有自己的body属性,反过来又返回视图,而这些视图也有自己的body属性……好吧,你找到关键点了。

这看起来像是SwiftUI为自己构建了一个无限循环:如果所有视图都是由其他视图组成的,那么它实际上在哪里结束?

很明显,它确实结束了,否则我们的SwiftUI代码都不会真正工作。诀窍在于苹果称之为原始视图(primitive views)的东西,即SwiftUI的绝对裸构建块,它符合View,但返回一些固定内容,而不是呈现其他类型的视图。

有很多这样的构建块,它们不会带来太多的惊喜——例如,文本Text、图片Image、颜色Color和间隔符Spacer等都是一个构建块。最终,我们构建的所有UI都是在这些构建块之上创建的,这就打破了看似无限的循环。

Key points - 要点

有三个要点值得更详细地讨论。这在一定程度上是在回顾我们所学到的东西——用不同的例子重新审视一遍,以确保它们是清楚的——但我也希望借此机会回答一些迄今可能出现的问题。

Structs vs Classes - 结构体 VS 类

首先,有一些你应该记忆犹新的东西:结构体和类。这两种方法都可以让我们使用属性和方法构建复杂的数据类型,但它们的工作方式(更具体地说,它们的不同之处)很重要。

如果您还记得,结构体和类之间有五个关键区别:

1.结构体默认有一个自动实现的成员初始化器类没有需要自己实现。
2.类可以使用继承来构建功能;结构不能。
3.如果复制一个类,两个副本都指向同一个数据;结构体的副本总是唯一的。
4.类可以有反初始化器;结构体没有。
5.可以更改常量类中的变量属性;无论属性是常量还是变量,常量结构体中的属性都是固定的。

在苹果最初的编程语言Objective-C中,我们几乎什么都用类——我们别无选择,因为它确实融入了我们的工作方式。

在Swift,我们确实有另一个选择,这个选择应该基于上述因素。我之所以说“应该”,是因为经常会看到一些人不关心这些差异,所以总是使用类或结构体,而不考虑这种选择的后果。

选择结构体还是类取决于您和您正在解决的特定问题。不过,我想让你考虑一下它是如何传达你的意图的。Donald Knuth说“程序应该是给人读的,只是顺带让计算机执行(programs are meant to be read by humans and only incidentally for computers to execute)”,这真的触及了我所说的核心:当有人读到你的代码时,他们对你的意图清楚吗?

如果您大多数时候使用结构体,那么在一个特定情况使用类就会传达一些意图:这个东西是不同的,需要不同的使用方式。如果你总是使用类,这种区别就会消失——毕竟,你不太可能经常需要它们。

提示:SwiftUI的一个迷人的细节是它如何完全颠倒我们使用结构体和类的方式。在UIKit中,我们会使用struct来表示数据,使用class来表示UI,但在SwiftUI中,情况完全相反,这很好地提醒了我们学习东西的重要性,即使你认为有些东西不是立马有用的。

使用ForEach

我想讨论的第二件事是ForEach,我们已经将它用于如下代码:

ForEach(0 ..< 100) { number in
    Text("Row \(number)")
}

ForEach是一个视图,就像SwiftUI中的大多数其他东西一样,但它允许我们在循环中创建其他视图。在这样做的同时,它还允许我们绕过SwiftUI规定的10个子视图的限制——ForEach本身成为了10个子视图中的一个,而不是它里面的视图。

现在考虑这样一个字符串数组:

let agents = ["Cyril", "Lana", "Pam", "Sterling"]

我们如何循环这些并生成文本视图?

一种选择是使用我们已经使用过的结构:

VStack {
    ForEach(0 ..< agents.count) {
        Text(self.agents[$0])
    }
}

但是SwiftUI提供了另一种选择:我们可以直接在数组上循环。这需要更多的思考,因为SwiftUI想知道如何识别数组中的每个项。

考虑一下:如果我们在一个包含四项的数组上循环,我们将创建四个视图,但是如果body被重新调用,并且我们的数组现在包含五项,那么SwiftUI需要知道哪个视图是新的,这样它就可以在UI中显示。SwiftIUI最不想做的就是扔掉它的整个布局,每次做一个小小的改变就从头开始。相反,它希望尽可能减少工作量:它希望保留现有的四个视图,只添加第五个视图。

所以,我们回到Swift如何识别数组中的值。当我们使用0..<50..这样的范围时,Swift确信每个项目都是唯一的,因为它将使用范围中的数字——每个数字在循环中只使用一次,所以它肯定是唯一的。

在我们的字符串数组中,这是不可能的,但是我们可以清楚地看到每个值都是唯一的:在[“Cyril”,“Lana”,“Pam”,“Sterling”]中的值不会重复。所以,我们可以告诉SwiftUI字符串本身——“Cyril”、“Lana”等可以用来唯一地标识循环中的每个视图。

我们可以在代码里面这样写:

VStack {
    ForEach(agents, id: \.self) {
        Text($0)
    }
}

因此,我们现在直接读取数组中的项,而不是在整数上循环并使用它读入数组,就像for循环一样。

随着您使用SwiftUI的进展,我们将研究识别视图的第三种方法——使用Identifiable协议。

使用绑定

当我们使用PickerTextField等控件时,我们使用$propertyName为它们创建到某种@State属性的双向绑定。这对简单的属性非常有用,但是有时候——只是有时候,希望如此!——您可能需要更高级的东西:如果您想运行一些逻辑来计算当前值,该怎么办?或者,如果你不只是想在写的时候隐藏一个值呢?

如果我们想对绑定中的更改做出反应,我们可能会尝试利用Swift的didSet属性观察,但您会失望的。这就是自定义绑定的来源:它们可以像@State绑定一样使用,但我们无法完全控制它们的工作方式。

SwiftUI为我们做的每件事都可以手工完成,尽管依赖自动解决方案几乎总是更好的,但在窥视一下幕后确实会有帮助,这样你就能了解它在为你做什么。

首先,让我们看看最简单的自定义绑定形式,它将值存储在另一个@State属性中,并将其返回:

struct ContentView: View {
    @State var selection = 0

    var body: some View {
        let binding = Binding(
            get: { self.selection },
            set: { self.selection = $0 }
        )

        return VStack {
            Picker("Select a number", selection: binding) {
                ForEach(0 ..< 3) {
                    Text("Item \($0)")
                }
            }.pickerStyle(SegmentedPickerStyle())
        }
    }
}

因此,该绑定实际上只是作为一个传递——它本身不存储或计算任何数据,而是作为UI和被操纵的底层状态值之间的一个填充。

但是,请注意,选择器现在是使用selection:binding进行的,不需要美元符号。我们不需要在这里明确要求双向绑定,因为它已经是了。

如果我们愿意的话,我们可以创建一个更高级的绑定,它不仅仅传递一个值。例如,假设我们有一个带有三个切换开关的表单:用户是否同意条款和条件,同意隐私策略,并同意接收有关发货的电子邮件。

我们可以将其表示为三个布尔型的@State属性:

@State var agreedToTerms = false
@State var agreedToPrivacyPolicy = false
@State var agreedToEmails = false

尽管用户可以手动切换它们,但我们可以使用自定义绑定一次完成所有操作。如果这三个布尔值都为真,则此绑定将为真,但如果更改了绑定,则它将更新所有布尔值,如下所示:

let agreedToAll = Binding(
    get: {
        self.agreedToTerms && self.agreedToPrivacyPolicy && self.agreedToEmails
    },
    set: {
        self.agreedToTerms = $0
        self.agreedToPrivacyPolicy = $0
        self.agreedToEmails = $0
    }
)

所以现在我们可以创建四个切换开关:这三个选择都使用布尔值表示同意或不同意,一个同时同意或不同意所有三个的控制开关:

struct ContentView: View {
    @State var agreedToTerms = false
    @State var agreedToPrivacyPolicy = false
    @State var agreedToEmails = false

    var body: some View {
        let agreedToAll = Binding(
            get: {
                self.agreedToTerms && self.agreedToPrivacyPolicy && self.agreedToEmails
            },
            set: {
                self.agreedToTerms = $0
                self.agreedToPrivacyPolicy = $0
                self.agreedToEmails = $0
            }
        )

        return VStack {
            Toggle(isOn: $agreedToTerms) {
                Text("Agree to terms")
            }

            Toggle(isOn: $agreedToPrivacyPolicy) {
                Text("Agree to privacy policy")
            }

            Toggle(isOn: $agreedToEmails) {
                Text("Agree to receive shipping emails")
            }

            Toggle(isOn: agreedToAll) {
                Text("Agree to all")
            }
        }
    }
}

再说一遍,定制绑定并不是你经常想要的,但是花点时间看看幕后,了解发生了什么是非常重要的。尽管它非常聪明,但它只是一个工具,而不是魔法!

Challenge - 挑战

你对数组、状态、视图、图片、文本等有基本的了解,所以让我们把它们放在一起:你的挑战是制作一个大脑训练游戏:剪刀石头布。

那么,粗略地说一下:

  • 游戏的每个回合,应用程序都会随机选择剪刀、石头、或布。
  • 应用程序每回合都会提示玩家的胜负。
  • 然后玩家必须点击正确的动作来赢得或输掉游戏。
  • 如果他们是对的,他们就得一分;否则他们就失去一分。
  • 10个回合后比赛结束,显示他们的最终得分。

因此,如果应用程序选择“ 锤子”和“赢”,玩家需要选择“布”,但如果应用程序选择“锤子”和“输”,则玩家需要选择“剪刀”。

要解决此难题,您需要利用在教程1和教程2中学习到的技能:

  • 1、从单一视图应用程序模板开始,然后创建一个属性来存储三个可能的动作:[“剪刀”,“石头”,“布”]
  • 2、您需要创建两个@State属性来存储应用程序的当前选择,以及玩家应该赢还是输。
  • 3、可以使用Int.random(in:)选择随机动作。如果你想的话,你可以用它来决定玩家是否也应该赢,但是还有一个更简单的选择:Bool.random()是随机的true还是false。
  • 4、创建一个VStack,显示玩家的分数、应用程序的动作以及玩家应该赢还是输。您可以使用if shouldWin返回两个不同文本视图中的一个。
  • 5、最重要的部分是制作三个按钮来响应玩家的动作:剪刀、石头、或布。

这应该是一个有趣的练习:有一点Swift,有一点SwiftUI,有一点逻辑,所有的一切都在一个游戏中结束,你真的可以去做你想要做的事情。

译自 Hacking with iOS: SwiftUI Edition - 里程碑:项目1 - 3
Milestone: Projects 1-3 What you learned
Milestone: Projects 1-3 Key points
Milestone: Projects 1-3 Challenge

Previous: 视图和修饰符项目——挑战 Hacking with iOS: SwiftUI Edition Next: 使用 Stepper 输入数字

赏我一个赞吧~~~

你可能感兴趣的:(Hacking with iOS: SwiftUI Edition - 里程碑:项目1 - 3)