web-dev-qa-db-ja.com

SwiftUI-EnvironmentObjectをビューモデルに渡す方法

(ビューだけでなく)ビューモデルからアクセスできるEnvironmentObjectを作成しようとしています。

Environmentオブジェクトは、アプリケーションセッションデータを追跡します。 logsIn、アクセストークンなど、このデータはビューモデル(または必要に応じてサービスクラス)に渡され、このEnvironmentObjectsからデータを渡すためのAPIの呼び出しを許可します。

セッションオブジェクトをビューからビューモデルクラスの初期化子に渡そうとしましたが、エラーが発生しました。

swiftUIを使用してEnvironmentObjectにアクセス/ビューモデルに渡すにはどうすればよいですか?

テストプロジェクトへのリンクを参照してください: https://gofile.io/?c=vgHLVx

15
Michael

すべきではない。 SwiftUIがMVVMで最適に動作するのはよくある誤解です。

MVVMはSwfitUIにありません。あなたは長方形を突き出すことができるかどうかを尋ねています

三角形にフィットします。合いません。

いくつかの事実から始めて、段階的に作業してみましょう。

  1. ViewModelはMVVMのモデルです。

  2. MVVMは、値の型(Javaにはそのようなものがないなど)を考慮に入れません。

  3. 値型モデル(状態のないモデル)は参照よりも安全であると見なされます

    タイプモデル(状態を持つモデル)は不変性の意味で。

MVVMでは、モデルが変更されるたびに、

あらかじめ決められた方法でビューを更新する。これはバインディングと呼ばれます。

拘束力がないと、懸念事項を適切に分離できません。リファクタリング

モデルと関連する状態、およびそれらをビューから分離した状態に保ちます。

これらは、ほとんどのiOS MVVM開発者が失敗する2つの問題です。

  1. iOSには従来のJavaの意味での「バインド」メカニズムはありません。

    バインドを無視して、オブジェクトViewModelを呼び出すと考える人もいます。

    すべてを自動的に解決します。一部はKVOベースのRxを導入し、

    mVVMが物事をより単純にすることになっている場合、すべてを複雑にします。

  2. 状態のモデルは危険すぎる

    mVVMはViewModelに過度の重点を置き、状態管理にはあまりにも重点を置いているため

    および統制の管理における一般的な分野。ほとんどの開発者は結局

    ビューの更新に使用される状態を持つモデルは再利用可能であり、かつ

    テスト可能

    Swiftは最初に値タイプを導入する理由です;なしのモデル

    状態。

さてあなたの質問に:あなたのViewModelがEnvironmentObject(EO)にアクセスできるかどうか尋ねますか?

すべきではない。 SwiftUIでは、Viewに準拠するモデルには自動的に

eOへの参照。例えば。;

struct Model: View {
    @EnvironmentObject state: State
    // automatic binding in body
    var body: some View {...}
}

コンパクトなSDKがどのように設計されているかを人々に理解してもらいたいと思います。

SwiftUIでは、MVVMはautomaticです。個別のViewModelオブジェクトは必要ありません。

これは、EO参照を渡す必要があるビューに手動でバインドします。

上記のコードis MVVM。例えば。;ビューにバインドするモデル。

しかし、モデルは値型なので、モデルをリファクタリングする代わりに、

モデルを表示すると、(プロトコル拡張などで)コントロールをリファクタリングします。

これは、デザインパターンを言語機能に適合させるだけでなく、公式のSDKです。

それを実施します。フォームよりも実質。

あなたのソリューションを見てください、あなたは基本的にグローバルであるシングルトンを使わなければなりません。君は

の保護なしにどこにでもグローバルにアクセスすることがいかに危険であるかを知る必要があります

不変性。参照型モデルを使用する必要があるため、これはありません。

TL; DR

SwiftUIでは、Java=でMVVMを実行しません。そして、Swift-yで実行する方法は必要ありません。

