Swift Combine 合并多个管道以更新 UI 元素 从入门到精通十七

Combine 系列

  1. Swift Combine 从入门到精通一
  2. Swift Combine 发布者订阅者操作者 从入门到精通二
  3. Swift Combine 管道 从入门到精通三
  4. Swift Combine 发布者publisher的生命周期 从入门到精通四
  5. Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五
  6. Swift Combine 订阅者Subscriber的生命周期 从入门到精通六
  7. Swift 使用 Combine 进行开发 从入门到精通七
  8. Swift 使用 Combine 管道和线程进行开发 从入门到精通八
  9. Swift Combine 使用 sink, assign 创建一个订阅者 从入门到精通九
  10. Swift Combine 使用 dataTaskPublisher 发起网络请求 从入门到精通十
  11. Swift Combine 用 Future 来封装异步请求 从入门到精通十一
  12. Swift Combine 有序的异步操作 从入门到精通十二
  13. Swift Combine 使用 flatMap 和 catch错误处理 从入门到精通十三
  14. Swift Combine 网络受限时从备用 URL 请求数据 从入门到精通十四
  15. Swift Combine 通过用户输入更新声明式 UI 从入门到精通十五
  16. Swift Combine 级联多个 UI 更新,包括网络请求 从入门到精通十六
    在这里插入图片描述

1. 合并多个管道以更新 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 将两条验证管道合并为一条管道。 此组合的管道上连接了订阅者,以确定是否应启用提交按钮。

下面的示例将这些结合起来进行了展示。
UIKit-Combine/FormViewController.swift

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. 此代码的开头遵照了 通过用户输入更新声明式 UI 中的模式. 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 协议上调用,该协议明确设置为支持存储可用于取消管道的引用。在这里插入代码片

参考

https://heckj.github.io/swiftui-notes/index_zh-CN.html

代码

https://github.com/heckj/swiftui-notes

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值