はじめに
SwiftUI には some
, @State
, @Binding
など色々な呪文が存在します。SwiftUI の呪文は下記のやつが関係しているそうです。例えば、同じ階層に View
が 10 個までしか置けないのは Function Builder のしくみによるものです。
- Implicit returns from single-expression functions
- Function Builder
- Opaque Result Type
- Property Wrapper
今回は Property Wrapper が関係する @State
や @Binding
などについて記載します。
Property Wrapper
SwiftUI でよくみかける下記はすべて Property Wrapper です。
@State
@Binding
@Published
@StateObject
@ObservedObject
@EnvironmentObject
ざっくりいうと Property Wrapper はプロパティへのアクセス方法を指定できるしくみです。詳細は下記など参考にしてもらえれば。
Property Wrapper は下記のように wrappedValue
とは別に projectedValue
を定義できます。この projectedValue
にアクセスする場合には変数の前に $
をつけます。@State
のバインディングでみかける $
の正体はこいつです!後述する $
はすべて Property Wrapper の projectedValue
だったのです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@propertyWrapper struct SmallNumber { private var number = 0 var projectedValue = false var wrappedValue: Int { get { return number } set { if newValue > 12 { number = 12 projectedValue = true } else { number = newValue projectedValue = false } } } } |
State
View
(struct)で値を更新するためのやつ。値の保持は SwiftUI フレームワークがおこなってくれており、値が更新されるたびに View
(body とその子要素)も更新される模様。
1 |
@frozen @propertyWrapper struct State<Value> |
1 2 |
var wrappedValue: Value var projectedValue: Binding<Value> |
簡易実装。
1 2 3 4 5 6 7 8 9 10 11 |
struct ContentView : View { @State private var value = false var body: some View { Button(action: { value.toggle() }, label: { Text(value ? "True" : "False") }) } } |
参考:Data Flow Through SwiftUI(WWDC2019)
Binding
@State
と違い親 View
とか外部から値(参照)が渡されるときに使うやつ?
1 |
@frozen @propertyWrapper @dynamicMemberLookup struct Binding<Value> |
1 2 |
var wrappedValue: Value var projectedValue: Binding<Value> |
簡易実装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
struct PlayerView: View { private let title = "Title" @State private var isPlaying = false var body: some View { VStack { Text(title) PlayButton(isPlaying: $isPlaying) } } } struct PlayButton: View { @Binding var isPlaying: Bool var body: some View { Button(action: { isPlaying.toggle() }) { Image(systemName: isPlaying ? "pause.circle" : "play.circle") } } } |
ObservableObjectとPublished
ObservableObject
プロトコルに準拠したクラスの @Published
プロパティの変更を通知するやつ。クラスの場合プロパティが変更されてもインスタンス自体は変更されないので @Published
プロパティの変更を監視する。値が更新されると View
も更新される。
1 |
protocol ObservableObject : AnyObject |
1 |
@propertyWrapper struct Published<Value> |
1 |
var projectedValue: Published<Value>.Publisher |
クラスでしか使えないようです。
Important
The @Published attribute is class constrained. Use it with properties of classes, not with non-class types like structures.
どちらも Combine
フレームワークのやつで Foundation
に下記のように定義されているので Foundation
をインポートするだけで使える模様。
1 2 |
public typealias ObservableObject = ObservableObject public typealias Published = Published |
簡易実装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
final class Counter: ObservableObject { @Published var count: Int init(count: Int) { self.count = count } func increment() -> Int { count += 1 return count } } let counter = Counter(count: 0) cancellable = counter.objectWillChange .sink { _ in print("\(counter.count) will change") } print(counter.increment()) // Prints "0 will change" // Prints "1" |
StateObject
@State
のクラス版?
ライフサイクルは View
の onAppear から onDisappear まで。View
自身で生成するときに使うやつ?下記の通り ObservableObject
にのみ使える。
1 |
@frozen @propertyWrapper struct StateObject<ObjectType> where ObjectType : ObservableObject |
1 2 |
var wrappedValue: ObjectType var projectedValue: ObservedObject<ObjectType>.Wrapper |
簡易実装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct StateObjectView: View { @StateObject private var counter = Counter(count: 0) var body: some View { Button(action: { _ = counter.increment() }, label: { HStack { Text("StateObject") Text("\(counter.count)") } }) } } |
ObservedObject
@Binding
のクラス版?
ライフサイクルは View
の body
が更新されるまで。@StateObject
と違い親 View
とか外部から値(参照)が渡されるときに使うやつ?下記の通り ObservableObject
にのみ使える。
1 |
@propertyWrapper @frozen struct ObservedObject<ObjectType> where ObjectType : ObservableObject |
1 2 |
var wrappedValue: ObjectType var projectedValue: ObservedObject<ObjectType>.Wrapper |
簡易実装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
struct ContentView: View { @StateObject private var counter = Counter(count: 0) var body: some View { Text("StateObject: \(counter.count)") ObservedObjectView(counter: counter) } } struct ObservedObjectView: View { @ObservedObject var counter: Counter var body: some View { Button(action: { _ = counter.increment() }, label: { HStack { Text("ObservedObject") Text("\(counter.count)") } }) } } |
下記のように ObservedObject
と StateObject
を使うとライフサイクルの違いでボタン押下時に ObservedObject
はリセットされます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
struct ContentView : View { @State private var value = true var body: some View { VStack(spacing: 16) { Button(action: { value.toggle() }, label: { Text(value ? "再描画するよ" : "再描画したよ") }) ObservedObjectView() StateObjectView() } } } struct ObservedObjectView: View { // ここで初期化 @ObservedObject private var counter = Counter(count: 0) var body: some View { Button(action: { _ = counter.increment() }, label: { HStack { Text("ObservedObject") Text("\(counter.count)") } }) } } |
EnvironmentObject
ObservedObject
と似たようなやつ?親 View
で .environmentObject
を設定すると子孫 View
からそのデータオブジェクトにアクセスできる。アプリ全体の設定値とかで使う?下記の通り ObservableObject
にのみ使える。
1 |
@frozen @propertyWrapper struct EnvironmentObject<ObjectType> where ObjectType : ObservableObject |
1 2 |
var wrappedValue: ObjectType var projectedValue: EnvironmentObject<ObjectType>.Wrapper |
簡易実装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
@main struct HogeApp: App { var body: some Scene { WindowGroup { ParentView().environmentObject(Counter(count: 0)) } } } struct ParentView : View { @State private var value = true var body: some View { VStack(spacing: 16) { Button(action: { value.toggle() }, label: { Text(value ? "再描画するよ" : "再描画したよ") }) ChildView() } } } struct ChildView: View { var body: some View { GrandchildView() } } struct GrandchildView: View { @EnvironmentObject var counter: Counter var body: some View { Button(action: { _ = counter.increment() }, label: { HStack { Text("EnvironmentObject") Text("\(counter.count)") } }) } } |
おわりに
なんとなく SwiftUI わかってきた気がする。たぶん SwiftUI のデータフローでは Single Source of Truth ていうのが重要な概念。
Data Essentials in SwiftUI(WWDC2020)08:55~で View を追加する際は下記の 3 つの質問を念頭に置けと言ってた。これも重要そう。
What data does this view need?
How will it use that data?
Where does the data come from?
参考
- State and Data Flow
- SwiftUIのProperty Wrappersとデータへのアクセス方法
- 動作原理から理解するSwiftUI
- Data Essentials in SwiftUI(WWDC2020)
- SwiftUIのデータ管理 Property Wrapper編
コメント