Skip to content

Latest commit

 

History

History
164 lines (129 loc) · 7.26 KB

pattern-merging-streams-interface.adoc

File metadata and controls

164 lines (129 loc) · 7.26 KB

合并多个管道以更新 UI 元素

目的
  • 观察并响应多个 UI 元素发送的值,并将更新的值联合起来以更新界面。

参考
另请参阅
代码和解释

此示例故意模仿许多 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)
    }
}
  1. 此代码的开头遵照了 patterns.adoc 中的模式. IBAction 消息用于更新 @Published 属性,触发对所连接的任何订阅者的更新。

  2. 第一个验证管道使用 map 操作符接收字符串值输入,如果与验证规则不符,则将其转换为 nil。 这也将发布者属性的输出类型从 <String> 转换为可选的 <String?>。 同样的逻辑也用于触发消息文本的更新,以提供有关所需内容的信息。

  3. 由于我们正在更新用户界面元素,因此我们明确将这些更新包裹在 DispatchQueue.main.async 中,以在主线程上调用。

  4. combineLatest 将两个发布者合并到一个管道中,该管道的输出类型是每个上游发布者的合并值。 在这个例子中,输出类型是 (<String>, <String>) 的元组。

  5. 与其使用 DispatchQueue.main.async,不如使用 receive 操作符明确在主线程上执行下一个操作符,因为它将执行 UI 更新。

  6. 两条验证管道通过 combineLatest 相结合,并将经过检查的输出合并为单个元组输出。

  7. 我们可以将分配的管道存储为 AnyCancellable? 引用(将其映射到 viewcontroller 的生命周期),但另一种选择是创建一个变量来收集所有可取消的引用。 这从空集合开始,任何 sink 或 assign 的订阅者都可以被添加到其中,以持有对它们的引用,以便他们在 viewcontroller 的整个生命周期内运行。 如果你正在创建多个管道,这可能是保持对所有管道的引用的便捷方式。

  8. 如果任何值为 nil,则 map 操作符将向管道传递 false 值。 对 nil 值的检查提供了用于启用(或禁用)提交按钮的布尔值。

  9. store 方法可在 Cancellable 协议上调用,该协议明确设置为支持存储可用于取消管道的引用。