-
总结笔记
在 UIKit 开发时,我们经常会接触一些像是 viewDidLoad,viewWillAppear 这样的生命周期的方法,并在里面进行一些配置。SwiftUI 里也有一部分这类生命周期的方法,比如 .onAppear 和 .onDisappear,它们也被“统一”在了 modifier 这面大旗下。
但是相对于 UIKit 来说,SwiftUI 中能 hook 的生命周期方法比较少,而且相对要通用一些。本身在生命周期中做操作这种方式就和声明式的编程理念有些相悖,看上去就像是加上了一些命令式的 hack。我个人比较期待 View 和 Combine能再深度结合一些,把像是 self.draftProfile = self.profile 这类依赖生命周期的操作也用绑定的方式搞定。
相比于 .onAppear 和 .onDisappear,更通用的事件响应 hook 是 .onReceive(_:perform:),它定义了一个可以响应目标 Publisher 的任意的 View,一旦订阅的 Publisher 发出新的事件时,onReceive 就将被调用。因为我们可以自行定义这些 publisher,所以它是完备的,这在把现有的 UIKit View 转换到 SwiftUI View 时会十分有用。
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
NotificationCenter.default.post(name: .countdownTimer, object: nil)
}
.onReceive(NotificationCenter.default.publisher(for: .countdownTimer)) { output in
}
结构体 vs 类Swift 里有两样东西对你来说一定不陌生:结构体和类。它们都是可以让我们构建拥有属性和方法的复杂数据类型的方法,但它们的工作方式,尤其是两者间的差异,是一个 (Swift 语言设计中) 很要紧的存在。如果你还记得的话,结构体和类之间有五个关键的差异:类没有逐一成员构造函数;结构体默认获得逐一构造成函数。类可以使用继承来构建功能;结构不能。如果你复制一个类,两份拷贝都会指向相同的数据;但结构体的拷贝,其数据是各自独立的。类可以有析构函数;结构体没有。你可以在常量类实例里改变变量属性的值;但常量结构体实例里的属性是固定的,不管它是常量还是变量。在 Apple 原来的编程语言 Objective-C 中,我们几乎为所有的事物使用类 —— 因为没的选,几乎所有东西都基于类来运作。Swift 中情况不一样,我们拥有选择权,选择应该基于上面提到的那些差异中的因素。之所以说”应该“,是因为即便你看到有人不关心两者的差异,总是只用 class 或者 struct, 也并不稀奇。选择结构体或者类取决于你和你要解决的问题。不过我希望你思考它们是如何传递你的意图的。 Donald Knuth 说过,“程序是给人读的,偶尔给计算机运行”,这句话正合我意:当别人阅读你的代码时,你的意图是否能清晰地传达给他们?如果你大部分时候用的是结构体,然后在一个特定的地方换成了类,这就传达了一个意图: 这个东西不一样,需要不同的用法。但如果你总是使用类,这种区别就消失了 —— 毕竟,不太可能你在大多数情况下都得使用类。提示: SwiftUI 有一个迷人的细节是它扭转了我们使用结构体和类的方式。在 UIKit 中我们针对数据使用结构体,针对 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 需要知道如何识别数组中的每一项。思考一下:如果我们遍历一个 4 个元素的数组,我们会创建 4 个视图,但是如果 body 重新调用,我们的数组现在包含 5 个元素了,SwiftUI 需要知道哪个视图是新的以便展示它。 SwiftUI 最不愿意做的事情:每当一个小改变发生时,丢弃整个布局,从头开始。相反,它希望做尽可能少的工作 —— 它希望保持已经存在的 4 个视图,只添加第 5 个。因此,让我们回到 Swift 识别数组中元素的地方。当我们用诸如 0 ..< 5 或者 0 ..< agents.count 这样的范围时,Swift 已经确信每个元素都是唯一的,因为每个元素在循环中都只使用一次,所以一定是唯一的。而在我们的字符串数组中,这一点变得不可能。我们无法清晰地确信每个值是唯一的: 它要求 ["Cyril", "Lana", "Pam", "Sterling"] 不重复。因此,我们能做的是把字符串本身告诉 SwiftUI —— “Cyril” ,“Lana” ,等等 —— 它们是用来在循环中唯一标识每个视图的东西。 代码是这样的:
VStack {
ForEach(agents, id: .self) {
Text($0)
}
}
相比遍历数字,然后再作为下标访问数组,我们现在是直接读取数组,就像 for 循环那样做。随着你对 SwiftUI 的精进,我们会看到第三种标识视图的方式,它是用 Identifiable协议,不过我们之后再来讨论。绑定当我们使用像 Picker 和 TextField 这样的控件时,我们通过属性前注解 @State 为它们添加双向绑定。对于简单属性来说,这种用法很奏效。不过有的时候 —— 希望只是偶尔 —— 你可能需要更高级的东西:假如你的当前数值需要通过运行代码逻辑来计算呢?或者当数值被写入时,你想的事情不局限于存储呢?对于绑定中的改变,如果我们需要做出反应,我们可能需要借助 Swift 的didSet 属性观察者。不过你可能要失望了。自定义绑定的工作方式是这样的:自定义绑定跟 @State绑定一模一样,除了我们拥有完全掌控而已。绑定并非魔法, @State 只是帮我们拿掉了一些无聊的样本代码,如果你愿意,自行创建和管理绑定代码完全没问题。重申一下,演示下面的写法并非它常见,实际上它并不常见,我希望你也别这么写。相反,我演示下面这样的写法是为了消除你可能认为 SwiftUI 在你的代码上施了什么魔法的念头。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 和下面的状态值之间的一个 ”夹片“ 。不过,注意一下,现在 picker 是通过 selection: binding 创建,不再需要 $ 符号了。 我们并不需要显式要求双向绑定,因为它本身已经是了。如果我们愿意,还可以创建更高级的绑定,不仅仅是透传一个数值。举个例子,想象我们有一个表单,里面有三个开关:用户同意条款,用户同意隐式政策,用户同意接收邮件。我们可能用三个布尔型的 @State 属性表示它们:@State var agreedToTerms = false @State var agreedToPrivacyPolicy = false @State var agreedToEmails = false虽然用户是逐个触发它们的,我们可以用一个自定义绑定来实现它们。这个绑定只有在三个布尔值都为 true 时才为 true ,像这样:
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<Bool>(
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")
}
}
}
}
-
fixedSize() TextField自动适应
TextField("1", text: $count, onEditingChanged: { (changed) in
print("onEditing: (changed)")
}, onCommit: {
})
.keyboardType(.numberPad)
.fixedSize()
- 渐变背景
.background(LinearGradient(gradient: Gradient(
colors: [Color(UIColor.colorWithHexStr("#1ED273")), Color(UIColor.colorWithHexStr("#11BC55"))]),
startPoint: UnitPoint(x: 0.02, y: 0.5),
endPoint: UnitPoint(x: 1, y: 0.5)))