IT アプリ開発

【SwiftUI】SwiftUIをMVVMフレームワークで実装しよう

2021年10月31日

swiftuiではmvvm(Model-ViewModel-Model)
というフレームワークで開発をすることができます。

画面の再構築がかなりしやすくなり、
Modelで定義した情報をいろんなViewで用いている場合、
もしModelの値が更新しても画面を再構築するという指示を別個で指定する必要があった。swiftの時

しかし、@Bindingという機能を用いることで、それが容易になり楽になった。

MVVMは以下のように3つの頭文字をとって構成されるフレームワークのことです。

項目 説明
Model データの定義書
ViewModel ViewからきたModelデータを処理してModelに値を反映する
View あくまで画面の表示のみ。

上記のプログラムについてイメージを書いていきたいと思います。

これをすることで、イメージですが、
ViewでModelの値を用いて画面の表示を行います。
そして、画面の中でModelの値の書き換えなどが生じる場合、Viewのプログラム内で行うのではなく、
データ更新処理はViewModelのプログラム内で行う。(ViewModelのメソッドなどを呼び出して更新するイメージ)
そしてViewModel内でModelの更新をすると、@Publishdにより、
@ObservedObject宣言しているViewModelをプロパティにもつViewが再構築という処理が走り、
対象のModelの値を用いて画面を表示しているViewについて再構築処理が走って最新の値で表示されるようになる。

Model

以下のModelでは、struct宣言して以下のように定義しています。

import SwiftUI

struct User{
    var text:String? = "aaaa"
    var id:String? = "1"
}

View

Viewでは値の更新が変わったことを示すために、わかりやすく画面遷移できるように、以下のようにTabbarを用いて、2つの画面の表示をして、
別タブ(SecondView)でModelの値の更新をした後、最初のタブ(FirstView)で画面の再構築が走るようにします。
そのため、両方で同じViewModelのインスタンスを共有する必要があるので、
親ViewであるTabbarでViewModelインスタンスを生成し、
子ViewになるFirstView、SecondViewにViewModelインスタンスを渡すような処理を行います。

MainTabView

import SwiftUI

struct MainTabBar: View {
    
    @ObservedObject var vm:ViewModel = ViewModel()              // ======== ViewModelをObservedObjectで宣言し、インスタンス生成
    
    var body: some View {
        TabView {
            FirstView(vm)                                       // ======== 親Viewから子ViewであるFirstViewにvmを渡す
                .tabItem {
                    Image(systemName: "house")
                    Text("First").font(.largeTitle)
                }

            SecondView(vm)                                      // ======== 親Viewから子ViewであるSecondViewにvmを渡す
                .tabItem {
                    Image(systemName: "ellipses.bubble.fill")
                    Text("Second").font(.largeTitle)
                }
                .padding(.top, 70)
                .frame(height: UIScreen.main.bounds.size.height)
                .edgesIgnoringSafeArea(.all)
            
            ThirdView(vm)                                       // ======== 親Viewから子ViewであるThirdViewにvmを渡す
                .tabItem {
                    Image(systemName: "ellipses.bubble.fill")
                    Text("Third").font(.largeTitle)
                }
                .padding(.top, 70)
                .frame(height: UIScreen.main.bounds.size.height)
                .edgesIgnoringSafeArea(.all)

        }
    }
}

FirstView

import SwiftUI

struct FirstView: View {
    
    @ObservedObject var vm:ViewModel                    // ======== 親Viewから渡されるvmを使用する
    @State var flag:Bool = false
    @State var text:String = ""
    @State var id:String = ""
    @State private var isEditing = false
    
    var body: some View {
        VStack(spacing: 20){
            Text("Hello, World!")
                .background(Color.blue)
            
            TextField("名前を入力してください",text: $text,
                onEditingChanged: { isBegin in
                    if isBegin {
    
                    } else {
                        self.vm.user.text = text        // ======== ModelのUserのプロパティtextに値を入れる(更新)
                        var _ = print(self.vm.user.text ?? "")
                    }
                },
                onCommit: {
                  
                })
                 .textFieldStyle(RoundedBorderTextFieldStyle())
                 .padding()
            Text("Hello!!、\(text)")
            
            TextField("idを入力してください",text: $id,
                onEditingChanged: { isBegin in
                    if isBegin {
          
                    } else {
                        self.vm.user.id = id            // ======== ModelのUserのプロパティidに値を入れる(更新)
                        var _ = print(self.vm.user.id ?? "")
                    }
                },
                onCommit: {
                  
                })
                 .textFieldStyle(RoundedBorderTextFieldStyle())
                 .padding()
            Text("Hello!!、\(id)番号")
        }
    }
}

