MVVMってなんやねーん

リソースは https://medium.com/ios-os-x-development/mvvm-in-ios-from-net-perspective-580eb7f4f129

Rxとかは使ってない。

表示するとき

struct ArticleViewModel {
    var title: String
    var description: String

    init(article: Article) {
        self.title = article.title
        self.description = article.description
    }
}

// テーブルビューで表示する用にViewModelつくる。
struct ArticleListViewModel {
    var title: String? = "Articles"
    var articles: [ArticleViewModel] = [ArticleViewModel]()

    init(articles: [ArticleViewModel]) {
        self.articles = articles
    }

    // APIを自分でたたいて自分でパースしてviewにバインド
    func loadArticles(callback: (([Article]) -> ())) {
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data {
                let json = try! JSONSerialization.jsonObject(with: data, options: [])
                let dictionary = json as! JSONDictionary
                let articleDictionaries = dictionary["articles"] as! [JSONDictionary]
                articles = articleDictionaries.flatMap { dictionary in
                    return Article(dictionary: dictionary)
                }
            }

            DispatchQueue.main.async {
                callback(articles)
            }
        }
    }
}

このloadArticlesメソッドみたいにViewModelからAPIをたたいてるやつもあるけどそうじゃなくてもいいとのこと(自分が好きなやつを使え)

ViewModelにAPIをたたかせない場合、Webserviceみたいなクラスを作ってそこに読ませたりする

 private func loadArticles() {
        let url = URL(string: "https://...")

        Webservice().getArticles(url: url) { articles in
            let articles = articles.map { article in
                return ArticleViewModel(article: article)
            }

            self.viewModel = ArticleListViewModel(articles: articles)
        }
    }

あとはUITableViewを実装しているところで

 private var viewModel: ArticleListViewModel = ArticleListViewModel() {
        didSet {
            self.tableView.reloadData()
        }
    }

とかやればいい

入力を受け付ける

テキストフィールドに入った言葉がリアタイでRegistrationViewModelとかなんかにバインドされるようにする
+モデル側から変更があったときはそれがビューに反映されるようにする
つまりこのようなコードを書く必要はなくなる

self.viewModel.title = self.titleTextField.text!
self.viewModel.description = self.descriptionTextField.text!

dynamic(RxでいうとVariableに近いもの?)を宣言する

class Dynamic<T> {
    var bind: (T) -> () = { _ in }
    
    var value: T? {
        didSet {
            bind(value!)
        }
    }

    init(_ v: T) {
        value = v
    }   
}

あとはViewController側のVMを保持しているプロパティにdidSetを生やす

    var viewModel :AddArticleViewModel! {   
        didSet {
            viewModel.title.bind = { [unowned self] in self.titleTextField.text = $0 }
            viewModel.description.bind = { [unowned self] in self.descriptionTextField.text = $0 }
        }
    }

これでbindプロパティに適切なコンポーネントの値を更新するような1行2行を書いておけばViewModel→View側の更新が入る

次は双方向にしていく

class BindingTextField : UITextField {    
    var textChanged: (String) -> () = { _ in }

    func bind(callback: @escaping (String) -> ()) {
        self.textChanged = callback
        self.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
    }
    
    @objc func textFieldDidChange(_ textField: UITextField) {
        self.textChanged(textField.text!)
    }
}

addTargetでイベントハンドラ登録して、イベントハンドラの中でクロージャを呼んでいる

あとはTextFieldにViewModelをbindしてあげれば良い

    @IBOutlet weak var titleTextField: BindingTextField! {
        didSet {
            titleTextField.bind { self.viewModel.title.value = $0 }
        }
    }

    @IBOutlet weak var descriptionTextField: BindingTextField! {
        didSet {
            descriptionTextField.bind { self.viewModel.description.value = $0 }
        }
    }

とにかくViewModelはModelの影、ModelについてViewModelが行うのはイベントに対する反応と戻り値のないメソッドの呼び出ししかない( http://ugaya40.hateblo.jp/entry/model-mistake )ということを考えてれば大丈夫(????)

https://academy.realm.io/jp/posts/slug-max-alexander-mvvm-rxswift/ ここ的には、ViewControllerの参照をもたない、UIKitをインポートしない、UIKitから参照しない、structで宣言するしstructしか持たないようにするとよいとのこと。

ViewModelがUIを更新しなければいけないとき

struct LoginViewModel {
    var username: String = "" {
        didSet {
            evaluateValidity()
        }
    }
    var password: String = "" {
        didSet { 
            evaluateValidity()
        }
    }
    var isValid : Bool = "" {
        didSet {
            isValidCallback?(isValid: isValid)
        }
    }
    var isValidCallback : ((isValid: Bool) -> Void)?

    func attemptToLogin() {
          //truncated for space
    }

    private func evaluateValidity(){
      isValid = username.characters.count > 0
            && password.characters.count > 0
    }
}

このisValidCallbackを見て

class LoginViewController {
    @IBOutlet var confirmButton: UIButton!
    var loginViewModel = LoginViewModel()

    override func viewDidLoad(){
        super.viewDidLoad()
        loginViewModel.isValidCallback = { [weak self] (isValid) in
            self?.confirmButton.isEnabled = isValid
        }
    }
}

こう

↑これをRxSwiftで書き直す例も載っているので https://academy.realm.io/jp/posts/slug-max-alexander-mvvm-rxswift/ を見るとよい

カスタムビューにbindする

class MyCustomView: UIView {
    var sink: AnyObserver<SomeComplexStructure> {
        return AnyObserver { [weak self] event in
            switch event {
                case .next(let data):
                    self?.something.text = data.property.text
                case .error(let error):
                    self?.backgroundColor = .red
                case .completed:
                    self.alpha = 0
            }
        }
    }
}

これにobservableをbindする

class ViewController {
    let myCustomView: MyCustomView

    override func viewDidLoad(){
        super.viewDidLoad()

        viewModel.dataStream
            .bindTo(myCustomView.sink)
            .addTo(disposeBag)
    }
}

UITableView

https://github.com/RxSwiftCommunity/RxDataSources/blob/master/Example/Example4_DifferentSectionAndItemTypes.swift

表示する値パターンを設定

enum SectionItem {
    case ImageSectionItem(image: UIImage, title: String)
    case ToggleableSectionItem(title: String, enabled: true)
    case StepperSectionItem(title: String)
}

// セクションが絡むときはこんな感じ
enum MultipleSectionModel {
    case ImageProvidableSection(title: String, items: [SectionItem])
    case TogglableSection(title: String, items: [SectionItem])
    case StepperableSection(title: String, items: [SectionItem])
}

// items取り出せるようにする
extension MultipleSectionModel: SectionModelType {
    typealias Item = SectionItem

    var items: [SectionItem] {
        switch self {
        case .ImageProvidableSection(title: _, items: let items):
            return items.map {$0}
        case .StepperableSection(title: _, items: let items):
            return items.map {$0}
        case .ToggleableSection(title: _, items: let items):
            return items.map {$0}
        }
    }

    // わからん
    init(original: MultipleSectionModel, items: [Item]) {
        switch original {
        case let .ImageProvidableSection(title: title, items: _):
            self = .ImageProvidableSection(title: title, items: items)
        case let .StepperableSection(title, _):
            self = .StepperableSection(title: title, items: items)
        case let .ToggleableSection(title, _):
            self = .ToggleableSection(title: title, items: items)
        }
    }
}

なるほど〜