} viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text =

Swiftチュートリアル:MVVMデザインパターンの概要

つまり、新しいiOSプロジェクトを開始すると、デザイナーから必要なすべての情報を受け取ります.pdfおよび.sketchドキュメントがあり、この新しいアプリをどのように構築するかについてのビジョンはすでにあります。

デザイナーのスケッチからViewControllerへのUI画面の転送を開始します.swift.xibおよび.storyboardファイル。

UITextFieldここで、UITableViewそこに、さらにいくつかUILabelsそしてUIButtonsのピンチ。 IBOutletsおよびIBActions含まれています。よし、まだUIゾーンにいる。



ただし、これらすべてのUI要素を使用して何かを行うときが来ました。 UIButtons指で触れるとUILabelsおよびUITableViews何をどの形式で表示するかを誰かに教える必要があります。

突然、3,000行を超えるコードが作成されました。

3,000行のSwiftコード

あなたはたくさんのスパゲッティコードになってしまいました。

これを解決するための最初のステップは、 Model-View-Controller (MVC)デザインパターン。ただし、このパターンには独自の問題があります。来る Model-View-ViewModel (MVVM)日を節約するデザインパターン。

スパゲッティコードの取り扱い

あっという間に、あなたのスタートViewControllerスマートになりすぎて、巨大になりすぎました。

ネットワークコード、データ解析コード、UIプレゼンテーションのデータ調整コード、アプリの状態通知、UIの状態変化。 if -ology内に閉じ込められたすべてのコードは、再利用できず、このプロジェクトにのみ収まります。

あなたのViewControllerコードは悪名高いスパゲッティコードになりました。

どうしてこうなりました?

考えられる理由は次のようなものです。

バックエンドデータがUITableView 内でどのように動作しているかを確認するために急いでいたので、数行のネットワークコードを 臨時雇用者 ViewControllerのメソッドそれをフェッチするだけです.jsonネットワークから。次に、その.json内のデータを処理する必要があったので、さらに別のデータを作成しました 臨時雇用者 それを達成する方法。または、さらに悪いことに、同じ方法でそれを行いました。

ViewControllerユーザー認証コードが登場したとき、成長を続けました。その後、データ形式が変化し始め、UIが進化し、いくつかの根本的な変更が必要になりました。そして、すでに大規模なif -ologyにifを追加し続けました。

しかし、どうしてUIViewController手に負えなくなったものは何ですか?

UIViewController UIコードの作業を開始するための論理的な場所です。これは、iOSデバイスでアプリを使用しているときに表示される物理的な画面を表します。 AppleでさえUIViewControllersを使用しています異なるアプリとアニメーション化されたUIを切り替えると、メインシステムアプリで。

AppleはUIの抽象化をUIViewControllerの内部に基づいています。これは、iOSのUIコードのコアであり、 MVC デザインパターン。

関連: iOS開発者が犯していることを知らない10の最も一般的な間違い

MVCデザインパターンへのアップグレード

MVCデザインパターン

MVCデザインパターンでは、 見る 非アクティブであると想定され、オンデマンドで準備されたデータのみを表示します。

コントローラ に取り組む必要があります モデル 準備するためのデータ ビュー 、次にそのデータを表示します。

見る 通知する責任もあります コントローラ ユーザーのタッチなどのアクションについて。

前述のように、UIViewController通常、UI画面を構築するための開始点です。その名前には、「ビュー」と「コントローラー」の両方が含まれていることに注意してください。これは、「ビューを制御する」ことを意味します。 「コントローラー」と「ビュー」の両方のコードを内部に含める必要があるという意味ではありません。

このビューとコントローラーコードの混合は、IBOutletsを移動したときによく発生します。 UIViewController内の小さなサブビューを作成し、UIViewControllerから直接それらのサブビューを操作します。代わりに、そのコードをカスタムの中にラップする必要がありますUIViewサブクラス。

これにより、ビューとコントローラーのコードパスが交差する可能性があることは簡単にわかります。

救助へのMVVM

これは、 MVVM パターンが重宝します。

UIViewController以降であるはずです コントローラ MVCパターンで、すでに多くのことを行っています ビュー 、それらをにマージできます 見る 私たちの新しいパターンの- MVVM

MVVMデザインパターン

MVVMデザインパターンでは、 モデル MVCパターンと同じです。単純なデータを表します。

見る UIViewで表されますまたはUIViewController .xibを伴うオブジェクトおよび.storyboard準備されたデータのみを表示するファイル。 (たとえば、ビュー内にNSDateFormatterコードを含めたくありません。)

から来る単純なフォーマットされた文字列のみ ViewModel

ViewModel すべての非同期ネットワークコード、ビジュアルプレゼンテーション用のデータ準備コード、およびリッスンするコードを非表示にします モデル 変化します。これらはすべて、この特定のものに合うようにモデル化された明確に定義されたAPIの背後に隠されています 見る

MVVMを使用する利点の1つは、テストです。以来 ViewModel 純粋ですNSObject (またはstructなど)、UIKitとは結合されていませんコードを使用すると、UIコードに影響を与えることなく、単体テストでより簡単にテストできます。

さて、 見るUIViewController / UIView)がはるかに簡単になりました ViewModel 間の接着剤として機能します モデル そして 見る

SwiftでのMVVMの適用

SwiftのMVVM

MVVMの動作を示すために、このチュートリアル用に作成されたサンプルXcodeプロジェクトをダウンロードして調べることができます。 ここに 。このプロジェクトでは、Swift3とXcode8.1を使用しています。

プロジェクトには2つのバージョンがあります。 スターター そして 終了しました

ザ・ 終了しました バージョンは完成したミニアプリケーションであり、 スターター 同じプロジェクトですが、メソッドとオブジェクトが実装されていません。

まず、ダウンロードすることをお勧めします スターター プロジェクトを作成し、このチュートリアルに従ってください。後で使用するためにプロジェクトのクイックリファレンスが必要な場合は、 終了しました 事業。

チュートリアルプロジェクトの紹介

チュートリアルプロジェクトは、ゲーム中のプレーヤーのアクションを追跡するためのバスケットボールアプリケーションです。

バスケットボールアプリケーション

これは、ユーザーの動きとピックアップゲームの全体的なスコアをすばやく追跡するために使用されます。

15のスコア(少なくとも2ポイントの差がある)に達するまで、2つのチームがプレーします。各プレーヤーは1ポイントから2ポイントを獲得でき、各プレーヤーはアシスト、リバウンド、ファウルを行うことができます。

プロジェクト階層は次のようになります。

プロジェクト階層

モデル

見る

ViewModel

ダウンロードしたXcodeプロジェクトには、 見る オブジェクト(UIViewおよびUIViewController)。プロジェクトには、データを提供する方法の1つをデモするために作成されたカスタムメイドのオブジェクトも含まれています。 ViewModel オブジェクト(Servicesグループ)。

Extensionsグループには、このチュートリアルの範囲外であり、自明であるUIコードの便利な拡張機能が含まれています。

この時点でアプリを実行すると、完成したUIが表示されますが、ユーザーがボタンを押しても何も起こりません。

これは、ビューとIBActionsのみを作成したためです。それらをアプリロジックに接続せず、UI要素にモデルからのデータ(後で学習するようにGameオブジェクトから)を入力しません。

ViewとModelをViewModelで接続する

MVVMデザインパターンでは、Viewはモデルについて何も知らないはずです。 Viewが知っているのは、ViewModelの操作方法だけです。

ビューを調べることから始めます。

GameScoreboardEditorViewController.swiftでファイル、fillUIこの時点でメソッドは空です。これは、UIにデータを入力する場所です。これを実現するには、ViewControllerのデータを提供する必要があります。これは、ViewModelオブジェクトを使用して行います。

まず、このViewControllerに必要なすべてのデータを含むViewModelオブジェクトを作成します。

空になるViewModelXcodeプロジェクトグループに移動し、GameScoreboardEditorViewModel.swiftを作成します。ファイルを作成し、プロトコルにします。

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

このようなプロトコルを使用すると、物事がきれいに保たれます。使用するデータのみを定義する必要があります。

次に、このプロトコルの実装を作成します。

GameScoreboardEditorViewModelFromGame.swiftという名前の新しいファイルを作成し、このオブジェクトをNSObjectのサブクラスにします。

また、GameScoreboardEditorViewModelに準拠するようにしますプロトコル:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

ViewModelがイニシャライザを介して機能するために必要なすべてを提供したことに注意してください。

あなたはそれにGameを提供しましたこのViewModelの下にあるモデルであるオブジェクト。

ここでアプリを実行しても、このViewModelデータをビュー自体に接続していないため、アプリは機能しません。

だから、GameScoreboardEditorViewController.swiftに戻ってくださいファイルを作成し、viewModelという名前のパブリックプロパティを作成します。

タイプGameScoreboardEditorViewModelにします。

viewDidLoadの直前に配置しますGameScoreboardEditorViewController.swift内のメソッド。

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

次に、fillUIを実装する必要があります方法。

このメソッドが2つの場所、viewModelからどのように呼び出されるかに注目してください。プロパティオブザーバー(didSet)およびviewDidLoad方法。これは、ViewControllerを作成できるためです。ビューにアタッチする前に(viewDidLoadメソッドが呼び出される前に)ViewModelを割り当てます。

一方、ViewControllerのビューを別のビューにアタッチしてviewDidLoadを呼び出すこともできますが、viewModelの場合その時点で設定されていない場合、何も起こりません。

そのため、最初に、UIを満たすためにデータがすべて設定されているかどうかを確認する必要があります。予期しない使用からコードを保護することが重要です。

したがって、fillUIに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

ここで、pauseButtonPressを実装します方法:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

今必要なのは、実際のviewModelを設定することだけです。このViewControllerのプロパティ。これは「外部から」行います。

開くHomeViewController.swift ViewModelをファイルしてコメントを外します。 showGameScoreboardEditorViewControllerで行を作成および設定します方法:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

次に、アプリを実行します。次のようになります。

iOSアプリ

スコア、時間、およびチーム名を担当する中央のビューには、InterfaceBuilderで設定された値が表示されなくなりました。

これで、実際のModelオブジェクト(Gameオブジェクト)からデータを取得するViewModelオブジェクト自体からの値が表示されます。

優秀な!しかし、プレイヤーの見解はどうですか?これらのボタンはまだ何もしません。

プレーヤーの動きの追跡には6つのビューがあることを知っています。

PlayerScoreboardMoveEditorViewという名前の別のサブビューを作成しましたそのため、今のところ実際のデータには何もせず、PlayerScoreboardMoveEditorView.xib内のInterfaceBuilderを介して設定された静的な値を表示しますファイル。

あなたはそれにいくつかのデータを与える必要があります。

GameScoreboardEditorViewControllerで行ったのと同じ方法で行いますおよびGameScoreboardEditorViewModel

XcodeプロジェクトでViewModelグループを開き、ここで新しいプロトコルを定義します。

PlayerScoreboardMoveEditorViewModel.swiftという名前の新しいファイルを作成し、その中に次のコードを配置します。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

このViewModelプロトコルは、親ビューで行ったのと同じように、PlayerScoreboardMoveEditorViewに合うように設計されていますGameScoreboardEditorViewController

ユーザーが実行できる5つの異なる動きの値が必要であり、ユーザーがアクションボタンの1つに触れたときに反応する必要があります。 Stringも必要ですプレイヤー名。

これを行った後、親ビュー(GameScoreboardEditorViewController)で行ったのと同じように、このプロトコルを実装する具象クラスを作成します。

次に、このプロトコルの実装を作成します。新しいファイルを作成し、PlayerScoreboardMoveEditorViewModelFromPlayer.swiftという名前を付けて、このオブジェクトをNSObjectのサブクラスにします。また、PlayerScoreboardMoveEditorViewModelに準拠するようにしますプロトコル:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

ここで、このインスタンスを「外部から」作成するオブジェクトを用意し、それをPlayerScoreboardMoveEditorView内のプロパティとして設定する必要があります。

覚えておいてくださいHomeViewController viewModelの設定を担当しましたGameScoreboardEditorViewControllerのプロパティ?

同様に、GameScoreboardEditorViewController PlayerScoreboardMoveEditorViewの親ビューですそしてそれGameScoreboardEditorViewController PlayerScoreboardMoveEditorViewModelの作成を担当しますオブジェクト。

GameScoreboardEditorViewModelを拡張する必要があります最初。

GameScoreboardEditorViewMode lを開き、次の2つのプロパティを追加します。

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

また、GameScoreboardEditorViewModelFromGameを更新しますinitWithGameのすぐ上にあるこれらの2つのプロパティ方法:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

initWithGame内に次の2行を追加します。

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

そしてもちろん、不足しているplayerViewModelsWithPlayersを追加します方法:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

すごい!

ビューモデル(GameScoreboardEditorViewModel)をホームプレーヤーとアウェイプレーヤーの配列で更新しました。これらの2つの配列を埋める必要があります。

これは、これを使用したのと同じ場所で行いますviewModel UIを埋めるために。

開くGameScoreboardEditorViewController fillUIに移動します方法。メソッドの最後に次の行を追加します。

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

今のところ、実際のviewModelを追加しなかったため、ビルドエラーが発生しましたPlayerScoreboardMoveEditorView内のプロパティ。

init method inside the PlayerScoreboardMoveEditorView`の上に次のコードを追加します。

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

そしてfillUIを実装します方法:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

最後に、アプリを実行して、UI要素のデータがGameからの実際のデータであるかどうかを確認します。オブジェクト。

iOSアプリ

この時点で、MVVMデザインパターンを使用する機能的なアプリができました。

モデルをビューからうまく非表示にし、ビューはMVCで慣れているよりもはるかにシンプルです。

ここまでで、ViewとそのViewModelを含むアプリを作成しました。

そのビューには、ViewModelを持つ同じサブビュー(プレーヤービュー)の6つのインスタンスもあります。

ただし、お気づきかもしれませんが、UIに表示できるデータは(fillUIメソッドで)1回のみであり、そのデータは静的です。

ビューのデータがそのビューの存続期間中に変更されない場合は、この方法でMVVMを使用するための優れたクリーンなソリューションがあります。

ViewModelを動的にする

データが変更されるため、ViewModelを動的にする必要があります。

これが意味するのは、モデルが変更されると、ViewModelはそのパブリックプロパティ値を変更する必要があるということです。変更をビューに伝播します。ビューはUIを更新します。

これを行うには多くの方法があります。

モデルが変更されると、ViewModelが最初に通知されます。

変更内容をビューまで伝達するには、何らかのメカニズムが必要です。

いくつかのオプションが含まれます RxSwift 、これはかなり大きなライブラリであり、慣れるのに少し時間がかかります。

ViewModelは、プロパティ値の変更ごとにNSNotificationを起動する可能性がありますが、これにより、通知のサブスクライブやビューの割り当て解除時のサブスクライブ解除など、追加の処理が必要な多くのコードが追加されます。

Key-Value-Observing(KVO) 別のオプションですが、ユーザーはそのAPIが派手ではないことを確認します。

このチュートリアルでは、Swiftのジェネリックスとクロージャーを使用します。これらは、 バインディング、ジェネリック、Swift、MVVMの記事

それでは、サンプルアプリに戻りましょう。

ViewModelプロジェクトグループに移動し、新しいSwiftファイルDynamic.swiftを作成します。

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

このクラスは、ビューのライフサイクル中に変更されると予想されるViewModelのプロパティに使用します。

まず、PlayerScoreboardMoveEditorViewから始めますおよびそのViewModel、PlayerScoreboardMoveEditorViewModel

開くPlayerScoreboardMoveEditorViewModelそしてその特性を見てください。

playerNameだから変更される予定はありません。そのままにしておくことができます。

他の5つのプロパティ(5つの移動タイプ)が変更されるため、それについて何かを行う必要があります。ソリューション?上記のDynamicプロジェクトに追加したばかりのクラス。

内部PlayerScoreboardMoveEditorViewModel移動カウントを表す5つの文字列の定義を削除し、次のように置き換えます。

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

ViewModelプロトコルは次のようになります。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

これDynamic typeを使用すると、その特定のプロパティの値を変更すると同時に、change-listenerオブジェクト(この場合はビュー)に通知できます。

ここで、実際のViewModel実装を更新しますPlayerScoreboardMoveEditorViewModelFromPlayer

これを置き換えます:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

次のように:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

注:これらのプロパティをletで定数として宣言しても問題ありません。実際のプロパティは変更しないためです。 valueを変更しますDynamicのプロパティオブジェクト。

Dynamicを初期化しなかったため、ビルドエラーが発生しましたオブジェクト。

PlayerScoreboardMoveEditorViewModelFromPlayerのinitメソッド内で、moveプロパティの初期化を次のように置き換えます。

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

内部PlayerScoreboardMoveEditorViewModelFromPlayer makeMoveに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

ご覧のとおり、Dynamicのインスタンスを作成しましたクラスを割り当て、String値。データを更新する必要がある場合は、Dynamicを変更しないでください。プロパティ自体;むしろ更新しますvalueプロパティ。

すごい! PlayerScoreboardMoveEditorViewModel今はダイナミックです。

それを利用して、実際にこれらの変更をリッスンするビューに移動しましょう。

開くPlayerScoreboardMoveEditorViewとそのfillUIメソッド(String値をDynamicオブジェクトタイプに割り当てようとしているため、この時点でこのメソッドにビルドエラーが表示されるはずです。)

「エラーのある」行を置き換えます。

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

次のように:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

次に、移動アクションを表す5つのメソッドを実装します( ボタンアクション セクション):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

アプリを実行し、いくつかの移動ボタンをクリックします。アクションボタンをクリックすると、プレーヤービュー内のカウンター値がどのように変化するかがわかります。

iOSアプリ

PlayerScoreboardMoveEditorViewで終了ですおよびPlayerScoreboardMoveEditorViewModel

これは簡単でした。

ここで、メインビュー(GameScoreboardEditorViewController)でも同じことを行う必要があります。

まず、GameScoreboardEditorViewModelを開きますビューのライフサイクル中にどの値が変化すると予想されるかを確認します。

timescoreisFinishedisPausedを置き換えますDynamicを使用した定義バージョン:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

ViewModel実装(GameScoreboardEditorViewModelFromGame)に移動し、プロトコルで宣言されているプロパティで同じことを行います。

これを置き換えます:

var time: String var score: String var isFinished: Bool var isPaused: Bool

次のように:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

ViewModelのタイプをStringから変更したため、いくつかのエラーが発生します。およびBoolDynamicおよびDynamic

それを修正しましょう。

togglePauseを修正します次のように置き換えてください。

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

唯一の変更点は、プロパティ値をプロパティに直接設定しなくなったことです。代わりに、オブジェクトのvalueに設定しますプロパティ。

ここで、initWithGameを修正しますこれを置き換えることによる方法:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

次のように:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

あなたは今ポイントを取得する必要があります。

StringIntなどのプリミティブ値をラップしていますおよびBoolDynamicこれらのオブジェクトのバージョン。これにより、軽量のバインディングメカニズムが提供されます。

修正するエラーがもう1つあります。

startTimerでメソッドの場合、エラー行を次のように置き換えます。

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

プレーヤーのViewModelの場合と同じように、ViewModelを動的にアップグレードしました。ただし、ビューを更新する必要があります(GameScoreboardEditorViewController)。

fillUI全体を置き換えますこれを使った方法:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

唯一の違いは、4つの動的プロパティを変更し、それぞれに変更リスナーを追加したことです。

この時点で、アプリを実行している場合は、 開始/一時停止 ボタンはゲームタイマーを開始および一時停止します。これは、ゲーム中のタイムアウトに使用されます。

ポイントボタン(1および2ポイントボタン)のいずれかを押したときに、UIでスコアが変更されないことを除いて、ほぼ完了です。

これは、基になるGameでスコアの変更を実際に伝播していないためです。 ViewModelまでのモデルオブジェクト。

だから、Gameを開きます少し調べるためのモデルオブジェクト。そのupdateScoreを確認してください方法。

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

この方法は2つの重要なことを行います。

まず、isFinishedを設定しますプロパティto true両方のチームのスコアに基づいてゲームが終了した場合。

その後、スコアが変更されたという通知を投稿します。この通知はGameScoreboardEditorViewModelFromGameで聞きます通知ハンドラーメソッドで動的スコア値を更新します。

initWithGameの下部にこの行を追加しますメソッド(エラーを回避するためにsuper.init()呼び出しを忘れないでください):

super.init() subscribeToNotifications()

以下initWithGameメソッド、追加deinitクリーンアップを適切に実行し、NotificationCenterによって引き起こされるクラッシュを回避する必要があるためです。

deinit { unsubscribeFromNotifications() }

最後に、これらのメソッドの実装を追加します。このセクションをdeinitのすぐ下に追加します方法:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

次に、アプリを実行し、プレーヤーのビューをクリックしてスコアを変更します。すでに動的に接続しているのでscoreおよびisFinished Viewを使用したViewModelでは、ViewModel内のスコア値を変更するとすべてが機能するはずです。

アプリをさらに改善する方法

常に改善の余地がありますが、これはこのチュートリアルの範囲外です。

たとえば、ゲームが終了したとき(チームの1つが15ポイントに達したとき)に時間を自動的に停止するのではなく、プレーヤーのビューを非表示にするだけです。

必要に応じてアプリで遊んだり、アップグレードして「ゲームクリエイター」ビューを表示したりできます。このビューでは、ゲームの作成、チーム名の割り当て、プレーヤー名の割り当て、Gameの作成が行われます。 GameScoreboardEditorViewControllerを提示するために使用できるオブジェクト。

UITableViewを使用する別の「ゲームリスト」ビューを作成できます。進行中の複数のゲームを表示し、テーブルセルに詳細情報を表示します。セル選択では、GameScoreboardEditorViewControllerを表示できます選択したGameで。

GameLibraryすでに実装されています。そのライブラリ参照をイニシャライザのViewModelオブジェクトに渡すことを忘れないでください。たとえば、「ゲームクリエイター」のViewModelには、GameLibraryのインスタンスが必要です。作成されたGameを挿入できるように初期化子を通過しましたライブラリへのオブジェクト。 「ゲームリスト」のViewModelでも、ライブラリからすべてのゲームをフェッチするためにこの参照が必要になります。これはUITableViewで必要になります。

アイデアは、ViewModel内のすべてのダーティ(非UI)作業を非表示にし、UI(ビュー)が準備されたプレゼンテーションデータでのみ動作するようにすることです。

今何?

MVVMに慣れたら、を使用してさらに改善することができます ボブおじさんのクリーンアーキテクチャルール

追加の良い読み物は、Androidアーキテクチャに関する3部構成のチュートリアルです。

例はJava(Android用)で書かれており、Java(Swiftに非常に近く、Objective-CはJavaに近い)に精通している場合は、ViewModelオブジェクト内でコードをさらにリファクタリングする方法についてのアイデアが得られます。 iOSモジュール(UIKitまたはCoreLocationなど)をインポートしないこと。

これらのiOSモジュールは、純粋なNSObjectsの背後に隠すことができます。これは、コードの再利用に適しています。

MVVMはほとんどの人にとって良い選択です ios アプリ、そしてうまくいけば、あなたはあなたの次のプロジェクトでそれを試してみるでしょう。または、UIViewControllerを作成するときに、現在のプロジェクトで試してみてください。

関連: 静的パターンの操作:SwiftMVVMチュートリアル } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text =

Swiftチュートリアル:MVVMデザインパターンの概要

つまり、新しいiOSプロジェクトを開始すると、デザイナーから必要なすべての情報を受け取ります.pdfおよび.sketchドキュメントがあり、この新しいアプリをどのように構築するかについてのビジョンはすでにあります。

デザイナーのスケッチからViewControllerへのUI画面の転送を開始します.swift.xibおよび.storyboardファイル。

UITextFieldここで、UITableViewそこに、さらにいくつかUILabelsそしてUIButtonsのピンチ。 IBOutletsおよびIBActions含まれています。よし、まだUIゾーンにいる。



ただし、これらすべてのUI要素を使用して何かを行うときが来ました。 UIButtons指で触れるとUILabelsおよびUITableViews何をどの形式で表示するかを誰かに教える必要があります。

突然、3,000行を超えるコードが作成されました。

3,000行のSwiftコード

あなたはたくさんのスパゲッティコードになってしまいました。

これを解決するための最初のステップは、 Model-View-Controller (MVC)デザインパターン。ただし、このパターンには独自の問題があります。来る Model-View-ViewModel (MVVM)日を節約するデザインパターン。

スパゲッティコードの取り扱い

あっという間に、あなたのスタートViewControllerスマートになりすぎて、巨大になりすぎました。

ネットワークコード、データ解析コード、UIプレゼンテーションのデータ調整コード、アプリの状態通知、UIの状態変化。 if -ology内に閉じ込められたすべてのコードは、再利用できず、このプロジェクトにのみ収まります。

あなたのViewControllerコードは悪名高いスパゲッティコードになりました。

どうしてこうなりました?

考えられる理由は次のようなものです。

バックエンドデータがUITableView 内でどのように動作しているかを確認するために急いでいたので、数行のネットワークコードを 臨時雇用者 ViewControllerのメソッドそれをフェッチするだけです.jsonネットワークから。次に、その.json内のデータを処理する必要があったので、さらに別のデータを作成しました 臨時雇用者 それを達成する方法。または、さらに悪いことに、同じ方法でそれを行いました。

ViewControllerユーザー認証コードが登場したとき、成長を続けました。その後、データ形式が変化し始め、UIが進化し、いくつかの根本的な変更が必要になりました。そして、すでに大規模なif -ologyにifを追加し続けました。

しかし、どうしてUIViewController手に負えなくなったものは何ですか?

UIViewController UIコードの作業を開始するための論理的な場所です。これは、iOSデバイスでアプリを使用しているときに表示される物理的な画面を表します。 AppleでさえUIViewControllersを使用しています異なるアプリとアニメーション化されたUIを切り替えると、メインシステムアプリで。

AppleはUIの抽象化をUIViewControllerの内部に基づいています。これは、iOSのUIコードのコアであり、 MVC デザインパターン。

関連: iOS開発者が犯していることを知らない10の最も一般的な間違い

MVCデザインパターンへのアップグレード

MVCデザインパターン

MVCデザインパターンでは、 見る 非アクティブであると想定され、オンデマンドで準備されたデータのみを表示します。

コントローラ に取り組む必要があります モデル 準備するためのデータ ビュー 、次にそのデータを表示します。

見る 通知する責任もあります コントローラ ユーザーのタッチなどのアクションについて。

前述のように、UIViewController通常、UI画面を構築するための開始点です。その名前には、「ビュー」と「コントローラー」の両方が含まれていることに注意してください。これは、「ビューを制御する」ことを意味します。 「コントローラー」と「ビュー」の両方のコードを内部に含める必要があるという意味ではありません。

このビューとコントローラーコードの混合は、IBOutletsを移動したときによく発生します。 UIViewController内の小さなサブビューを作成し、UIViewControllerから直接それらのサブビューを操作します。代わりに、そのコードをカスタムの中にラップする必要がありますUIViewサブクラス。

これにより、ビューとコントローラーのコードパスが交差する可能性があることは簡単にわかります。

救助へのMVVM

これは、 MVVM パターンが重宝します。

UIViewController以降であるはずです コントローラ MVCパターンで、すでに多くのことを行っています ビュー 、それらをにマージできます 見る 私たちの新しいパターンの- MVVM

MVVMデザインパターン

MVVMデザインパターンでは、 モデル MVCパターンと同じです。単純なデータを表します。

見る UIViewで表されますまたはUIViewController .xibを伴うオブジェクトおよび.storyboard準備されたデータのみを表示するファイル。 (たとえば、ビュー内にNSDateFormatterコードを含めたくありません。)

から来る単純なフォーマットされた文字列のみ ViewModel

ViewModel すべての非同期ネットワークコード、ビジュアルプレゼンテーション用のデータ準備コード、およびリッスンするコードを非表示にします モデル 変化します。これらはすべて、この特定のものに合うようにモデル化された明確に定義されたAPIの背後に隠されています 見る

MVVMを使用する利点の1つは、テストです。以来 ViewModel 純粋ですNSObject (またはstructなど)、UIKitとは結合されていませんコードを使用すると、UIコードに影響を与えることなく、単体テストでより簡単にテストできます。

さて、 見るUIViewController / UIView)がはるかに簡単になりました ViewModel 間の接着剤として機能します モデル そして 見る

SwiftでのMVVMの適用

SwiftのMVVM

MVVMの動作を示すために、このチュートリアル用に作成されたサンプルXcodeプロジェクトをダウンロードして調べることができます。 ここに 。このプロジェクトでは、Swift3とXcode8.1を使用しています。

プロジェクトには2つのバージョンがあります。 スターター そして 終了しました

ザ・ 終了しました バージョンは完成したミニアプリケーションであり、 スターター 同じプロジェクトですが、メソッドとオブジェクトが実装されていません。

まず、ダウンロードすることをお勧めします スターター プロジェクトを作成し、このチュートリアルに従ってください。後で使用するためにプロジェクトのクイックリファレンスが必要な場合は、 終了しました 事業。

チュートリアルプロジェクトの紹介

チュートリアルプロジェクトは、ゲーム中のプレーヤーのアクションを追跡するためのバスケットボールアプリケーションです。

バスケットボールアプリケーション

これは、ユーザーの動きとピックアップゲームの全体的なスコアをすばやく追跡するために使用されます。

15のスコア(少なくとも2ポイントの差がある)に達するまで、2つのチームがプレーします。各プレーヤーは1ポイントから2ポイントを獲得でき、各プレーヤーはアシスト、リバウンド、ファウルを行うことができます。

プロジェクト階層は次のようになります。

プロジェクト階層

モデル

見る

ViewModel

ダウンロードしたXcodeプロジェクトには、 見る オブジェクト(UIViewおよびUIViewController)。プロジェクトには、データを提供する方法の1つをデモするために作成されたカスタムメイドのオブジェクトも含まれています。 ViewModel オブジェクト(Servicesグループ)。

Extensionsグループには、このチュートリアルの範囲外であり、自明であるUIコードの便利な拡張機能が含まれています。

この時点でアプリを実行すると、完成したUIが表示されますが、ユーザーがボタンを押しても何も起こりません。

これは、ビューとIBActionsのみを作成したためです。それらをアプリロジックに接続せず、UI要素にモデルからのデータ(後で学習するようにGameオブジェクトから)を入力しません。

ViewとModelをViewModelで接続する

MVVMデザインパターンでは、Viewはモデルについて何も知らないはずです。 Viewが知っているのは、ViewModelの操作方法だけです。

ビューを調べることから始めます。

GameScoreboardEditorViewController.swiftでファイル、fillUIこの時点でメソッドは空です。これは、UIにデータを入力する場所です。これを実現するには、ViewControllerのデータを提供する必要があります。これは、ViewModelオブジェクトを使用して行います。

まず、このViewControllerに必要なすべてのデータを含むViewModelオブジェクトを作成します。

空になるViewModelXcodeプロジェクトグループに移動し、GameScoreboardEditorViewModel.swiftを作成します。ファイルを作成し、プロトコルにします。

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

このようなプロトコルを使用すると、物事がきれいに保たれます。使用するデータのみを定義する必要があります。

次に、このプロトコルの実装を作成します。

GameScoreboardEditorViewModelFromGame.swiftという名前の新しいファイルを作成し、このオブジェクトをNSObjectのサブクラスにします。

また、GameScoreboardEditorViewModelに準拠するようにしますプロトコル:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

ViewModelがイニシャライザを介して機能するために必要なすべてを提供したことに注意してください。

あなたはそれにGameを提供しましたこのViewModelの下にあるモデルであるオブジェクト。

ここでアプリを実行しても、このViewModelデータをビュー自体に接続していないため、アプリは機能しません。

だから、GameScoreboardEditorViewController.swiftに戻ってくださいファイルを作成し、viewModelという名前のパブリックプロパティを作成します。

タイプGameScoreboardEditorViewModelにします。

viewDidLoadの直前に配置しますGameScoreboardEditorViewController.swift内のメソッド。

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

次に、fillUIを実装する必要があります方法。

このメソッドが2つの場所、viewModelからどのように呼び出されるかに注目してください。プロパティオブザーバー(didSet)およびviewDidLoad方法。これは、ViewControllerを作成できるためです。ビューにアタッチする前に(viewDidLoadメソッドが呼び出される前に)ViewModelを割り当てます。

一方、ViewControllerのビューを別のビューにアタッチしてviewDidLoadを呼び出すこともできますが、viewModelの場合その時点で設定されていない場合、何も起こりません。

そのため、最初に、UIを満たすためにデータがすべて設定されているかどうかを確認する必要があります。予期しない使用からコードを保護することが重要です。

したがって、fillUIに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

ここで、pauseButtonPressを実装します方法:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

今必要なのは、実際のviewModelを設定することだけです。このViewControllerのプロパティ。これは「外部から」行います。

開くHomeViewController.swift ViewModelをファイルしてコメントを外します。 showGameScoreboardEditorViewControllerで行を作成および設定します方法:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

次に、アプリを実行します。次のようになります。

iOSアプリ

スコア、時間、およびチーム名を担当する中央のビューには、InterfaceBuilderで設定された値が表示されなくなりました。

これで、実際のModelオブジェクト(Gameオブジェクト)からデータを取得するViewModelオブジェクト自体からの値が表示されます。

優秀な!しかし、プレイヤーの見解はどうですか?これらのボタンはまだ何もしません。

プレーヤーの動きの追跡には6つのビューがあることを知っています。

PlayerScoreboardMoveEditorViewという名前の別のサブビューを作成しましたそのため、今のところ実際のデータには何もせず、PlayerScoreboardMoveEditorView.xib内のInterfaceBuilderを介して設定された静的な値を表示しますファイル。

あなたはそれにいくつかのデータを与える必要があります。

GameScoreboardEditorViewControllerで行ったのと同じ方法で行いますおよびGameScoreboardEditorViewModel

XcodeプロジェクトでViewModelグループを開き、ここで新しいプロトコルを定義します。

PlayerScoreboardMoveEditorViewModel.swiftという名前の新しいファイルを作成し、その中に次のコードを配置します。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

このViewModelプロトコルは、親ビューで行ったのと同じように、PlayerScoreboardMoveEditorViewに合うように設計されていますGameScoreboardEditorViewController

ユーザーが実行できる5つの異なる動きの値が必要であり、ユーザーがアクションボタンの1つに触れたときに反応する必要があります。 Stringも必要ですプレイヤー名。

これを行った後、親ビュー(GameScoreboardEditorViewController)で行ったのと同じように、このプロトコルを実装する具象クラスを作成します。

次に、このプロトコルの実装を作成します。新しいファイルを作成し、PlayerScoreboardMoveEditorViewModelFromPlayer.swiftという名前を付けて、このオブジェクトをNSObjectのサブクラスにします。また、PlayerScoreboardMoveEditorViewModelに準拠するようにしますプロトコル:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

ここで、このインスタンスを「外部から」作成するオブジェクトを用意し、それをPlayerScoreboardMoveEditorView内のプロパティとして設定する必要があります。

覚えておいてくださいHomeViewController viewModelの設定を担当しましたGameScoreboardEditorViewControllerのプロパティ?

同様に、GameScoreboardEditorViewController PlayerScoreboardMoveEditorViewの親ビューですそしてそれGameScoreboardEditorViewController PlayerScoreboardMoveEditorViewModelの作成を担当しますオブジェクト。

GameScoreboardEditorViewModelを拡張する必要があります最初。

GameScoreboardEditorViewMode lを開き、次の2つのプロパティを追加します。

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

また、GameScoreboardEditorViewModelFromGameを更新しますinitWithGameのすぐ上にあるこれらの2つのプロパティ方法:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

initWithGame内に次の2行を追加します。

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

そしてもちろん、不足しているplayerViewModelsWithPlayersを追加します方法:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

すごい!

ビューモデル(GameScoreboardEditorViewModel)をホームプレーヤーとアウェイプレーヤーの配列で更新しました。これらの2つの配列を埋める必要があります。

これは、これを使用したのと同じ場所で行いますviewModel UIを埋めるために。

開くGameScoreboardEditorViewController fillUIに移動します方法。メソッドの最後に次の行を追加します。

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

今のところ、実際のviewModelを追加しなかったため、ビルドエラーが発生しましたPlayerScoreboardMoveEditorView内のプロパティ。

init method inside the PlayerScoreboardMoveEditorView`の上に次のコードを追加します。

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

