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
表示する値パターンを設定
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) } } }
なるほど〜