Takahiro Octopress Blog

-1から始める情弱プログラミング

カスタムDelegateのRx対応

はじめに

先日、GeolocationSampleから学ぶdelegateのRx対応を紹介しました。
今回は下からニュッと出るPickerを作ろう!で作成した PickerView に実装されている DelegateRx 対応させたいと思います。

PickerViewクラスの確認

まずは、元となる PickerView クラスを提示します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import Foundation
import UIKit

/// ピッカービュー
public class PickerView: UIView {

    // MARK: - IBOutlets
    @IBOutlet weak var toolBar: UIToolbar!
    @IBOutlet weak var picker: UIPickerView!

    // MARK: - Static Properties
    static private let screenWidth = UIScreen.main.bounds.size.width
    static private let screenHeight = UIScreen.main.bounds.size.height
    static private let defaultPickerHeight: CGFloat = 260.0
    static private let duration = 0.2

    // MARK: - Properties
    public weak var delegate: PickerViewDelegate?
    private var selectItems = [String]()
    private var selectedRowIndex: Int = 0

    // MARK: - Initial Methods
    required init(frame: CGRect = CGRect(x: 0, y: screenHeight, width: screenWidth, height: defaultPickerHeight),
                  selectItems: [String]) {
        var frame = frame
        if let safeAreaTopInsets = UIApplication.shared.keyWindow?.safeAreaInsets.top, safeAreaTopInsets > CGFloat(0.0) {
            // iPhoneX , XS, XS MAX, XRの場合はUIPickerViewの高さを調整する
            frame = CGRect(x: 0, y: frame.origin.y, width: frame.size.width, height: (frame.size.height + 100.0))
        }
        super.init(frame: frame)
        self.selectItems = selectItems
        self.xibViewSet()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
        self.xibViewSet()
    }

    internal func xibViewSet() {
        if let view = R.nib.pickerView.firstView(owner: self) {
            view.frame = self.bounds
            self.addSubview(view)

            picker.delegate = self
            picker.dataSource = self
            picker.showsSelectionIndicator = true
        }
    }

    // MARK: - Picker Move Function
    // PickerViewを表示する
    func showPickerView() {
        let pickerViewWidth = self.frame.size.width
        let pickerViewHeight = self.frame.size.height
        let pickerViewYPosition = PickerView.screenHeight - pickerViewHeight
        UIView.animate(withDuration: PickerView.duration) {
            self.frame = CGRect.init(x: 0, y: pickerViewYPosition, width: pickerViewWidth, height: pickerViewHeight)
        }
    }

    // PickerViewを非表示にする
    func hiddenPickerView() {
        let pickerViewWidth = self.frame.size.width
        let pickerViewHeight = self.frame.size.height
        UIView.animate(withDuration: PickerView.duration) {
            self.frame = CGRect.init(x: 0, y: PickerView.screenHeight, width: pickerViewWidth, height: pickerViewHeight)
        }
    }

    // MARK: - IBActions
    @IBAction func cancelSelection(_ sender: Any) {
        delegate?.closePickerView()
        hiddenPickerView()
    }

    @IBAction func doneSelection(_ sender: Any) {
        delegate?.selectedItem(index: selectedRowIndex, title: selectItems[selectedRowIndex])
        hiddenPickerView()
    }
}

/// MARK: - UIPickerViewDelegate
extension PickerView: UIPickerViewDelegate {

    public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return selectItems[row]
    }

    public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        selectedRowIndex = row
    }
}

/// MARK: - UIPickerViewDataSource
extension PickerView: UIPickerViewDataSource {

    public func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }

    public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return selectItems.count
    }
}

/// MARK: - PickerViewDelegate
@objc
public protocol PickerViewDelegate: class {
    func selectedItem(index: Int, title: String)
    func closePickerView()
}

では早速 Rx 対応させていきましょう。

DelegateProxyとDelegateProxyTypeへの対応

基本的には、 CLLocationManagerDelegate と同じです。
DelegateProxyDelegateProxyType を継承したクラスを実装します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import Foundation
import RxSwift
import RxCocoa

// 説明(1)
extension PickerView: HasDelegate {
    public typealias Delegate = PickerViewDelegate
}