そしてfillUIを実装します方法:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

最後に、アプリを実行して、UI要素のデータがGameからの実際のデータであるかどうかを確認します。オブジェクト。

iOSアプリ

この時点で、MVVMデザインパターンを使用する機能的なアプリができました。

モデルをビューからうまく非表示にし、ビューはMVCで慣れているよりもはるかにシンプルです。

ここまでで、ViewとそのViewModelを含むアプリを作成しました。

そのビューには、ViewModelを持つ同じサブビュー(プレーヤービュー)の6つのインスタンスもあります。

ただし、お気づきかもしれませんが、UIに表示できるデータは(fillUIメソッドで)1回のみであり、そのデータは静的です。

ビューのデータがそのビューの存続期間中に変更されない場合は、この方法でMVVMを使用するための優れたクリーンなソリューションがあります。

ViewModelを動的にする

データが変更されるため、ViewModelを動的にする必要があります。

これが意味するのは、モデルが変更されると、ViewModelはそのパブリックプロパティ値を変更する必要があるということです。変更をビューに伝播します。ビューはUIを更新します。

これを行うには多くの方法があります。

モデルが変更されると、ViewModelが最初に通知されます。

変更内容をビューまで伝達するには、何らかのメカニズムが必要です。

いくつかのオプションが含まれます RxSwift 、これはかなり大きなライブラリであり、慣れるのに少し時間がかかります。

ViewModelは、プロパティ値の変更ごとにNSNotificationを起動する可能性がありますが、これにより、通知のサブスクライブやビューの割り当て解除時のサブスクライブ解除など、追加の処理が必要な多くのコードが追加されます。

Key-Value-Observing(KVO) 別のオプションですが、ユーザーはそのAPIが派手ではないことを確認します。

このチュートリアルでは、Swiftのジェネリックスとクロージャーを使用します。これらは、 バインディング、ジェネリック、Swift、MVVMの記事

それでは、サンプルアプリに戻りましょう。

ViewModelプロジェクトグループに移動し、新しいSwiftファイルDynamic.swiftを作成します。

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

このクラスは、ビューのライフサイクル中に変更されると予想されるViewModelのプロパティに使用します。

まず、PlayerScoreboardMoveEditorViewから始めますおよびそのViewModel、PlayerScoreboardMoveEditorViewModel

開くPlayerScoreboardMoveEditorViewModelそしてその特性を見てください。

playerNameだから変更される予定はありません。そのままにしておくことができます。

他の5つのプロパティ(5つの移動タイプ)が変更されるため、それについて何かを行う必要があります。ソリューション?上記のDynamicプロジェクトに追加したばかりのクラス。

内部PlayerScoreboardMoveEditorViewModel移動カウントを表す5つの文字列の定義を削除し、次のように置き換えます。

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

ViewModelプロトコルは次のようになります。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

これDynamic typeを使用すると、その特定のプロパティの値を変更すると同時に、change-listenerオブジェクト(この場合はビュー)に通知できます。

ここで、実際のViewModel実装を更新しますPlayerScoreboardMoveEditorViewModelFromPlayer

これを置き換えます:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

次のように:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

注:これらのプロパティをletで定数として宣言しても問題ありません。実際のプロパティは変更しないためです。 valueを変更しますDynamicのプロパティオブジェクト。

Dynamicを初期化しなかったため、ビルドエラーが発生しましたオブジェクト。

PlayerScoreboardMoveEditorViewModelFromPlayerのinitメソッド内で、moveプロパティの初期化を次のように置き換えます。

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

内部PlayerScoreboardMoveEditorViewModelFromPlayer makeMoveに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

ご覧のとおり、Dynamicのインスタンスを作成しましたクラスを割り当て、String値。データを更新する必要がある場合は、Dynamicを変更しないでください。プロパティ自体;むしろ更新しますvalueプロパティ。

すごい! PlayerScoreboardMoveEditorViewModel今はダイナミックです。

それを利用して、実際にこれらの変更をリッスンするビューに移動しましょう。

開くPlayerScoreboardMoveEditorViewとそのfillUIメソッド(String値をDynamicオブジェクトタイプに割り当てようとしているため、この時点でこのメソッドにビルドエラーが表示されるはずです。)

「エラーのある」行を置き換えます。

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

次のように:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

次に、移動アクションを表す5つのメソッドを実装します( ボタンアクション セクション):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

アプリを実行し、いくつかの移動ボタンをクリックします。アクションボタンをクリックすると、プレーヤービュー内のカウンター値がどのように変化するかがわかります。

iOSアプリ

PlayerScoreboardMoveEditorViewで終了ですおよびPlayerScoreboardMoveEditorViewModel

これは簡単でした。

ここで、メインビュー(GameScoreboardEditorViewController)でも同じことを行う必要があります。

まず、GameScoreboardEditorViewModelを開きますビューのライフサイクル中にどの値が変化すると予想されるかを確認します。

timescoreisFinishedisPausedを置き換えますDynamicを使用した定義バージョン:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

ViewModel実装(GameScoreboardEditorViewModelFromGame)に移動し、プロトコルで宣言されているプロパティで同じことを行います。

これを置き換えます:

var time: String var score: String var isFinished: Bool var isPaused: Bool

次のように:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

ViewModelのタイプをStringから変更したため、いくつかのエラーが発生します。およびBoolDynamicおよびDynamic

それを修正しましょう。

togglePauseを修正します次のように置き換えてください。

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

唯一の変更点は、プロパティ値をプロパティに直接設定しなくなったことです。代わりに、オブジェクトのvalueに設定しますプロパティ。

ここで、initWithGameを修正しますこれを置き換えることによる方法:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

次のように:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

あなたは今ポイントを取得する必要があります。

StringIntなどのプリミティブ値をラップしていますおよびBoolDynamicこれらのオブジェクトのバージョン。これにより、軽量のバインディングメカニズムが提供されます。

修正するエラーがもう1つあります。

startTimerでメソッドの場合、エラー行を次のように置き換えます。

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

プレーヤーのViewModelの場合と同じように、ViewModelを動的にアップグレードしました。ただし、ビューを更新する必要があります(GameScoreboardEditorViewController)。

fillUI全体を置き換えますこれを使った方法:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

唯一の違いは、4つの動的プロパティを変更し、それぞれに変更リスナーを追加したことです。

この時点で、アプリを実行している場合は、 開始/一時停止 ボタンはゲームタイマーを開始および一時停止します。これは、ゲーム中のタイムアウトに使用されます。

ポイントボタン(1および2ポイントボタン)のいずれかを押したときに、UIでスコアが変更されないことを除いて、ほぼ完了です。

これは、基になるGameでスコアの変更を実際に伝播していないためです。 ViewModelまでのモデルオブジェクト。

だから、Gameを開きます少し調べるためのモデルオブジェクト。そのupdateScoreを確認してください方法。

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

この方法は2つの重要なことを行います。

まず、isFinishedを設定しますプロパティto true両方のチームのスコアに基づいてゲームが終了した場合。

その後、スコアが変更されたという通知を投稿します。この通知はGameScoreboardEditorViewModelFromGameで聞きます通知ハンドラーメソッドで動的スコア値を更新します。

initWithGameの下部にこの行を追加しますメソッド(エラーを回避するためにsuper.init()呼び出しを忘れないでください):

super.init() subscribeToNotifications()

以下initWithGameメソッド、追加deinitクリーンアップを適切に実行し、NotificationCenterによって引き起こされるクラッシュを回避する必要があるためです。

deinit { unsubscribeFromNotifications() }

最後に、これらのメソッドの実装を追加します。このセクションをdeinitのすぐ下に追加します方法:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

次に、アプリを実行し、プレーヤーのビューをクリックしてスコアを変更します。すでに動的に接続しているのでscoreおよびisFinished Viewを使用したViewModelでは、ViewModel内のスコア値を変更するとすべてが機能するはずです。

アプリをさらに改善する方法

常に改善の余地がありますが、これはこのチュートリアルの範囲外です。

たとえば、ゲームが終了したとき(チームの1つが15ポイントに達したとき)に時間を自動的に停止するのではなく、プレーヤーのビューを非表示にするだけです。

必要に応じてアプリで遊んだり、アップグレードして「ゲームクリエイター」ビューを表示したりできます。このビューでは、ゲームの作成、チーム名の割り当て、プレーヤー名の割り当て、Gameの作成が行われます。 GameScoreboardEditorViewControllerを提示するために使用できるオブジェクト。

UITableViewを使用する別の「ゲームリスト」ビューを作成できます。進行中の複数のゲームを表示し、テーブルセルに詳細情報を表示します。セル選択では、GameScoreboardEditorViewControllerを表示できます選択したGameで。

GameLibraryすでに実装されています。そのライブラリ参照をイニシャライザのViewModelオブジェクトに渡すことを忘れないでください。たとえば、「ゲームクリエイター」のViewModelには、GameLibraryのインスタンスが必要です。作成されたGameを挿入できるように初期化子を通過しましたライブラリへのオブジェクト。 「ゲームリスト」のViewModelでも、ライブラリからすべてのゲームをフェッチするためにこの参照が必要になります。これはUITableViewで必要になります。

アイデアは、ViewModel内のすべてのダーティ(非UI)作業を非表示にし、UI(ビュー)が準備されたプレゼンテーションデータでのみ動作するようにすることです。

今何?

MVVMに慣れたら、を使用してさらに改善することができます ボブおじさんのクリーンアーキテクチャルール

追加の良い読み物は、Androidアーキテクチャに関する3部構成のチュートリアルです。

例はJava(Android用)で書かれており、Java(Swiftに非常に近く、Objective-CはJavaに近い)に精通している場合は、ViewModelオブジェクト内でコードをさらにリファクタリングする方法についてのアイデアが得られます。 iOSモジュール(UIKitまたはCoreLocationなど)をインポートしないこと。

これらのiOSモジュールは、純粋なNSObjectsの背後に隠すことができます。これは、コードの再利用に適しています。

MVVMはほとんどの人にとって良い選択です ios アプリ、そしてうまくいけば、あなたはあなたの次のプロジェクトでそれを試してみるでしょう。または、UIViewControllerを作成するときに、現在のプロジェクトで試してみてください。

関連: 静的パターンの操作:SwiftMVVMチュートリアル } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text =

Swiftチュートリアル:MVVMデザインパターンの概要

つまり、新しいiOSプロジェクトを開始すると、デザイナーから必要なすべての情報を受け取ります.pdfおよび.sketchドキュメントがあり、この新しいアプリをどのように構築するかについてのビジョンはすでにあります。

デザイナーのスケッチからViewControllerへのUI画面の転送を開始します.swift.xibおよび.storyboardファイル。

UITextFieldここで、UITableViewそこに、さらにいくつかUILabelsそしてUIButtonsのピンチ。 IBOutletsおよびIBActions含まれています。よし、まだUIゾーンにいる。



ただし、これらすべてのUI要素を使用して何かを行うときが来ました。 UIButtons指で触れるとUILabelsおよびUITableViews何をどの形式で表示するかを誰かに教える必要があります。

突然、3,000行を超えるコードが作成されました。

3,000行のSwiftコード

あなたはたくさんのスパゲッティコードになってしまいました。

これを解決するための最初のステップは、 Model-View-Controller (MVC)デザインパターン。ただし、このパターンには独自の問題があります。来る Model-View-ViewModel (MVVM)日を節約するデザインパターン。

スパゲッティコードの取り扱い

あっという間に、あなたのスタートViewControllerスマートになりすぎて、巨大になりすぎました。

ネットワークコード、データ解析コード、UIプレゼンテーションのデータ調整コード、アプリの状態通知、UIの状態変化。 if -ology内に閉じ込められたすべてのコードは、再利用できず、このプロジェクトにのみ収まります。

あなたのViewControllerコードは悪名高いスパゲッティコードになりました。

どうしてこうなりました?

考えられる理由は次のようなものです。

バックエンドデータがUITableView 内でどのように動作しているかを確認するために急いでいたので、数行のネットワークコードを 臨時雇用者 ViewControllerのメソッドそれをフェッチするだけです.jsonネットワークから。次に、その.json内のデータを処理する必要があったので、さらに別のデータを作成しました 臨時雇用者 それを達成する方法。または、さらに悪いことに、同じ方法でそれを行いました。

ViewControllerユーザー認証コードが登場したとき、成長を続けました。その後、データ形式が変化し始め、UIが進化し、いくつかの根本的な変更が必要になりました。そして、すでに大規模なif -ologyにifを追加し続けました。

しかし、どうしてUIViewController手に負えなくなったものは何ですか?

UIViewController UIコードの作業を開始するための論理的な場所です。これは、iOSデバイスでアプリを使用しているときに表示される物理的な画面を表します。 AppleでさえUIViewControllersを使用しています異なるアプリとアニメーション化されたUIを切り替えると、メインシステムアプリで。

AppleはUIの抽象化をUIViewControllerの内部に基づいています。これは、iOSのUIコードのコアであり、 MVC デザインパターン。

関連: iOS開発者が犯していることを知らない10の最も一般的な間違い

MVCデザインパターンへのアップグレード

MVCデザインパターン

MVCデザインパターンでは、 見る 非アクティブであると想定され、オンデマンドで準備されたデータのみを表示します。

コントローラ に取り組む必要があります モデル 準備するためのデータ ビュー 、次にそのデータを表示します。

見る 通知する責任もあります コントローラ ユーザーのタッチなどのアクションについて。

前述のように、UIViewController通常、UI画面を構築するための開始点です。その名前には、「ビュー」と「コントローラー」の両方が含まれていることに注意してください。これは、「ビューを制御する」ことを意味します。 「コントローラー」と「ビュー」の両方のコードを内部に含める必要があるという意味ではありません。

このビューとコントローラーコードの混合は、IBOutletsを移動したときによく発生します。 UIViewController内の小さなサブビューを作成し、UIViewControllerから直接それらのサブビューを操作します。代わりに、そのコードをカスタムの中にラップする必要がありますUIViewサブクラス。

これにより、ビューとコントローラーのコードパスが交差する可能性があることは簡単にわかります。

救助へのMVVM

これは、 MVVM パターンが重宝します。

UIViewController以降であるはずです コントローラ MVCパターンで、すでに多くのことを行っています ビュー 、それらをにマージできます 見る 私たちの新しいパターンの- MVVM

