淺談 @State property wrapper in SwiftUI
利用 SwiftUI 開發的過程中,經常會在 ContentView 底下將 property 冠上 @State
這個 property wrapper,並將其作為 parameter 引入其他 view 或 controller。若 view 或 controller 同時是與使用者互動的 UI(user interface),parameter 通常必須是 Binding<Value>
,此時,我們會進一步在 property 變數前加上 $
。範例如下:
struct ContenView: View {
@State private var fontSize: CGFloat = 35
var body: some View {
Text("Hello world")
.font(.custom(name: "Helvetica", size: fontSize))
Slider(value: $fontSize, in: 10...60)
}
}
根據 SwiftUI 的定義,@State
property wrapper 實際上是一個 struct,而存入的值其實是放置在名為 wrappedValue 的變數中。
@propertyWrapper public struct State<Value> : DynamicProperty {
...
public var wrappedValue: Value { get nonmutating set }
}
wrappedValue 本身可以被讀取(get)、也可以被寫入(set),但 SwiftUI 是在其他地方保存值以進行利用,所以寫入不會對 struct 產生任何改變(nonmutating)。因此,我們無法利用 property observer 來觀察或加入其他動作(e.g. print(newValue)
),對於 property observer 來說,它所負責監看的,是 包裹著變數的 State struct 本身究竟有無改變,既然沒有改變就無法觸發 observer 內的動作。
如果非得要觀察或加入動作的話,必須透過 Binding<Value>
來達成。其定義如下:
@propertyWrapper @dynamicMemberLookup public struct Binding<Value> {
...
public init(get: @escaping () -> Value, set: @escaping (Value) -> Void)
}
做法是在原先的 view 或 controller 底下加入一個 Binding 變數,然後在 getter 中將 State 變數指定給它,而在 setter 中則是將 newValue(亦可用 shortcut $0
)設定給 Binding 變數。最後,只要將原先 view 或 controller 底下的替換掉即可。範例如下:
struct ContenView: View {
@State private var fontSize: CGFloat = 35
var body: some View {
// 新增的 Binding 變數
let size: Binding<CGFloat>(
get: {
return self.fontSize
}, set: { newValue in
self.fontSize = newValue
print("New value is \(self.fontSize)")
}
)
Text("Hello world")
.font(.custom(name: "Helvetica", size: fontSize))
// 將原先的參數從 $fontSize 替換為 size
Slider(value: size, in: 10...60)
}
}
如此一來,當使用者滑動 slider 時,新的值會透過 setter 傳遞給 fontSize,同時也會 print 出新的值(原先無法以 property observer 完成的動作),而 Text view 也會隨著 fontSize 的改變而加大字體或縮小。
Comments