public class RxPickerViewDelegateProxy: DelegateProxy<PickerView, PickerViewDelegate>,
    DelegateProxyType,
    PickerViewDelegate {

    public init(pickerView: PickerView) {
        super.init(parentObject: pickerView, delegateProxy: RxPickerViewDelegateProxy.self)
    }

    // 説明(2)
    public static func registerKnownImplementations() {
        self.register { RxPickerViewDelegateProxy(pickerView: $0) }
    }

    // 説明(3)
    internal lazy var selectedItemSubject = PublishSubject<(Int, String)>()
    internal lazy var closePickerViewSubject = PublishSubject<Void>()

    // 説明(4)
    public func selectedItem(index: Int, title: String) {
        selectedItemSubject.onNext((index, title))
    }

    public func closePickerView() {
        closePickerViewSubject.onNext(Void())
    }

    // 説明(5)
    deinit {
        self.selectedItemSubject.on(.completed)
        self.closePickerViewSubject.on(.completed)
    }
}

細かく見ていきましょう。

説明(1)

currentDelegate および setCurrentDelegate に対応する代わりに、 HasDelegate を継承させましょう。

説明(2)

自身で定義した DelegateProxy の継承クラスを登録するために、 registerKnownImplementations 内で DelegateProxySubclass.register() を実行します。

説明(3)

delegate メソッドが呼び出されて処理が実行されたことを Subscriber に伝えるために、 PublishSubject 型のプロパティを用意します。

説明(4)

PickerViewDelegateselectedItem(index:title:)closePickerView() メソッドは必須メソッドです。
RxPickerViewDelegateProxy はもちろん PickerViewDelegate も継承しますので、上記2つのメソッドを定義する必要があります。

これが呼び出されたタイミングで Subscriber に伝えるために、 PublishSubject.onNext(element:) を実行します。

説明(5)

deinit が呼ばれるタイミングで、初期化したオブジェクトが破棄されるので、
PublishSubject からイベント送信完了を知らせるように実装しましょう。

ReactiveへのPickerViewの適応

これも CLLocationManager と適応方法は同じです。
まずは、全体像から…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// PickerView+Rx.swift
import Foundation
import RxSwift
import RxCocoa

extension Reactive where Base: PickerView {

    // 説明(1)
    public var delegate: DelegateProxy<PickerView, PickerViewDelegate> {
        return RxPickerViewDelegateProxy.proxy(for: base)
    }

    // 説明(2)
    public var selectedItem: Observable<(Int, String)> {
        return RxPickerViewDelegateProxy.proxy(for: base).selectedItemSubject.asObservable()
    }

    public var closePickerView: Observable<Void> {
        return RxPickerViewDelegateProxy.proxy(for: base).closePickerViewSubject.asObservable()
    }
}

1つずつ説明します。

説明(1)

delegateDelegateProxy 型として定義します。
DelegateProxy の取得は DelegateProxyType プロトコルの proxy メソッドを利用します。

説明(2)

delegate メソッドが実行されたことを補足(監視)するために Observable 型の selectedItemclosePickerView を用意します。

利用方法

では、早速利用してみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ViewController.swift

private let disposeBag = DisposeBag()

private func bind() {
  pickerView?.rx.selectedItem
      .asObservable()
      .subscribe({ [weak self] event in
          // Subscriberとして補足した情報を取得
          guard let strongSelf = self else { return }
          guard let index = event.element?.0 else { return }
          guard let title = event.element?.1 else { return }

          Observable.just(index, title)
              // ViewModelにsampleActionが定義されているとします
              .bind(to: strongSelf.viewModel.sampleAction)
              .disposed(by: strongSelf.disposeBag)
              })
              .disposed(by: disposeBag)
}

上記のような形で ViewController にて Subscriber としてアクションを補足し、 ViewModel に伝えることができるでしょう。

まとめ

さて、如何でしたでしょうか?
1つ1つの意味を理解することはもちろん大切ですが、
何だか型にはまって書き方を覚えれば、自身で Rx 対応が簡単にできる気がしてきますね。

Rx の癖が強いが故に、慣れれば利用しやすいということなのでしょう。
と言ったところで本日はここまで。

Comments