カテゴリー: iOS

[SwiftUI]SwiftUIで動的にViewの更新をする方法

はじめに

こんにちはsuzukiです。今回はSwiftUIで動的なViewの更新方法について基本的な考え方についてまとめました。

SwfitUIのViewについて

従来のViewは、UIViewなどのClassで表現されていましたが、SwiftUIでは異なります。
参照を追っていくとわかるのですが、Structになってます。
StructとClassについては左辺に渡した時にの挙動がファイルのコピペとショートカットのような違いがあります。
let a = hogeのようなコードを書いた際に下記のようになります。
hogeがStruct(値型)
 Structの中身をコピーしたaという値型が代入される
hogeがClass(参照)
 aにはclassの参照が代入される。

値の更新について

SwiftUIでなければ、例えばUIViewControllerにUILabelのインスタンス変数を保持しておけば、任意のタイミングで(ユーザーがボタンをタップした時など)、UILable文字色を切り替えるなどが簡単にできました。
SwiftUIでは下記の3つを利用して動的な変更に対応します。
・State
・ObservedObject
・EnvironmentObject
それぞれ違いはあるのですが、基本的には特定の値を監視し変更があった時の動作を事前に記述しておくという形です。

Stateの特徴

@Stateを宣言することで、状態の管理をSwiftUIに委譲し、値の変更があった際にbody以下が再レンダリングされます。
@Stateは宣言したプロパティを保持するViewの中で使います。

struct ContentView: View {
    @State var buttonCount: Int = 0
    var body: some View {
        VStack {
            Text("\(buttonCount)")
                .foregroundColor(.red)
            Button(action: {
                self.buttonCount += 1
            }) {
                Text("COUNT UP")
            }
            Button(action: {
                self.buttonCount -= 1
            }) {
                Text("COUNT DOWN")
            }
        }        
    }
}

上記のコードだけでボタンを押し際にbuttonCountの値が更新され、buttonCountを参照しているTextの値が更新されます。

ObservedObjectの特徴

Stateとできることは似ています。ただクラスを設定することが可能でViewModelとしての運用やView間の情報の連携が行いやすいです。
イメージとしては複数のStateをまとめてモデルとして運用ができるような感じです。
また、初期値の設定を自分で行わず、呼び出し元から渡すことも初期値として設定することも可能です。

//ObservableObect プロトコルに準拠したクラス
class ObservedViewModel : ObservableObject {
    @Published var buttonCount: Int = 0
}

書き方は通常のClassとほとんど変わりません。Stateと同じように監視したい値は@Publishを設定してください。

呼び出しもと

// SceneDelegate


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        var model = ViewModel()
        model.buttonCount = 4
        var contentView = ContentView(viewModel: model)
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }
struct ContentView : View {
    
    @ObservedObject var viewModel: ViewModel
    var body: some View {
         return VStack {
            TextContent(viewModel: viewModel)
            ButtonContent(viewModel: viewModel)
        }
    }
}

class ViewModel: ObservableObject {
    @Published var buttonCount: Int = 0
}

struct TextContent: View {
    @ObservedObject var viewModel: ViewModel
    var body: some View {
        Text("\(self.viewModel.buttonCount)")
            .foregroundColor(.red)
    }
}

struct ButtonContent:View{
    @ObservedObject var viewModel: ViewModel
    var body: some View {
        VStack {
            Button(action: {
                self.viewModel.buttonCount += 1
            }) {
                Text("COUNT UP")
            }
            Button(action: {
                self.viewModel.buttonCount -= 1
            }) {
                Text("COUNT DOWN")
            }
        }
    }
}

後述のEnvironmentObjectと違い参照を渡していく必要があります。

EnvironmentObjectの特徴

EnvironmentObjectはアプリ共通の設定や値などに使われることが多いようです。
下記のように、modelをそれぞれのの子Viewに渡すことなく、子Viewから使用することが可能です。
使用するViewでは値が入っている前提のコードを書くため想定する値が存在しないとクラッシュの原因にもなるので、気をつけなければなりません。

呼び出しもと

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let model = ViewModel()
        model.buttonCount = 4
        let contentView = ContentView().environmentObject(model)
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }
struct ContentView : View {
    
//    @ObservedObject var viewModel: ViewModel
    var body: some View {
         return VStack {
            TextContent()
            ButtonContent()
        }
    }
}

class ViewModel: ObservableObject {
    @Published var buttonCount: Int = 0
}

struct TextContent: View {
    @EnvironmentObject var viewModel: ViewModel
    var body: some View {
        Text("\(self.viewModel.buttonCount)")
            .foregroundColor(.red)
    }
}

struct ButtonContent:View{
    @EnvironmentObject var viewModel: ViewModel
    var body: some View {
        VStack {
            Button(action: {
                self.viewModel.buttonCount += 1
            }) {
                Text("COUNT UP")
            }
            Button(action: {
                self.viewModel.buttonCount -= 1
            }) {
                Text("COUNT DOWN")
            }
        }
    }
}

さいごに

肝心のレイアウトの作成方法が触れていないのでまた今度触れていこうと思います。
少し触った所感としては、既存のアプリのリプレースは大変そうだと思いました。新しいOS向けに新規に作る際に使って行きたいと思います。

おすすめ書籍

suzuki

シェア
執筆者:
suzuki
タグ: SwiftUI

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

2週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

4週間 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前