MVVMとFluxを勉強してみた。

個人でアプリを作成するついでにMVVMとFluxを勉強してみました。
間違えや改善点あれば忌憚ない意見をお願いします。

# Github
https://github.com/wakizaka24/architecture_ios

# サンプルの内容
デバイスの向きを検知してFlux構造で回しています。
①Store(BehaviorRelayのため初期値を渡す)→ViewModel→View
②Viewで検知→ViewModel→ActionCretorアクション生成
→Dispatcher→Store→ViewModel→View

ActionCreatorでもStoreを監視しつつ、WebAPIやロジックを呼び出し、
新たなActionを作成する想定です。

# RxCocoaの使い方
【6/2追記】すみません。asObservable()は不要かと思います。
asObservable()しなくてもbind可能です。

```swift
// 1.自分のクラスにRelay変数を持つ
private let portraitRelay = BehaviorRelay<Bool?>(value: nil)
private let portraitToViewRelay = PublishRelay<Bool?>()

// 2.受信するクラスにObservableクラスを渡す
// 例1
var portraitObservable: Observable<Bool?> {
    return portraitRelay.asObservable()
}
// 例2
portraitViewModel = PortraitViewModel(
portraitObservable: portraitRelay.asObservable())

// 3.別のクラスでObservableを監視または、Bindする
// disposeBagのインスタンス解放に合わせて、監視も止まります。
// 監視
portraitViewModel.portraitObservable
.bind(to: Binder(self) { `self`, portrait in
    self.portrait = portrait
}).disposed(by: disposeBag)
// Bind
portraitObservable.bind(to: portraitToActionRelay).disposed(by: disposeBag)

// 4.自分のクラスで変更を通知
portraitToViewRelay.accept(true)
```

# Bind関係
Rxでデータのやり取りをしている部分は、矢印の方向にObservableを渡して、
自分を監視してもらいます。また、ViewModelはActionCreatorに通知する場合と、
Viewに通知する場合がありそれぞれにRelayを用意します。
Fluxの部分のやり取りは書籍を参考にしました。
![MVVM+Flux.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203986/0f94d5f0-bed8-3081-242e-6b9b2d768c88.png)


# BaseViewControllerクラス
ViewControllerの基本クラスです。処理をプロトコルに切り分けることで、
今回のサンプルのデバイス向き検知を全画面ではなく、特定の画面のみで検知したくなった場合など、
実装変更時のフットワークが軽くなります。

```swift
//
//  BaseViewController.swift
//

import UIKit
import RxSwift
import RxCocoa

class BaseViewController: UIViewController, ClassInfoAdded, PortraitDetectionView {
    private var baseViewModel: BaseViewModel!
    private let viewDidLoadRelay = PublishRelay<Void>()
    
    // PortraitDetectionView
    internal var portraitViewModel: PortraitViewModel!
    internal var portrait: Bool?
    internal let portraitRelay = PublishRelay<Bool?>()
    
    internal let disposeBag = DisposeBag()
    
    deinit {
        print("deinit BaseViewController")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        addPortraitDetection()
        
        baseViewModel = BaseViewModel(viewControllerName: ViewControllerName(
            rawValue: className)!, viewDidLoadObservable:
            viewDidLoadRelay.asObservable())
        viewDidLoadRelay.accept(())
    }
}
```

# ClassInfoAddedクラス
クラス情報の取得を追加します。拡張される範囲（スコープ）を狭めるために、
プロトコルに処理を持たせました。

```swift
//
//  ClassInfoAdded.swift
//

import Foundation

protocol ClassInfoAdded: class {}

extension ClassInfoAdded {
    var className: String {
        get {
            return String(describing: type(of: self))
        }
    }
}
```

# PortraitDetectionViewクラス
プロトコルでデバイスの向きを検知する機能を切り出しています。

```swift
//
//  PortraitDetectionView.swift
//

import UIKit
import RxSwift
import RxCocoa

protocol PortraitDetectionView: class {
    var portraitViewModel: PortraitViewModel! { get set }
    var portrait: Bool? { get set }
    var portraitRelay: PublishRelay<Bool?> { get }
    var disposeBag: DisposeBag { get }
}

extension PortraitDetectionView {
    func addPortraitDetection() {
        portraitViewModel = PortraitViewModel(
            portraitObservable: portraitRelay.asObservable())
        portraitViewModel.portraitObservable
            .bind(to: Binder(self) { `self`, portrait in
                self.portrait = portrait
            }).disposed(by: disposeBag)
        
        NotificationCenter.default.rx.notification(
            UIDevice.orientationDidChangeNotification)
            .bind(to: Binder(self) { `self`, notification in
                self.orientationDidChange(notification: notification)
            }).disposed(by: disposeBag)
    }
    
    func orientationDidChange(notification: Notification) {
        let orientation = UIDevice.current.orientation
        //print("orientation: \(orientation)")
        if orientation != .portrait && orientation != .landscapeLeft && orientation != .landscapeRight {
            return
        }
        
        let currentPortrait = orientation == .portrait
        if portrait == nil || currentPortrait != portrait {
            portrait = currentPortrait
            portraitRelay.accept(portrait)
        }
    }
}
```

