実現したいこと
-
Binder
やObservable
を作成してコードの整理を行いたい
環境
- ReactiveCocoa/ReactiveSwift 6.7.0
- ReactiveX/RxSwift 6.2.0
- Xcode Version 13.1 (13A1030d)
基本形: .rx.
の形で呼び出せるようにする
- 元を辿ると
.rx.
でアクセスしているのはReactive<Base>
- 定義上
.rx.
でアクセス可能なReactive<Base>
のBase
はNSObject
になる - 拡張したい
class
をBase
として持つReactive<Base>
を拡張することで.rx.
で呼び出せるようにする
SampleClass+Rx.swift
// MARK: - SampleClass
extension Reactive where Base: SampleClass {
// ここに何か .rx.◯◯ とアクセスしたいプロパティを定義する.
}
パターン1: Binder
で受け口を作る
- 通常のプロパティに関しては RxSwift ライブラリ側で自動で
Binder
が作られる- 例:
isEnabled
,isHidden
など - 内部的には KeyPath Member Lookup を利用してプロパティごとに
Binder
を作成している
- 例:
- ユースケースとして基礎的なクラス(
UIButton
など)に対して、元の値を触らず実効的にはdidSet
を行うような挙動が可能
UILabel+Rx.swift
// MARK: - UILabel
extension Reactive where Base: UILabel {
/// 赤字による文字更新.
var textWithRed: Binder<String?> {
return Binder<String?>(self.base) { (label: UILabel, value: String?) in
label.text = value
label.textColor = .red
}
}
}
パターン2: イベントが呼び出されたタイミングを知りたい
-
@objc
となっているイベントは呼び出されたタイミングを取得可能 - ユースケースとしては画面のライフサイクルに関連するイベントが呼び出された際にそれに応じた処理を行うなど
UIViewController+Rx.swift
// MARK: - UIViewController
extension Reactive Base: UIViewController {
/// view の表示直前を表す Observable
var viewWillAppear: Observable<Void> {
return self.sentMessage(#selector(base.viewWillAppear(_:)))
.map { _ -> Void in return () }
.share(replay: 1)
}
}
-
Reactive.sentMessage(#selector)
の返却値はObservable<[Any]>
- 今回はタイミングが知りたいだけなので
map
でVoid
に変換している -
.share(replay: 1)
については こちらの記事 が詳しいです- ざっくり言うと複数購読されても余計なストリームを流さないようにしている
パターン3: クロージャによる非同期処理を Rx に変換する
- 以下のようなクロージャによる非同期処理を持つクラスがあった場合に、処理を Rx 化したい
SampleClass.swift
// MARK: - SampleClass
class SampleClass: NSObject {
func fetchData(url: URL, completion: @escaping ((Result<Data, Error>) -> Void)) {
// テストのため3秒後に .success を返却する.
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
completion(.success(Data()))
}
}
}
SampleClass+Rx.swift
// MARK: - SampleClass
extension Reactive where Base: SampleClass {
func fetchData(url: URL) -> Observable<Result<Data, Error>> {
return Observable<Result<Data, Error>>.create { [weak base] (observer: AnyObserver<Result<Data, Error>>) in
base?.fetchData(url: url, completion: { (result: Result<Data, Error>) in
observer.onNext(result)
observer.onCompleted()
})
return Disposables.create()
}
}
}
パターン4: デリゲートメソッドが発火したタイミングが知りたい
- 以下のようなデリゲートを持つクラスがあった場合に、デリゲートのメソッドが呼ばれたタイミングを知りたい
SampleClass.swift
// MARK: - SampleClass
open class SampleClass: NSObject {
weak var delegate: SampleClassDelegate?
public func processA() {
self.delegate?.processA?()
}
public func processB(by value: String) -> String? {
return self.delegate?.processB(by: value)
}
}
// MARK: - SampleClassDelegate
@objc public protocol SampleClassDelegate: AnyObject {
@objc optional func processA()
func processB(by: String) -> String?
}
手順1: Rx**DelegateProxy
を作成
-
DelegateProxyType
とSampleClassDelegate
を準拠させる -
SampleClassDelegate
でoptional
として定義されているかどうかで扱いが異なるため注意 - 同様に返却値が
Void
かどうかでも扱いが異なるため注意
RxSampleClassDelegateProxy.swift
// MARK: - RxSampleClassDelegateProxy
public class RxSampleClassDelegateProxy: DelegateProxy<SampleClass, SampleClassDelegate> {
// processB のイベント発火検知用 Subject
internal lazy var processBDidInvokedSubject = PublishSubject<String?>()
// ParentObject および Delegate は DelegateProxy で定義されている.
// 今回は SampleClass が ParentObject 、 SampleClassDelegate が Delegate にあたる.
public init(by value: ParentObject) {
super.init(parentObject: value, delegateProxy: RxSampleClassDelegateProxy.self)
}
// Proxy が破棄されるタイミングで complete をイベントとして流す.
deinit {
self.processBDidInvokedSubject.onCompleted()
}
// DelegateProxy の Subclass である RxSampleClassDelegateProxy を factory に登録する.(実装必須)
public static func registerKnownImplementations() {
self.register { RxSampleClassDelegateProxy(by: $0) }
}
}
// MARK: - DelegateProxyType
extension RxSampleClassDelegateProxy: DelegateProxyType {
// ParentObject が持つ delegate を返却する.
public static func currentDelegate(for object: ParentObject) -> Delegate? {
return object.delegate
}
// ParentObject が持つべき delegate を設定する.
public static func setCurrentDelegate(_ delegate: Delegate?, to object: ParentObject) {
object.delegate = delegate
}
}
// MARK: - SampleClassDelegate
extension RxSampleClassDelegateProxy: SampleClassDelegate {
// delegate の定義上で必須となっているメソッドの定義方法.
// Proxy を通じて本来の delegate 実装先を発火させる.
// ここで定義したメソッドは sentMessage / methodInvoked ではイベント検出できない.
// また、返却値のあるメソッドは sentMessage / methodInvoked が呼ばれないためここでイベントを流す必要がある.
public func processB(by value: String) -> String? {
let result: String? = _forwardToDelegate?.processB(by: value)
self.processBDidInvokedSubject.onNext(result)
return result
}
}
手順2: Reactive
の extension を作成
- 以下の2パターン
-
sentMessage
/methodInvoked
を利用する -
Rx**DelegateProxy
で定義しておいたPublishSubject
を利用してObservable
を作成
-
SampleClass+Rx.swift
// MARK: - Reactive
extension Reactive where Base: SampleClass {
public var delegateProxy: RxSampleClassDelegateProxy {
RxSampleClassDelegateProxy.proxy(for: self.base)
}
// void を返却する optional のメソッドは sentMessage / methodInvoked が利用可能.
public var processAWillCalled: Observable<Void> {
return self.delegateProxy
.sentMessage(#selector(SampleClassDelegate.processA))
.map { _ -> Void in return () }
.share(replay: 1)
}
public var processADidCalled: Observable<Void> {
return self.delegateProxy
.methodInvoked(#selector(SampleClassDelegate.processA))
.map { _ -> Void in return () }
.share(replay: 1)
}
public var processBDidCalled: Observable<String?> {
return self.delegateProxy.processBDidInvokedSubject.asObservable()
}
// Rx**DelegateProxy で定義した delegate のメソッドは sentMessage / methodInvoked が呼ばれない.
// 以下の実装は呼ばれない.
/*
public var processBWillCalled: Observable<Void> {
return self.delegateProxy
.sentMessage(#selector(SampleClassDelegate.processB(by:)))
.map { _ -> Void in return () }
.share(replay: 1)
}
public var processBDidCalled: Observable<Void> {
return self.delegateProxy
.methodInvoked(#selector(SampleClassDelegate.processB(by:)))
.map { _ -> Void in return () }
.share(replay: 1)
}
*/
}
使用例
// MARK: - TestViewController
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.setupRx()
self.test()
}
// MARK: - Private
private let disposeBag = DisposeBag()
private let sampleClass = SampleClass()
private func setupRx() {
self.sampleClass.rx.processAWillCalled
.subscribe(onNext: {
print("Process A Will Called")
})
.disposed(by: self.disposeBag)
self.sampleClass.rx.processADidCalled
.subscribe(onNext: {
print("Process A Did Called")
})
.disposed(by: self.disposeBag)
self.sampleClass.rx.processBDidCalled
.subscribe(onNext: {
print("Process B Did Called")
})
.disposed(by: self.disposeBag)
}
private func test() {
// SampleClassDelegate を実装しなくても rx にストリームを流せる.
self.sampleClass().processA()
_ = self.sampleClass().processB()
}
}
注意点
-
SampleClassDelegate
メソッドを実装しなくてもイベントは検出可能になるが、あくまで「串(Proxy)」を刺してイベントの発火を検出しているだけなので扱いには注意
不明点
DelegateProxy.swift の sentMessage
の定義箇所に以下のコメントがある
Only methods that have
void
return value can be observed using this method because those methods are used as a notification mechanism. It doesn't matter if they are optional or not.
メソッドの返却値が void
であれば optional
であっても sentMessage
が使えるような書き振りだが、
実際 optional
ではないメソッドは Rx**DelegateProxy
に定義する必要があり、
そのため sentMessage
でイベントを取得できなかった
なにか別の実装方法があるのかもしれませんが、ちょっと思いつきませんでした