SwiftUI ModelData
- SwiftUIはUIのデザインに宣言型のアプローチを提供する
- Viewの階層を構成するときに、Viewのデータ依存関係も指定する
- 外部イベントやユーザーアクションによってデータが変更されるとSwiftUIはUIの影響点を識別して自動的に更新する
→ 以前のフレームワークでいうViewControllerが担っていた処理のほとんどをSwiftUIでやってくれるようになる。
- つまりはSwiftUIを使うことで、極力ViewControllerが担っていた処理をやらなくて良くなる。
- でもアプリケーションのDataとViewを紐付けるところに関してはどうしても必要になる。
- ドキュメントではグルーコードと呼んでいる
- このグルーコードを極力減らして、楽に管理できる仕組みをSwiftUIでは用意している感じになる
- StateやBindingやObservableなどがこの機能にあたるらしい
→ SwiftUIのModel Dataのセクションでは、このグルーコードをどのように実装する仕組みがあるのか、解説していってくれるっぽい。
- StateはView内部でデータを保持するのに使える
- Bindingはローカルデータを保持して参照を共有するのに使える
- Observableはデータ更新監視可能なStateという立ち位置
プロパティラッパーを利用すれば、ゲッターやセッターでやるバリデーション処理などを共通化することができる。
@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"
SwiftUIのStateやBindingはこのプロパティラッパーを利用して実装されているらしい。
- Single of truthを実現するために、データを必要とするViewの一番低い階層のところにデータを保持する
- StateのライフサイクルはViewのライフサイクルに依存するので、永続ストレージにStateを利用するのはNGらしい。
- State
- Viewが変更可能なデータを保存する場合はStateのPropertyWrapperを使う
こんな感じで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()
}
不変の値を格納したいのであれば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")
}
@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)
}
}
試してみたけど別に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)
}
}
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)
}
}
アプリ構造はAppプロトコルを使って定義する。
- AppプロトコルではbodyにUIを定義して、Sceneを返すようにする。
- @mainをつけるとメイン関数として認識されて、ここから呼び出されるようになるらしい。
- @mainをつけるのはAppの全ファイルの中で1つだけにする必要がある
EnvironmentObjectとは
説明を読む限りEnvironmentObjectを指定したら、そのObservableObjectが変化したら、その指定したViewを無効化して再生成するというように見える。
実装を見るに@EnvironmentObjectをつけて、内部のViewで渡せるようにしておいて、上の方でModifier経由で渡してあげると、値がつけるみたいな感じだった。