MVVMデザインパターン

MVVMデザインパターンでは、 モデル MVCパターンと同じです。単純なデータを表します。

見る UIViewで表されますまたはUIViewController .xibを伴うオブジェクトおよび.storyboard準備されたデータのみを表示するファイル。 (たとえば、ビュー内にNSDateFormatterコードを含めたくありません。)

から来る単純なフォーマットされた文字列のみ ViewModel

ViewModel すべての非同期ネットワークコード、ビジュアルプレゼンテーション用のデータ準備コード、およびリッスンするコードを非表示にします モデル 変化します。これらはすべて、この特定のものに合うようにモデル化された明確に定義されたAPIの背後に隠されています 見る

MVVMを使用する利点の1つは、テストです。以来 ViewModel 純粋ですNSObject (またはstructなど)、UIKitとは結合されていませんコードを使用すると、UIコードに影響を与えることなく、単体テストでより簡単にテストできます。

さて、 見るUIViewController / UIView)がはるかに簡単になりました ViewModel 間の接着剤として機能します モデル そして 見る

SwiftでのMVVMの適用

SwiftのMVVM

MVVMの動作を示すために、このチュートリアル用に作成されたサンプルXcodeプロジェクトをダウンロードして調べることができます。 ここに 。このプロジェクトでは、Swift3とXcode8.1を使用しています。

プロジェクトには2つのバージョンがあります。 スターター そして 終了しました

ザ・ 終了しました バージョンは完成したミニアプリケーションであり、 スターター 同じプロジェクトですが、メソッドとオブジェクトが実装されていません。

まず、ダウンロードすることをお勧めします スターター プロジェクトを作成し、このチュートリアルに従ってください。後で使用するためにプロジェクトのクイックリファレンスが必要な場合は、 終了しました 事業。

チュートリアルプロジェクトの紹介

チュートリアルプロジェクトは、ゲーム中のプレーヤーのアクションを追跡するためのバスケットボールアプリケーションです。

バスケットボールアプリケーション

これは、ユーザーの動きとピックアップゲームの全体的なスコアをすばやく追跡するために使用されます。

15のスコア(少なくとも2ポイントの差がある)に達するまで、2つのチームがプレーします。各プレーヤーは1ポイントから2ポイントを獲得でき、各プレーヤーはアシスト、リバウンド、ファウルを行うことができます。

プロジェクト階層は次のようになります。

プロジェクト階層

モデル

見る

ViewModel

ダウンロードしたXcodeプロジェクトには、 見る オブジェクト(UIViewおよびUIViewController)。プロジェクトには、データを提供する方法の1つをデモするために作成されたカスタムメイドのオブジェクトも含まれています。 ViewModel オブジェクト(Servicesグループ)。

Extensionsグループには、このチュートリアルの範囲外であり、自明であるUIコードの便利な拡張機能が含まれています。

この時点でアプリを実行すると、完成したUIが表示されますが、ユーザーがボタンを押しても何も起こりません。

これは、ビューとIBActionsのみを作成したためです。それらをアプリロジックに接続せず、UI要素にモデルからのデータ(後で学習するようにGameオブジェクトから)を入力しません。

ViewとModelをViewModelで接続する

MVVMデザインパターンでは、Viewはモデルについて何も知らないはずです。 Viewが知っているのは、ViewModelの操作方法だけです。

ビューを調べることから始めます。

GameScoreboardEditorViewController.swiftでファイル、fillUIこの時点でメソッドは空です。これは、UIにデータを入力する場所です。これを実現するには、ViewControllerのデータを提供する必要があります。これは、ViewModelオブジェクトを使用して行います。

まず、このViewControllerに必要なすべてのデータを含むViewModelオブジェクトを作成します。

空になるViewModelXcodeプロジェクトグループに移動し、GameScoreboardEditorViewModel.swiftを作成します。ファイルを作成し、プロトコルにします。

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

このようなプロトコルを使用すると、物事がきれいに保たれます。使用するデータのみを定義する必要があります。

次に、このプロトコルの実装を作成します。

GameScoreboardEditorViewModelFromGame.swiftという名前の新しいファイルを作成し、このオブジェクトをNSObjectのサブクラスにします。

また、GameScoreboardEditorViewModelに準拠するようにしますプロトコル:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

ViewModelがイニシャライザを介して機能するために必要なすべてを提供したことに注意してください。

あなたはそれにGameを提供しましたこのViewModelの下にあるモデルであるオブジェクト。

ここでアプリを実行しても、このViewModelデータをビュー自体に接続していないため、アプリは機能しません。

だから、GameScoreboardEditorViewController.swiftに戻ってくださいファイルを作成し、viewModelという名前のパブリックプロパティを作成します。

タイプGameScoreboardEditorViewModelにします。

viewDidLoadの直前に配置しますGameScoreboardEditorViewController.swift内のメソッド。

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

次に、fillUIを実装する必要があります方法。

このメソッドが2つの場所、viewModelからどのように呼び出されるかに注目してください。プロパティオブザーバー(didSet)およびviewDidLoad方法。これは、ViewControllerを作成できるためです。ビューにアタッチする前に(viewDidLoadメソッドが呼び出される前に)ViewModelを割り当てます。

一方、ViewControllerのビューを別のビューにアタッチしてviewDidLoadを呼び出すこともできますが、viewModelの場合その時点で設定されていない場合、何も起こりません。

そのため、最初に、UIを満たすためにデータがすべて設定されているかどうかを確認する必要があります。予期しない使用からコードを保護することが重要です。

したがって、fillUIに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

ここで、pauseButtonPressを実装します方法:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

今必要なのは、実際のviewModelを設定することだけです。このViewControllerのプロパティ。これは「外部から」行います。

開くHomeViewController.swift ViewModelをファイルしてコメントを外します。 showGameScoreboardEditorViewControllerで行を作成および設定します方法:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

次に、アプリを実行します。次のようになります。

iOSアプリ

スコア、時間、およびチーム名を担当する中央のビューには、InterfaceBuilderで設定された値が表示されなくなりました。

これで、実際のModelオブジェクト(Gameオブジェクト)からデータを取得するViewModelオブジェクト自体からの値が表示されます。

優秀な!しかし、プレイヤーの見解はどうですか?これらのボタンはまだ何もしません。

プレーヤーの動きの追跡には6つのビューがあることを知っています。

PlayerScoreboardMoveEditorViewという名前の別のサブビューを作成しましたそのため、今のところ実際のデータには何もせず、PlayerScoreboardMoveEditorView.xib内のInterfaceBuilderを介して設定された静的な値を表示しますファイル。

あなたはそれにいくつかのデータを与える必要があります。

GameScoreboardEditorViewControllerで行ったのと同じ方法で行いますおよびGameScoreboardEditorViewModel

XcodeプロジェクトでViewModelグループを開き、ここで新しいプロトコルを定義します。

PlayerScoreboardMoveEditorViewModel.swiftという名前の新しいファイルを作成し、その中に次のコードを配置します。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

このViewModelプロトコルは、親ビューで行ったのと同じように、PlayerScoreboardMoveEditorViewに合うように設計されていますGameScoreboardEditorViewController

ユーザーが実行できる5つの異なる動きの値が必要であり、ユーザーがアクションボタンの1つに触れたときに反応する必要があります。 Stringも必要ですプレイヤー名。

これを行った後、親ビュー(GameScoreboardEditorViewController)で行ったのと同じように、このプロトコルを実装する具象クラスを作成します。

次に、このプロトコルの実装を作成します。新しいファイルを作成し、PlayerScoreboardMoveEditorViewModelFromPlayer.swiftという名前を付けて、このオブジェクトをNSObjectのサブクラスにします。また、PlayerScoreboardMoveEditorViewModelに準拠するようにしますプロトコル:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

ここで、このインスタンスを「外部から」作成するオブジェクトを用意し、それをPlayerScoreboardMoveEditorView内のプロパティとして設定する必要があります。

覚えておいてくださいHomeViewController viewModelの設定を担当しましたGameScoreboardEditorViewControllerのプロパティ?

同様に、GameScoreboardEditorViewController PlayerScoreboardMoveEditorViewの親ビューですそしてそれGameScoreboardEditorViewController PlayerScoreboardMoveEditorViewModelの作成を担当しますオブジェクト。

GameScoreboardEditorViewModelを拡張する必要があります最初。

GameScoreboardEditorViewMode lを開き、次の2つのプロパティを追加します。

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

また、GameScoreboardEditorViewModelFromGameを更新しますinitWithGameのすぐ上にあるこれらの2つのプロパティ方法:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

initWithGame内に次の2行を追加します。

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

そしてもちろん、不足しているplayerViewModelsWithPlayersを追加します方法:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

すごい!

ビューモデル(GameScoreboardEditorViewModel)をホームプレーヤーとアウェイプレーヤーの配列で更新しました。これらの2つの配列を埋める必要があります。

これは、これを使用したのと同じ場所で行いますviewModel UIを埋めるために。

開くGameScoreboardEditorViewController fillUIに移動します方法。メソッドの最後に次の行を追加します。

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

今のところ、実際のviewModelを追加しなかったため、ビルドエラーが発生しましたPlayerScoreboardMoveEditorView内のプロパティ。

init method inside the PlayerScoreboardMoveEditorView`の上に次のコードを追加します。

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

そしてfillUIを実装します方法:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

最後に、アプリを実行して、UI要素のデータがGameからの実際のデータであるかどうかを確認します。オブジェクト。

iOSアプリ

この時点で、MVVMデザインパターンを使用する機能的なアプリができました。

モデルをビューからうまく非表示にし、ビューはMVCで慣れているよりもはるかにシンプルです。

ここまでで、ViewとそのViewModelを含むアプリを作成しました。

そのビューには、ViewModelを持つ同じサブビュー(プレーヤービュー)の6つのインスタンスもあります。

ただし、お気づきかもしれませんが、UIに表示できるデータは(fillUIメソッドで)1回のみであり、そのデータは静的です。

ビューのデータがそのビューの存続期間中に変更されない場合は、この方法でMVVMを使用するための優れたクリーンなソリューションがあります。

ViewModelを動的にする

データが変更されるため、ViewModelを動的にする必要があります。

これが意味するのは、モデルが変更されると、ViewModelはそのパブリックプロパティ値を変更する必要があるということです。変更をビューに伝播します。ビューはUIを更新します。

これを行うには多くの方法があります。

モデルが変更されると、ViewModelが最初に通知されます。

変更内容をビューまで伝達するには、何らかのメカニズムが必要です。

いくつかのオプションが含まれます RxSwift 、これはかなり大きなライブラリであり、慣れるのに少し時間がかかります。

ViewModelは、プロパティ値の変更ごとにNSNotificationを起動する可能性がありますが、これにより、通知のサブスクライブやビューの割り当て解除時のサブスクライブ解除など、追加の処理が必要な多くのコードが追加されます。

Key-Value-Observing(KVO) 別のオプションですが、ユーザーはそのAPIが派手ではないことを確認します。

このチュートリアルでは、Swiftのジェネリックスとクロージャーを使用します。これらは、 バインディング、ジェネリック、Swift、MVVMの記事

それでは、サンプルアプリに戻りましょう。

ViewModelプロジェクトグループに移動し、新しいSwiftファイルDynamic.swiftを作成します。

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

このクラスは、ビューのライフサイクル中に変更されると予想されるViewModelのプロパティに使用します。

まず、PlayerScoreboardMoveEditorViewから始めますおよびそのViewModel、PlayerScoreboardMoveEditorViewModel

開くPlayerScoreboardMoveEditorViewModelそしてその特性を見てください。

playerNameだから変更される予定はありません。そのままにしておくことができます。

他の5つのプロパティ(5つの移動タイプ)が変更されるため、それについて何かを行う必要があります。ソリューション?上記のDynamicプロジェクトに追加したばかりのクラス。

内部PlayerScoreboardMoveEditorViewModel移動カウントを表す5つの文字列の定義を削除し、次のように置き換えます。

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

ViewModelプロトコルは次のようになります。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

これDynamic typeを使用すると、その特定のプロパティの値を変更すると同時に、change-listenerオブジェクト(この場合はビュー)に通知できます。

ここで、実際のViewModel実装を更新しますPlayerScoreboardMoveEditorViewModelFromPlayer

これを置き換えます:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

次のように:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

注:これらのプロパティをletで定数として宣言しても問題ありません。実際のプロパティは変更しないためです。 valueを変更しますDynamicのプロパティオブジェクト。

Dynamicを初期化しなかったため、ビルドエラーが発生しましたオブジェクト。

PlayerScoreboardMoveEditorViewModelFromPlayerのinitメソッド内で、moveプロパティの初期化を次のように置き換えます。

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

内部PlayerScoreboardMoveEditorViewModelFromPlayer makeMoveに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

ご覧のとおり、Dynamicのインスタンスを作成しましたクラスを割り当て、String値。データを更新する必要がある場合は、Dynamicを変更しないでください。プロパティ自体;むしろ更新しますvalueプロパティ。

すごい! PlayerScoreboardMoveEditorViewModel今はダイナミックです。

それを利用して、実際にこれらの変更をリッスンするビューに移動しましょう。

開くPlayerScoreboardMoveEditorViewとそのfillUIメソッド(String値をDynamicオブジェクトタイプに割り当てようとしているため、この時点でこのメソッドにビルドエラーが表示されるはずです。)

「エラーのある」行を置き換えます。

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

次のように:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

次に、移動アクションを表す5つのメソッドを実装します( ボタンアクション セクション):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

アプリを実行し、いくつかの移動ボタンをクリックします。アクションボタンをクリックすると、プレーヤービュー内のカウンター値がどのように変化するかがわかります。

iOSアプリ

PlayerScoreboardMoveEditorViewで終了ですおよびPlayerScoreboardMoveEditorViewModel

これは簡単でした。

ここで、メインビュー(GameScoreboardEditorViewController)でも同じことを行う必要があります。

まず、GameScoreboardEditorViewModelを開きますビューのライフサイクル中にどの値が変化すると予想されるかを確認します。

timescoreisFinishedisPausedを置き換えますDynamicを使用した定義バージョン:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

ViewModel実装(GameScoreboardEditorViewModelFromGame)に移動し、プロトコルで宣言されているプロパティで同じことを行います。

これを置き換えます:

var time: String var score: String var isFinished: Bool var isPaused: Bool

次のように:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

ViewModelのタイプをStringから変更したため、いくつかのエラーが発生します。およびBoolDynamicおよびDynamic

それを修正しましょう。

togglePauseを修正します次のように置き換えてください。

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

唯一の変更点は、プロパティ値をプロパティに直接設定しなくなったことです。代わりに、オブジェクトのvalueに設定しますプロパティ。

ここで、initWithGameを修正しますこれを置き換えることによる方法:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

次のように:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

あなたは今ポイントを取得する必要があります。

StringIntなどのプリミティブ値をラップしていますおよびBoolDynamicこれらのオブジェクトのバージョン。これにより、軽量のバインディングメカニズムが提供されます。

修正するエラーがもう1つあります。

startTimerでメソッドの場合、エラー行を次のように置き換えます。

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

プレーヤーのViewModelの場合と同じように、ViewModelを動的にアップグレードしました。ただし、ビューを更新する必要があります(GameScoreboardEditorViewController)。

fillUI全体を置き換えますこれを使った方法:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

唯一の違いは、4つの動的プロパティを変更し、それぞれに変更リスナーを追加したことです。

この時点で、アプリを実行している場合は、 開始/一時停止 ボタンはゲームタイマーを開始および一時停止します。これは、ゲーム中のタイムアウトに使用されます。

ポイントボタン(1および2ポイントボタン)のいずれかを押したときに、UIでスコアが変更されないことを除いて、ほぼ完了です。

これは、基になるGameでスコアの変更を実際に伝播していないためです。 ViewModelまでのモデルオブジェクト。

だから、Gameを開きます少し調べるためのモデルオブジェクト。そのupdateScoreを確認してください方法。

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

この方法は2つの重要なことを行います。

まず、isFinishedを設定しますプロパティto true両方のチームのスコアに基づいてゲームが終了した場合。

その後、スコアが変更されたという通知を投稿します。この通知はGameScoreboardEditorViewModelFromGameで聞きます通知ハンドラーメソッドで動的スコア値を更新します。

initWithGameの下部にこの行を追加しますメソッド(エラーを回避するためにsuper.init()呼び出しを忘れないでください):

super.init() subscribeToNotifications()

以下initWithGameメソッド、追加deinitクリーンアップを適切に実行し、NotificationCenterによって引き起こされるクラッシュを回避する必要があるためです。

deinit { unsubscribeFromNotifications() }

最後に、これらのメソッドの実装を追加します。このセクションをdeinitのすぐ下に追加します方法:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

次に、アプリを実行し、プレーヤーのビューをクリックしてスコアを変更します。すでに動的に接続しているのでscoreおよびisFinished Viewを使用したViewModelでは、ViewModel内のスコア値を変更するとすべてが機能するはずです。

アプリをさらに改善する方法

常に改善の余地がありますが、これはこのチュートリアルの範囲外です。

たとえば、ゲームが終了したとき(チームの1つが15ポイントに達したとき)に時間を自動的に停止するのではなく、プレーヤーのビューを非表示にするだけです。

必要に応じてアプリで遊んだり、アップグレードして「ゲームクリエイター」ビューを表示したりできます。このビューでは、ゲームの作成、チーム名の割り当て、プレーヤー名の割り当て、Gameの作成が行われます。 GameScoreboardEditorViewControllerを提示するために使用できるオブジェクト。

UITableViewを使用する別の「ゲームリスト」ビューを作成できます。進行中の複数のゲームを表示し、テーブルセルに詳細情報を表示します。セル選択では、GameScoreboardEditorViewControllerを表示できます選択したGameで。

GameLibraryすでに実装されています。そのライブラリ参照をイニシャライザのViewModelオブジェクトに渡すことを忘れないでください。たとえば、「ゲームクリエイター」のViewModelには、GameLibraryのインスタンスが必要です。作成されたGameを挿入できるように初期化子を通過しましたライブラリへのオブジェクト。 「ゲームリスト」のViewModelでも、ライブラリからすべてのゲームをフェッチするためにこの参照が必要になります。これはUITableViewで必要になります。

アイデアは、ViewModel内のすべてのダーティ(非UI)作業を非表示にし、UI(ビュー)が準備されたプレゼンテーションデータでのみ動作するようにすることです。

今何?

MVVMに慣れたら、を使用してさらに改善することができます ボブおじさんのクリーンアーキテクチャルール

追加の良い読み物は、Androidアーキテクチャに関する3部構成のチュートリアルです。

例はJava(Android用)で書かれており、Java(Swiftに非常に近く、Objective-CはJavaに近い)に精通している場合は、ViewModelオブジェクト内でコードをさらにリファクタリングする方法についてのアイデアが得られます。 iOSモジュール(UIKitまたはCoreLocationなど)をインポートしないこと。

これらのiOSモジュールは、純粋なNSObjectsの背後に隠すことができます。これは、コードの再利用に適しています。

MVVMはほとんどの人にとって良い選択です ios アプリ、そしてうまくいけば、あなたはあなたの次のプロジェクトでそれを試してみるでしょう。または、UIViewControllerを作成するときに、現在のプロジェクトで試してみてください。

関連: 静的パターンの操作:SwiftMVVMチュートリアル } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text =

Swiftチュートリアル:MVVMデザインパターンの概要

つまり、新しいiOSプロジェクトを開始すると、デザイナーから必要なすべての情報を受け取ります.pdfおよび.sketchドキュメントがあり、この新しいアプリをどのように構築するかについてのビジョンはすでにあります。

デザイナーのスケッチからViewControllerへのUI画面の転送を開始します.swift.xibおよび.storyboardファイル。

UITextFieldここで、UITableViewそこに、さらにいくつかUILabelsそしてUIButtonsのピンチ。 IBOutletsおよびIBActions含まれています。よし、まだUIゾーンにいる。



ただし、これらすべてのUI要素を使用して何かを行うときが来ました。 UIButtons指で触れるとUILabelsおよびUITableViews何をどの形式で表示するかを誰かに教える必要があります。

突然、3,000行を超えるコードが作成されました。

3,000行のSwiftコード

あなたはたくさんのスパゲッティコードになってしまいました。

これを解決するための最初のステップは、 Model-View-Controller (MVC)デザインパターン。ただし、このパターンには独自の問題があります。来る Model-View-ViewModel (MVVM)日を節約するデザインパターン。

スパゲッティコードの取り扱い

あっという間に、あなたのスタートViewControllerスマートになりすぎて、巨大になりすぎました。

ネットワークコード、データ解析コード、UIプレゼンテーションのデータ調整コード、アプリの状態通知、UIの状態変化。 if -ology内に閉じ込められたすべてのコードは、再利用できず、このプロジェクトにのみ収まります。

あなたのViewControllerコードは悪名高いスパゲッティコードになりました。

どうしてこうなりました?

考えられる理由は次のようなものです。

バックエンドデータがUITableView 内でどのように動作しているかを確認するために急いでいたので、数行のネットワークコードを 臨時雇用者 ViewControllerのメソッドそれをフェッチするだけです.jsonネットワークから。次に、その.json内のデータを処理する必要があったので、さらに別のデータを作成しました 臨時雇用者 それを達成する方法。または、さらに悪いことに、同じ方法でそれを行いました。

ViewControllerユーザー認証コードが登場したとき、成長を続けました。その後、データ形式が変化し始め、UIが進化し、いくつかの根本的な変更が必要になりました。そして、すでに大規模なif -ologyにifを追加し続けました。

しかし、どうしてUIViewController手に負えなくなったものは何ですか?

UIViewController UIコードの作業を開始するための論理的な場所です。これは、iOSデバイスでアプリを使用しているときに表示される物理的な画面を表します。 AppleでさえUIViewControllersを使用しています異なるアプリとアニメーション化されたUIを切り替えると、メインシステムアプリで。

AppleはUIの抽象化をUIViewControllerの内部に基づいています。これは、iOSのUIコードのコアであり、 MVC デザインパターン。

関連: iOS開発者が犯していることを知らない10の最も一般的な間違い

MVCデザインパターンへのアップグレード

MVCデザインパターン

MVCデザインパターンでは、 見る 非アクティブであると想定され、オンデマンドで準備されたデータのみを表示します。

コントローラ に取り組む必要があります モデル 準備するためのデータ ビュー 、次にそのデータを表示します。

見る 通知する責任もあります コントローラ ユーザーのタッチなどのアクションについて。

前述のように、UIViewController通常、UI画面を構築するための開始点です。その名前には、「ビュー」と「コントローラー」の両方が含まれていることに注意してください。これは、「ビューを制御する」ことを意味します。 「コントローラー」と「ビュー」の両方のコードを内部に含める必要があるという意味ではありません。

このビューとコントローラーコードの混合は、IBOutletsを移動したときによく発生します。 UIViewController内の小さなサブビューを作成し、UIViewControllerから直接それらのサブビューを操作します。代わりに、そのコードをカスタムの中にラップする必要がありますUIViewサブクラス。

これにより、ビューとコントローラーのコードパスが交差する可能性があることは簡単にわかります。

救助へのMVVM

これは、 MVVM パターンが重宝します。

UIViewController以降であるはずです コントローラ MVCパターンで、すでに多くのことを行っています ビュー 、それらをにマージできます 見る 私たちの新しいパターンの- MVVM

MVVMデザインパターン

MVVMデザインパターンでは、 モデル MVCパターンと同じです。単純なデータを表します。

見る UIViewで表されますまたはUIViewController .xibを伴うオブジェクトおよび.storyboard準備されたデータのみを表示するファイル。 (たとえば、ビュー内にNSDateFormatterコードを含めたくありません。)

から来る単純なフォーマットされた文字列のみ ViewModel

ViewModel すべての非同期ネットワークコード、ビジュアルプレゼンテーション用のデータ準備コード、およびリッスンするコードを非表示にします モデル 変化します。これらはすべて、この特定のものに合うようにモデル化された明確に定義されたAPIの背後に隠されています 見る

MVVMを使用する利点の1つは、テストです。以来 ViewModel 純粋ですNSObject (またはstructなど)、UIKitとは結合されていませんコードを使用すると、UIコードに影響を与えることなく、単体テストでより簡単にテストできます。

さて、 見るUIViewController / UIView)がはるかに簡単になりました ViewModel 間の接着剤として機能します モデル そして 見る

SwiftでのMVVMの適用

SwiftのMVVM

MVVMの動作を示すために、このチュートリアル用に作成されたサンプルXcodeプロジェクトをダウンロードして調べることができます。 ここに 。このプロジェクトでは、Swift3とXcode8.1を使用しています。

プロジェクトには2つのバージョンがあります。 スターター そして 終了しました

ザ・ 終了しました バージョンは完成したミニアプリケーションであり、 スターター 同じプロジェクトですが、メソッドとオブジェクトが実装されていません。

まず、ダウンロードすることをお勧めします スターター プロジェクトを作成し、このチュートリアルに従ってください。後で使用するためにプロジェクトのクイックリファレンスが必要な場合は、 終了しました 事業。

チュートリアルプロジェクトの紹介

チュートリアルプロジェクトは、ゲーム中のプレーヤーのアクションを追跡するためのバスケットボールアプリケーションです。

バスケットボールアプリケーション

これは、ユーザーの動きとピックアップゲームの全体的なスコアをすばやく追跡するために使用されます。

15のスコア(少なくとも2ポイントの差がある)に達するまで、2つのチームがプレーします。各プレーヤーは1ポイントから2ポイントを獲得でき、各プレーヤーはアシスト、リバウンド、ファウルを行うことができます。

プロジェクト階層は次のようになります。

プロジェクト階層

モデル

見る

ViewModel

ダウンロードしたXcodeプロジェクトには、 見る オブジェクト(UIViewおよびUIViewController)。プロジェクトには、データを提供する方法の1つをデモするために作成されたカスタムメイドのオブジェクトも含まれています。 ViewModel オブジェクト(Servicesグループ)。

Extensionsグループには、このチュートリアルの範囲外であり、自明であるUIコードの便利な拡張機能が含まれています。

この時点でアプリを実行すると、完成したUIが表示されますが、ユーザーがボタンを押しても何も起こりません。

これは、ビューとIBActionsのみを作成したためです。それらをアプリロジックに接続せず、UI要素にモデルからのデータ(後で学習するようにGameオブジェクトから)を入力しません。

ViewとModelをViewModelで接続する

MVVMデザインパターンでは、Viewはモデルについて何も知らないはずです。 Viewが知っているのは、ViewModelの操作方法だけです。

ビューを調べることから始めます。

GameScoreboardEditorViewController.swiftでファイル、fillUIこの時点でメソッドは空です。これは、UIにデータを入力する場所です。これを実現するには、ViewControllerのデータを提供する必要があります。これは、ViewModelオブジェクトを使用して行います。

まず、このViewControllerに必要なすべてのデータを含むViewModelオブジェクトを作成します。

空になるViewModelXcodeプロジェクトグループに移動し、GameScoreboardEditorViewModel.swiftを作成します。ファイルを作成し、プロトコルにします。

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

このようなプロトコルを使用すると、物事がきれいに保たれます。使用するデータのみを定義する必要があります。

次に、このプロトコルの実装を作成します。

GameScoreboardEditorViewModelFromGame.swiftという名前の新しいファイルを作成し、このオブジェクトをNSObjectのサブクラスにします。

また、GameScoreboardEditorViewModelに準拠するようにしますプロトコル:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

ViewModelがイニシャライザを介して機能するために必要なすべてを提供したことに注意してください。

あなたはそれにGameを提供しましたこのViewModelの下にあるモデルであるオブジェクト。

ここでアプリを実行しても、このViewModelデータをビュー自体に接続していないため、アプリは機能しません。

だから、GameScoreboardEditorViewController.swiftに戻ってくださいファイルを作成し、viewModelという名前のパブリックプロパティを作成します。

タイプGameScoreboardEditorViewModelにします。

viewDidLoadの直前に配置しますGameScoreboardEditorViewController.swift内のメソッド。

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

次に、fillUIを実装する必要があります方法。

このメソッドが2つの場所、viewModelからどのように呼び出されるかに注目してください。プロパティオブザーバー(didSet)およびviewDidLoad方法。これは、ViewControllerを作成できるためです。ビューにアタッチする前に(viewDidLoadメソッドが呼び出される前に)ViewModelを割り当てます。

一方、ViewControllerのビューを別のビューにアタッチしてviewDidLoadを呼び出すこともできますが、viewModelの場合その時点で設定されていない場合、何も起こりません。

そのため、最初に、UIを満たすためにデータがすべて設定されているかどうかを確認する必要があります。予期しない使用からコードを保護することが重要です。

したがって、fillUIに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

ここで、pauseButtonPressを実装します方法:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

今必要なのは、実際のviewModelを設定することだけです。このViewControllerのプロパティ。これは「外部から」行います。

開くHomeViewController.swift ViewModelをファイルしてコメントを外します。 showGameScoreboardEditorViewControllerで行を作成および設定します方法:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

次に、アプリを実行します。次のようになります。

iOSアプリ

スコア、時間、およびチーム名を担当する中央のビューには、InterfaceBuilderで設定された値が表示されなくなりました。

これで、実際のModelオブジェクト(Gameオブジェクト)からデータを取得するViewModelオブジェクト自体からの値が表示されます。

優秀な!しかし、プレイヤーの見解はどうですか?これらのボタンはまだ何もしません。

プレーヤーの動きの追跡には6つのビューがあることを知っています。

PlayerScoreboardMoveEditorViewという名前の別のサブビューを作成しましたそのため、今のところ実際のデータには何もせず、PlayerScoreboardMoveEditorView.xib内のInterfaceBuilderを介して設定された静的な値を表示しますファイル。

あなたはそれにいくつかのデータを与える必要があります。

GameScoreboardEditorViewControllerで行ったのと同じ方法で行いますおよびGameScoreboardEditorViewModel

XcodeプロジェクトでViewModelグループを開き、ここで新しいプロトコルを定義します。

PlayerScoreboardMoveEditorViewModel.swiftという名前の新しいファイルを作成し、その中に次のコードを配置します。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

このViewModelプロトコルは、親ビューで行ったのと同じように、PlayerScoreboardMoveEditorViewに合うように設計されていますGameScoreboardEditorViewController

ユーザーが実行できる5つの異なる動きの値が必要であり、ユーザーがアクションボタンの1つに触れたときに反応する必要があります。 Stringも必要ですプレイヤー名。

これを行った後、親ビュー(GameScoreboardEditorViewController)で行ったのと同じように、このプロトコルを実装する具象クラスを作成します。

次に、このプロトコルの実装を作成します。新しいファイルを作成し、PlayerScoreboardMoveEditorViewModelFromPlayer.swiftという名前を付けて、このオブジェクトをNSObjectのサブクラスにします。また、PlayerScoreboardMoveEditorViewModelに準拠するようにしますプロトコル:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

ここで、このインスタンスを「外部から」作成するオブジェクトを用意し、それをPlayerScoreboardMoveEditorView内のプロパティとして設定する必要があります。

覚えておいてくださいHomeViewController viewModelの設定を担当しましたGameScoreboardEditorViewControllerのプロパティ?

同様に、GameScoreboardEditorViewController PlayerScoreboardMoveEditorViewの親ビューですそしてそれGameScoreboardEditorViewController PlayerScoreboardMoveEditorViewModelの作成を担当しますオブジェクト。

GameScoreboardEditorViewModelを拡張する必要があります最初。

GameScoreboardEditorViewMode lを開き、次の2つのプロパティを追加します。

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

また、GameScoreboardEditorViewModelFromGameを更新しますinitWithGameのすぐ上にあるこれらの2つのプロパティ方法:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

initWithGame内に次の2行を追加します。

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

そしてもちろん、不足しているplayerViewModelsWithPlayersを追加します方法:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

すごい!

ビューモデル(GameScoreboardEditorViewModel)をホームプレーヤーとアウェイプレーヤーの配列で更新しました。これらの2つの配列を埋める必要があります。

これは、これを使用したのと同じ場所で行いますviewModel UIを埋めるために。

開くGameScoreboardEditorViewController fillUIに移動します方法。メソッドの最後に次の行を追加します。

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

今のところ、実際のviewModelを追加しなかったため、ビルドエラーが発生しましたPlayerScoreboardMoveEditorView内のプロパティ。

init method inside the PlayerScoreboardMoveEditorView`の上に次のコードを追加します。

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

