私はiOS開発者であり、プロジェクトにマッシブビューコントローラーがあることに罪があるので、プロジェクトを構造化するためのより良い方法を探していて、MVVM(Model-View-ViewModel)アーキテクチャに出くわしました。私はiOSで多くのMVVMを読んでいますが、いくつか質問があります。問題を例を挙げて説明します。
LoginViewController
というビューコントローラがあります。
LoginViewController.Swift
_import UIKit
class LoginViewController: UIViewController {
@IBOutlet private var usernameTextField: UITextField!
@IBOutlet private var passwordTextField: UITextField!
private let loginViewModel = LoginViewModel()
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func loginButtonPressed(sender: UIButton) {
loginViewModel.login()
}
}
_
Modelクラスはありません。しかし、検証ロジックとネットワーク呼び出しを配置するために、LoginViewModel
というビューモデルを作成しました。
LoginViewModel.Swift
_import Foundation
class LoginViewModel {
var username: String?
var password: String?
init(username: String? = nil, password: String? = nil) {
self.username = username
self.password = password
}
func validate() {
if username == nil || password == nil {
// Show the user an alert with the error
}
}
func login() {
// Call the login() method in ApiHandler
let api = ApiHandler()
api.login(username!, password: password!, success: { (data) -> Void in
// Go to the next view controller
}) { (error) -> Void in
// Show the user an alert with the error
}
}
}
_
私の最初の質問は、単にMVVM実装が正しいかどうかです。たとえば、ログインボタンのタップイベント(loginButtonPressed
)をコントローラーに配置したため、この疑問があります。いくつかのテキストフィールドとボタンしかないため、ログイン画面用に別のビューを作成しませんでした。コントローラーがUI要素に関連付けられたイベントメソッドを持つことは許容されますか?
次の質問は、ログインボタンについてもです。ユーザーがボタンをタップすると、検証のためにユーザー名とパスワードの値がLoginViewModelに渡され、成功した場合はAPI呼び出しに渡されます。値をビューモデルに渡す方法について質問します。 2つのパラメーターをlogin()
メソッドに追加して、ビューコントローラーから呼び出すときにそれらを渡す必要がありますか?または、ビューモデルでそれらのプロパティを宣言し、ビューコントローラからそれらの値を設定する必要がありますか? MVVMで許容できるのはどれですか?
ビューモデルでvalidate()
メソッドを取得します。どちらかが空の場合は、ユーザーに通知する必要があります。つまり、チェック後、結果をビューコントローラーに返して必要なアクションを実行する必要があります(アラートを表示)。 login()
メソッドでも同じことが言えます。リクエストが失敗した場合はユーザーに警告し、成功した場合は次のView Controllerに進みます。ビューモデルからこれらのイベントをコントローラーに通知するにはどうすればよいですか?このような場合にKVOのようなバインディングメカニズムを使用することは可能ですか?
IOSでMVVMを使用する場合、他のバインディングメカニズムは何ですか? KVOは1つです。しかし、大量のボイラープレートコード(オブザーバーの登録/登録解除など)を必要とするため、大規模なプロジェクトにはあまり適していないと私は読んだ。他のオプションは何ですか? ReactiveCocoaがこれに使用されるフレームワークであることは知っていますが、他にネイティブなものがあるかどうかを確認しようとしています。
私がインターネット上のMVVMで見つけたすべての資料は、私が明確にしようとしているこれらの部分に関する情報をほとんどまたはまったく提供していなかったので、私は本当にあなたの応答に感謝します。
ワダップ男!
1a-あなたは正しい方向に向かっています。あなたはloginButtonPressedをビューコントローラーに入れ、それはまさにそれがあるべき場所です。コントロールのイベントハンドラーは常にビューコントローラーに移動する必要があるため、これは正しいことです。
1b-ビューモデルに、「ユーザーにエラーのアラートを表示する」というコメントがあります。検証関数内からそのエラーを表示する必要はありません。代わりに、関連付けられた値を持つenumを作成します(値はユーザーに表示するエラーメッセージです)。その列挙型を返すように検証メソッドを変更します。次に、ビューコントローラー内でその戻り値を評価し、そこからアラートダイアログを表示します。 UIKitに関連するクラスのみをビューコントローラー内でのみ使用し、ビューモデルからは使用しないようにしてください。ビューモデルにはビジネスロジックのみを含める必要があります。
enum StatusCodes : Equatable
{
case PassedValidation
case FailedValidation(String)
func getFailedMessage() -> String
{
switch self
{
case StatusCodes.FailedValidation(let msg):
return msg
case StatusCodes.OperationFailed(let msg):
return msg
default:
return ""
}
}
}
func ==(lhs : StatusCodes, rhs : StatusCodes) -> Bool
{
switch (lhs, rhs)
{
case (.PassedValidation, .PassedValidation):
return true
case (.FailedValidation, .FailedValidation):
return true
default:
return false
}
}
func !=(lhs : StatusCodes, rhs : StatusCodes) -> Bool
{
return !(lhs == rhs)
}
func validate(username : String, password : String) -> StatusCodes
{
if username.isEmpty || password.isEmpty
{
return StatusCodes.FailedValidation("Username and password are required")
}
return StatusCodes.PassedValidation
}
2-これは好みの問題であり、最終的にはアプリの要件によって決定されます。私のアプリでは、これらの値をlogin()メソッドを介して渡します。つまり、login(username、password)です。
3-LoginEventsDelegateという名前のプロトコルを作成し、その中にメソッドを含める:
func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String)
ただし、このメソッドは、リモートサーバーへのログイン試行の実際の結果をビューコントローラに通知するためにのみ使用してください。検証部分とは何の関係もないはずです。検証ルーチンは、上記の#1で説明したように処理されます。ビューコントローラーにLoginEventsDelegateを実装させます。そして、ビューモデルにパブリックプロパティを作成します。
class LoginViewModel {
var delegate : LoginEventsDelegate?
}
次に、API呼び出しの完了ブロックで、デリゲートを介してビューコントローラーに通知できます。
func login() {
// Call the login() method in ApiHandler
let api = ApiHandler()
let successBlock =
{
[weak self](data) -> Void in
if let this = self {
this.delegate?.loginViewModel_LoginCallFinished(true, "")
}
}
let errorBlock =
{
[weak self] (error) -> Void in
if let this = self {
var errMsg = (error != nil) ? error.description : ""
this.delegate?.loginViewModel_LoginCallFinished(error == nil, errMsg)
}
}
api.login(username!, password: password!, success: successBlock, error: errorBlock)
}
そしてあなたのビューコントローラは次のようになります:
class loginViewController : LoginEventsDelegate {
func viewDidLoad() {
viewModel.delegate = self
}
func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String) {
if successful {
//segue to another view controller here
} else {
MsgBox(errMsg)
}
}
}
ログインメソッドにクロージャを渡して、プロトコルを完全にスキップできると言う人もいます。これが悪い考えだと思う理由はいくつかあります。
UIレイヤー(UIL)からビジネスロジックレイヤー(BLL)にクロージャーを渡すと、関心の分離(SOC)が無効になります。 Login()メソッドはBLLに常駐しているため、基本的には「BLLがこのUILロジックを実行してくれます」と言えます。これはSOCです。
BLLはデリゲート通知を介してのみUILと通信する必要があります。このようにして、BLLは基本的に「UILさん、ロジックの実行が終わりました。必要に応じてUIコントロールを操作するために使用できるデータ引数をいくつか示します」と述べています。
したがって、UILはBLLにUIコントロールロジックを実行するように要求することはできません。 BLLに通知するよう依頼するだけです。
4-私はReactiveCocoaを見たことがありますが、それについて良いことを聞きましたが、使用したことがありません。だから、個人的な経験からそれを話すことはできません。 (#3で説明されているように)単純なデリゲート通知を使用すると、シナリオでどのように機能するかがわかります。それがニーズを満たしている場合は素晴らしいですが、もう少し複雑なものを探している場合は、ReactiveCocoaを検討してください。
ところで、これも技術的にはMVVMのアプローチではありません。バインディングとコマンドが使用されていないためです。 "ta-mah-toe"が私見をつまんでいます。 SOCの原則は、使用するMV *アプローチに関係なくすべて同じです。
IOSのMVVMとは、モデルクラスとは別に、画面で使用するデータで満たされたオブジェクトを作成することです。通常、ラベル、テキストボックス、データソース、動的画像など、データを消費または生成するUIのすべてのアイテムをマッピングします。多くの場合、バリデーターを使用して、入力(空のフィールド、有効な電子メールかどうか、正の数、スイッチがオンかどうか)を簡単に検証します。これらのバリデーターは通常、インラインロジックではなく別個のクラスです。
ビューレイヤーはこのVMクラスを認識し、その変更を監視してそれらを反映し、ユーザーがデータを入力するとVMクラスを更新します。すべてのプロパティVMは、UIのアイテムに関連付けられています。たとえば、ユーザーがユーザー登録画面に移動すると、この画面には、VMのプロパティがありません。ステータスが「未完了」のステータスプロパティを除きます。ビューは完了フォームのみを送信できることを認識しているため、[送信]ボタンを現在非アクティブに設定しています。
次に、ユーザーは詳細の入力を開始し、電子メールアドレスの形式を間違えます。 VM内のそのフィールドのバリデーターがエラー状態を設定し、ビューがVM = UIのバリデーター。
最後に、VM内のすべての必須フィールドがステータスを取得すると、完了VMが完了であると、ビューはそれを監視し、送信ボタンをアクティブに設定します。ユーザーはそれを送信できます。送信ボタンアクションはVCに関連付けられており、VCはVMが適切なモデルにリンクされ、保存されます。モデルがVMとして直接使用される場合があります。これは、より単純なCRUDのような画面がある場合に役立つことがあります。
私はこのパターンをWPFで使用してきましたが、それは本当にうまくいきました。 Viewsでこれらすべてのオブザーバーを設定し、ModelクラスとViewModelクラスに多くのフィールドを配置するのは大変なことのように思えますが、優れたMVVMフレームワークが役立ちます。 UI要素を適切なタイプのVM要素にリンクし、適切なバリデーターを割り当てるだけで済みます。そうすれば、すべてのボイラープレートコードを自分で追加する必要なく、この配管の多くが実行されます。
このパターンのいくつかの利点:
短所:
IOSのMVVMアーキテクチャは、サードパーティの依存関係を使用せずに簡単に実装できます。データバインディングでは、ClosureとdidSetの単純な組み合わせを使用して、サードパーティの依存関係を回避できます。
public final class Observable<Value> {
private var closure: ((Value) -> ())?
public var value: Value {
didSet { closure?(value) }
}
public init(_ value: Value) {
self.value = value
}
public func observe(_ closure: @escaping (Value) -> Void) {
self.closure = closure
closure(value)
}
}
ViewControllerからのデータバインディングの例:
final class ExampleViewController: UIViewController {
private func bind(to viewModel: ViewModel) {
viewModel.items.observe(on: self) { [weak self] items in
self?.tableViewController?.items = items
// self?.tableViewController?.items = viewModel.items.value // This would be Momory leak. You can access viewModel only with self?.viewModel
}
// Or in one line:
viewModel.items.observe(on: self) { [weak self] in self?.tableViewController?.items = $0 }
}
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
}
protocol ViewModelInput {
func viewDidLoad()
}
protocol ViewModelOutput {
var items: Observable<[ItemViewModel]> { get }
}
protocol ViewModel: ViewModelInput, ViewModelOutput {}
final class DefaultViewModel: ViewModel {
let items: Observable<[ItemViewModel]> = Observable([])
// Implmentation details...
}
後でSwiftUIとCombineで置き換えることができます(アプリのiOSの最小バージョンが13の場合)
この記事では、MVVMのより詳細な説明があります https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b