Open20

SwiftUI ModelData

katzkatz
  • SwiftUIはUIのデザインに宣言型のアプローチを提供する
  • Viewの階層を構成するときに、Viewのデータ依存関係も指定する
  • 外部イベントやユーザーアクションによってデータが変更されるとSwiftUIはUIの影響点を識別して自動的に更新する

→ 以前のフレームワークでいうViewControllerが担っていた処理のほとんどをSwiftUIでやってくれるようになる。

katzkatz
  • つまりはSwiftUIを使うことで、極力ViewControllerが担っていた処理をやらなくて良くなる。
  • でもアプリケーションのDataとViewを紐付けるところに関してはどうしても必要になる。
    • ドキュメントではグルーコードと呼んでいる
  • このグルーコードを極力減らして、楽に管理できる仕組みをSwiftUIでは用意している感じになる
    • StateやBindingやObservableなどがこの機能にあたるらしい

→ SwiftUIのModel Dataのセクションでは、このグルーコードをどのように実装する仕組みがあるのか、解説していってくれるっぽい。

katzkatz
  • StateはView内部でデータを保持するのに使える
  • Bindingはローカルデータを保持して参照を共有するのに使える
  • Observableはデータ更新監視可能なStateという立ち位置
katzkatz

プロパティラッパーを利用すれば、ゲッターやセッターでやるバリデーション処理などを共通化することができる。

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/#Property-Wrappers

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

こういうのを定義すると、以下のようにInt型のセッターとゲッターのバリデーション処理を共通化できるらしい。

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}


var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"


rectangle.height = 10
print(rectangle.height)
// Prints "10"


rectangle.height = 24
print(rectangle.height)
// Prints "12"
katzkatz

SwiftUIのStateやBindingはこのプロパティラッパーを利用して実装されているらしい。

katzkatz
  • Single of truthを実現するために、データを必要とするViewの一番低い階層のところにデータを保持する
  • StateのライフサイクルはViewのライフサイクルに依存するので、永続ストレージにStateを利用するのはNGらしい。
katzkatz
  • State
    • Viewが変更可能なデータを保存する場合はStateのPropertyWrapperを使う
katzkatz

こんな感じでStateをもたせると、Buttonをクリックしたら、Textが変化するViewを作成できる。

import SwiftUI

struct ContentView: View {
    @State private var isClicked: Bool = false

    var body: some View {
        VStack {
            let text = "\(isClicked)".uppercased()
            Text(text)
            Button(
                action: {
                    isClicked = !isClicked
                },
                label: {
                    Text("Toggle State")
                }
            )
        }.padding(10)
    }
}

#Preview {
    ContentView()
}

katzkatz

不変の値を格納したいのであればletでプロパティを宣言すればよい


struct ContentView: View {
    let title: String
    @State private var isClicked: Bool = false

    var body: some View {
        VStack {
            let text = "\(isClicked)".uppercased()
            Text(title)
            Text(text)
            Button(
                action: {
                    isClicked = !isClicked
                },
                label: {
                    Text("Toggle State")
                }
            )
        }.padding(10)
    }
}

#Preview {
    ContentView(title: "HELLO TITLE")
}
katzkatz

@Bindingを利用すると、@Stateで宣言した値を子Viewに流し込める。Stateで宣言した値を$で渡してあげると、うまく動くらしいのでここは注意が必要。

struct ParentView: View {
    @State private var isClicked: Bool = false
    var body: some View {
        VStack {
            let text = "\(isClicked)".uppercased()
            Text(text)
            Button(
                action: {
                    isClicked = !isClicked
                },
                label: {
                    Text("Toggle State")
                }
            )
            
            Divider()
            
            ChildView(isClicked: $isClicked)
        }.padding(10)
    }
}

struct ChildView: View {
    @Binding var isClicked: Bool
    var body: some View {
        VStack {
            let text = "\(isClicked)".uppercased()
            Text(text)
        }.padding(10)
    }
}
katzkatz

試してみたけど別にBindingを使わなくても同じ動作をするものは作れる。しかしこの場合だとParentとChildで同じデータをそれぞれで保持するようになるので、パフォーマンス的には望ましくない。

struct ParentView: View {
    @State private var isClicked: Bool = false
    var body: some View {
        VStack {
            let text = "\(isClicked)".uppercased()
            Text(text)
            Button(
                action: {
                    isClicked = !isClicked
                },
                label: {
                    Text("Toggle State")
                }
            )
            
            Divider()
            
            ChildView(isClicked: isClicked)
        }.padding(10)
    }
}

struct ChildView: View {
    var isClicked: Bool
    var body: some View {
        VStack {
            let text = "\(isClicked)".uppercased()
            Text(text)
        }.padding(10)
    }
}
katzkatz

withAnimationでStateを書き換えると、それの変更点を全てSwiftUIではアニメーション化してくれるらしい。

struct ParentView: View {
    @State private var isClicked: Bool = false
    var body: some View {
        VStack {
            let text = "\(isClicked)".uppercased()
            Text(text)
            Button(
                action: {
                    withAnimation(.easeInOut(duration: 1)) {
                        isClicked = !isClicked
                    }
                },
                label: {
                    Text("Toggle State")
                }
            )
            
            Divider()
            
            ChildView(isClicked: $isClicked)
        }.padding(10).scaleEffect(isClicked ? 1: 1.5)
    }
}

struct ChildView: View {
    @Binding var isClicked: Bool
    var body: some View {
        VStack {
            let text = "\(isClicked)".uppercased()
            Text(text)
        }.padding(10).background(isClicked ? Color.green : Color.red)
    }
}
katzkatz
  • AppプロトコルではbodyにUIを定義して、Sceneを返すようにする。
  • @mainをつけるとメイン関数として認識されて、ここから呼び出されるようになるらしい。
  • @mainをつけるのはAppの全ファイルの中で1つだけにする必要がある
katzkatz

説明を読む限りEnvironmentObjectを指定したら、そのObservableObjectが変化したら、その指定したViewを無効化して再生成するというように見える。

katzkatz

実装を見るに@EnvironmentObjectをつけて、内部のViewで渡せるようにしておいて、上の方でModifier経由で渡してあげると、値がつけるみたいな感じだった。