そしてfillUIを実装します方法:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

最後に、アプリを実行して、UI要素のデータがGameからの実際のデータであるかどうかを確認します。オブジェクト。

iOSアプリ

この時点で、MVVMデザインパターンを使用する機能的なアプリができました。

モデルをビューからうまく非表示にし、ビューはMVCで慣れているよりもはるかにシンプルです。

ここまでで、ViewとそのViewModelを含むアプリを作成しました。

そのビューには、ViewModelを持つ同じサブビュー(プレーヤービュー)の6つのインスタンスもあります。

ただし、お気づきかもしれませんが、UIに表示できるデータは(fillUIメソッドで)1回のみであり、そのデータは静的です。

ビューのデータがそのビューの存続期間中に変更されない場合は、この方法でMVVMを使用するための優れたクリーンなソリューションがあります。

ViewModelを動的にする

データが変更されるため、ViewModelを動的にする必要があります。

これが意味するのは、モデルが変更されると、ViewModelはそのパブリックプロパティ値を変更する必要があるということです。変更をビューに伝播します。ビューはUIを更新します。

これを行うには多くの方法があります。

モデルが変更されると、ViewModelが最初に通知されます。

変更内容をビューまで伝達するには、何らかのメカニズムが必要です。

いくつかのオプションが含まれます RxSwift 、これはかなり大きなライブラリであり、慣れるのに少し時間がかかります。

ViewModelは、プロパティ値の変更ごとにNSNotificationを起動する可能性がありますが、これにより、通知のサブスクライブやビューの割り当て解除時のサブスクライブ解除など、追加の処理が必要な多くのコードが追加されます。

Key-Value-Observing(KVO) 別のオプションですが、ユーザーはそのAPIが派手ではないことを確認します。

このチュートリアルでは、Swiftのジェネリックスとクロージャーを使用します。これらは、 バインディング、ジェネリック、Swift、MVVMの記事

それでは、サンプルアプリに戻りましょう。

ViewModelプロジェクトグループに移動し、新しいSwiftファイルDynamic.swiftを作成します。

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

このクラスは、ビューのライフサイクル中に変更されると予想されるViewModelのプロパティに使用します。

まず、PlayerScoreboardMoveEditorViewから始めますおよびそのViewModel、PlayerScoreboardMoveEditorViewModel

開くPlayerScoreboardMoveEditorViewModelそしてその特性を見てください。

playerNameだから変更される予定はありません。そのままにしておくことができます。

他の5つのプロパティ(5つの移動タイプ)が変更されるため、それについて何かを行う必要があります。ソリューション?上記のDynamicプロジェクトに追加したばかりのクラス。

内部PlayerScoreboardMoveEditorViewModel移動カウントを表す5つの文字列の定義を削除し、次のように置き換えます。

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

ViewModelプロトコルは次のようになります。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

これDynamic typeを使用すると、その特定のプロパティの値を変更すると同時に、change-listenerオブジェクト(この場合はビュー)に通知できます。

ここで、実際のViewModel実装を更新しますPlayerScoreboardMoveEditorViewModelFromPlayer

これを置き換えます:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

次のように:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

注:これらのプロパティをletで定数として宣言しても問題ありません。実際のプロパティは変更しないためです。 valueを変更しますDynamicのプロパティオブジェクト。

Dynamicを初期化しなかったため、ビルドエラーが発生しましたオブジェクト。

PlayerScoreboardMoveEditorViewModelFromPlayerのinitメソッド内で、moveプロパティの初期化を次のように置き換えます。

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

内部PlayerScoreboardMoveEditorViewModelFromPlayer makeMoveに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

ご覧のとおり、Dynamicのインスタンスを作成しましたクラスを割り当て、String値。データを更新する必要がある場合は、Dynamicを変更しないでください。プロパティ自体;むしろ更新しますvalueプロパティ。

すごい! PlayerScoreboardMoveEditorViewModel今はダイナミックです。

それを利用して、実際にこれらの変更をリッスンするビューに移動しましょう。

開くPlayerScoreboardMoveEditorViewとそのfillUIメソッド(String値をDynamicオブジェクトタイプに割り当てようとしているため、この時点でこのメソッドにビルドエラーが表示されるはずです。)

「エラーのある」行を置き換えます。

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

次のように:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

次に、移動アクションを表す5つのメソッドを実装します( ボタンアクション セクション):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

アプリを実行し、いくつかの移動ボタンをクリックします。アクションボタンをクリックすると、プレーヤービュー内のカウンター値がどのように変化するかがわかります。

iOSアプリ

PlayerScoreboardMoveEditorViewで終了ですおよびPlayerScoreboardMoveEditorViewModel

これは簡単でした。

ここで、メインビュー(GameScoreboardEditorViewController)でも同じことを行う必要があります。

まず、GameScoreboardEditorViewModelを開きますビューのライフサイクル中にどの値が変化すると予想されるかを確認します。

timescoreisFinishedisPausedを置き換えますDynamicを使用した定義バージョン:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

ViewModel実装(GameScoreboardEditorViewModelFromGame)に移動し、プロトコルで宣言されているプロパティで同じことを行います。

これを置き換えます:

var time: String var score: String var isFinished: Bool var isPaused: Bool

次のように:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

ViewModelのタイプをStringから変更したため、いくつかのエラーが発生します。およびBoolDynamicおよびDynamic

それを修正しましょう。

togglePauseを修正します次のように置き換えてください。

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

唯一の変更点は、プロパティ値をプロパティに直接設定しなくなったことです。代わりに、オブジェクトのvalueに設定しますプロパティ。

ここで、initWithGameを修正しますこれを置き換えることによる方法:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

次のように:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

あなたは今ポイントを取得する必要があります。

StringIntなどのプリミティブ値をラップしていますおよびBoolDynamicこれらのオブジェクトのバージョン。これにより、軽量のバインディングメカニズムが提供されます。

修正するエラーがもう1つあります。

startTimerでメソッドの場合、エラー行を次のように置き換えます。

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

プレーヤーのViewModelの場合と同じように、ViewModelを動的にアップグレードしました。ただし、ビューを更新する必要があります(GameScoreboardEditorViewController)。

fillUI全体を置き換えますこれを使った方法:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

唯一の違いは、4つの動的プロパティを変更し、それぞれに変更リスナーを追加したことです。

この時点で、アプリを実行している場合は、 開始/一時停止 ボタンはゲームタイマーを開始および一時停止します。これは、ゲーム中のタイムアウトに使用されます。

ポイントボタン(1および2ポイントボタン)のいずれかを押したときに、UIでスコアが変更されないことを除いて、ほぼ完了です。

これは、基になるGameでスコアの変更を実際に伝播していないためです。 ViewModelまでのモデルオブジェクト。

だから、Gameを開きます少し調べるためのモデルオブジェクト。そのupdateScoreを確認してください方法。

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

この方法は2つの重要なことを行います。

まず、isFinishedを設定しますプロパティto true両方のチームのスコアに基づいてゲームが終了した場合。

その後、スコアが変更されたという通知を投稿します。この通知はGameScoreboardEditorViewModelFromGameで聞きます通知ハンドラーメソッドで動的スコア値を更新します。

initWithGameの下部にこの行を追加しますメソッド(エラーを回避するためにsuper.init()呼び出しを忘れないでください):

super.init() subscribeToNotifications()

以下initWithGameメソッド、追加deinitクリーンアップを適切に実行し、NotificationCenterによって引き起こされるクラッシュを回避する必要があるためです。

deinit { unsubscribeFromNotifications() }

最後に、これらのメソッドの実装を追加します。このセクションをdeinitのすぐ下に追加します方法:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

次に、アプリを実行し、プレーヤーのビューをクリックしてスコアを変更します。すでに動的に接続しているのでscoreおよびisFinished Viewを使用したViewModelでは、ViewModel内のスコア値を変更するとすべてが機能するはずです。

アプリをさらに改善する方法

常に改善の余地がありますが、これはこのチュートリアルの範囲外です。

たとえば、ゲームが終了したとき(チームの1つが15ポイントに達したとき)に時間を自動的に停止するのではなく、プレーヤーのビューを非表示にするだけです。

必要に応じてアプリで遊んだり、アップグレードして「ゲームクリエイター」ビューを表示したりできます。このビューでは、ゲームの作成、チーム名の割り当て、プレーヤー名の割り当て、Gameの作成が行われます。 GameScoreboardEditorViewControllerを提示するために使用できるオブジェクト。

UITableViewを使用する別の「ゲームリスト」ビューを作成できます。進行中の複数のゲームを表示し、テーブルセルに詳細情報を表示します。セル選択では、GameScoreboardEditorViewControllerを表示できます選択したGameで。

GameLibraryすでに実装されています。そのライブラリ参照をイニシャライザのViewModelオブジェクトに渡すことを忘れないでください。たとえば、「ゲームクリエイター」のViewModelには、GameLibraryのインスタンスが必要です。作成されたGameを挿入できるように初期化子を通過しましたライブラリへのオブジェクト。 「ゲームリスト」のViewModelでも、ライブラリからすべてのゲームをフェッチするためにこの参照が必要になります。これはUITableViewで必要になります。

アイデアは、ViewModel内のすべてのダーティ(非UI)作業を非表示にし、UI(ビュー)が準備されたプレゼンテーションデータでのみ動作するようにすることです。

今何?

MVVMに慣れたら、を使用してさらに改善することができます ボブおじさんのクリーンアーキテクチャルール

追加の良い読み物は、Androidアーキテクチャに関する3部構成のチュートリアルです。

例はJava(Android用)で書かれており、Java(Swiftに非常に近く、Objective-CはJavaに近い)に精通している場合は、ViewModelオブジェクト内でコードをさらにリファクタリングする方法についてのアイデアが得られます。 iOSモジュール(UIKitまたはCoreLocationなど)をインポートしないこと。

これらのiOSモジュールは、純粋なNSObjectsの背後に隠すことができます。これは、コードの再利用に適しています。

MVVMはほとんどの人にとって良い選択です ios アプリ、そしてうまくいけば、あなたはあなたの次のプロジェクトでそれを試してみるでしょう。または、UIViewControllerを作成するときに、現在のプロジェクトで試してみてください。

関連: 静的パターンの操作:SwiftMVVMチュートリアル }

次に、移動アクションを表す5つのメソッドを実装します( ボタンアクション セクション):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

アプリを実行し、いくつかの移動ボタンをクリックします。アクションボタンをクリックすると、プレーヤービュー内のカウンター値がどのように変化するかがわかります。

iOSアプリ

PlayerScoreboardMoveEditorViewで終了ですおよびPlayerScoreboardMoveEditorViewModel

これは簡単でした。

ここで、メインビュー(GameScoreboardEditorViewController)でも同じことを行う必要があります。

まず、GameScoreboardEditorViewModelを開きますビューのライフサイクル中にどの値が変化すると予想されるかを確認します。

timescoreisFinishedisPausedを置き換えますDynamicを使用した定義バージョン:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

ViewModel実装(GameScoreboardEditorViewModelFromGame)に移動し、プロトコルで宣言されているプロパティで同じことを行います。

これを置き換えます:

var time: String var score: String var isFinished: Bool var isPaused: Bool

次のように:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

ViewModelのタイプをStringから変更したため、いくつかのエラーが発生します。およびBoolDynamicおよびDynamic

それを修正しましょう。

togglePauseを修正します次のように置き換えてください。

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

唯一の変更点は、プロパティ値をプロパティに直接設定しなくなったことです。代わりに、オブジェクトのvalueに設定しますプロパティ。

ここで、initWithGameを修正しますこれを置き換えることによる方法:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

次のように:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

あなたは今ポイントを取得する必要があります。

StringIntなどのプリミティブ値をラップしていますおよびBoolDynamicこれらのオブジェクトのバージョン。これにより、軽量のバインディングメカニズムが提供されます。

修正するエラーがもう1つあります。

startTimerでメソッドの場合、エラー行を次のように置き換えます。

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

プレーヤーのViewModelの場合と同じように、ViewModelを動的にアップグレードしました。ただし、ビューを更新する必要があります(GameScoreboardEditorViewController)。

fillUI全体を置き換えますこれを使った方法:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text =

Swiftチュートリアル:MVVMデザインパターンの概要

つまり、新しいiOSプロジェクトを開始すると、デザイナーから必要なすべての情報を受け取ります.pdfおよび.sketchドキュメントがあり、この新しいアプリをどのように構築するかについてのビジョンはすでにあります。

デザイナーのスケッチからViewControllerへのUI画面の転送を開始します.swift.xibおよび.storyboardファイル。

UITextFieldここで、UITableViewそこに、さらにいくつかUILabelsそしてUIButtonsのピンチ。 IBOutletsおよびIBActions含まれています。よし、まだUIゾーンにいる。



ただし、これらすべてのUI要素を使用して何かを行うときが来ました。 UIButtons指で触れるとUILabelsおよびUITableViews何をどの形式で表示するかを誰かに教える必要があります。

突然、3,000行を超えるコードが作成されました。

3,000行のSwiftコード

あなたはたくさんのスパゲッティコードになってしまいました。

これを解決するための最初のステップは、 Model-View-Controller (MVC)デザインパターン。ただし、このパターンには独自の問題があります。来る Model-View-ViewModel (MVVM)日を節約するデザインパターン。

スパゲッティコードの取り扱い

あっという間に、あなたのスタートViewControllerスマートになりすぎて、巨大になりすぎました。

ネットワークコード、データ解析コード、UIプレゼンテーションのデータ調整コード、アプリの状態通知、UIの状態変化。 if -ology内に閉じ込められたすべてのコードは、再利用できず、このプロジェクトにのみ収まります。

あなたのViewControllerコードは悪名高いスパゲッティコードになりました。

どうしてこうなりました?

考えられる理由は次のようなものです。

バックエンドデータがUITableView 内でどのように動作しているかを確認するために急いでいたので、数行のネットワークコードを 臨時雇用者 ViewControllerのメソッドそれをフェッチするだけです.jsonネットワークから。次に、その.json内のデータを処理する必要があったので、さらに別のデータを作成しました 臨時雇用者 それを達成する方法。または、さらに悪いことに、同じ方法でそれを行いました。

ViewControllerユーザー認証コードが登場したとき、成長を続けました。その後、データ形式が変化し始め、UIが進化し、いくつかの根本的な変更が必要になりました。そして、すでに大規模なif -ologyにifを追加し続けました。

しかし、どうしてUIViewController手に負えなくなったものは何ですか?

UIViewController UIコードの作業を開始するための論理的な場所です。これは、iOSデバイスでアプリを使用しているときに表示される物理的な画面を表します。 AppleでさえUIViewControllersを使用しています異なるアプリとアニメーション化されたUIを切り替えると、メインシステムアプリで。

AppleはUIの抽象化をUIViewControllerの内部に基づいています。これは、iOSのUIコードのコアであり、 MVC デザインパターン。

関連: iOS開発者が犯していることを知らない10の最も一般的な間違い

MVCデザインパターンへのアップグレード

MVCデザインパターン

MVCデザインパターンでは、 見る 非アクティブであると想定され、オンデマンドで準備されたデータのみを表示します。

コントローラ に取り組む必要があります モデル 準備するためのデータ ビュー 、次にそのデータを表示します。

見る 通知する責任もあります コントローラ ユーザーのタッチなどのアクションについて。

前述のように、UIViewController通常、UI画面を構築するための開始点です。その名前には、「ビュー」と「コントローラー」の両方が含まれていることに注意してください。これは、「ビューを制御する」ことを意味します。 「コントローラー」と「ビュー」の両方のコードを内部に含める必要があるという意味ではありません。

このビューとコントローラーコードの混合は、IBOutletsを移動したときによく発生します。 UIViewController内の小さなサブビューを作成し、UIViewControllerから直接それらのサブビューを操作します。代わりに、そのコードをカスタムの中にラップする必要がありますUIViewサブクラス。

これにより、ビューとコントローラーのコードパスが交差する可能性があることは簡単にわかります。

救助へのMVVM

これは、 MVVM パターンが重宝します。

UIViewController以降であるはずです コントローラ MVCパターンで、すでに多くのことを行っています ビュー 、それらをにマージできます 見る 私たちの新しいパターンの- MVVM

MVVMデザインパターン

MVVMデザインパターンでは、 モデル MVCパターンと同じです。単純なデータを表します。

見る UIViewで表されますまたはUIViewController .xibを伴うオブジェクトおよび.storyboard準備されたデータのみを表示するファイル。 (たとえば、ビュー内にNSDateFormatterコードを含めたくありません。)

から来る単純なフォーマットされた文字列のみ ViewModel

ViewModel すべての非同期ネットワークコード、ビジュアルプレゼンテーション用のデータ準備コード、およびリッスンするコードを非表示にします モデル 変化します。これらはすべて、この特定のものに合うようにモデル化された明確に定義されたAPIの背後に隠されています 見る

MVVMを使用する利点の1つは、テストです。以来 ViewModel 純粋ですNSObject (またはstructなど)、UIKitとは結合されていませんコードを使用すると、UIコードに影響を与えることなく、単体テストでより簡単にテストできます。

さて、 見るUIViewController / UIView)がはるかに簡単になりました ViewModel 間の接着剤として機能します モデル そして 見る

SwiftでのMVVMの適用

SwiftのMVVM

MVVMの動作を示すために、このチュートリアル用に作成されたサンプルXcodeプロジェクトをダウンロードして調べることができます。 ここに 。このプロジェクトでは、Swift3とXcode8.1を使用しています。

プロジェクトには2つのバージョンがあります。 スターター そして 終了しました

ザ・ 終了しました バージョンは完成したミニアプリケーションであり、 スターター 同じプロジェクトですが、メソッドとオブジェクトが実装されていません。

まず、ダウンロードすることをお勧めします スターター プロジェクトを作成し、このチュートリアルに従ってください。後で使用するためにプロジェクトのクイックリファレンスが必要な場合は、 終了しました 事業。

チュートリアルプロジェクトの紹介

チュートリアルプロジェクトは、ゲーム中のプレーヤーのアクションを追跡するためのバスケットボールアプリケーションです。

バスケットボールアプリケーション

これは、ユーザーの動きとピックアップゲームの全体的なスコアをすばやく追跡するために使用されます。

15のスコア(少なくとも2ポイントの差がある)に達するまで、2つのチームがプレーします。各プレーヤーは1ポイントから2ポイントを獲得でき、各プレーヤーはアシスト、リバウンド、ファウルを行うことができます。

