Last Updated:
Photo by Joshua Reddekopp on Unsplash

淺談 @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