それを行うために、それはすでに組み込まれています。

これはよくある質問のように思われたので、もっと多くの開発者にこれを見てもらいたい。

1
Jim lai

以下は私のために働くアプローチを提供しました。 Xcode 11.1で始まった多くのソリューションでテスト済み。

問題は、EnvironmentObjectがビュー、一般的なスキーマに注入される方法に起因します

SomeView().environmentObject(SomeEO())

つまり、最初に-作成されたビュー、2番目に作成された環境オブジェクト、3番目の環境オブジェクトにビューに挿入

したがって、ビューコンストラクターでビューモデルを作成/設定する必要がある場合、環境オブジェクトはまだそこに存在しません。

解決策:すべてを分解し、明示的な依存関係注入を使用する

これは、コードでどのように見えるか(汎用スキーマ)です。

// somewhere, say, in SceneDelegate

let someEO = SomeEO()                            // create environment object
let someVM = SomeVM(eo: someEO)                  // create view model
let someView = SomeView(vm: someVM)              // create view 
                   .environmentObject(someEO)

ViewModelとEnvironmentObjectは仕様上、参照型(実際にはObservableObject)であるため、ここではトレードオフはありません。そのため、ここでは参照(ポインター)のみを渡します。

class SomeEO: ObservableObject {
}

class BaseVM: ObservableObject {
    let eo: SomeEO
    init(eo: SomeEO) {
       self.eo = eo
    }
}

class SomeVM: BaseVM {
}

class ChildVM: BaseVM {
}

struct SomeView: View {
    @EnvironmentObject var eo: SomeEO
    @ObservedObject var vm: SomeVM

    init(vm: SomeVM) {
       self.vm = vm
    }

    var body: some View {
        // environment object will be injected automatically if declared inside ChildView
        ChildView(vm: ChildVM(eo: self.eo)) 
    }
}

struct ChildView: View {
    @EnvironmentObject var eo: SomeEO
    @ObservedObject var vm: ChildVM

    init(vm: ChildVM) {
       self.vm = vm
    }

    var body: some View {
        Text("Just demo stub")
    }
}
1
Asperi

ViewModelがないことを選択します。 (たぶん新しいパターンの時間ですか?)

RootViewといくつかの子ビューを使用してプロジェクトをセットアップしました。 RootViewAppオブジェクトをEnvironmentObjectとしてセットアップしました。 ViewModelがモデルにアクセスする代わりに、すべてのビューがAppのクラスにアクセスします。 ViewModelがレイアウトを決定する代わりに、ビュー階層がレイアウトを決定します。いくつかのアプリで実際にこれを行うことから、私の見解は小さく、具体的なままであることがわかりました。単純化しすぎて:

class App {
   @Published var user = User()

   let networkManager: NetworkManagerProtocol
   lazy var userService = UserService(networkManager: networkManager)

   init(networkManager: NetworkManagerProtocol) {
      self.networkManager = networkManager
   }

   convenience init() {
      self.init(networkManager: NetworkManager())
   }
}
struct RootView {
    @EnvironmentObject var app: App

    var body: some View {
        if !app.user.isLoggedIn {
            LoginView()
        } else {
            HomeView()
        }
    }
}
struct HomeView: View {
    @EnvironmentObject var app: App

    var body: some View {
       VStack {
          Text("User name: \(app.user.name)")
          Button(action: { app.userService.logout() }) {
             Text("Logout")
          }
       }
    }
}

私のプレビューでは、MockAppのサブクラスであるAppを初期化しています。 MockAppは、指定された初期化子をMockedオブジェクトで初期化します。ここでは、UserServiceをモックする必要はありませんが、データソース(NetworkManagerProtocolなど)はモックしています。

struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            HomeView()
                .environmentObject(MockApp() as App) // <- This is needed for EnvironmentObject to treat the MockApp as an App Type
        }
    }

}
1