プロジェクト階層は次のようになります。

プロジェクト階層

モデル

見る

ViewModel

ダウンロードしたXcodeプロジェクトには、 見る オブジェクト(UIViewおよびUIViewController)。プロジェクトには、データを提供する方法の1つをデモするために作成されたカスタムメイドのオブジェクトも含まれています。 ViewModel オブジェクト(Servicesグループ)。

Extensionsグループには、このチュートリアルの範囲外であり、自明であるUIコードの便利な拡張機能が含まれています。

この時点でアプリを実行すると、完成したUIが表示されますが、ユーザーがボタンを押しても何も起こりません。

これは、ビューとIBActionsのみを作成したためです。それらをアプリロジックに接続せず、UI要素にモデルからのデータ(後で学習するようにGameオブジェクトから)を入力しません。

ViewとModelをViewModelで接続する

MVVMデザインパターンでは、Viewはモデルについて何も知らないはずです。 Viewが知っているのは、ViewModelの操作方法だけです。

ビューを調べることから始めます。

GameScoreboardEditorViewController.swiftでファイル、fillUIこの時点でメソッドは空です。これは、UIにデータを入力する場所です。これを実現するには、ViewControllerのデータを提供する必要があります。これは、ViewModelオブジェクトを使用して行います。

まず、このViewControllerに必要なすべてのデータを含むViewModelオブジェクトを作成します。

空になるViewModelXcodeプロジェクトグループに移動し、GameScoreboardEditorViewModel.swiftを作成します。ファイルを作成し、プロトコルにします。

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

このようなプロトコルを使用すると、物事がきれいに保たれます。使用するデータのみを定義する必要があります。

次に、このプロトコルの実装を作成します。

GameScoreboardEditorViewModelFromGame.swiftという名前の新しいファイルを作成し、このオブジェクトをNSObjectのサブクラスにします。

また、GameScoreboardEditorViewModelに準拠するようにしますプロトコル:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

ViewModelがイニシャライザを介して機能するために必要なすべてを提供したことに注意してください。

あなたはそれにGameを提供しましたこのViewModelの下にあるモデルであるオブジェクト。

ここでアプリを実行しても、このViewModelデータをビュー自体に接続していないため、アプリは機能しません。

だから、GameScoreboardEditorViewController.swiftに戻ってくださいファイルを作成し、viewModelという名前のパブリックプロパティを作成します。

タイプGameScoreboardEditorViewModelにします。

viewDidLoadの直前に配置しますGameScoreboardEditorViewController.swift内のメソッド。

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

次に、fillUIを実装する必要があります方法。

このメソッドが2つの場所、viewModelからどのように呼び出されるかに注目してください。プロパティオブザーバー(didSet)およびviewDidLoad方法。これは、ViewControllerを作成できるためです。ビューにアタッチする前に(viewDidLoadメソッドが呼び出される前に)ViewModelを割り当てます。

一方、ViewControllerのビューを別のビューにアタッチしてviewDidLoadを呼び出すこともできますが、viewModelの場合その時点で設定されていない場合、何も起こりません。

そのため、最初に、UIを満たすためにデータがすべて設定されているかどうかを確認する必要があります。予期しない使用からコードを保護することが重要です。

したがって、fillUIに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

ここで、pauseButtonPressを実装します方法:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

今必要なのは、実際のviewModelを設定することだけです。このViewControllerのプロパティ。これは「外部から」行います。

開くHomeViewController.swift ViewModelをファイルしてコメントを外します。 showGameScoreboardEditorViewControllerで行を作成および設定します方法:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

次に、アプリを実行します。次のようになります。

iOSアプリ

スコア、時間、およびチーム名を担当する中央のビューには、InterfaceBuilderで設定された値が表示されなくなりました。

これで、実際のModelオブジェクト(Gameオブジェクト)からデータを取得するViewModelオブジェクト自体からの値が表示されます。

優秀な!しかし、プレイヤーの見解はどうですか?これらのボタンはまだ何もしません。

プレーヤーの動きの追跡には6つのビューがあることを知っています。

PlayerScoreboardMoveEditorViewという名前の別のサブビューを作成しましたそのため、今のところ実際のデータには何もせず、PlayerScoreboardMoveEditorView.xib内のInterfaceBuilderを介して設定された静的な値を表示しますファイル。

あなたはそれにいくつかのデータを与える必要があります。

GameScoreboardEditorViewControllerで行ったのと同じ方法で行いますおよびGameScoreboardEditorViewModel

XcodeプロジェクトでViewModelグループを開き、ここで新しいプロトコルを定義します。

PlayerScoreboardMoveEditorViewModel.swiftという名前の新しいファイルを作成し、その中に次のコードを配置します。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

このViewModelプロトコルは、親ビューで行ったのと同じように、PlayerScoreboardMoveEditorViewに合うように設計されていますGameScoreboardEditorViewController

ユーザーが実行できる5つの異なる動きの値が必要であり、ユーザーがアクションボタンの1つに触れたときに反応する必要があります。 Stringも必要ですプレイヤー名。

これを行った後、親ビュー(GameScoreboardEditorViewController)で行ったのと同じように、このプロトコルを実装する具象クラスを作成します。

次に、このプロトコルの実装を作成します。新しいファイルを作成し、PlayerScoreboardMoveEditorViewModelFromPlayer.swiftという名前を付けて、このオブジェクトをNSObjectのサブクラスにします。また、PlayerScoreboardMoveEditorViewModelに準拠するようにしますプロトコル:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

ここで、このインスタンスを「外部から」作成するオブジェクトを用意し、それをPlayerScoreboardMoveEditorView内のプロパティとして設定する必要があります。

覚えておいてくださいHomeViewController viewModelの設定を担当しましたGameScoreboardEditorViewControllerのプロパティ?

同様に、GameScoreboardEditorViewController PlayerScoreboardMoveEditorViewの親ビューですそしてそれGameScoreboardEditorViewController PlayerScoreboardMoveEditorViewModelの作成を担当しますオブジェクト。

GameScoreboardEditorViewModelを拡張する必要があります最初。

GameScoreboardEditorViewMode lを開き、次の2つのプロパティを追加します。

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

また、GameScoreboardEditorViewModelFromGameを更新しますinitWithGameのすぐ上にあるこれらの2つのプロパティ方法:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

initWithGame内に次の2行を追加します。

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

そしてもちろん、不足しているplayerViewModelsWithPlayersを追加します方法:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

すごい!

ビューモデル(GameScoreboardEditorViewModel)をホームプレーヤーとアウェイプレーヤーの配列で更新しました。これらの2つの配列を埋める必要があります。

これは、これを使用したのと同じ場所で行いますviewModel UIを埋めるために。

開くGameScoreboardEditorViewController fillUIに移動します方法。メソッドの最後に次の行を追加します。

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

今のところ、実際のviewModelを追加しなかったため、ビルドエラーが発生しましたPlayerScoreboardMoveEditorView内のプロパティ。

init method inside the PlayerScoreboardMoveEditorView`の上に次のコードを追加します。

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

そしてfillUIを実装します方法:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

最後に、アプリを実行して、UI要素のデータがGameからの実際のデータであるかどうかを確認します。オブジェクト。

iOSアプリ

この時点で、MVVMデザインパターンを使用する機能的なアプリができました。

モデルをビューからうまく非表示にし、ビューはMVCで慣れているよりもはるかにシンプルです。

ここまでで、ViewとそのViewModelを含むアプリを作成しました。

そのビューには、ViewModelを持つ同じサブビュー(プレーヤービュー)の6つのインスタンスもあります。

ただし、お気づきかもしれませんが、UIに表示できるデータは(fillUIメソッドで)1回のみであり、そのデータは静的です。

ビューのデータがそのビューの存続期間中に変更されない場合は、この方法でMVVMを使用するための優れたクリーンなソリューションがあります。

ViewModelを動的にする

データが変更されるため、ViewModelを動的にする必要があります。

これが意味するのは、モデルが変更されると、ViewModelはそのパブリックプロパティ値を変更する必要があるということです。変更をビューに伝播します。ビューはUIを更新します。

これを行うには多くの方法があります。

モデルが変更されると、ViewModelが最初に通知されます。

変更内容をビューまで伝達するには、何らかのメカニズムが必要です。

いくつかのオプションが含まれます RxSwift 、これはかなり大きなライブラリであり、慣れるのに少し時間がかかります。

ViewModelは、プロパティ値の変更ごとにNSNotificationを起動する可能性がありますが、これにより、通知のサブスクライブやビューの割り当て解除時のサブスクライブ解除など、追加の処理が必要な多くのコードが追加されます。

Key-Value-Observing(KVO) 別のオプションですが、ユーザーはそのAPIが派手ではないことを確認します。

このチュートリアルでは、Swiftのジェネリックスとクロージャーを使用します。これらは、 バインディング、ジェネリック、Swift、MVVMの記事

それでは、サンプルアプリに戻りましょう。

ViewModelプロジェクトグループに移動し、新しいSwiftファイルDynamic.swiftを作成します。

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

このクラスは、ビューのライフサイクル中に変更されると予想されるViewModelのプロパティに使用します。

まず、PlayerScoreboardMoveEditorViewから始めますおよびそのViewModel、PlayerScoreboardMoveEditorViewModel

開くPlayerScoreboardMoveEditorViewModelそしてその特性を見てください。

playerNameだから変更される予定はありません。そのままにしておくことができます。

他の5つのプロパティ(5つの移動タイプ)が変更されるため、それについて何かを行う必要があります。ソリューション?上記のDynamicプロジェクトに追加したばかりのクラス。

内部PlayerScoreboardMoveEditorViewModel移動カウントを表す5つの文字列の定義を削除し、次のように置き換えます。

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

ViewModelプロトコルは次のようになります。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

これDynamic typeを使用すると、その特定のプロパティの値を変更すると同時に、change-listenerオブジェクト(この場合はビュー)に通知できます。

ここで、実際のViewModel実装を更新しますPlayerScoreboardMoveEditorViewModelFromPlayer

これを置き換えます:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

次のように:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

注:これらのプロパティをletで定数として宣言しても問題ありません。実際のプロパティは変更しないためです。 valueを変更しますDynamicのプロパティオブジェクト。

Dynamicを初期化しなかったため、ビルドエラーが発生しましたオブジェクト。

PlayerScoreboardMoveEditorViewModelFromPlayerのinitメソッド内で、moveプロパティの初期化を次のように置き換えます。

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

内部PlayerScoreboardMoveEditorViewModelFromPlayer makeMoveに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

ご覧のとおり、Dynamicのインスタンスを作成しましたクラスを割り当て、String値。データを更新する必要がある場合は、Dynamicを変更しないでください。プロパティ自体;むしろ更新しますvalueプロパティ。

すごい! PlayerScoreboardMoveEditorViewModel今はダイナミックです。

それを利用して、実際にこれらの変更をリッスンするビューに移動しましょう。

開くPlayerScoreboardMoveEditorViewとそのfillUIメソッド(String値をDynamicオブジェクトタイプに割り当てようとしているため、この時点でこのメソッドにビルドエラーが表示されるはずです。)

「エラーのある」行を置き換えます。

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

次のように:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

次に、移動アクションを表す5つのメソッドを実装します( ボタンアクション セクション):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

アプリを実行し、いくつかの移動ボタンをクリックします。アクションボタンをクリックすると、プレーヤービュー内のカウンター値がどのように変化するかがわかります。

iOSアプリ

PlayerScoreboardMoveEditorViewで終了ですおよびPlayerScoreboardMoveEditorViewModel

これは簡単でした。

ここで、メインビュー(GameScoreboardEditorViewController)でも同じことを行う必要があります。

まず、GameScoreboardEditorViewModelを開きますビューのライフサイクル中にどの値が変化すると予想されるかを確認します。

timescoreisFinishedisPausedを置き換えますDynamicを使用した定義バージョン:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

ViewModel実装(GameScoreboardEditorViewModelFromGame)に移動し、プロトコルで宣言されているプロパティで同じことを行います。

これを置き換えます:

var time: String var score: String var isFinished: Bool var isPaused: Bool

次のように:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

ViewModelのタイプをStringから変更したため、いくつかのエラーが発生します。およびBoolDynamicおよびDynamic

それを修正しましょう。

togglePauseを修正します次のように置き換えてください。

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

唯一の変更点は、プロパティ値をプロパティに直接設定しなくなったことです。代わりに、オブジェクトのvalueに設定しますプロパティ。

ここで、initWithGameを修正しますこれを置き換えることによる方法:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

次のように:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

あなたは今ポイントを取得する必要があります。

StringIntなどのプリミティブ値をラップしていますおよびBoolDynamicこれらのオブジェクトのバージョン。これにより、軽量のバインディングメカニズムが提供されます。

修正するエラーがもう1つあります。

startTimerでメソッドの場合、エラー行を次のように置き換えます。

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

プレーヤーのViewModelの場合と同じように、ViewModelを動的にアップグレードしました。ただし、ビューを更新する必要があります(GameScoreboardEditorViewController)。

fillUI全体を置き換えますこれを使った方法:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

唯一の違いは、4つの動的プロパティを変更し、それぞれに変更リスナーを追加したことです。

この時点で、アプリを実行している場合は、 開始/一時停止 ボタンはゲームタイマーを開始および一時停止します。これは、ゲーム中のタイムアウトに使用されます。

ポイントボタン(1および2ポイントボタン)のいずれかを押したときに、UIでスコアが変更されないことを除いて、ほぼ完了です。

これは、基になるGameでスコアの変更を実際に伝播していないためです。 ViewModelまでのモデルオブジェクト。

だから、Gameを開きます少し調べるためのモデルオブジェクト。そのupdateScoreを確認してください方法。

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

この方法は2つの重要なことを行います。

まず、isFinishedを設定しますプロパティto true両方のチームのスコアに基づいてゲームが終了した場合。

その後、スコアが変更されたという通知を投稿します。この通知はGameScoreboardEditorViewModelFromGameで聞きます通知ハンドラーメソッドで動的スコア値を更新します。

initWithGameの下部にこの行を追加しますメソッド(エラーを回避するためにsuper.init()呼び出しを忘れないでください):

super.init() subscribeToNotifications()

以下initWithGameメソッド、追加deinitクリーンアップを適切に実行し、NotificationCenterによって引き起こされるクラッシュを回避する必要があるためです。

deinit { unsubscribeFromNotifications() }

最後に、これらのメソッドの実装を追加します。このセクションをdeinitのすぐ下に追加します方法:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

次に、アプリを実行し、プレーヤーのビューをクリックしてスコアを変更します。すでに動的に接続しているのでscoreおよびisFinished Viewを使用したViewModelでは、ViewModel内のスコア値を変更するとすべてが機能するはずです。

アプリをさらに改善する方法

常に改善の余地がありますが、これはこのチュートリアルの範囲外です。

たとえば、ゲームが終了したとき(チームの1つが15ポイントに達したとき)に時間を自動的に停止するのではなく、プレーヤーのビューを非表示にするだけです。

必要に応じてアプリで遊んだり、アップグレードして「ゲームクリエイター」ビューを表示したりできます。このビューでは、ゲームの作成、チーム名の割り当て、プレーヤー名の割り当て、Gameの作成が行われます。 GameScoreboardEditorViewControllerを提示するために使用できるオブジェクト。

UITableViewを使用する別の「ゲームリスト」ビューを作成できます。進行中の複数のゲームを表示し、テーブルセルに詳細情報を表示します。セル選択では、GameScoreboardEditorViewControllerを表示できます選択したGameで。

GameLibraryすでに実装されています。そのライブラリ参照をイニシャライザのViewModelオブジェクトに渡すことを忘れないでください。たとえば、「ゲームクリエイター」のViewModelには、GameLibraryのインスタンスが必要です。作成されたGameを挿入できるように初期化子を通過しましたライブラリへのオブジェクト。 「ゲームリスト」のViewModelでも、ライブラリからすべてのゲームをフェッチするためにこの参照が必要になります。これはUITableViewで必要になります。

アイデアは、ViewModel内のすべてのダーティ(非UI)作業を非表示にし、UI(ビュー)が準備されたプレゼンテーションデータでのみ動作するようにすることです。

今何?

MVVMに慣れたら、を使用してさらに改善することができます ボブおじさんのクリーンアーキテクチャルール

追加の良い読み物は、Androidアーキテクチャに関する3部構成のチュートリアルです。

例はJava(Android用)で書かれており、Java(Swiftに非常に近く、Objective-CはJavaに近い)に精通している場合は、ViewModelオブジェクト内でコードをさらにリファクタリングする方法についてのアイデアが得られます。 iOSモジュール(UIKitまたはCoreLocationなど)をインポートしないこと。

これらのiOSモジュールは、純粋なNSObjectsの背後に隠すことができます。これは、コードの再利用に適しています。

MVVMはほとんどの人にとって良い選択です ios アプリ、そしてうまくいけば、あなたはあなたの次のプロジェクトでそれを試してみるでしょう。または、UIViewControllerを作成するときに、現在のプロジェクトで試してみてください。

関連: 静的パターンの操作:SwiftMVVMチュートリアル } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text =

Swiftチュートリアル:MVVMデザインパターンの概要

つまり、新しいiOSプロジェクトを開始すると、デザイナーから必要なすべての情報を受け取ります.pdfおよび.sketchドキュメントがあり、この新しいアプリをどのように構築するかについてのビジョンはすでにあります。

デザイナーのスケッチからViewControllerへのUI画面の転送を開始します.swift.xibおよび.storyboardファイル。

UITextFieldここで、UITableViewそこに、さらにいくつかUILabelsそしてUIButtonsのピンチ。 IBOutletsおよびIBActions含まれています。よし、まだUIゾーンにいる。



ただし、これらすべてのUI要素を使用して何かを行うときが来ました。 UIButtons指で触れるとUILabelsおよびUITableViews何をどの形式で表示するかを誰かに教える必要があります。

突然、3,000行を超えるコードが作成されました。

3,000行のSwiftコード

あなたはたくさんのスパゲッティコードになってしまいました。

これを解決するための最初のステップは、 Model-View-Controller (MVC)デザインパターン。ただし、このパターンには独自の問題があります。来る Model-View-ViewModel (MVVM)日を節約するデザインパターン。

スパゲッティコードの取り扱い

あっという間に、あなたのスタートViewControllerスマートになりすぎて、巨大になりすぎました。

ネットワークコード、データ解析コード、UIプレゼンテーションのデータ調整コード、アプリの状態通知、UIの状態変化。 if -ology内に閉じ込められたすべてのコードは、再利用できず、このプロジェクトにのみ収まります。

あなたのViewControllerコードは悪名高いスパゲッティコードになりました。

どうしてこうなりました?

考えられる理由は次のようなものです。

バックエンドデータがUITableView 内でどのように動作しているかを確認するために急いでいたので、数行のネットワークコードを 臨時雇用者 ViewControllerのメソッドそれをフェッチするだけです.jsonネットワークから。次に、その.json内のデータを処理する必要があったので、さらに別のデータを作成しました 臨時雇用者 それを達成する方法。または、さらに悪いことに、同じ方法でそれを行いました。

ViewControllerユーザー認証コードが登場したとき、成長を続けました。その後、データ形式が変化し始め、UIが進化し、いくつかの根本的な変更が必要になりました。そして、すでに大規模なif -ologyにifを追加し続けました。

しかし、どうしてUIViewController手に負えなくなったものは何ですか?

UIViewController UIコードの作業を開始するための論理的な場所です。これは、iOSデバイスでアプリを使用しているときに表示される物理的な画面を表します。 AppleでさえUIViewControllersを使用しています異なるアプリとアニメーション化されたUIを切り替えると、メインシステムアプリで。

AppleはUIの抽象化をUIViewControllerの内部に基づいています。これは、iOSのUIコードのコアであり、 MVC デザインパターン。

関連: iOS開発者が犯していることを知らない10の最も一般的な間違い

MVCデザインパターンへのアップグレード

MVCデザインパターン

MVCデザインパターンでは、 見る 非アクティブであると想定され、オンデマンドで準備されたデータのみを表示します。

コントローラ に取り組む必要があります モデル 準備するためのデータ ビュー 、次にそのデータを表示します。

見る 通知する責任もあります コントローラ ユーザーのタッチなどのアクションについて。

前述のように、UIViewController通常、UI画面を構築するための開始点です。その名前には、「ビュー」と「コントローラー」の両方が含まれていることに注意してください。これは、「ビューを制御する」ことを意味します。 「コントローラー」と「ビュー」の両方のコードを内部に含める必要があるという意味ではありません。

このビューとコントローラーコードの混合は、IBOutletsを移動したときによく発生します。 UIViewController内の小さなサブビューを作成し、UIViewControllerから直接それらのサブビューを操作します。代わりに、そのコードをカスタムの中にラップする必要がありますUIViewサブクラス。

これにより、ビューとコントローラーのコードパスが交差する可能性があることは簡単にわかります。

救助へのMVVM

これは、 MVVM パターンが重宝します。

UIViewController以降であるはずです コントローラ MVCパターンで、すでに多くのことを行っています ビュー 、それらをにマージできます 見る 私たちの新しいパターンの- MVVM

MVVMデザインパターン

MVVMデザインパターンでは、 モデル MVCパターンと同じです。単純なデータを表します。

見る UIViewで表されますまたはUIViewController .xibを伴うオブジェクトおよび.storyboard準備されたデータのみを表示するファイル。 (たとえば、ビュー内にNSDateFormatterコードを含めたくありません。)

から来る単純なフォーマットされた文字列のみ ViewModel

ViewModel すべての非同期ネットワークコード、ビジュアルプレゼンテーション用のデータ準備コード、およびリッスンするコードを非表示にします モデル 変化します。これらはすべて、この特定のものに合うようにモデル化された明確に定義されたAPIの背後に隠されています 見る

MVVMを使用する利点の1つは、テストです。以来 ViewModel 純粋ですNSObject (またはstructなど)、UIKitとは結合されていませんコードを使用すると、UIコードに影響を与えることなく、単体テストでより簡単にテストできます。

さて、 見るUIViewController / UIView)がはるかに簡単になりました ViewModel 間の接着剤として機能します モデル そして 見る

SwiftでのMVVMの適用

SwiftのMVVM

MVVMの動作を示すために、このチュートリアル用に作成されたサンプルXcodeプロジェクトをダウンロードして調べることができます。 ここに 。このプロジェクトでは、Swift3とXcode8.1を使用しています。

プロジェクトには2つのバージョンがあります。 スターター そして 終了しました

ザ・ 終了しました バージョンは完成したミニアプリケーションであり、 スターター 同じプロジェクトですが、メソッドとオブジェクトが実装されていません。

まず、ダウンロードすることをお勧めします スターター プロジェクトを作成し、このチュートリアルに従ってください。後で使用するためにプロジェクトのクイックリファレンスが必要な場合は、 終了しました 事業。

チュートリアルプロジェクトの紹介

チュートリアルプロジェクトは、ゲーム中のプレーヤーのアクションを追跡するためのバスケットボールアプリケーションです。

バスケットボールアプリケーション

これは、ユーザーの動きとピックアップゲームの全体的なスコアをすばやく追跡するために使用されます。

15のスコア(少なくとも2ポイントの差がある)に達するまで、2つのチームがプレーします。各プレーヤーは1ポイントから2ポイントを獲得でき、各プレーヤーはアシスト、リバウンド、ファウルを行うことができます。

プロジェクト階層は次のようになります。

プロジェクト階層

モデル

見る

ViewModel

ダウンロードしたXcodeプロジェクトには、 見る オブジェクト(UIViewおよびUIViewController)。プロジェクトには、データを提供する方法の1つをデモするために作成されたカスタムメイドのオブジェクトも含まれています。 ViewModel オブジェクト(Servicesグループ)。

Extensionsグループには、このチュートリアルの範囲外であり、自明であるUIコードの便利な拡張機能が含まれています。

この時点でアプリを実行すると、完成したUIが表示されますが、ユーザーがボタンを押しても何も起こりません。

これは、ビューとIBActionsのみを作成したためです。それらをアプリロジックに接続せず、UI要素にモデルからのデータ(後で学習するようにGameオブジェクトから)を入力しません。

ViewとModelをViewModelで接続する

MVVMデザインパターンでは、Viewはモデルについて何も知らないはずです。 Viewが知っているのは、ViewModelの操作方法だけです。

ビューを調べることから始めます。

GameScoreboardEditorViewController.swiftでファイル、fillUIこの時点でメソッドは空です。これは、UIにデータを入力する場所です。これを実現するには、ViewControllerのデータを提供する必要があります。これは、ViewModelオブジェクトを使用して行います。

まず、このViewControllerに必要なすべてのデータを含むViewModelオブジェクトを作成します。

空になるViewModelXcodeプロジェクトグループに移動し、GameScoreboardEditorViewModel.swiftを作成します。ファイルを作成し、プロトコルにします。

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

このようなプロトコルを使用すると、物事がきれいに保たれます。使用するデータのみを定義する必要があります。

次に、このプロトコルの実装を作成します。

GameScoreboardEditorViewModelFromGame.swiftという名前の新しいファイルを作成し、このオブジェクトをNSObjectのサブクラスにします。

また、GameScoreboardEditorViewModelに準拠するようにしますプロトコル:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

ViewModelがイニシャライザを介して機能するために必要なすべてを提供したことに注意してください。

あなたはそれにGameを提供しましたこのViewModelの下にあるモデルであるオブジェクト。

ここでアプリを実行しても、このViewModelデータをビュー自体に接続していないため、アプリは機能しません。

だから、GameScoreboardEditorViewController.swiftに戻ってくださいファイルを作成し、viewModelという名前のパブリックプロパティを作成します。

タイプGameScoreboardEditorViewModelにします。

viewDidLoadの直前に配置しますGameScoreboardEditorViewController.swift内のメソッド。

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

次に、fillUIを実装する必要があります方法。

このメソッドが2つの場所、viewModelからどのように呼び出されるかに注目してください。プロパティオブザーバー(didSet)およびviewDidLoad方法。これは、ViewControllerを作成できるためです。ビューにアタッチする前に(viewDidLoadメソッドが呼び出される前に)ViewModelを割り当てます。

一方、ViewControllerのビューを別のビューにアタッチしてviewDidLoadを呼び出すこともできますが、viewModelの場合その時点で設定されていない場合、何も起こりません。

そのため、最初に、UIを満たすためにデータがすべて設定されているかどうかを確認する必要があります。予期しない使用からコードを保護することが重要です。

したがって、fillUIに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

ここで、pauseButtonPressを実装します方法:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

今必要なのは、実際のviewModelを設定することだけです。このViewControllerのプロパティ。これは「外部から」行います。

開くHomeViewController.swift ViewModelをファイルしてコメントを外します。 showGameScoreboardEditorViewControllerで行を作成および設定します方法:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

次に、アプリを実行します。次のようになります。

iOSアプリ

スコア、時間、およびチーム名を担当する中央のビューには、InterfaceBuilderで設定された値が表示されなくなりました。

これで、実際のModelオブジェクト(Gameオブジェクト)からデータを取得するViewModelオブジェクト自体からの値が表示されます。

優秀な!しかし、プレイヤーの見解はどうですか?これらのボタンはまだ何もしません。

プレーヤーの動きの追跡には6つのビューがあることを知っています。

PlayerScoreboardMoveEditorViewという名前の別のサブビューを作成しましたそのため、今のところ実際のデータには何もせず、PlayerScoreboardMoveEditorView.xib内のInterfaceBuilderを介して設定された静的な値を表示しますファイル。

あなたはそれにいくつかのデータを与える必要があります。

GameScoreboardEditorViewControllerで行ったのと同じ方法で行いますおよびGameScoreboardEditorViewModel

XcodeプロジェクトでViewModelグループを開き、ここで新しいプロトコルを定義します。

PlayerScoreboardMoveEditorViewModel.swiftという名前の新しいファイルを作成し、その中に次のコードを配置します。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

このViewModelプロトコルは、親ビューで行ったのと同じように、PlayerScoreboardMoveEditorViewに合うように設計されていますGameScoreboardEditorViewController

ユーザーが実行できる5つの異なる動きの値が必要であり、ユーザーがアクションボタンの1つに触れたときに反応する必要があります。 Stringも必要ですプレイヤー名。

これを行った後、親ビュー(GameScoreboardEditorViewController)で行ったのと同じように、このプロトコルを実装する具象クラスを作成します。

次に、このプロトコルの実装を作成します。新しいファイルを作成し、PlayerScoreboardMoveEditorViewModelFromPlayer.swiftという名前を付けて、このオブジェクトをNSObjectのサブクラスにします。また、PlayerScoreboardMoveEditorViewModelに準拠するようにしますプロトコル:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

ここで、このインスタンスを「外部から」作成するオブジェクトを用意し、それをPlayerScoreboardMoveEditorView内のプロパティとして設定する必要があります。

覚えておいてくださいHomeViewController viewModelの設定を担当しましたGameScoreboardEditorViewControllerのプロパティ?

同様に、GameScoreboardEditorViewController PlayerScoreboardMoveEditorViewの親ビューですそしてそれGameScoreboardEditorViewController PlayerScoreboardMoveEditorViewModelの作成を担当しますオブジェクト。

GameScoreboardEditorViewModelを拡張する必要があります最初。

GameScoreboardEditorViewMode lを開き、次の2つのプロパティを追加します。

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

また、GameScoreboardEditorViewModelFromGameを更新しますinitWithGameのすぐ上にあるこれらの2つのプロパティ方法:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

initWithGame内に次の2行を追加します。

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

そしてもちろん、不足しているplayerViewModelsWithPlayersを追加します方法:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

すごい!

ビューモデル(GameScoreboardEditorViewModel)をホームプレーヤーとアウェイプレーヤーの配列で更新しました。これらの2つの配列を埋める必要があります。

これは、これを使用したのと同じ場所で行いますviewModel UIを埋めるために。

開くGameScoreboardEditorViewController fillUIに移動します方法。メソッドの最後に次の行を追加します。

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

今のところ、実際のviewModelを追加しなかったため、ビルドエラーが発生しましたPlayerScoreboardMoveEditorView内のプロパティ。

init method inside the PlayerScoreboardMoveEditorView`の上に次のコードを追加します。

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