# BaseViewModelクラス
BaseViewModelは検知した結果をActionCreatorに渡すだけなので、
単一方向バインドになっています。

```swift
//
//  BaseViewModel.swift
//

import Foundation
import RxSwift
import RxCocoa

class BaseViewModel {
    private let actionCreator: ActionCreator = .shared
    private let viewDidLoadToActionRelay = PublishRelay<Void>()
    private let disposeBag = DisposeBag()
    
    init(viewControllerName: ViewControllerName, viewDidLoadObservable: Observable<Void>) {
        // View -> ViewModel
        viewDidLoadObservable.bind(to: viewDidLoadToActionRelay).disposed(by: disposeBag)

        // ViewModel -> ActionCreator
        actionCreator.setViewDidLoadObservable(
            viewControllerName: viewControllerName,
            viewDidLoadToActionRelay.asObservable(), disposeBag: disposeBag)
    }
}
```

# PortraitViewModelクラス
Viewとの双方向バインド＋Flux間とも双方向バインドになります。
View行きのRelayとActionCreator行きのRelayを、
二つ作ることになります。

```swift
//
//  PortraitViewModel.swift
//

import Foundation
import RxSwift
import RxCocoa

class PortraitViewModel {
    private let actionCreator: ActionCreator = .shared
    private let portraitToViewRelay = PublishRelay<Bool?>()
    private let portraitToActionRelay = PublishRelay<Bool?>()
    private let disposeBag = DisposeBag()
    
    init(portraitObservable: Observable<Bool?>) {
        // View -> ViewModel
        portraitObservable.bind(to: portraitToActionRelay).disposed(by: disposeBag)
        
        // ViewModel -> ActionCreator
        actionCreator.setPortraitObservable(
            portraitToActionRelay.asObservable(), disposeBag: disposeBag)
        
        // Store -> ViewModel
        ScreenInfoStore.shared.portraitObservable
            .bind(to: portraitToViewRelay).disposed(by: disposeBag)
    }
    
    var portraitObservable: Observable<Bool?> {
        // ViewModel -> View
        return portraitToViewRelay.asObservable()
    }
}
```

# Actionクラス
FluxのActionを示すEnumになります。TableViewの特定のセルを押したなど、
データの変更がないアクションもCaseに加えます。Actionを発生させて、
ActionCreatorでActionに伴うActionを発生させます。

```swift
//
//  Action.swift
//

import Foundation

enum Action {
    case changedPortrait(Bool?)
}
```

# ActionCreatorクラス
膨大にならないように、Repositoryクラスなどを作る必要があります。
全画面の処理を一括で制御できるため、操作性と可読性がよくなると思います。

```swift
//
//  ActionCreator.swift
//

import Foundation
import RxSwift
import RxCocoa

class ActionCreator {
    static let shared = ActionCreator()
    private let dispatcher: Dispatcher = .shared

    init() {
        // Storeなどを監視して新たなActionを作る。
    }
    
    func setPortraitObservable(_ observable: Observable<Bool?>, disposeBag: DisposeBag) {
        observable.bind(to: Binder(self) { `self`, portrait in
            self.dispatcher.dispatch(.changedPortrait(portrait))
        }).disposed(by: disposeBag)
    }
    
    func setViewDidLoadObservable(viewControllerName: ViewControllerName,
        _ observable: Observable<Void>, disposeBag: DisposeBag) {
        
        switch viewControllerName {
        case .calendar:
            
            
            break
        }
    }
}
```

# Dispatcherクラス
Storeを分割するために存在するのかと思います。

```swift
//
//  Dispatcher.swift
//

import Foundation
import RxSwift
import RxCocoa

class Dispatcher {
    static let shared = Dispatcher()
    private let actionRelay = PublishRelay<Action>()
    
    func register(callback: @escaping (Action) -> ()) -> Disposable {
        return actionRelay.subscribe(onNext: callback)
    }
    
    func dispatch(_ action: Action) {
        actionRelay.accept(action)
    }
}
```

# ScreenInfoStoreクラス
ViewModelを外部から変更可能なのはStoreのみです。
画面情報を1つのStoreにしてみました。

```swift
//
//  ScreenInfoStore.swift
//

import Foundation
import RxCocoa
import RxSwift

class ScreenInfoStore {
    static let shared = ScreenInfoStore()
    private let portraitRelay = BehaviorRelay<Bool?>(value: nil)
    private let disposeBag = DisposeBag()
    
    init() {
        let dispatcher: Dispatcher = .shared
        dispatcher.register(callback: { action in
            switch action {
            case let .changedPortrait(portrait):
                self.portraitRelay.accept(portrait)
            }
        }).disposed(by: disposeBag)
    }
    
    var portraitObservable: Observable<Bool?> {
        return portraitRelay.asObservable()
    }
}
```

# ContentView乱用
最後に、作成途中ですがContentViewを使いまくってみました。
こんな時MVVMとFluxは重宝すると思います。
<img width="648" alt="ContentView乱用.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203986/9cda6508-ddd5-d291-82dd-4d346aa00a2b.png">