FirstViewでは、ModelのUserプロパティの値を直に更新しています。
TextFieldで入力するたびにUserのプロパティ値が更新されていきます。
想定としてはvmを経由してmodelを参照しているため、modelのuserのプロパティの値が変わっているので、
ViewModelは参照しているView全てにViewの再構築処理をかけると想定できます。

※ MVVMではViewではModelに値を直接入れることはMVVMの構成上ダメですが、今回は簡単に値が更新されたところを見るため、
Viewで直に参照して代入して変更を見ています。

SecondView

import SwiftUI

struct SecondView: View {
    
    @ObservedObject var vm:ViewModel                    // ======== 親Viewから渡されるvmを使用する
    
    var body: some View {
        VStack(spacing: 20){
            Text("Second View!")
                .background(Color.green)
            
            var _ = print("Second View!")
            var _ = print(self.vm.user.text)            
            Text(self.vm.user.text ?? "")
        }
    }
}

FirstViewでuserプロパティのtextを更新して、SecondViewを表示すると、
Viewの再構築が走り、FirstViewで入力した値が反映される。

ThirdView

import SwiftUI

struct ThirdView: View {
    
    @ObservedObject var vm:ViewModel                   // ======== 親Viewから渡されるvmを使用する
    
    var body: some View {
        VStack(spacing: 20){
            Text("Third View!")
                .background(Color.yellow)
            
            var _ = print("Third View!")
            var _ = print(self.vm.user.id)
            Text(self.vm.user.id ?? "")
        }
    }
}

FirstViewでuserプロパティのidを更新して、ThirdViewを表示すると、
Viewの再構築が走り、FirstViewで入力した値が反映される。

ViewModel

ViewModelでは@PublishedでModelを初期化します。

import SwiftUI

class ViewModel: ObservableObject {
    
    @Published var user:User = User()

}

上記のイメージ図としては以下のような感じになります。
イメージ

FirstViewにでModelの入力値を更新。
それにより、TabViewでObservedObjectを通じてModelが生成され、
そのModelを子ViewであるFirstView、SecondView、ThirdViewが引き継いでる状態。

その中で、FirstViewでModelのプロパティの値を更新すると、
一緒に引き継いでいるSecondView、ThirdViewも、@Publishedの効果により、
Viewの再構築が走り、Modelの最新値で画面構築が走る。

上記コードを実際に記載して実行してみると以下のように、
SecondViewで値を更新した後、再度FirstViewに戻ると値が更新されてViewの再構築が走っています。

アプリではすでにクライアントサイドに画面があるので、どうしても値が更新されたら、
自分でViewの再構築処理をしないと値が更新されても画面の値は自動的に更新はされません。
そのため、上記のようにMVVMを用いることで処理の更新が用意になります。

ここで注意。
子Viewとかで、VMを渡さず、@ObservedObjectを宣言して初期化すると、
またVMの初期化を生成するので、そもそものVMの中のModelの更新が走ったVMと違うものが生まれているので、
更新が走りません。

さらには、Modelを子Viewで初期化するような処理をすると、
こちらも更新処理が走りません。
なので初期化はせず、
親Viewと子ViewでModelの更新などが走った時にデータを更新する場合は、
以下のようにデータが更新されているのがわかる

例えば、アプリ起動時にFirestoreからデータを引っ張ってくるとかをする場合は、
基本的にはModelにそれらの値を付与するので、
Modelにデータを付与する処理ということで、基本的にはViewModelのinitで行うことがベストかなとは思います。
Modelのinitでも基本的に処理は変わらないですが。
※ ViewModelの中で@Published public var model:Model = Model()
というように、Modelインスタンスを生成する処理を行なっているので、Model内でinitしてその時にfirestoreからデータを取得しても問題ないっちゃ問題ない。

-IT, アプリ開発
-

© 2022 Yosshi Blog Powered by AFFINGER5