そしてfillUIを実装します方法:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

最後に、アプリを実行して、UI要素のデータがGameからの実際のデータであるかどうかを確認します。オブジェクト。

iOSアプリ

この時点で、MVVMデザインパターンを使用する機能的なアプリができました。

モデルをビューからうまく非表示にし、ビューはMVCで慣れているよりもはるかにシンプルです。

ここまでで、ViewとそのViewModelを含むアプリを作成しました。

そのビューには、ViewModelを持つ同じサブビュー(プレーヤービュー)の6つのインスタンスもあります。

ただし、お気づきかもしれませんが、UIに表示できるデータは(fillUIメソッドで)1回のみであり、そのデータは静的です。

ビューのデータがそのビューの存続期間中に変更されない場合は、この方法でMVVMを使用するための優れたクリーンなソリューションがあります。

ViewModelを動的にする

データが変更されるため、ViewModelを動的にする必要があります。

これが意味するのは、モデルが変更されると、ViewModelはそのパブリックプロパティ値を変更する必要があるということです。変更をビューに伝播します。ビューはUIを更新します。

これを行うには多くの方法があります。

モデルが変更されると、ViewModelが最初に通知されます。

変更内容をビューまで伝達するには、何らかのメカニズムが必要です。

いくつかのオプションが含まれます RxSwift 、これはかなり大きなライブラリであり、慣れるのに少し時間がかかります。

ViewModelは、プロパティ値の変更ごとにNSNotificationを起動する可能性がありますが、これにより、通知のサブスクライブやビューの割り当て解除時のサブスクライブ解除など、追加の処理が必要な多くのコードが追加されます。

Key-Value-Observing(KVO) 別のオプションですが、ユーザーはそのAPIが派手ではないことを確認します。

このチュートリアルでは、Swiftのジェネリックスとクロージャーを使用します。これらは、 バインディング、ジェネリック、Swift、MVVMの記事

それでは、サンプルアプリに戻りましょう。

ViewModelプロジェクトグループに移動し、新しいSwiftファイルDynamic.swiftを作成します。

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

このクラスは、ビューのライフサイクル中に変更されると予想されるViewModelのプロパティに使用します。

まず、PlayerScoreboardMoveEditorViewから始めますおよびそのViewModel、PlayerScoreboardMoveEditorViewModel

開くPlayerScoreboardMoveEditorViewModelそしてその特性を見てください。

playerNameだから変更される予定はありません。そのままにしておくことができます。

他の5つのプロパティ(5つの移動タイプ)が変更されるため、それについて何かを行う必要があります。ソリューション?上記のDynamicプロジェクトに追加したばかりのクラス。

内部PlayerScoreboardMoveEditorViewModel移動カウントを表す5つの文字列の定義を削除し、次のように置き換えます。

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

ViewModelプロトコルは次のようになります。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

これDynamic typeを使用すると、その特定のプロパティの値を変更すると同時に、change-listenerオブジェクト(この場合はビュー)に通知できます。

ここで、実際のViewModel実装を更新しますPlayerScoreboardMoveEditorViewModelFromPlayer

これを置き換えます:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

次のように:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

注:これらのプロパティをletで定数として宣言しても問題ありません。実際のプロパティは変更しないためです。 valueを変更しますDynamicのプロパティオブジェクト。

Dynamicを初期化しなかったため、ビルドエラーが発生しましたオブジェクト。

PlayerScoreboardMoveEditorViewModelFromPlayerのinitメソッド内で、moveプロパティの初期化を次のように置き換えます。

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

内部PlayerScoreboardMoveEditorViewModelFromPlayer makeMoveに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

ご覧のとおり、Dynamicのインスタンスを作成しましたクラスを割り当て、String値。データを更新する必要がある場合は、Dynamicを変更しないでください。プロパティ自体;むしろ更新しますvalueプロパティ。

すごい! PlayerScoreboardMoveEditorViewModel今はダイナミックです。

それを利用して、実際にこれらの変更をリッスンするビューに移動しましょう。

開くPlayerScoreboardMoveEditorViewとそのfillUIメソッド(String値をDynamicオブジェクトタイプに割り当てようとしているため、この時点でこのメソッドにビルドエラーが表示されるはずです。)

「エラーのある」行を置き換えます。

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

次のように:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

次に、移動アクションを表す5つのメソッドを実装します( ボタンアクション セクション):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

アプリを実行し、いくつかの移動ボタンをクリックします。アクションボタンをクリックすると、プレーヤービュー内のカウンター値がどのように変化するかがわかります。

iOSアプリ

PlayerScoreboardMoveEditorViewで終了ですおよびPlayerScoreboardMoveEditorViewModel

これは簡単でした。

ここで、メインビュー(GameScoreboardEditorViewController)でも同じことを行う必要があります。

まず、GameScoreboardEditorViewModelを開きますビューのライフサイクル中にどの値が変化すると予想されるかを確認します。

timescoreisFinishedisPausedを置き換えますDynamicを使用した定義バージョン:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

ViewModel実装(GameScoreboardEditorViewModelFromGame)に移動し、プロトコルで宣言されているプロパティで同じことを行います。

これを置き換えます:

var time: String var score: String var isFinished: Bool var isPaused: Bool

次のように:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

ViewModelのタイプをStringから変更したため、いくつかのエラーが発生します。およびBoolDynamicおよびDynamic

それを修正しましょう。

togglePauseを修正します次のように置き換えてください。

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

唯一の変更点は、プロパティ値をプロパティに直接設定しなくなったことです。代わりに、オブジェクトのvalueに設定しますプロパティ。

ここで、initWithGameを修正しますこれを置き換えることによる方法:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

次のように:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

あなたは今ポイントを取得する必要があります。

StringIntなどのプリミティブ値をラップしていますおよびBoolDynamicこれらのオブジェクトのバージョン。これにより、軽量のバインディングメカニズムが提供されます。

修正するエラーがもう1つあります。

startTimerでメソッドの場合、エラー行を次のように置き換えます。

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

プレーヤーのViewModelの場合と同じように、ViewModelを動的にアップグレードしました。ただし、ビューを更新する必要があります(GameScoreboardEditorViewController)。

fillUI全体を置き換えますこれを使った方法:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

唯一の違いは、4つの動的プロパティを変更し、それぞれに変更リスナーを追加したことです。

この時点で、アプリを実行している場合は、 開始/一時停止 ボタンはゲームタイマーを開始および一時停止します。これは、ゲーム中のタイムアウトに使用されます。

ポイントボタン(1および2ポイントボタン)のいずれかを押したときに、UIでスコアが変更されないことを除いて、ほぼ完了です。

これは、基になるGameでスコアの変更を実際に伝播していないためです。 ViewModelまでのモデルオブジェクト。

だから、Gameを開きます少し調べるためのモデルオブジェクト。そのupdateScoreを確認してください方法。

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

この方法は2つの重要なことを行います。

まず、isFinishedを設定しますプロパティto true両方のチームのスコアに基づいてゲームが終了した場合。

その後、スコアが変更されたという通知を投稿します。この通知はGameScoreboardEditorViewModelFromGameで聞きます通知ハンドラーメソッドで動的スコア値を更新します。

initWithGameの下部にこの行を追加しますメソッド(エラーを回避するためにsuper.init()呼び出しを忘れないでください):

super.init() subscribeToNotifications()

以下initWithGameメソッド、追加deinitクリーンアップを適切に実行し、NotificationCenterによって引き起こされるクラッシュを回避する必要があるためです。

deinit { unsubscribeFromNotifications() }

最後に、これらのメソッドの実装を追加します。このセクションをdeinitのすぐ下に追加します方法:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

次に、アプリを実行し、プレーヤーのビューをクリックしてスコアを変更します。すでに動的に接続しているのでscoreおよびisFinished Viewを使用したViewModelでは、ViewModel内のスコア値を変更するとすべてが機能するはずです。

アプリをさらに改善する方法

常に改善の余地がありますが、これはこのチュートリアルの範囲外です。

たとえば、ゲームが終了したとき(チームの1つが15ポイントに達したとき)に時間を自動的に停止するのではなく、プレーヤーのビューを非表示にするだけです。

必要に応じてアプリで遊んだり、アップグレードして「ゲームクリエイター」ビューを表示したりできます。このビューでは、ゲームの作成、チーム名の割り当て、プレーヤー名の割り当て、Gameの作成が行われます。 GameScoreboardEditorViewControllerを提示するために使用できるオブジェクト。

UITableViewを使用する別の「ゲームリスト」ビューを作成できます。進行中の複数のゲームを表示し、テーブルセルに詳細情報を表示します。セル選択では、GameScoreboardEditorViewControllerを表示できます選択したGameで。

GameLibraryすでに実装されています。そのライブラリ参照をイニシャライザのViewModelオブジェクトに渡すことを忘れないでください。たとえば、「ゲームクリエイター」のViewModelには、GameLibraryのインスタンスが必要です。作成されたGameを挿入できるように初期化子を通過しましたライブラリへのオブジェクト。 「ゲームリスト」のViewModelでも、ライブラリからすべてのゲームをフェッチするためにこの参照が必要になります。これはUITableViewで必要になります。

アイデアは、ViewModel内のすべてのダーティ(非UI)作業を非表示にし、UI(ビュー)が準備されたプレゼンテーションデータでのみ動作するようにすることです。

今何?

MVVMに慣れたら、を使用してさらに改善することができます ボブおじさんのクリーンアーキテクチャルール

追加の良い読み物は、Androidアーキテクチャに関する3部構成のチュートリアルです。

例はJava(Android用)で書かれており、Java(Swiftに非常に近く、Objective-CはJavaに近い)に精通している場合は、ViewModelオブジェクト内でコードをさらにリファクタリングする方法についてのアイデアが得られます。 iOSモジュール(UIKitまたはCoreLocationなど)をインポートしないこと。

これらのiOSモジュールは、純粋なNSObjectsの背後に隠すことができます。これは、コードの再利用に適しています。

MVVMはほとんどの人にとって良い選択です ios アプリ、そしてうまくいけば、あなたはあなたの次のプロジェクトでそれを試してみるでしょう。または、UIViewControllerを作成するときに、現在のプロジェクトで試してみてください。

関連: 静的パターンの操作:SwiftMVVMチュートリアル } viewModel.isFinished.bindAndFire { [unowned self] in if

Swiftチュートリアル:MVVMデザインパターンの概要

つまり、新しいiOSプロジェクトを開始すると、デザイナーから必要なすべての情報を受け取ります.pdfおよび.sketchドキュメントがあり、この新しいアプリをどのように構築するかについてのビジョンはすでにあります。

デザイナーのスケッチからViewControllerへのUI画面の転送を開始します.swift.xibおよび.storyboardファイル。

UITextFieldここで、UITableViewそこに、さらにいくつかUILabelsそしてUIButtonsのピンチ。 IBOutletsおよびIBActions含まれています。よし、まだUIゾーンにいる。



ただし、これらすべてのUI要素を使用して何かを行うときが来ました。 UIButtons指で触れるとUILabelsおよびUITableViews何をどの形式で表示するかを誰かに教える必要があります。

突然、3,000行を超えるコードが作成されました。

3,000行のSwiftコード

あなたはたくさんのスパゲッティコードになってしまいました。

これを解決するための最初のステップは、 Model-View-Controller (MVC)デザインパターン。ただし、このパターンには独自の問題があります。来る Model-View-ViewModel (MVVM)日を節約するデザインパターン。

スパゲッティコードの取り扱い

あっという間に、あなたのスタートViewControllerスマートになりすぎて、巨大になりすぎました。

ネットワークコード、データ解析コード、UIプレゼンテーションのデータ調整コード、アプリの状態通知、UIの状態変化。 if -ology内に閉じ込められたすべてのコードは、再利用できず、このプロジェクトにのみ収まります。

あなたのViewControllerコードは悪名高いスパゲッティコードになりました。

どうしてこうなりました?

考えられる理由は次のようなものです。

バックエンドデータがUITableView 内でどのように動作しているかを確認するために急いでいたので、数行のネットワークコードを 臨時雇用者 ViewControllerのメソッドそれをフェッチするだけです.jsonネットワークから。次に、その.json内のデータを処理する必要があったので、さらに別のデータを作成しました 臨時雇用者 それを達成する方法。または、さらに悪いことに、同じ方法でそれを行いました。

ViewControllerユーザー認証コードが登場したとき、成長を続けました。その後、データ形式が変化し始め、UIが進化し、いくつかの根本的な変更が必要になりました。そして、すでに大規模なif -ologyにifを追加し続けました。

しかし、どうしてUIViewController手に負えなくなったものは何ですか?

UIViewController UIコードの作業を開始するための論理的な場所です。これは、iOSデバイスでアプリを使用しているときに表示される物理的な画面を表します。 AppleでさえUIViewControllersを使用しています異なるアプリとアニメーション化されたUIを切り替えると、メインシステムアプリで。

AppleはUIの抽象化をUIViewControllerの内部に基づいています。これは、iOSのUIコードのコアであり、 MVC デザインパターン。

関連: iOS開発者が犯していることを知らない10の最も一般的な間違い

MVCデザインパターンへのアップグレード

MVCデザインパターン

MVCデザインパターンでは、 見る 非アクティブであると想定され、オンデマンドで準備されたデータのみを表示します。

コントローラ に取り組む必要があります モデル 準備するためのデータ ビュー 、次にそのデータを表示します。

見る 通知する責任もあります コントローラ ユーザーのタッチなどのアクションについて。

前述のように、UIViewController通常、UI画面を構築するための開始点です。その名前には、「ビュー」と「コントローラー」の両方が含まれていることに注意してください。これは、「ビューを制御する」ことを意味します。 「コントローラー」と「ビュー」の両方のコードを内部に含める必要があるという意味ではありません。

このビューとコントローラーコードの混合は、IBOutletsを移動したときによく発生します。 UIViewController内の小さなサブビューを作成し、UIViewControllerから直接それらのサブビューを操作します。代わりに、そのコードをカスタムの中にラップする必要がありますUIViewサブクラス。

これにより、ビューとコントローラーのコードパスが交差する可能性があることは簡単にわかります。

救助へのMVVM

これは、 MVVM パターンが重宝します。

UIViewController以降であるはずです コントローラ MVCパターンで、すでに多くのことを行っています ビュー 、それらをにマージできます 見る 私たちの新しいパターンの- MVVM

MVVMデザインパターン

MVVMデザインパターンでは、 モデル MVCパターンと同じです。単純なデータを表します。

見る UIViewで表されますまたはUIViewController .xibを伴うオブジェクトおよび.storyboard準備されたデータのみを表示するファイル。 (たとえば、ビュー内にNSDateFormatterコードを含めたくありません。)

から来る単純なフォーマットされた文字列のみ ViewModel

ViewModel すべての非同期ネットワークコード、ビジュアルプレゼンテーション用のデータ準備コード、およびリッスンするコードを非表示にします モデル 変化します。これらはすべて、この特定のものに合うようにモデル化された明確に定義されたAPIの背後に隠されています 見る

MVVMを使用する利点の1つは、テストです。以来 ViewModel 純粋ですNSObject (またはstructなど)、UIKitとは結合されていませんコードを使用すると、UIコードに影響を与えることなく、単体テストでより簡単にテストできます。

さて、 見るUIViewController / UIView)がはるかに簡単になりました ViewModel 間の接着剤として機能します モデル そして 見る

SwiftでのMVVMの適用

SwiftのMVVM

MVVMの動作を示すために、このチュートリアル用に作成されたサンプルXcodeプロジェクトをダウンロードして調べることができます。 ここに 。このプロジェクトでは、Swift3とXcode8.1を使用しています。

プロジェクトには2つのバージョンがあります。 スターター そして 終了しました

ザ・ 終了しました バージョンは完成したミニアプリケーションであり、 スターター 同じプロジェクトですが、メソッドとオブジェクトが実装されていません。

まず、ダウンロードすることをお勧めします スターター プロジェクトを作成し、このチュートリアルに従ってください。後で使用するためにプロジェクトのクイックリファレンスが必要な場合は、 終了しました 事業。

チュートリアルプロジェクトの紹介

チュートリアルプロジェクトは、ゲーム中のプレーヤーのアクションを追跡するためのバスケットボールアプリケーションです。

バスケットボールアプリケーション

これは、ユーザーの動きとピックアップゲームの全体的なスコアをすばやく追跡するために使用されます。

15のスコア(少なくとも2ポイントの差がある)に達するまで、2つのチームがプレーします。各プレーヤーは1ポイントから2ポイントを獲得でき、各プレーヤーはアシスト、リバウンド、ファウルを行うことができます。

プロジェクト階層は次のようになります。

プロジェクト階層

モデル

見る

ViewModel

ダウンロードしたXcodeプロジェクトには、 見る オブジェクト(UIViewおよびUIViewController)。プロジェクトには、データを提供する方法の1つをデモするために作成されたカスタムメイドのオブジェクトも含まれています。 ViewModel オブジェクト(Servicesグループ)。

Extensionsグループには、このチュートリアルの範囲外であり、自明であるUIコードの便利な拡張機能が含まれています。

この時点でアプリを実行すると、完成したUIが表示されますが、ユーザーがボタンを押しても何も起こりません。

これは、ビューとIBActionsのみを作成したためです。それらをアプリロジックに接続せず、UI要素にモデルからのデータ(後で学習するようにGameオブジェクトから)を入力しません。

ViewとModelをViewModelで接続する

MVVMデザインパターンでは、Viewはモデルについて何も知らないはずです。 Viewが知っているのは、ViewModelの操作方法だけです。

ビューを調べることから始めます。

GameScoreboardEditorViewController.swiftでファイル、fillUIこの時点でメソッドは空です。これは、UIにデータを入力する場所です。これを実現するには、ViewControllerのデータを提供する必要があります。これは、ViewModelオブジェクトを使用して行います。

まず、このViewControllerに必要なすべてのデータを含むViewModelオブジェクトを作成します。

空になるViewModelXcodeプロジェクトグループに移動し、GameScoreboardEditorViewModel.swiftを作成します。ファイルを作成し、プロトコルにします。

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

このようなプロトコルを使用すると、物事がきれいに保たれます。使用するデータのみを定義する必要があります。

次に、このプロトコルの実装を作成します。

GameScoreboardEditorViewModelFromGame.swiftという名前の新しいファイルを作成し、このオブジェクトをNSObjectのサブクラスにします。

また、GameScoreboardEditorViewModelに準拠するようにしますプロトコル:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

ViewModelがイニシャライザを介して機能するために必要なすべてを提供したことに注意してください。

あなたはそれにGameを提供しましたこのViewModelの下にあるモデルであるオブジェクト。

ここでアプリを実行しても、このViewModelデータをビュー自体に接続していないため、アプリは機能しません。

だから、GameScoreboardEditorViewController.swiftに戻ってくださいファイルを作成し、viewModelという名前のパブリックプロパティを作成します。

タイプGameScoreboardEditorViewModelにします。

viewDidLoadの直前に配置しますGameScoreboardEditorViewController.swift内のメソッド。

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

次に、fillUIを実装する必要があります方法。

このメソッドが2つの場所、viewModelからどのように呼び出されるかに注目してください。プロパティオブザーバー(didSet)およびviewDidLoad方法。これは、ViewControllerを作成できるためです。ビューにアタッチする前に(viewDidLoadメソッドが呼び出される前に)ViewModelを割り当てます。

一方、ViewControllerのビューを別のビューにアタッチしてviewDidLoadを呼び出すこともできますが、viewModelの場合その時点で設定されていない場合、何も起こりません。

そのため、最初に、UIを満たすためにデータがすべて設定されているかどうかを確認する必要があります。予期しない使用からコードを保護することが重要です。

したがって、fillUIに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

ここで、pauseButtonPressを実装します方法:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

今必要なのは、実際のviewModelを設定することだけです。このViewControllerのプロパティ。これは「外部から」行います。

開くHomeViewController.swift ViewModelをファイルしてコメントを外します。 showGameScoreboardEditorViewControllerで行を作成および設定します方法:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

次に、アプリを実行します。次のようになります。

iOSアプリ

スコア、時間、およびチーム名を担当する中央のビューには、InterfaceBuilderで設定された値が表示されなくなりました。

これで、実際のModelオブジェクト(Gameオブジェクト)からデータを取得するViewModelオブジェクト自体からの値が表示されます。

優秀な!しかし、プレイヤーの見解はどうですか?これらのボタンはまだ何もしません。

プレーヤーの動きの追跡には6つのビューがあることを知っています。

