RxCocoaのUIBindingObserverでLINEのようなUIテーマを全てのViewに反映させるサンプルコード

## 概要
[RxSwift Advent Calendar 2016](http://qiita.com/advent-calendar/2016/rxswift) の11日目の記事です。
LINEやEvernoteなどのようにUIのテーマを変えたいことがありますよね？
テーマをアプリ全体のViewに反映させるような場合に、RxCocoaのUIBindingObserverを使う方法を考えたので紹介します。

[RxSwift/UIBindingObserver.swift at master · ReactiveX/RxSwift · GitHub](https://github.com/ReactiveX/RxSwift/blob/master/RxCocoa/CocoaUnits/UIBindingObserver.swift)

## 環境
RxSwift 3.0.1
Xcode 8.1(8B62)
Swift 3.0

## UIBindingObserverとは
UIKitの各クラスのプロパティのObserverです。UILabelでは`label.rx.text`のようにアクセスできます。サンプルコードは以下です。

```swift
// サンプルコード
Observable<String?>.just("text").asDriver(onErrorJustReturn: nil)
    .drive(label.rx.text)
    .addDisposableTo(disposeBag)

// RxCocoa内の定義は以下のようになっています
extension Reactive where Base: UILabel {
    
    /// Bindable sink for `text` property.
    public var text: UIBindingObserver<Base, String?> {
        return UIBindingObserver(UIElement: self.base) { label, text in
            label.text = text
        }
    }
}
```

UIBindingObserverは`UIElement`にAnyObject（大体UIKitのクラスになると思います）を渡し、`binding`のクロージャ内で`UIElement`にイベントの値をバインドします。上記UILabelの例でいうと、渡ってきた`String?`の値を`label.text`にバインドしています。UIBindingObserver のRxCocoa内の定義は以下のようになっています。

```swift
/**
Observer that enforces interface binding rules:
 * can't bind errors (in debug builds binding of errors causes `fatalError` in release builds errors are being logged)
 * ensures binding is performed on main thread
 
`UIBindingObserver` doesn't retain target interface and in case owned interface element is released, element isn't bound.
*/
public class UIBindingObserver<UIElementType, Value> : ObserverType where UIElementType: AnyObject {
    public typealias E = Value

    weak var UIElement: UIElementType?

    let binding: (UIElementType, Value) -> Void

    /// Initializes `ViewBindingObserver` using
    public init(UIElement: UIElementType, binding: @escaping (UIElementType, Value) -> Void) {
        self.UIElement = UIElement
        self.binding = binding
    }

    /// Binds next element to owner view as described in `binding`.
    public func on(_ event: Event<Value>) {
        MainScheduler.ensureExecutingOnScheduler(errorMessage: "Element can be bound to user interface only on MainThread.")

        switch event {
        case .next(let element):
            if let view = self.UIElement {
                binding(view, element)
            }
        case .error(let error):
            bindingErrorToInterface(error)
        case .completed:
            break
        }
    }

    /// Erases type of observer.
    ///
    /// - returns: type erased observer.
    public func asObserver() -> AnyObserver<Value> {
        return AnyObserver(eventHandler: on)
    }
}
```


## テーマの準備
`Theme`や`ThemeService`はアプリごとで適当に定義してください。今回はラベルやボタンごとにテーマの適用方法を決められるようにネストしたstructで定義してみました。

```swift
struct Theme {
    
    struct Label {
        let textColor: UIColor
        static let `default` = Label(textColor: .black)
    }
    
    struct Button {
        let textColor: UIColor
        let backgroundColor: UIColor
        static let `default` = Button(textColor: .white, backgroundColor: .blue)
    }
    
    let label: Label
    let button: Button
    
    static let `default` = Theme(
        label: Label.default,
        button: Button.default
    )
}

final class ThemeService {
    
    static let instance = ThemeService()
    
    let theme: Driver<Theme>
    
    init() {
        
        self.theme = Observable.just(Theme.default).asDriver(onErrorJustReturn: Theme.default)
    }
}
```


## 目指すインターフェース
以下のようにView1個1個にバインドしていくのは簡単に書けるのですが、これは面倒なので避けたいです。

```swift
ThemeService.instance.theme
    .drive(label.rx.driveTheme)
    .addDisposableTo(disposeBag)
```

テーマを適用するボタンやラベルなどのViewの**配列**を渡して、テーマが変わるとすべてに適用されるようにします。以下が目指すインターフェースです。

```swift
class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var button1: UIButton!
    @IBOutlet weak var button2: UIButton!
    
    let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        ThemeService.instance.theme
            .drive([label, button1, button2].rx.driveTheme)
            .addDisposableTo(disposeBag)
    }
}
```


## ThemeApplicableプロトコル
Viewそれぞれでテーマの適用方法を定義していきます。
そのためのプロトコルが`ThemeApplicable`です。

```swift
protocol ThemeApplicable: NSObjectProtocol {
    
    func applyTheme(_ theme: Theme)
}
```

UILabelとUIButtonにテーマを適用していきます。

```swift
extension UILabel: ThemeApplicable {
    
    func applyTheme(_ theme: Theme) {
        
        textColor = theme.label.textColor
    }
}

extension UIButton: ThemeApplicable {
    
    func applyTheme(_ theme: Theme) {
            
        setTitleColor(theme.button.textColor, for: .normal)
        backgroundColor = theme.button.backgroundColor
    }
}
```

## テーマを適用するUIBindingObserverを書く
`ThemeApplicable`を配列として持つ`ThemeApplicableCollection`というクラスを定義します。

```swift
// .rxにアクセスできるようにReactiveCompatibleプロトコルを採用します。
final class ThemeApplicableCollection: ReactiveCompatible {
    
    let items: [ThemeApplicable]
    
    init(_ items: [ThemeApplicable]) {
        
        self.items = items
    }
}
```

あとは`UIBindingObserver`のextensionを書くだけです。

```swift
// .rx.driveThemeにアクセスできるようにextensionを書きます。
extension Reactive where Base: ThemeApplicableCollection {
    
    // 値はThemeにします。<, Theme>の部分
    var driveTheme: UIBindingObserver<Base, Theme> {
        
        return UIBindingObserver(UIElement: base) { collection, theme in
            // Themeの値が渡ってきたら、すべてのViewに適用します。
            collection.items.forEach {
                
                $0.applyTheme(theme)
            }
        }
    }
}
```

## 完成品
これで以下のように書くことができます。
注意点としては、`UIBindingObserver`の`UIElement`（今回でいうThemeApplicableCollection）がweakで定義されているため、プロパティとして定義してRetainしてあげないと動作しませんでした。

```swift
class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var button1: UIButton!
    @IBOutlet weak var button2: UIButton!
    
    let disposeBag = DisposeBag()
    var themeApplicableCollection: ThemeApplicableCollection!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        themeApplicableCollection = ThemeApplicableCollection([label, button1, button2])
        
        ThemeService.instance.theme
            .drive(themeApplicableCollection.rx.driveTheme)
            .addDisposableTo(disposeBag)
    }
}
```

## 最後に
extensionで独自のUIBindingObserverをいくらでも書けるので、
他にも活用できる場所は多そうです。
今回紹介したコードで改善できる箇所がありましたら、コメントをいただけると嬉しいです！