基于Combine的响应式UIControl

2023-04-21 16:38:01 浏览数 (2)

一、概述

iOS开发中UIKit中控件的交互方式默认是Target-Action,这种方式简单且直观。不过,一个问题在于编码方式太过于繁琐,需要定义一个方法,然后调用addTartget方式进行绑定;在复杂页面交互,需要跨多级数据传递的时候,就变得异常繁琐。

后面响应式和函数式编程兴起,诞生RxSwift等的响应式框架,全新的开发体验确实提高的开发效率,不过带来的问题就是堆栈太深,排查问题不利于排查。也会有一定的损耗,这么多的堆栈必然占用更多的系统资源,性能的话会有一定影响。

iOS13后,apple要推广swiftUI带来了Combine,其实apple的响应式框架,亲儿子,在框架底层和Swift层面都进行一定的优化,堆栈和性能会比RxSwift等更优。随着iOS13的不断普及,Combine会越来越受欢迎。

不过SwiftUI发展必然不会那么快速,项目中还是有很多的UIKit的代码需要维护。

本文不在于介绍Combine的理论知识,而是在于扩展UIKit的UIControl支持响应式编程方式。

二、如何实现?

自定义 Publisher 和 Subscriber

* 第一步,自定义Subscription 中介对象

* 第二步,自定义Publisher 发布者

* 第三部,扩展第三方支持Publisher

代码语言:javascript复制
```
/// 自定义
extension Publishers {
    /// 1、自定义 Subscription
    /// 定义输入类型为UIControl,错误类型为Never
    private final class UIControlSubscription<S:Subscriber, Control:UIControl> : Subscription where S.Input == Control, S.Failure == Never {
        private var subscriber: S?
        private var control: Control
        private var events: Control.Event
        /// Step 1 : 初始化
        init(subscriber: S?, control: Control, events: Control.Event) {
            self.subscriber = subscriber
            self.control = control
            self.events = events
            configControl()
        }
        deinit {
            print("UIControlSubscription deinit~~~")
        }
代码语言:javascript复制
        /// Step 2 : 关联 与 控制
        func configControl() {
            self.control.addTarget(self, action: #selector(eventHandler), for: self.events)
        }
        
        @objc func eventHandler() {
            // 忽略返回值
            _ = self.subscriber?.receive(self.control)
        }
        
        func request(_ demand: Subscribers.Demand) {
            
        }
        
        /// Step 3 : 销毁
        func cancel() {
            // 销毁订阅者
            subscriber = nil
        }

    }
代码语言:javascript复制
    /// 2、自定义 Publisher
    struct UIControlPublisher<Control: UIControl> : Publisher {
        typealias Output = UIControl
        typealias Failure = Never
        
        private var control: Control
        private var events: Control.Event
        /// Step 1 : 初始化
        init(control: Control, events: Control.Event) {
            self.control = control
            self.events = events
        }
        
        /// Step 2 :通过 Subscription 将 订阅者Subscriber 连接到 发布者Publisher
        func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, UIControl == S.Input {
            let subscription = UIControlSubscription(subscriber: subscriber, control: self.control, events: self.events)
            subscriber.receive(subscription: subscription)
        }
    }
}

extension UIControl {
    func publisher(events: UIControl.Event) -> Publishers.UIControlPublisher<UIControl> {
        return Publishers.UIControlPublisher(control: self, events: events)
    }
}
extension UISwitch {
    func publisher() -> AnyPublisher<Bool, Never> {
        return Publishers.UIControlPublisher(control: self, events: .valueChanged)
            .map{ ($0 as! UISwitch).isOn }
            .eraseToAnyPublisher()
    }
}
extension UISlider {
    func publisher() -> AnyPublisher<Float, Never> {
        return Publishers.UIControlPublisher(control: self, events: .valueChanged)
            .map{ ($0 as! UISlider).value }
            .eraseToAnyPublisher()
    }
}

extension UITextField {
    
}
```

三、如何用?

代码语言:javascript复制
```
private var cancelList: Set<AnyCancellable> = []

let btn_2 = UIButton.init(type: .custom)
btn_2.setTitle("combine", for: .normal)
btn_2.backgroundColor = .blue
addSubview(btn_2)

btn_2.publisher(events: .touchUpInside)
            .receive(on: RunLoop.main)
            .sink { [weak self] (btn) in
                guard let `self` = self else {
                    return
                }
                print("btn combint click")
            }.store(in: &cancelList)
```

还可以再简化,只保留闭包即可,这部分封装就留着自由发挥了。

如下看起来清爽的多

代码语言:javascript复制
```
btn_2.action(events: .touchUpInside) { [weak self] (btn) in
    guard let `self` = self else {
      return
    }
    print("btn combint click")
}
```

0 人点赞