PlayerScoreboardMoveEditorViewという名前の別のサブビューを作成しましたそのため、今のところ実際のデータには何もせず、PlayerScoreboardMoveEditorView.xib内のInterfaceBuilderを介して設定された静的な値を表示しますファイル。

あなたはそれにいくつかのデータを与える必要があります。

GameScoreboardEditorViewControllerで行ったのと同じ方法で行いますおよびGameScoreboardEditorViewModel

XcodeプロジェクトでViewModelグループを開き、ここで新しいプロトコルを定義します。

PlayerScoreboardMoveEditorViewModel.swiftという名前の新しいファイルを作成し、その中に次のコードを配置します。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

このViewModelプロトコルは、親ビューで行ったのと同じように、PlayerScoreboardMoveEditorViewに合うように設計されていますGameScoreboardEditorViewController

ユーザーが実行できる5つの異なる動きの値が必要であり、ユーザーがアクションボタンの1つに触れたときに反応する必要があります。 Stringも必要ですプレイヤー名。

これを行った後、親ビュー(GameScoreboardEditorViewController)で行ったのと同じように、このプロトコルを実装する具象クラスを作成します。

次に、このプロトコルの実装を作成します。新しいファイルを作成し、PlayerScoreboardMoveEditorViewModelFromPlayer.swiftという名前を付けて、このオブジェクトをNSObjectのサブクラスにします。また、PlayerScoreboardMoveEditorViewModelに準拠するようにしますプロトコル:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

ここで、このインスタンスを「外部から」作成するオブジェクトを用意し、それをPlayerScoreboardMoveEditorView内のプロパティとして設定する必要があります。

覚えておいてくださいHomeViewController viewModelの設定を担当しましたGameScoreboardEditorViewControllerのプロパティ?

同様に、GameScoreboardEditorViewController PlayerScoreboardMoveEditorViewの親ビューですそしてそれGameScoreboardEditorViewController PlayerScoreboardMoveEditorViewModelの作成を担当しますオブジェクト。

GameScoreboardEditorViewModelを拡張する必要があります最初。

GameScoreboardEditorViewMode lを開き、次の2つのプロパティを追加します。

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

また、GameScoreboardEditorViewModelFromGameを更新しますinitWithGameのすぐ上にあるこれらの2つのプロパティ方法:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

initWithGame内に次の2行を追加します。

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

そしてもちろん、不足しているplayerViewModelsWithPlayersを追加します方法:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

すごい!

ビューモデル(GameScoreboardEditorViewModel)をホームプレーヤーとアウェイプレーヤーの配列で更新しました。これらの2つの配列を埋める必要があります。

これは、これを使用したのと同じ場所で行いますviewModel UIを埋めるために。

開くGameScoreboardEditorViewController fillUIに移動します方法。メソッドの最後に次の行を追加します。

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

今のところ、実際のviewModelを追加しなかったため、ビルドエラーが発生しましたPlayerScoreboardMoveEditorView内のプロパティ。

init method inside the PlayerScoreboardMoveEditorView`の上に次のコードを追加します。

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

そしてfillUIを実装します方法:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

最後に、アプリを実行して、UI要素のデータがGameからの実際のデータであるかどうかを確認します。オブジェクト。

iOSアプリ

この時点で、MVVMデザインパターンを使用する機能的なアプリができました。

モデルをビューからうまく非表示にし、ビューはMVCで慣れているよりもはるかにシンプルです。

ここまでで、ViewとそのViewModelを含むアプリを作成しました。

そのビューには、ViewModelを持つ同じサブビュー(プレーヤービュー)の6つのインスタンスもあります。

ただし、お気づきかもしれませんが、UIに表示できるデータは(fillUIメソッドで)1回のみであり、そのデータは静的です。

ビューのデータがそのビューの存続期間中に変更されない場合は、この方法でMVVMを使用するための優れたクリーンなソリューションがあります。

ViewModelを動的にする

データが変更されるため、ViewModelを動的にする必要があります。

これが意味するのは、モデルが変更されると、ViewModelはそのパブリックプロパティ値を変更する必要があるということです。変更をビューに伝播します。ビューはUIを更新します。

これを行うには多くの方法があります。

モデルが変更されると、ViewModelが最初に通知されます。

変更内容をビューまで伝達するには、何らかのメカニズムが必要です。

いくつかのオプションが含まれます RxSwift 、これはかなり大きなライブラリであり、慣れるのに少し時間がかかります。

ViewModelは、プロパティ値の変更ごとにNSNotificationを起動する可能性がありますが、これにより、通知のサブスクライブやビューの割り当て解除時のサブスクライブ解除など、追加の処理が必要な多くのコードが追加されます。

Key-Value-Observing(KVO) 別のオプションですが、ユーザーはそのAPIが派手ではないことを確認します。

このチュートリアルでは、Swiftのジェネリックスとクロージャーを使用します。これらは、 バインディング、ジェネリック、Swift、MVVMの記事

それでは、サンプルアプリに戻りましょう。

ViewModelプロジェクトグループに移動し、新しいSwiftファイルDynamic.swiftを作成します。

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

このクラスは、ビューのライフサイクル中に変更されると予想されるViewModelのプロパティに使用します。

まず、PlayerScoreboardMoveEditorViewから始めますおよびそのViewModel、PlayerScoreboardMoveEditorViewModel

開くPlayerScoreboardMoveEditorViewModelそしてその特性を見てください。

playerNameだから変更される予定はありません。そのままにしておくことができます。

他の5つのプロパティ(5つの移動タイプ)が変更されるため、それについて何かを行う必要があります。ソリューション?上記のDynamicプロジェクトに追加したばかりのクラス。

内部PlayerScoreboardMoveEditorViewModel移動カウントを表す5つの文字列の定義を削除し、次のように置き換えます。

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

ViewModelプロトコルは次のようになります。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

これDynamic typeを使用すると、その特定のプロパティの値を変更すると同時に、change-listenerオブジェクト(この場合はビュー)に通知できます。

ここで、実際のViewModel実装を更新しますPlayerScoreboardMoveEditorViewModelFromPlayer

これを置き換えます:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

次のように:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

注:これらのプロパティをletで定数として宣言しても問題ありません。実際のプロパティは変更しないためです。 valueを変更しますDynamicのプロパティオブジェクト。

Dynamicを初期化しなかったため、ビルドエラーが発生しましたオブジェクト。

PlayerScoreboardMoveEditorViewModelFromPlayerのinitメソッド内で、moveプロパティの初期化を次のように置き換えます。

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

内部PlayerScoreboardMoveEditorViewModelFromPlayer makeMoveに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

ご覧のとおり、Dynamicのインスタンスを作成しましたクラスを割り当て、String値。データを更新する必要がある場合は、Dynamicを変更しないでください。プロパティ自体;むしろ更新しますvalueプロパティ。

すごい! PlayerScoreboardMoveEditorViewModel今はダイナミックです。

それを利用して、実際にこれらの変更をリッスンするビューに移動しましょう。

開くPlayerScoreboardMoveEditorViewとそのfillUIメソッド(String値をDynamicオブジェクトタイプに割り当てようとしているため、この時点でこのメソッドにビルドエラーが表示されるはずです。)

「エラーのある」行を置き換えます。

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

次のように:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

次に、移動アクションを表す5つのメソッドを実装します( ボタンアクション セクション):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

アプリを実行し、いくつかの移動ボタンをクリックします。アクションボタンをクリックすると、プレーヤービュー内のカウンター値がどのように変化するかがわかります。

iOSアプリ

PlayerScoreboardMoveEditorViewで終了ですおよびPlayerScoreboardMoveEditorViewModel

これは簡単でした。

ここで、メインビュー(GameScoreboardEditorViewController)でも同じことを行う必要があります。

まず、GameScoreboardEditorViewModelを開きますビューのライフサイクル中にどの値が変化すると予想されるかを確認します。

timescoreisFinishedisPausedを置き換えますDynamicを使用した定義バージョン:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

ViewModel実装(GameScoreboardEditorViewModelFromGame)に移動し、プロトコルで宣言されているプロパティで同じことを行います。

これを置き換えます:

var time: String var score: String var isFinished: Bool var isPaused: Bool

次のように:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

ViewModelのタイプをStringから変更したため、いくつかのエラーが発生します。およびBoolDynamicおよびDynamic

それを修正しましょう。

togglePauseを修正します次のように置き換えてください。

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

唯一の変更点は、プロパティ値をプロパティに直接設定しなくなったことです。代わりに、オブジェクトのvalueに設定しますプロパティ。

ここで、initWithGameを修正しますこれを置き換えることによる方法:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

次のように:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

あなたは今ポイントを取得する必要があります。

StringIntなどのプリミティブ値をラップしていますおよびBoolDynamicこれらのオブジェクトのバージョン。これにより、軽量のバインディングメカニズムが提供されます。

修正するエラーがもう1つあります。

startTimerでメソッドの場合、エラー行を次のように置き換えます。

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

プレーヤーのViewModelの場合と同じように、ViewModelを動的にアップグレードしました。ただし、ビューを更新する必要があります(GameScoreboardEditorViewController)。

fillUI全体を置き換えますこれを使った方法:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

唯一の違いは、4つの動的プロパティを変更し、それぞれに変更リスナーを追加したことです。

この時点で、アプリを実行している場合は、 開始/一時停止 ボタンはゲームタイマーを開始および一時停止します。これは、ゲーム中のタイムアウトに使用されます。

ポイントボタン(1および2ポイントボタン)のいずれかを押したときに、UIでスコアが変更されないことを除いて、ほぼ完了です。

これは、基になるGameでスコアの変更を実際に伝播していないためです。 ViewModelまでのモデルオブジェクト。

だから、Gameを開きます少し調べるためのモデルオブジェクト。そのupdateScoreを確認してください方法。

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

この方法は2つの重要なことを行います。

まず、isFinishedを設定しますプロパティto true両方のチームのスコアに基づいてゲームが終了した場合。

その後、スコアが変更されたという通知を投稿します。この通知はGameScoreboardEditorViewModelFromGameで聞きます通知ハンドラーメソッドで動的スコア値を更新します。

initWithGameの下部にこの行を追加しますメソッド(エラーを回避するためにsuper.init()呼び出しを忘れないでください):

super.init() subscribeToNotifications()

以下initWithGameメソッド、追加deinitクリーンアップを適切に実行し、NotificationCenterによって引き起こされるクラッシュを回避する必要があるためです。

deinit { unsubscribeFromNotifications() }

最後に、これらのメソッドの実装を追加します。このセクションをdeinitのすぐ下に追加します方法:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

次に、アプリを実行し、プレーヤーのビューをクリックしてスコアを変更します。すでに動的に接続しているのでscoreおよびisFinished Viewを使用したViewModelでは、ViewModel内のスコア値を変更するとすべてが機能するはずです。

アプリをさらに改善する方法

常に改善の余地がありますが、これはこのチュートリアルの範囲外です。

たとえば、ゲームが終了したとき(チームの1つが15ポイントに達したとき)に時間を自動的に停止するのではなく、プレーヤーのビューを非表示にするだけです。

必要に応じてアプリで遊んだり、アップグレードして「ゲームクリエイター」ビューを表示したりできます。このビューでは、ゲームの作成、チーム名の割り当て、プレーヤー名の割り当て、Gameの作成が行われます。 GameScoreboardEditorViewControllerを提示するために使用できるオブジェクト。

UITableViewを使用する別の「ゲームリスト」ビューを作成できます。進行中の複数のゲームを表示し、テーブルセルに詳細情報を表示します。セル選択では、GameScoreboardEditorViewControllerを表示できます選択したGameで。

GameLibraryすでに実装されています。そのライブラリ参照をイニシャライザのViewModelオブジェクトに渡すことを忘れないでください。たとえば、「ゲームクリエイター」のViewModelには、GameLibraryのインスタンスが必要です。作成されたGameを挿入できるように初期化子を通過しましたライブラリへのオブジェクト。 「ゲームリスト」のViewModelでも、ライブラリからすべてのゲームをフェッチするためにこの参照が必要になります。これはUITableViewで必要になります。

アイデアは、ViewModel内のすべてのダーティ(非UI)作業を非表示にし、UI(ビュー)が準備されたプレゼンテーションデータでのみ動作するようにすることです。

今何?

MVVMに慣れたら、を使用してさらに改善することができます ボブおじさんのクリーンアーキテクチャルール

追加の良い読み物は、Androidアーキテクチャに関する3部構成のチュートリアルです。

例はJava(Android用)で書かれており、Java(Swiftに非常に近く、Objective-CはJavaに近い)に精通している場合は、ViewModelオブジェクト内でコードをさらにリファクタリングする方法についてのアイデアが得られます。 iOSモジュール(UIKitまたはCoreLocationなど)をインポートしないこと。

これらのiOSモジュールは、純粋なNSObjectsの背後に隠すことができます。これは、コードの再利用に適しています。

MVVMはほとんどの人にとって良い選択です ios アプリ、そしてうまくいけば、あなたはあなたの次のプロジェクトでそれを試してみるでしょう。または、UIViewControllerを作成するときに、現在のプロジェクトで試してみてください。

関連: 静的パターンの操作:SwiftMVVMチュートリアル { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title =

Swiftチュートリアル:MVVMデザインパターンの概要

つまり、新しいiOSプロジェクトを開始すると、デザイナーから必要なすべての情報を受け取ります.pdfおよび.sketchドキュメントがあり、この新しいアプリをどのように構築するかについてのビジョンはすでにあります。

デザイナーのスケッチからViewControllerへのUI画面の転送を開始します.swift.xibおよび.storyboardファイル。

UITextFieldここで、UITableViewそこに、さらにいくつかUILabelsそしてUIButtonsのピンチ。 IBOutletsおよびIBActions含まれています。よし、まだUIゾーンにいる。



ただし、これらすべてのUI要素を使用して何かを行うときが来ました。 UIButtons指で触れるとUILabelsおよびUITableViews何をどの形式で表示するかを誰かに教える必要があります。

突然、3,000行を超えるコードが作成されました。

3,000行のSwiftコード

あなたはたくさんのスパゲッティコードになってしまいました。

これを解決するための最初のステップは、 Model-View-Controller (MVC)デザインパターン。ただし、このパターンには独自の問題があります。来る Model-View-ViewModel (MVVM)日を節約するデザインパターン。

スパゲッティコードの取り扱い

あっという間に、あなたのスタートViewControllerスマートになりすぎて、巨大になりすぎました。

ネットワークコード、データ解析コード、UIプレゼンテーションのデータ調整コード、アプリの状態通知、UIの状態変化。 if -ology内に閉じ込められたすべてのコードは、再利用できず、このプロジェクトにのみ収まります。

あなたのViewControllerコードは悪名高いスパゲッティコードになりました。

どうしてこうなりました?

考えられる理由は次のようなものです。

バックエンドデータがUITableView 内でどのように動作しているかを確認するために急いでいたので、数行のネットワークコードを 臨時雇用者 ViewControllerのメソッドそれをフェッチするだけです.jsonネットワークから。次に、その.json内のデータを処理する必要があったので、さらに別のデータを作成しました 臨時雇用者 それを達成する方法。または、さらに悪いことに、同じ方法でそれを行いました。

ViewControllerユーザー認証コードが登場したとき、成長を続けました。その後、データ形式が変化し始め、UIが進化し、いくつかの根本的な変更が必要になりました。そして、すでに大規模なif -ologyにifを追加し続けました。

しかし、どうしてUIViewController手に負えなくなったものは何ですか?

UIViewController UIコードの作業を開始するための論理的な場所です。これは、iOSデバイスでアプリを使用しているときに表示される物理的な画面を表します。 AppleでさえUIViewControllersを使用しています異なるアプリとアニメーション化されたUIを切り替えると、メインシステムアプリで。

AppleはUIの抽象化をUIViewControllerの内部に基づいています。これは、iOSのUIコードのコアであり、 MVC デザインパターン。

関連: iOS開発者が犯していることを知らない10の最も一般的な間違い

MVCデザインパターンへのアップグレード

MVCデザインパターン

MVCデザインパターンでは、 見る 非アクティブであると想定され、オンデマンドで準備されたデータのみを表示します。

コントローラ に取り組む必要があります モデル 準備するためのデータ ビュー 、次にそのデータを表示します。

見る 通知する責任もあります コントローラ ユーザーのタッチなどのアクションについて。

前述のように、UIViewController通常、UI画面を構築するための開始点です。その名前には、「ビュー」と「コントローラー」の両方が含まれていることに注意してください。これは、「ビューを制御する」ことを意味します。 「コントローラー」と「ビュー」の両方のコードを内部に含める必要があるという意味ではありません。

このビューとコントローラーコードの混合は、IBOutletsを移動したときによく発生します。 UIViewController内の小さなサブビューを作成し、UIViewControllerから直接それらのサブビューを操作します。代わりに、そのコードをカスタムの中にラップする必要がありますUIViewサブクラス。

これにより、ビューとコントローラーのコードパスが交差する可能性があることは簡単にわかります。

救助へのMVVM

これは、 MVVM パターンが重宝します。

UIViewController以降であるはずです コントローラ MVCパターンで、すでに多くのことを行っています ビュー 、それらをにマージできます 見る 私たちの新しいパターンの- MVVM

MVVMデザインパターン

MVVMデザインパターンでは、 モデル MVCパターンと同じです。単純なデータを表します。

見る UIViewで表されますまたはUIViewController .xibを伴うオブジェクトおよび.storyboard準備されたデータのみを表示するファイル。 (たとえば、ビュー内にNSDateFormatterコードを含めたくありません。)

から来る単純なフォーマットされた文字列のみ ViewModel

ViewModel すべての非同期ネットワークコード、ビジュアルプレゼンテーション用のデータ準備コード、およびリッスンするコードを非表示にします モデル 変化します。これらはすべて、この特定のものに合うようにモデル化された明確に定義されたAPIの背後に隠されています 見る

MVVMを使用する利点の1つは、テストです。以来 ViewModel 純粋ですNSObject (またはstructなど)、UIKitとは結合されていませんコードを使用すると、UIコードに影響を与えることなく、単体テストでより簡単にテストできます。

さて、 見るUIViewController / UIView)がはるかに簡単になりました ViewModel 間の接着剤として機能します モデル そして 見る

SwiftでのMVVMの適用

SwiftのMVVM

MVVMの動作を示すために、このチュートリアル用に作成されたサンプルXcodeプロジェクトをダウンロードして調べることができます。 ここに 。このプロジェクトでは、Swift3とXcode8.1を使用しています。

プロジェクトには2つのバージョンがあります。 スターター そして 終了しました

ザ・ 終了しました バージョンは完成したミニアプリケーションであり、 スターター 同じプロジェクトですが、メソッドとオブジェクトが実装されていません。

まず、ダウンロードすることをお勧めします スターター プロジェクトを作成し、このチュートリアルに従ってください。後で使用するためにプロジェクトのクイックリファレンスが必要な場合は、 終了しました 事業。

チュートリアルプロジェクトの紹介

チュートリアルプロジェクトは、ゲーム中のプレーヤーのアクションを追跡するためのバスケットボールアプリケーションです。

バスケットボールアプリケーション

これは、ユーザーの動きとピックアップゲームの全体的なスコアをすばやく追跡するために使用されます。

15のスコア(少なくとも2ポイントの差がある)に達するまで、2つのチームがプレーします。各プレーヤーは1ポイントから2ポイントを獲得でき、各プレーヤーはアシスト、リバウンド、ファウルを行うことができます。

プロジェクト階層は次のようになります。

プロジェクト階層

モデル

見る

ViewModel

ダウンロードしたXcodeプロジェクトには、 見る オブジェクト(UIViewおよびUIViewController)。プロジェクトには、データを提供する方法の1つをデモするために作成されたカスタムメイドのオブジェクトも含まれています。 ViewModel オブジェクト(Servicesグループ)。

Extensionsグループには、このチュートリアルの範囲外であり、自明であるUIコードの便利な拡張機能が含まれています。

この時点でアプリを実行すると、完成したUIが表示されますが、ユーザーがボタンを押しても何も起こりません。

これは、ビューとIBActionsのみを作成したためです。それらをアプリロジックに接続せず、UI要素にモデルからのデータ(後で学習するようにGameオブジェクトから)を入力しません。

ViewとModelをViewModelで接続する

MVVMデザインパターンでは、Viewはモデルについて何も知らないはずです。 Viewが知っているのは、ViewModelの操作方法だけです。

ビューを調べることから始めます。

GameScoreboardEditorViewController.swiftでファイル、fillUIこの時点でメソッドは空です。これは、UIにデータを入力する場所です。これを実現するには、ViewControllerのデータを提供する必要があります。これは、ViewModelオブジェクトを使用して行います。

まず、このViewControllerに必要なすべてのデータを含むViewModelオブジェクトを作成します。

空になるViewModelXcodeプロジェクトグループに移動し、GameScoreboardEditorViewModel.swiftを作成します。ファイルを作成し、プロトコルにします。

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: String { get } var score: String { get } var isFinished: Bool { get } var isPaused: Bool { get } func togglePause(); }

このようなプロトコルを使用すると、物事がきれいに保たれます。使用するデータのみを定義する必要があります。

次に、このプロトコルの実装を作成します。

GameScoreboardEditorViewModelFromGame.swiftという名前の新しいファイルを作成し、このオブジェクトをNSObjectのサブクラスにします。

また、GameScoreboardEditorViewModelに準拠するようにしますプロトコル:

import Foundation class GameScoreboardEditorViewModelFromGame: NSObject, GameScoreboardEditorViewModel { let game: Game struct Formatter { static let durationFormatter: DateComponentsFormatter = { let dateFormatter = DateComponentsFormatter() dateFormatter.unitsStyle = .positional return dateFormatter }() } // MARK: GameScoreboardEditorViewModel protocol var homeTeam: String var awayTeam: String var time: String var score: String var isFinished: Bool var isPaused: Bool func togglePause() { if isPaused { startTimer() } else { pauseTimer() } self.isPaused = !isPaused } // MARK: Init init(withGame game: Game) { self.game = game self.homeTeam = game.homeTeam.name self.awayTeam = game.awayTeam.name self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) self.isFinished = game.isFinished self.isPaused = true } // MARK: Private fileprivate var gameTimer: Timer? fileprivate func startTimer() { let interval: TimeInterval = 0.001 gameTimer = Timer.schedule(repeatInterval: interval) { timer in self.game.time += interval self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game) } } fileprivate func pauseTimer() { gameTimer?.invalidate() gameTimer = nil } // MARK: String Utils fileprivate static func timeFormatted(totalMillis: Int) -> String { let millis: Int = totalMillis % 1000 / 100 // '/ 100' String { return timeFormatted(totalMillis: Int(game.time * 1000)) } fileprivate static func scorePretty(for game: Game) -> String { return String(format: '(game.homeTeamScore) - (game.awayTeamScore)') } }

ViewModelがイニシャライザを介して機能するために必要なすべてを提供したことに注意してください。

あなたはそれにGameを提供しましたこのViewModelの下にあるモデルであるオブジェクト。

ここでアプリを実行しても、このViewModelデータをビュー自体に接続していないため、アプリは機能しません。

だから、GameScoreboardEditorViewController.swiftに戻ってくださいファイルを作成し、viewModelという名前のパブリックプロパティを作成します。

タイプGameScoreboardEditorViewModelにします。

viewDidLoadの直前に配置しますGameScoreboardEditorViewController.swift内のメソッド。

var viewModel: GameScoreboardEditorViewModel? { didSet { fillUI() } }

次に、fillUIを実装する必要があります方法。

このメソッドが2つの場所、viewModelからどのように呼び出されるかに注目してください。プロパティオブザーバー(didSet)およびviewDidLoad方法。これは、ViewControllerを作成できるためです。ビューにアタッチする前に(viewDidLoadメソッドが呼び出される前に)ViewModelを割り当てます。

一方、ViewControllerのビューを別のビューにアタッチしてviewDidLoadを呼び出すこともできますが、viewModelの場合その時点で設定されていない場合、何も起こりません。

そのため、最初に、UIを満たすためにデータがすべて設定されているかどうかを確認する必要があります。予期しない使用からコードを保護することが重要です。

したがって、fillUIに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } // we are sure here that we have all the setup done self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam self.scoreLabel.text = viewModel.score self.timeLabel.text = viewModel.time let title: String = viewModel.isPaused ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) }

ここで、pauseButtonPressを実装します方法:

@IBAction func pauseButtonPress(_ sender: AnyObject) { viewModel?.togglePause() }

今必要なのは、実際のviewModelを設定することだけです。このViewControllerのプロパティ。これは「外部から」行います。

開くHomeViewController.swift ViewModelをファイルしてコメントを外します。 showGameScoreboardEditorViewControllerで行を作成および設定します方法:

// uncomment this when view model is implemented let viewModel = GameScoreboardEditorViewModelFromGame(withGame: game) controller.viewModel = viewModel

次に、アプリを実行します。次のようになります。

iOSアプリ

スコア、時間、およびチーム名を担当する中央のビューには、InterfaceBuilderで設定された値が表示されなくなりました。

これで、実際のModelオブジェクト(Gameオブジェクト)からデータを取得するViewModelオブジェクト自体からの値が表示されます。

優秀な!しかし、プレイヤーの見解はどうですか?これらのボタンはまだ何もしません。

プレーヤーの動きの追跡には6つのビューがあることを知っています。

PlayerScoreboardMoveEditorViewという名前の別のサブビューを作成しましたそのため、今のところ実際のデータには何もせず、PlayerScoreboardMoveEditorView.xib内のInterfaceBuilderを介して設定された静的な値を表示しますファイル。

あなたはそれにいくつかのデータを与える必要があります。

GameScoreboardEditorViewControllerで行ったのと同じ方法で行いますおよびGameScoreboardEditorViewModel

XcodeプロジェクトでViewModelグループを開き、ここで新しいプロトコルを定義します。

PlayerScoreboardMoveEditorViewModel.swiftという名前の新しいファイルを作成し、その中に次のコードを配置します。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: String { get } var twoPointMoveCount: String { get } var assistMoveCount: String { get } var reboundMoveCount: String { get } var foulMoveCount: String { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

このViewModelプロトコルは、親ビューで行ったのと同じように、PlayerScoreboardMoveEditorViewに合うように設計されていますGameScoreboardEditorViewController

ユーザーが実行できる5つの異なる動きの値が必要であり、ユーザーがアクションボタンの1つに触れたときに反応する必要があります。 Stringも必要ですプレイヤー名。

これを行った後、親ビュー(GameScoreboardEditorViewController)で行ったのと同じように、このプロトコルを実装する具象クラスを作成します。

次に、このプロトコルの実装を作成します。新しいファイルを作成し、PlayerScoreboardMoveEditorViewModelFromPlayer.swiftという名前を付けて、このオブジェクトをNSObjectのサブクラスにします。また、PlayerScoreboardMoveEditorViewModelに準拠するようにしますプロトコル:

import Foundation class PlayerScoreboardMoveEditorViewModelFromPlayer: NSObject, PlayerScoreboardMoveEditorViewModel { fileprivate let player: Player fileprivate let game: Game // MARK: PlayerScoreboardMoveEditorViewModel protocol let playerName: String var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String func onePointMove() { makeMove(.onePoint) } func twoPointsMove() { makeMove(.twoPoints) } func assistMove() { makeMove(.assist) } func reboundMove() { makeMove(.rebound) } func foulMove() { makeMove(.foul) } // MARK: Init init(withGame game: Game, player: Player) { self.game = game self.player = player self.playerName = player.name self.onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' self.twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' self.assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' self.reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' self.foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } // MARK: Private fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount = '(game.playerMoveCount(for: player, move: .foul))' } }

ここで、このインスタンスを「外部から」作成するオブジェクトを用意し、それをPlayerScoreboardMoveEditorView内のプロパティとして設定する必要があります。

覚えておいてくださいHomeViewController viewModelの設定を担当しましたGameScoreboardEditorViewControllerのプロパティ?

同様に、GameScoreboardEditorViewController PlayerScoreboardMoveEditorViewの親ビューですそしてそれGameScoreboardEditorViewController PlayerScoreboardMoveEditorViewModelの作成を担当しますオブジェクト。

GameScoreboardEditorViewModelを拡張する必要があります最初。

GameScoreboardEditorViewMode lを開き、次の2つのプロパティを追加します。

var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get }

また、GameScoreboardEditorViewModelFromGameを更新しますinitWithGameのすぐ上にあるこれらの2つのプロパティ方法:

let homePlayers: [PlayerScoreboardMoveEditorViewModel] let awayPlayers: [PlayerScoreboardMoveEditorViewModel]

initWithGame内に次の2行を追加します。

self.homePlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.homeTeam.players, game: game) self.awayPlayers = GameScoreboardEditorViewModelFromGame.playerViewModels(from: game.awayTeam.players, game: game)

そしてもちろん、不足しているplayerViewModelsWithPlayersを追加します方法:

// MARK: Private Init fileprivate static func playerViewModels(from players: [Player], game: Game) -> [PlayerScoreboardMoveEditorViewModel] { var playerViewModels: [PlayerScoreboardMoveEditorViewModel] = [PlayerScoreboardMoveEditorViewModel]() for player in players { playerViewModels.append(PlayerScoreboardMoveEditorViewModelFromPlayer(withGame: game, player: player)) } return playerViewModels }

すごい!

ビューモデル(GameScoreboardEditorViewModel)をホームプレーヤーとアウェイプレーヤーの配列で更新しました。これらの2つの配列を埋める必要があります。

これは、これを使用したのと同じ場所で行いますviewModel UIを埋めるために。

開くGameScoreboardEditorViewController fillUIに移動します方法。メソッドの最後に次の行を追加します。

homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2]

今のところ、実際のviewModelを追加しなかったため、ビルドエラーが発生しましたPlayerScoreboardMoveEditorView内のプロパティ。

init method inside the PlayerScoreboardMoveEditorView`の上に次のコードを追加します。

