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


16

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

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

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

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

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


EOとしてviewmodelを渡さないのはなぜですか?
E.Coms

上に見えるように、多くのビューモデルがあります。私がリンクしたアップロードは、単純化された例にすぎません
Michael

2
なぜこの質問が反対票を投じられたのかはわかりませんが、同じことを考えています。私がやったことでお答えします。うまくいけば、他の誰かがもっと良いものを思いつくかもしれません。
Michael Ozeryansky

2
@ E.Coms一般に、EnvironmentObjectは1つのオブジェクトであると想定していました。私は複数の作業を知っています。そのようにグローバルにアクセスできるようにするコードのにおいのようです。
Michael Ozeryansky

@マイケルあなたはこれに対する解決策を見つけましたか?
ブレット

回答:


3

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

RootViewといくつかの子ビューを使用してプロジェクトをセットアップしました。EnvironmentObjectとしてオブジェクトをセットアップRootViewしましたApp。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
        }
    }

}

ただのメモ:のような連鎖は避ける方が良いと思いますapp.userService.logout()userServiceプライベートであり、アプリクラス内からのみアクセスできる必要があります。上記のコードは次のようになります。Button(action: { app.logout() })ログアウト関数が直接呼び出されますuserService.logout()
pawello2222

@ pawello2222それは良くはありません、それは何の利点もない単なるファサードパターンですが、あなたはあなたが望むように行うことができます。
Michael Ozeryansky

3

すべきではない。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は自動です。個別のViewModelオブジェクトは必要ありません。

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

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

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

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

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

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

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

の保護なしにどこにでもグローバルにアクセスすることはどれほど危険か

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

TL; DR

SwiftUIではJavaの方法でMVVMを実行しません。そしてそれを行うSwift-yの方法は必要ありません

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

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


1

以下は私のために働くアプローチを提供しました。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")
    }
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.