前言
這幾天有空就玩一下去年蘋果新推出的兩大功能 - Combine & SwiftUI。
SwiftUI 對我來說是一個全新的境界,跟之前碰過的程式語言都較為不同,目前還在熟悉中。
但是 Apple 推出的響應式編程 - Combine,以之前已經碰過 RxSwift 的我來說較為熟悉。
觀念大致相同,不同的是用法跟語法吧。
但就在我練習當中發現 Combine 跟 UIKit 的相容性似乎比較差(也可以說完全沒有= =),可能因為有 SwiftUI 輔助了吧。
所以就上網查了一下相關資訊,發現蠻多人是使用 UIKit + Combine ,只不過 Publisher 要自己寫就是。
Create Custom Publisher
在 “How to create custom Publisher in Combine” 裡面有指出如果要做出一個客製化的 Publisher ,需要三件事情:
- Publisher
- Subscription
- Subscriber
發布者(Publisher)
簡單來說,可以先把他看做是報社的角色,他的職責就是負責製作報紙、送報紙。
而在這邊我們要送的是 UIControl ,因為所有的元件都繼承這個 class 。所以我們就先製作一個 送 UIControl 的 Publisher。
如果有看到 Combine 的 framework,就可以發現 Publisher 其實是一個 protocol 而非實體
public protocol Publisher {
/// The kind of values published by this publisher.
associatedtype Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
associatedtype Failure : Error
/// Attaches the specified subscriber to this publisher.
///
/// Implementations of ``Publisher`` must implement this method.
///
/// The provided implementation of ``Publisher/subscribe(_:)-4u8kn``calls this method.
///
/// - Parameter subscriber: The subscriber to attach to this ``Publisher``, after which it can receive values.
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
所以我們就產生一個 Class 來實作 Publisher
// Publisher
final class CustomePublisher<C:UIControl> : Publisher {
typealias Output = C
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber, CustomePublisher.Failure == S.Failure, CustomePublisher.Output == S.Input {
}
}
協議(Subscription)
也就是明定這份報紙在什麼情況下會送到訂閱用戶手上。
final class CustomSubscription<S: Subscriber, C: UIControl> : Subscription where S.Input == C {
func request(_ demand: Subscribers.Demand) {
//TODO:...
}
func cancel() {
//TODO:...
}
}
訂閱者(Subscriber)
也就是訂閱用戶,用戶可以向報社訂閱報紙。
protocol CombineCompatible { }
extension UIControl: CombineCompatible { }
// Subscriber
extension CombineCompatible where Self:UIControl {
func controlEvent(for event:UIControl.Event) -> CustomePublisher<UIControl> {
}
}
到這邊為止,所有 Class 的架構已經做好,接下來就要進行內部實作。
就先從 訂閱者(Subscriber) 下手,目前在 extension 的 func controlEvent(for:) 還沒有回傳一個CustomePublisher,而我們需要在 UIControl 的 addTarget 去把事件註冊進去。
所以就在 CustomePublisher 新增建構子
final class CustomePublisher<C:UIControl> : Publisher {
typealias Output = C
typealias Failure = Never
//=== 新增建構子===
private let control:C
private let event:UIControl.Event
init(control:C, event:UIControl.Event) {
self.control = control
self.event = event
}
func receive<S>(subscriber: S) where S : Subscriber, CustomePublisher.Failure == S.Failure, CustomePublisher.Output == S.Input {
//TODO:...
}
}
// Subscriber
extension CombineCompatible where Self:UIControl {
func controlEvent(for event:UIControl.Event) -> CustomePublisher<UIControl> {
//=== 補上init function ===
return CustomePublisher(control: self, event: event)
}
}
Subscription (協議)
接下來是最重要的部分 - Subscription,就在上面我們創造了一個名為 CustomSubscription 的協議,這個協議裡面就會寫著誰(UIControl)在什麼時間點(UIControl.Event)做什麼事情(#selector(eventHandler)),實際程式碼如下:
final class CustomSubscription<S: Subscriber, C: UIControl> : Subscription where S.Input == C {
private var subscriber:S?
private let control:C
init(subscriber:S, control:C, event:UIControl.Event) {
self.subscriber = subscriber
self.control = control
self.control.addTarget(self, action: #selector(eventHandler), for: event)
}
func request(_ demand: Subscribers.Demand) {
//TODO:...
}
func cancel() {
subscriber = nil
}
@objc private func eventHandler() {
_ = subscriber?.receive(control)
}
deinit {
control.removeTarget(self, action: nil, for: .allEvents)
}
}
//CustomePublisher
func receive<S>(subscriber: S) where S : Subscriber, CustomePublisher.Failure == S.Failure, CustomePublisher.Output == S.Input {
//補上程式碼
let subscription = CustomSubscription(subscriber: subscriber, control: control, event: event)
subscriber.receive(subscription: subscription)
}
寫到這邊就已經完成一個客製化的Publisher了。實際上的應用可以寫成這樣:
//UIButton Tap Event
_ = button.controlEvent(for: .touchUpInside).sink { (control) in
print("touchUpInside")
}
//UISwitch Switch Event
_ = customSwitch.controlEvent(for: .valueChanged).sink(receiveValue: { (control) in
print("valueChanged")
})
寫到這邊忽然發現一件事情,就是如果我要拿 UISwitch 裡面的值,在這個block裡面就要將 control 轉成 UISwitch 才能拿到裡面的 isOn 。
_ = switchTerms.controlEvent(for: .valueChanged).sink(receiveValue: { (control) in
if let mSwitch = control as? UISwitch { //這邊每次都要轉換成該元件才能拿到 UI 元件的相關資料
print(mSwitch.isOn)
}
})
那有沒有辦法是直接就轉成該元件就好,不需要再自己轉換一次呢?有的,我們把原本 controlEvent 回傳的值從 UIControl 改成 Self 就好了唷。
func controlEvent(for event:UIControl.Event) -> CustomePublisher<UIControl> {
return .init(control: self, event: event)
}
func controlEvent(for event:UIControl.Event) -> CustomePublisher<Self> {
return .init(control: self, event: event)
}
再把想要擴充的元件加上一個 Extension
extension UISwitch {
var valueChanged:CustomePublisher<UISwitch> {
return ControlEvent(for: .valueChanged)
}
}
_ = customSwitch.valueChanged.sink(receiveValue: { (s) in
print(s.isOn)
})
//也可以跟其他元件做綁定
customSwitch.valueChanged.map({ $0.isOn })
.assign(to: \.isEnabled, on: button)
.store(in: &cancellables)
這樣是不是很方便呢?
這幾天測試下來的心得,Combine 如果使用在 Model 層面個人覺得是綽綽有餘了, 雖然它對 UI 的支援性不夠好,但是有 SwiftUI 做輔助也夠了。如果真的再不行,網路上也是很多大神針對 Combine + UIKit 做很好的擴充。
今天這篇文章就到這邊,希望能幫上你們的忙唷。
Reference
- How to create custom Publisher in Combine - By Dmitry Lupich
- Combine Framework with UIKit - By Fatih Kagan Emre
- Creating a custom Combine Publisher to extend UIKit - By ANTOINE VAN DER LEE