var viewModel: PlayerScoreboardMoveEditorViewModel? { didSet { fillUI() } }

そしてfillUIを実装します方法:

fileprivate func fillUI() { guard let viewModel = viewModel else { return } self.name.text = viewModel.playerName self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount }

最後に、アプリを実行して、UI要素のデータがGameからの実際のデータであるかどうかを確認します。オブジェクト。

iOSアプリ

この時点で、MVVMデザインパターンを使用する機能的なアプリができました。

モデルをビューからうまく非表示にし、ビューはMVCで慣れているよりもはるかにシンプルです。

ここまでで、ViewとそのViewModelを含むアプリを作成しました。

そのビューには、ViewModelを持つ同じサブビュー(プレーヤービュー)の6つのインスタンスもあります。

ただし、お気づきかもしれませんが、UIに表示できるデータは(fillUIメソッドで)1回のみであり、そのデータは静的です。

ビューのデータがそのビューの存続期間中に変更されない場合は、この方法でMVVMを使用するための優れたクリーンなソリューションがあります。

ViewModelを動的にする

データが変更されるため、ViewModelを動的にする必要があります。

これが意味するのは、モデルが変更されると、ViewModelはそのパブリックプロパティ値を変更する必要があるということです。変更をビューに伝播します。ビューはUIを更新します。

これを行うには多くの方法があります。

モデルが変更されると、ViewModelが最初に通知されます。

変更内容をビューまで伝達するには、何らかのメカニズムが必要です。

いくつかのオプションが含まれます RxSwift 、これはかなり大きなライブラリであり、慣れるのに少し時間がかかります。

ViewModelは、プロパティ値の変更ごとにNSNotificationを起動する可能性がありますが、これにより、通知のサブスクライブやビューの割り当て解除時のサブスクライブ解除など、追加の処理が必要な多くのコードが追加されます。

Key-Value-Observing(KVO) 別のオプションですが、ユーザーはそのAPIが派手ではないことを確認します。

このチュートリアルでは、Swiftのジェネリックスとクロージャーを使用します。これらは、 バインディング、ジェネリック、Swift、MVVMの記事

それでは、サンプルアプリに戻りましょう。

ViewModelプロジェクトグループに移動し、新しいSwiftファイルDynamic.swiftを作成します。

class Dynamic { typealias Listener = (T) -> () var listener: Listener? func bind(_ listener: Listener?) { self.listener = listener } func bindAndFire(_ listener: Listener?) { self.listener = listener listener?(value) } var value: T { didSet { listener?(value) } } init(_ v: T) { value = v } }

このクラスは、ビューのライフサイクル中に変更されると予想されるViewModelのプロパティに使用します。

まず、PlayerScoreboardMoveEditorViewから始めますおよびそのViewModel、PlayerScoreboardMoveEditorViewModel

開くPlayerScoreboardMoveEditorViewModelそしてその特性を見てください。

playerNameだから変更される予定はありません。そのままにしておくことができます。

他の5つのプロパティ(5つの移動タイプ)が変更されるため、それについて何かを行う必要があります。ソリューション?上記のDynamicプロジェクトに追加したばかりのクラス。

内部PlayerScoreboardMoveEditorViewModel移動カウントを表す5つの文字列の定義を削除し、次のように置き換えます。

var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get }

ViewModelプロトコルは次のようになります。

import Foundation protocol PlayerScoreboardMoveEditorViewModel { var playerName: String { get } var onePointMoveCount: Dynamic { get } var twoPointMoveCount: Dynamic { get } var assistMoveCount: Dynamic { get } var reboundMoveCount: Dynamic { get } var foulMoveCount: Dynamic { get } func onePointMove() func twoPointsMove() func assistMove() func reboundMove() func foulMove() }

これDynamic typeを使用すると、その特定のプロパティの値を変更すると同時に、change-listenerオブジェクト(この場合はビュー)に通知できます。

ここで、実際のViewModel実装を更新しますPlayerScoreboardMoveEditorViewModelFromPlayer

これを置き換えます:

var onePointMoveCount: String var twoPointMoveCount: String var assistMoveCount: String var reboundMoveCount: String var foulMoveCount: String

次のように:

let onePointMoveCount: Dynamic let twoPointMoveCount: Dynamic let assistMoveCount: Dynamic let reboundMoveCount: Dynamic let foulMoveCount: Dynamic

注:これらのプロパティをletで定数として宣言しても問題ありません。実際のプロパティは変更しないためです。 valueを変更しますDynamicのプロパティオブジェクト。

Dynamicを初期化しなかったため、ビルドエラーが発生しましたオブジェクト。

PlayerScoreboardMoveEditorViewModelFromPlayerのinitメソッド内で、moveプロパティの初期化を次のように置き換えます。

self.onePointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .onePoint))') self.twoPointMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .twoPoints))') self.assistMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .assist))') self.reboundMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .rebound))') self.foulMoveCount = Dynamic('(game.playerMoveCount(for: player, move: .foul))')

内部PlayerScoreboardMoveEditorViewModelFromPlayer makeMoveに移動しますメソッドを作成し、次のコードに置き換えます。

fileprivate func makeMove(_ move: PlayerInGameMove) { game.addPlayerMove(move, for: player) onePointMoveCount.value = '(game.playerMoveCount(for: player, move: .onePoint))' twoPointMoveCount.value = '(game.playerMoveCount(for: player, move: .twoPoints))' assistMoveCount.value = '(game.playerMoveCount(for: player, move: .assist))' reboundMoveCount.value = '(game.playerMoveCount(for: player, move: .rebound))' foulMoveCount.value = '(game.playerMoveCount(for: player, move: .foul))' }

ご覧のとおり、Dynamicのインスタンスを作成しましたクラスを割り当て、String値。データを更新する必要がある場合は、Dynamicを変更しないでください。プロパティ自体;むしろ更新しますvalueプロパティ。

すごい! PlayerScoreboardMoveEditorViewModel今はダイナミックです。

それを利用して、実際にこれらの変更をリッスンするビューに移動しましょう。

開くPlayerScoreboardMoveEditorViewとそのfillUIメソッド(String値をDynamicオブジェクトタイプに割り当てようとしているため、この時点でこのメソッドにビルドエラーが表示されるはずです。)

「エラーのある」行を置き換えます。

self.onePointCountLabel.text = viewModel.onePointMoveCount self.twoPointCountLabel.text = viewModel.twoPointMoveCount self.assistCountLabel.text = viewModel.assistMoveCount self.reboundCountLabel.text = viewModel.reboundMoveCount self.foulCountLabel.text = viewModel.foulMoveCount

次のように:

viewModel.onePointMoveCount.bindAndFire { [unowned self] in self.onePointCountLabel.text = $0 } viewModel.twoPointMoveCount.bindAndFire { [unowned self] in self.twoPointCountLabel.text = $0 } viewModel.assistMoveCount.bindAndFire { [unowned self] in self.assistCountLabel.text = $0 } viewModel.reboundMoveCount.bindAndFire { [unowned self] in self.reboundCountLabel.text = $0 } viewModel.foulMoveCount.bindAndFire { [unowned self] in self.foulCountLabel.text = $0 }

次に、移動アクションを表す5つのメソッドを実装します( ボタンアクション セクション):

@IBAction func onePointAction(_ sender: Any) { viewModel?.onePointMove() } @IBAction func twoPointsAction(_ sender: Any) { viewModel?.twoPointsMove() } @IBAction func assistAction(_ sender: Any) { viewModel?.assistMove() } @IBAction func reboundAction(_ sender: Any) { viewModel?.reboundMove() } @IBAction func foulAction(_ sender: Any) { viewModel?.foulMove() }

アプリを実行し、いくつかの移動ボタンをクリックします。アクションボタンをクリックすると、プレーヤービュー内のカウンター値がどのように変化するかがわかります。

iOSアプリ

PlayerScoreboardMoveEditorViewで終了ですおよびPlayerScoreboardMoveEditorViewModel

これは簡単でした。

ここで、メインビュー(GameScoreboardEditorViewController)でも同じことを行う必要があります。

まず、GameScoreboardEditorViewModelを開きますビューのライフサイクル中にどの値が変化すると予想されるかを確認します。

timescoreisFinishedisPausedを置き換えますDynamicを使用した定義バージョン:

import Foundation protocol GameScoreboardEditorViewModel { var homeTeam: String { get } var awayTeam: String { get } var time: Dynamic { get } var score: Dynamic { get } var isFinished: Dynamic { get } var isPaused: Dynamic { get } func togglePause() var homePlayers: [PlayerScoreboardMoveEditorViewModel] { get } var awayPlayers: [PlayerScoreboardMoveEditorViewModel] { get } }

ViewModel実装(GameScoreboardEditorViewModelFromGame)に移動し、プロトコルで宣言されているプロパティで同じことを行います。

これを置き換えます:

var time: String var score: String var isFinished: Bool var isPaused: Bool

次のように:

let time: Dynamic let score: Dynamic let isFinished: Dynamic let isPaused: Dynamic

ViewModelのタイプをStringから変更したため、いくつかのエラーが発生します。およびBoolDynamicおよびDynamic

それを修正しましょう。

togglePauseを修正します次のように置き換えてください。

func togglePause() { if isPaused.value { startTimer() } else { pauseTimer() } self.isPaused.value = !isPaused.value }

唯一の変更点は、プロパティ値をプロパティに直接設定しなくなったことです。代わりに、オブジェクトのvalueに設定しますプロパティ。

ここで、initWithGameを修正しますこれを置き換えることによる方法:

self.time = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(game) self.score = GameScoreboardEditorViewModelFromGame.scorePretty(game) self.isFinished = game.isFinished self.isPaused = true

次のように:

self.time = Dynamic(GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: game)) self.score = Dynamic(GameScoreboardEditorViewModelFromGame.scorePretty(for: game)) self.isFinished = Dynamic(game.isFinished) self.isPaused = Dynamic(true)

あなたは今ポイントを取得する必要があります。

StringIntなどのプリミティブ値をラップしていますおよびBoolDynamicこれらのオブジェクトのバージョン。これにより、軽量のバインディングメカニズムが提供されます。

修正するエラーがもう1つあります。

startTimerでメソッドの場合、エラー行を次のように置き換えます。

self.time.value = GameScoreboardEditorViewModelFromGame.timeRemainingPretty(for: self.game)

プレーヤーのViewModelの場合と同じように、ViewModelを動的にアップグレードしました。ただし、ビューを更新する必要があります(GameScoreboardEditorViewController)。

fillUI全体を置き換えますこれを使った方法:

fileprivate func fillUI() { if !isViewLoaded { return } guard let viewModel = viewModel else { return } self.homeTeamNameLabel.text = viewModel.homeTeam self.awayTeamNameLabel.text = viewModel.awayTeam viewModel.score.bindAndFire { [unowned self] in self.scoreLabel.text = $0 } viewModel.time.bindAndFire { [unowned self] in self.timeLabel.text = $0 } viewModel.isFinished.bindAndFire { [unowned self] in if $0 { self.homePlayer1View.isHidden = true self.homePlayer2View.isHidden = true self.homePlayer3View.isHidden = true self.awayPlayer1View.isHidden = true self.awayPlayer2View.isHidden = true self.awayPlayer3View.isHidden = true } } viewModel.isPaused.bindAndFire { [unowned self] in let title = $0 ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

唯一の違いは、4つの動的プロパティを変更し、それぞれに変更リスナーを追加したことです。

この時点で、アプリを実行している場合は、 開始/一時停止 ボタンはゲームタイマーを開始および一時停止します。これは、ゲーム中のタイムアウトに使用されます。

ポイントボタン(1および2ポイントボタン)のいずれかを押したときに、UIでスコアが変更されないことを除いて、ほぼ完了です。

これは、基になるGameでスコアの変更を実際に伝播していないためです。 ViewModelまでのモデルオブジェクト。

だから、Gameを開きます少し調べるためのモデルオブジェクト。そのupdateScoreを確認してください方法。

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

この方法は2つの重要なことを行います。

まず、isFinishedを設定しますプロパティto true両方のチームのスコアに基づいてゲームが終了した場合。

その後、スコアが変更されたという通知を投稿します。この通知はGameScoreboardEditorViewModelFromGameで聞きます通知ハンドラーメソッドで動的スコア値を更新します。

initWithGameの下部にこの行を追加しますメソッド(エラーを回避するためにsuper.init()呼び出しを忘れないでください):

super.init() subscribeToNotifications()

以下initWithGameメソッド、追加deinitクリーンアップを適切に実行し、NotificationCenterによって引き起こされるクラッシュを回避する必要があるためです。

deinit { unsubscribeFromNotifications() }

最後に、これらのメソッドの実装を追加します。このセクションをdeinitのすぐ下に追加します方法:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

次に、アプリを実行し、プレーヤーのビューをクリックしてスコアを変更します。すでに動的に接続しているのでscoreおよびisFinished Viewを使用したViewModelでは、ViewModel内のスコア値を変更するとすべてが機能するはずです。

アプリをさらに改善する方法

常に改善の余地がありますが、これはこのチュートリアルの範囲外です。

たとえば、ゲームが終了したとき(チームの1つが15ポイントに達したとき)に時間を自動的に停止するのではなく、プレーヤーのビューを非表示にするだけです。

必要に応じてアプリで遊んだり、アップグレードして「ゲームクリエイター」ビューを表示したりできます。このビューでは、ゲームの作成、チーム名の割り当て、プレーヤー名の割り当て、Gameの作成が行われます。 GameScoreboardEditorViewControllerを提示するために使用できるオブジェクト。

UITableViewを使用する別の「ゲームリスト」ビューを作成できます。進行中の複数のゲームを表示し、テーブルセルに詳細情報を表示します。セル選択では、GameScoreboardEditorViewControllerを表示できます選択したGameで。

GameLibraryすでに実装されています。そのライブラリ参照をイニシャライザのViewModelオブジェクトに渡すことを忘れないでください。たとえば、「ゲームクリエイター」のViewModelには、GameLibraryのインスタンスが必要です。作成されたGameを挿入できるように初期化子を通過しましたライブラリへのオブジェクト。 「ゲームリスト」のViewModelでも、ライブラリからすべてのゲームをフェッチするためにこの参照が必要になります。これはUITableViewで必要になります。

アイデアは、ViewModel内のすべてのダーティ(非UI)作業を非表示にし、UI(ビュー)が準備されたプレゼンテーションデータでのみ動作するようにすることです。

今何?

MVVMに慣れたら、を使用してさらに改善することができます ボブおじさんのクリーンアーキテクチャルール

追加の良い読み物は、Androidアーキテクチャに関する3部構成のチュートリアルです。

例はJava(Android用)で書かれており、Java(Swiftに非常に近く、Objective-CはJavaに近い)に精通している場合は、ViewModelオブジェクト内でコードをさらにリファクタリングする方法についてのアイデアが得られます。 iOSモジュール(UIKitまたはCoreLocationなど)をインポートしないこと。

これらのiOSモジュールは、純粋なNSObjectsの背後に隠すことができます。これは、コードの再利用に適しています。

MVVMはほとんどの人にとって良い選択です ios アプリ、そしてうまくいけば、あなたはあなたの次のプロジェクトでそれを試してみるでしょう。または、UIViewControllerを作成するときに、現在のプロジェクトで試してみてください。

関連: 静的パターンの操作:SwiftMVVMチュートリアル ? 'Start' : 'Pause' self.pauseButton.setTitle(title, for: .normal) } homePlayer1View.viewModel = viewModel.homePlayers[0] homePlayer2View.viewModel = viewModel.homePlayers[1] homePlayer3View.viewModel = viewModel.homePlayers[2] awayPlayer1View.viewModel = viewModel.awayPlayers[0] awayPlayer2View.viewModel = viewModel.awayPlayers[1] awayPlayer3View.viewModel = viewModel.awayPlayers[2] }

唯一の違いは、4つの動的プロパティを変更し、それぞれに変更リスナーを追加したことです。

__________の設計原則を適用すると、関連するアイテムがグループ化されます。

この時点で、アプリを実行している場合は、 開始/一時停止 ボタンはゲームタイマーを開始および一時停止します。これは、ゲーム中のタイムアウトに使用されます。

ポイントボタン(1および2ポイントボタン)のいずれかを押したときに、UIでスコアが変更されないことを除いて、ほぼ完了です。

これは、基になるGameでスコアの変更を実際に伝播していないためです。 ViewModelまでのモデルオブジェクト。

だから、Gameを開きます少し調べるためのモデルオブジェクト。そのupdateScoreを確認してください方法。

fileprivate func updateScore(_ score: UInt, withScoringPlayer player: Player) { if isFinished || score == 0 { return } if homeTeam.containsPlayer(player) { homeTeamScore += score } else { assert(awayTeam.containsPlayer(player)) awayTeamScore += score } if checkIfFinished() { isFinished = true } NotificationCenter.default.post(name: Notification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: self) }

この方法は2つの重要なことを行います。

まず、isFinishedを設定しますプロパティto true両方のチームのスコアに基づいてゲームが終了した場合。

その後、スコアが変更されたという通知を投稿します。この通知はGameScoreboardEditorViewModelFromGameで聞きます通知ハンドラーメソッドで動的スコア値を更新します。

initWithGameの下部にこの行を追加しますメソッド(エラーを回避するためにsuper.init()呼び出しを忘れないでください):

super.init() subscribeToNotifications()

以下initWithGameメソッド、追加deinitクリーンアップを適切に実行し、NotificationCenterによって引き起こされるクラッシュを回避する必要があるためです。

deinit { unsubscribeFromNotifications() }

最後に、これらのメソッドの実装を追加します。このセクションをdeinitのすぐ下に追加します方法:

// MARK: Notifications (Private) fileprivate func subscribeToNotifications() { NotificationCenter.default.addObserver(self, selector: #selector(gameScoreDidChangeNotification(_:)), name: NSNotification.Name(rawValue: GameNotifications.GameScoreDidChangeNotification), object: game) } fileprivate func unsubscribeFromNotifications() { NotificationCenter.default.removeObserver(self) } @objc fileprivate func gameScoreDidChangeNotification(_ notification: NSNotification){ self.score.value = GameScoreboardEditorViewModelFromGame.scorePretty(for: game) if game.isFinished { self.isFinished.value = true } }

次に、アプリを実行し、プレーヤーのビューをクリックしてスコアを変更します。すでに動的に接続しているのでscoreおよびisFinished Viewを使用したViewModelでは、ViewModel内のスコア値を変更するとすべてが機能するはずです。

アプリをさらに改善する方法

常に改善の余地がありますが、これはこのチュートリアルの範囲外です。

たとえば、ゲームが終了したとき(チームの1つが15ポイントに達したとき)に時間を自動的に停止するのではなく、プレーヤーのビューを非表示にするだけです。

必要に応じてアプリで遊んだり、アップグレードして「ゲームクリエイター」ビューを表示したりできます。このビューでは、ゲームの作成、チーム名の割り当て、プレーヤー名の割り当て、Gameの作成が行われます。 GameScoreboardEditorViewControllerを提示するために使用できるオブジェクト。

UITableViewを使用する別の「ゲームリスト」ビューを作成できます。進行中の複数のゲームを表示し、テーブルセルに詳細情報を表示します。セル選択では、GameScoreboardEditorViewControllerを表示できます選択したGameで。

GameLibraryすでに実装されています。そのライブラリ参照をイニシャライザのViewModelオブジェクトに渡すことを忘れないでください。たとえば、「ゲームクリエイター」のViewModelには、GameLibraryのインスタンスが必要です。作成されたGameを挿入できるように初期化子を通過しましたライブラリへのオブジェクト。 「ゲームリスト」のViewModelでも、ライブラリからすべてのゲームをフェッチするためにこの参照が必要になります。これはUITableViewで必要になります。

アイデアは、ViewModel内のすべてのダーティ(非UI)作業を非表示にし、UI(ビュー)が準備されたプレゼンテーションデータでのみ動作するようにすることです。

今何?

MVVMに慣れたら、を使用してさらに改善することができます ボブおじさんのクリーンアーキテクチャルール

追加の良い読み物は、Androidアーキテクチャに関する3部構成のチュートリアルです。

例はJava(Android用)で書かれており、Java(Swiftに非常に近く、Objective-CはJavaに近い)に精通している場合は、ViewModelオブジェクト内でコードをさらにリファクタリングする方法についてのアイデアが得られます。 iOSモジュール(UIKitまたはCoreLocationなど)をインポートしないこと。

これらのiOSモジュールは、純粋なNSObjectsの背後に隠すことができます。これは、コードの再利用に適しています。

MVVMはほとんどの人にとって良い選択です ios アプリ、そしてうまくいけば、あなたはあなたの次のプロジェクトでそれを試してみるでしょう。または、UIViewControllerを作成するときに、現在のプロジェクトで試してみてください。

関連: 静的パターンの操作:SwiftMVVMチュートリアル