RxSwiftなどを利用しないMVVMのサンプル

# はじめに

[RxSwift](https://github.com/ReactiveX/RxSwift)や[ReactiveCocoa](https://github.com/ReactiveCocoa/ReactiveCocoa)などのReactive Programmingを実現するフレームワークを利用する方が増えているので、もともとデータバインディングの機構がなかったiOSアプリ開発においても**MVVM**を用いるられる機会が増えつつあると思います。
そのため、`RxSwfitなどを用いてデータバインディングを行うデザインパターン = MVVM`という雰囲気が漂っている気がしています。

# MVVM

MVVMは、`View`・`ViewModel`・`Model`によって構成されます。

`View`はユーザーからのアクションを受け取って、そのイベントを`ViewModel`にバインドします。
そして、そのバインドされたイベントによって`ViewModel`は`Model`の取得や更新を行います。
`ViewModel`は更新された情報をイベントとして`View`にバインドし、`View`はバインドされたイベントをもとに結果を反映させます。

<img width="861" alt="mvvm.png" src="https://qiita-image-store.s3.amazonaws.com/0/60325/25395146-f307-7407-b746-ed4731d7ca8c.png">

この`Data Binding`が行われている部分をRxSwiftなどを使わずに、ピュアなSwiftで実装するとどのようになるでしょうか。
下図のような、簡単なカウンタアプリを実装していこうと思います。

![counter.gif](https://qiita-image-store.s3.amazonaws.com/0/60325/1dc7668b-3dc9-310c-1f49-8247a05b69a0.gif)

仕様としては

- 🔼をタップするとカウントが増える
- 🔽をタップするとカウントが減る
- カウントが0の場合は、カウントを減らせない
- カウントが0の場合は、🔽は非活性

を満たすものとします。

## ViewModel

今回はViewModel側で[NotificationCenter](https://developer.apple.com/documentation/foundation/notificationcenter)をラップして、データバインディング機構のようなものを実装していこうと思います。
NotificationCenterを利用する理由としては、`func post(name:object:userInfo:)`を呼び出すと`func addObserver(forName:object:queue:using:)`などで監視をしている同一の`Notification.Name`に対して通知を送ることができるからです。

また、NotificationCenterは`NotificationCenter.default`から通知や監視を行うサンプルが多いため、アプリ全体に向けて通知を送るもののように利用されることが多いと思います。
しかし、下図のようにインスタンス化したNotificationCenterを保持することで、該当のインスタンス内でのみに通知を送ることも可能です。
<img width="543" alt="スクリーンショット 2018-02-06 13.15.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/60325/00d0fafc-9215-ab64-fb22-3d8cd18e5b05.png">

それでは、ViewModelの実装を見ていこうと思います。
今回作成するカウンタアプリは、一画面であるため、`1: ViewController`・`1: ViewModel`となります。
ここで利用するViewModelの名前は、`CountViewModel`とします。`CountViewModel`の実装は下記になります。

```swift
final class CountViewModel {

    private enum Names {
        static let countChanged = Notification.Name(rawValue: "CountViewModel.countChanged")
        static let isDecrementEnabledChanged = Notification.Name(rawValue: "CountViewModel.isDecrementEnabledChanged")
        static let decrementAlphaChanged = Notification.Name(rawValue: "CountViewModel.decrementAlphaChanged")
    }

    private(set) var count: String = "" {
        didSet { center.post(name: Names.countChanged, object: nil) }
    }
    private(set) var isDecrementEnabled: Bool = false {
        didSet { center.post(name: Names.isDecrementEnabledChanged, object: nil) }
    }
    private(set) var decrementAlpha: CGFloat = 0.5 {
        didSet { center.post(name: Names.decrementAlphaChanged, object: nil) }
    }

    private var observers: [NSObjectProtocol] = []
    private let center: NotificationCenter

    private var _count: Int = 0 {
        didSet {
            count = String(_count)
            isDecrementEnabled = _count > 0
            decrementAlpha = isDecrementEnabled ? 1 : 0.5
        }
    }

    deinit {
        observers.forEach { center.removeObserver($0) }
    }

    init(center: NotificationCenter = .init()) {
        self.center = center
        setInitialValue()
    }

    private func setInitialValue() {
        _count = 0
    }

    @objc func increment() {
        _count += 1
    }

    @objc func decrement() {
        _count -= 1
    }

    func observe<Value1, Target: AnyObject, Value2>(keyPath keyPath1: KeyPath<CountViewModel, Value1>,
                                                    on queue: OperationQueue? = .main,
                                                    bindTo target: Target,
                                                    _ keyPath2: ReferenceWritableKeyPath<Target, Value2>) {
        let name: Notification.Name
        switch keyPath1 {
        case \CountViewModel.count             : name = Names.countChanged
        case \CountViewModel.isDecrementEnabled: name = Names.isDecrementEnabledChanged
        case \CountViewModel.decrementAlpha    : name = Names.decrementAlphaChanged
        default                                : fatalError("not support observing \(keyPath1).")
        }

        let handler: () -> () = { [weak self, weak target] in
            guard let me = self, let target = target, let value = me[keyPath: keyPath1] as? Value2 else { return }
            target[keyPath: keyPath2] = value
        }

        handler()
        let observer = center.addObserver(forName: name, object: nil, queue: queue) { _ in handler() }
        observers.append(observer)
    }
}
```

`CountViewModel`では、`_count: Int`がカウント数を保持していますが、そちらは公開していません。
Viewにバインドする際に必要な値を、必要な型で公開するようにしています。

- count (_countをStringとして公開)
- isDecrementEnabled (カウントを減らすボタンが有効か)
- decrementAlpha (カウントを減らすボタンを活性・非活性にする際のAlpha)

上記は`_count`のdidSetが呼ばれた際に更新されるようになっています。

そして、それぞれのpropertyのdidSetでは、該当する`Notification.Name`を通知しています。

カウントを増やす・減らすを行う際に、`UIButton`の`func addTarget(_:action:for:)`から直接処理を行うことができるよう、`func increment()`と`func decrement()`を`@objc`として定義します。

それでは、それらの流れをViewにどのようにしてデータバインディングしていくのかを見ていこうと思います。

```swift
func observe<Value1, Target: AnyObject, Value2>(keyPath keyPath1: KeyPath<CountViewModel, Value1>,
                                                on queue: OperationQueue? = .main,
                                                bindTo target: Target,
                                                _ keyPath2: ReferenceWritableKeyPath<Target, Value2>)
```

KeyPathを利用して上記のメソッドから`CountViewModel`のpropertyを監視登録し、ターゲットとなるオブジェクトに変更後の値をバインドします。

```swift
let name: Notification.Name
switch keyPath1 {
case \CountViewModel.count             : name = Names.countChanged
case \CountViewModel.isDecrementEnabled: name = Names.isDecrementEnabledChanged
case \CountViewModel.decrementAlpha    : name = Names.decrementAlphaChanged
default                                : fatalError("not support observing \(keyPath1).")
}
```

keyPathをswitch-caseを利用して比較し、該当する`Notification.Name`を割り当てます。

```swift
let handler: () -> () = { [weak self, weak target] in
    guard let me = self, let target = target, let value = me[keyPath: keyPath1] as? Value2 else { return }
    target[keyPath: keyPath2] = value
}
```

通知が来た際のハンドラを定義し、keyPathを利用して通知を受け取った際の該当のpropteryの値を取得し、キャストをしてからターゲットのpropertyに対して代入します。

```swift
handler()
let observer = center.addObserver(forName: name, object: nil, queue: queue) { _ in handler() }
observers.append(observer)
```

現状の値を即座に渡すために、一度handlerを実行します。そして、NotificationCenterに通知登録を行います。
`func addObserver(forName:object:queue:using:)`はNSObjectProtocolのオブジェクトを返してくるので配列などで保持をし、`CountViewModel`が破棄させれる際に監視の解除を行うようにします。

```swift
deinit {
    observers.forEach { center.removeObserver($0) }
}
```

## ViewController

上記の`CountViewModel`をViewControllerで利用していきます。
UIのパーツとして

- incrementButton (カウントを増やすボタン)
- decrementButton (カウントを減らすボタン)
- countLabel (現在のカウントの状態を表示するラベル)

をViewControllerが保持しています。

```swift
final class MVVMSampleViewController: UIViewController {
    @IBOutlet private weak var incrementButton: UIButton!
    @IBOutlet private weak var decrementButton: UIButton!
    @IBOutlet private weak var countLabel: UILabel!

    private let viewModel = CountViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        incrementButton.addTarget(viewModel, action: #selector(CountViewModel.increment), for: .touchUpInside)
        decrementButton.addTarget(viewModel, action: #selector(CountViewModel.decrement), for: .touchUpInside)
        
        viewModel.observe(keyPath: \.count, bindTo: countLabel, \.text)
        viewModel.observe(keyPath: \.isDecrementEnabled, bindTo: decrementButton, \.isEnabled)
        viewModel.observe(keyPath: \.decrementAlpha, bindTo: decrementButton, \.alpha)
    }
}
```

`viewDidLoad`で、incrementButtonとdecrementButtonがそれぞれが該当する処理をaddTargetで登録します。
そして、viewModelの該当のpropertyを監視し、ViewControllerの該当のpropertyに対してバインドするようにします。

このように実装することで、RxSwiftなどを利用せずとも`View`からのイベントを`ViewModel`に対してバインドし、`ViewModel`内で状態を変更してその変更結果を`View`にバインドするという流れを実現することができます。

## その他

今回のViewModel内でのメソッドの定義では、型に関係なくバインドをしようとできてしまうため

```swift
func observe<Value, Target: AnyObject>(keyPath keyPath1: KeyPath<CountViewModel, Value>,
                                       on queue: OperationQueue? = .main,
                                       bindTo target: Target,
                                       _ keyPath2: ReferenceWritableKeyPath<Target, Value>)
```

として、同一のValueという型にだけバインドできるようにしたり、OptinalやImplicitlyUnwrappedOptionalのWrappedがValueと同じ型の場合にという定義をする必要があります。

それらの部分も含めて、NotificationCenterを利用したデータバインディング機構をまとめた、[Continuum](https://github.com/marty-suzuki/Continuum)を作ってみました。
NotificationCenterのインスタンスの`.continuum`にアクセスをすることで、`func observe(_:on:bindTo:_:)`にアクセスできるようになっています。
`Variable`または`Constant`という値をラップしているクラスを利用することで、値に変更があった際には内部で自動的に`func post(name:object:userInfo:)`を呼び出すようになっています。

```swift
final class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!

    private let viewModel: ViewModel = ViewModel()
    private let center = NotificationCenter()
    private let bag = ContinuumBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        center.continuum
            .observe(viewModel.text, on: .main, bindTo: label, \.text)
            .disposed(by: bag)

        viewModel.text.value = "Binding this text to label.text!"
    }
}

final class ViewModel {
    let text: Variable<String>

    init() {
        self.text = Variable(value: "")
    }
}
```

ちなみに、Continuumを利用して同様の仕様を満たす実装をすると下記のコードのようになります。

```swift
final class ViewController: UIViewController {
    @IBOutlet private weak var countLabel: UILabel!
    @IBOutlet private weak var incrementButton: UIButton!
    @IBOutlet private weak var decrementButton: UIButton!

    private let viewModel = CountViewModel()
    private let center = NotificationCenter()
    private let bag = ContinuumBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        incrementButton.addTarget(viewModel, action: #selector(CountViewModel.increment), for: .touchUpInside)
        decrementButton.addTarget(viewModel, action: #selector(CountViewModel.decrement), for: .touchUpInside)

        center.continuum
            .observe(viewModel.count, bindTo: countLabel, \.text)
            .disposed(by: bag)

        center.continuum
            .observe(viewModel.decrementAlpha, bindTo: decrementButton, \.alpha)
            .disposed(by: bag)

        center.continuum
            .observe(viewModel.isDecrementEnabled, bindTo: decrementButton, \.isEnabled)
            .disposed(by: bag)
    }
}

final class CountViewModel {
    let isDecrementEnabled: Constant<Bool>
    private let _isDecrementEnabled = Variable(value: false)

    let decrementAlpha: Constant<CGFloat>
    private let _decrementAlpha = Variable<CGFloat>(value: 0.5)

    let count: Constant<String>
    private let _count = Variable<String>(value: "")

    private var _rawCount: Int = 0 {
        didSet {
            _count.value = "\(_rawCount)"
            _isDecrementEnabled.value = _rawCount > 0
            _decrementAlpha.value = _isDecrementEnabled.value ? 1 : 0.5
        }
    }

    init() {
        self.isDecrementEnabled = Constant(variable: _isDecrementEnabled)
        self.decrementAlpha = Constant(variable: _decrementAlpha)
        self.count = Constant(variable: _count)
        setInitialValue()
    }

    private func setInitialValue() {
        _rawCount = 0
    }

    @objc func increment() {
        _rawCount += 1
    }

    @objc func decrement() {
        _rawCount -= 1
    }
}
```

# 最後に

RxSwiftなどを利用しないMVVMの実装の方法の一例としてNotificationCenterを利用する方法を紹介させていただきました。
ちなみに、RxSwiftを利用して同様の仕様を満たす実装をすると下記のコードのようになります。

```swift
final class MVVMWithRxSwiftSampleViewController: UIViewController {
    @IBOutlet private weak var incrementButton: UIButton!
    @IBOutlet private weak var decrementButton: UIButton!
    @IBOutlet private weak var countLabel: UILabel!

    private lazy var viewModel: CountViewModel = {
        return .init(incrementButtonTapped: self.incrementButton.rx.tap.asObservable(),
                     decrementButtonTapped: self.decrementButton.rx.tap.asObservable())
    }()

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.isDecrementEnabled
            .bind(to: decrementButton.rx.isEnabled)
            .disposed(by: disposeBag)

        viewModel.decrementAlpha
            .bind(to: decrementButton.rx.alpha)
            .disposed(by: disposeBag)

        viewModel.count
            .bind(to: countLabel.rx.text)
            .disposed(by: disposeBag)
    }
}

final class CountViewModel {
    let count: Observable<String>
    let isDecrementEnabled: Observable<Bool>
    let decrementAlpha: Observable<CGFloat>

    private let disposeBag = DisposeBag()

    init(incrementButtonTapped: Observable<Void>,
         decrementButtonTapped: Observable<Void>) {

        let _count = BehaviorSubject<Int>(value: 0)
        let _isDecrementEnabled = _count.map { $0 > 0 }

        self.isDecrementEnabled = _isDecrementEnabled
        self.count = _count.map(String.init)
        self.decrementAlpha = _isDecrementEnabled.map { $0 ? 1 : 0.5 }

        Observable.merge(incrementButtonTapped.map { 1 },
                         decrementButtonTapped.map { -1 })
            .withLatestFrom(_count.asObservable()) { $1 + $0 }
            .bind(to: _count)
            .disposed(by: disposeBag)
    }
}
```

この他にも、同様の仕様のカウンタアプリをいろんなデザインパターンで実装してみたので、興味がありましたらぜひご覧ください！
https://github.com/marty-suzuki/SimplestCounterSample