LoginSignup
12
5

More than 1 year has passed since last update.

【RxSwift】Rx の Extension を作成するパターン色々

Posted at

実現したいこと

  • BinderObservable を作成してコードの整理を行いたい

環境

  • ReactiveCocoa/ReactiveSwift 6.7.0
  • ReactiveX/RxSwift 6.2.0
  • Xcode Version 13.1 (13A1030d)

基本形: .rx. の形で呼び出せるようにする

  • 元を辿ると .rx. でアクセスしているのは Reactive<Base>
  • 定義上 .rx. でアクセス可能な Reactive<Base>BaseNSObject になる
  • 拡張したい classBase として持つ 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]>
  • 今回はタイミングが知りたいだけなので mapVoid に変換している
  • .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 を作成

  • DelegateProxyTypeSampleClassDelegate を準拠させる
  • SampleClassDelegateoptional として定義されているかどうかで扱いが異なるため注意
  • 同様に返却値が 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 でイベントを取得できなかった
なにか別の実装方法があるのかもしれませんが、ちょっと思いつきませんでした

参考

12
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
5