- 目的
-
-
观察并响应多个 UI 元素发送的值,并将更新的值联合起来以更新界面。
-
- 参考
-
-
带有此代码的 ViewController 在 github 项目中,位于 UIKit-Combine/FormViewController.swift
-
发布者: @Published,
-
操作符: combineLatest, map, receive
-
订阅者: assign
-
- 另请参阅
- 代码和解释
-
此示例故意模仿许多 Web 表单样式的验证场景,不过是在 UIKit 中使用 Combine。
ViewController 被配置了多个通过声明式更新的元素。 同时持有了 3 个主要的文本输入字段:
-
value1
-
value2
-
value2_repeat
它还有一个按钮来提交合并的值,以及两个 labels 来提供反馈。
这些字段的更新规则被实现为:
-
value1
中的条目至少有 3 个字符。 -
value2
中的条目至少有 5 个字符。 -
value2_repeat
中的条目必须与value2
相同。
如果这些规则中的任何一个未得到满足,则我们希望禁用提交按钮并显示相关消息,解释需要满足的内容。
这可以通过设置连接与合并在一起的一系列管道来实现。
-
有一个 @Published 属性匹配每个用户输入字段。 combineLatest 用于从属性中获取不断发布的更新,并将它们合并到单个管道中。 map 操作符强制执行所需字符和值必须相同的规则。 如果值与所需的输出不匹配,我们将在管道中传递 nil。
-
value1 还另外有一个验证管道,只使用了 map 操作符来验证值,或返回 nil。
-
执行验证的 map 操作符内部的逻辑也用于更新用户界面中的 label 信息。
-
最终管道使用 combineLatest 将两条验证管道合并为一条管道。 此组合的管道上连接了订阅者,以确定是否应启用提交按钮。
下面的示例将这些结合起来进行了展示。
import UIKit
import Combine
class FormViewController: UIViewController {
@IBOutlet weak var value1_input: UITextField!
@IBOutlet weak var value2_input: UITextField!
@IBOutlet weak var value2_repeat_input: UITextField!
@IBOutlet weak var submission_button: UIButton!
@IBOutlet weak var value1_message_label: UILabel!
@IBOutlet weak var value2_message_label: UILabel!
@IBAction func value1_updated(_ sender: UITextField) { (1)
value1 = sender.text ?? ""
}
@IBAction func value2_updated(_ sender: UITextField) {
value2 = sender.text ?? ""
}
@IBAction func value2_repeat_updated(_ sender: UITextField) {
value2_repeat = sender.text ?? ""
}
@Published var value1: String = ""
@Published var value2: String = ""
@Published var value2_repeat: String = ""
var validatedValue1: AnyPublisher<String?, Never> { (2)
return $value1.map { value1 in
guard value1.count > 2 else {
DispatchQueue.main.async { (3)
self.value1_message_label.text = "minimum of 3 characters required"
}
return nil
}
DispatchQueue.main.async {
self.value1_message_label.text = ""
}
return value1
}.eraseToAnyPublisher()
}
var validatedValue2: AnyPublisher<String?, Never> { (4)
return Publishers.CombineLatest($value2, $value2_repeat)
.receive(on: RunLoop.main) (5)
.map { value2, value2_repeat in
guard value2_repeat == value2, value2.count > 4 else {
self.value2_message_label.text = "values must match and have at least 5 characters"
return nil
}
self.value2_message_label.text = ""
return value2
}.eraseToAnyPublisher()
}
var readyToSubmit: AnyPublisher<(String, String)?, Never> { (6)
return Publishers.CombineLatest(validatedValue2, validatedValue1)
.map { value2, value1 in
guard let realValue2 = value2, let realValue1 = value1 else {
return nil
}
return (realValue2, realValue1)
}
.eraseToAnyPublisher()
}
private var cancellableSet: Set<AnyCancellable> = [] (7)
override func viewDidLoad() {
super.viewDidLoad()
self.readyToSubmit
.map { $0 != nil } (8)
.receive(on: RunLoop.main)
.assign(to: \.isEnabled, on: submission_button)
.store(in: &cancellableSet) (9)
}
}
-
此代码的开头遵照了 patterns.adoc 中的模式. IBAction 消息用于更新 @Published 属性,触发对所连接的任何订阅者的更新。
-
第一个验证管道使用 map 操作符接收字符串值输入,如果与验证规则不符,则将其转换为 nil。 这也将发布者属性的输出类型从
<String>
转换为可选的<String?>
。 同样的逻辑也用于触发消息文本的更新,以提供有关所需内容的信息。 -
由于我们正在更新用户界面元素,因此我们明确将这些更新包裹在
DispatchQueue.main.async
中,以在主线程上调用。 -
combineLatest 将两个发布者合并到一个管道中,该管道的输出类型是每个上游发布者的合并值。 在这个例子中,输出类型是
(<String>, <String>)
的元组。 -
与其使用
DispatchQueue.main.async
,不如使用 receive 操作符明确在主线程上执行下一个操作符,因为它将执行 UI 更新。 -
两条验证管道通过 combineLatest 相结合,并将经过检查的输出合并为单个元组输出。
-
我们可以将分配的管道存储为
AnyCancellable?
引用(将其映射到 viewcontroller 的生命周期),但另一种选择是创建一个变量来收集所有可取消的引用。 这从空集合开始,任何 sink 或 assign 的订阅者都可以被添加到其中,以持有对它们的引用,以便他们在 viewcontroller 的整个生命周期内运行。 如果你正在创建多个管道,这可能是保持对所有管道的引用的便捷方式。 -
如果任何值为 nil,则 map 操作符将向管道传递 false 值。 对 nil 值的检查提供了用于启用(或禁用)提交按钮的布尔值。
-
store
方法可在 Cancellable 协议上调用,该协议明确设置为支持存储可用于取消管道的引用。