为什么当它直接绑定到 SwiftUI 的 Toggle 时,@AppStorage 变量不会触发 willSet?

huangapple go评论72阅读模式
英文:

Why doesn't willSet trigger for an @AppStorage variable when it's directly bound to a SwiftUI Toggle?

问题

I'm using @AppStorage along with a SwiftUI Toggle to manage a UserDefaults variable. My expectation is to perform some operations when the variable changes due to the Toggle interaction, so I need to execute some code in the willSet part of my @AppStorage variable.

我正在使用@AppStorage与SwiftUI Toggle一起管理UserDefaults变量。我的期望是在Toggle交互导致变量更改时执行一些操作,因此我需要在@AppStorage变量的willSet部分执行一些代码。

However, when I directly bind the @AppStorage variable to the Toggle's isOn state, willSet doesn't trigger. In contrast, when I bind a @State variable to the Toggle and modify the @AppStorage variable when the @State variable changes, the code in willSet does get executed.

然而,当我直接将@AppStorage变量绑定到Toggle的isOn状态时,willSet不会触发。相反,当我将@State变量绑定到Toggle并在@State变量更改时修改@AppStorage变量时,willSet中的代码确实会执行。

Why is this the case? Why does the internal modification of the @AppStorage variable by the Toggle seem to bypass my willSet? Is there a more elegant way to achieve my requirements?

为什么会这样?为什么Toggle对@AppStorage变量的内部修改似乎绕过了我的willSet?有没有更优雅的方法来满足我的需求?

Here's the sample code to illustrate the issue:

以下是示例代码以说明这个问题:

struct ContentView: View {
    @State var stateVariable = false
    @AppStorage("userDefaultVariable") var userDefaultVariable: Bool = false {
        willSet {
            print("userDefaultVariable will set, newValue: \(newValue)")
        }
    }
    var body: some View {
        VStack{
            Toggle("Directly modify userDefaultVariable",isOn: $userDefaultVariable)    
            Toggle("Indirectly modify userDefaultVariable",isOn: $stateVariable) 
                .onChange(of: stateVariable, perform: { value in
                    userDefaultVariable = value
                })
        }
    }
}

In this code, the "Directly modify userDefaultVariable" Toggle does not trigger the print statement in the willSet of userDefaultVariable. However, the "Indirectly modify userDefaultVariable" Toggle does trigger the print statement when it modifies the @State variable stateVariable, which in turn modifies userDefaultVariable.

在此代码中,“Directly modify userDefaultVariable” Toggle不会触发willSet中userDefaultVariable的打印语句。然而,“Indirectly modify userDefaultVariable” Toggle在修改@State变量stateVariable时触发打印语句,然后修改userDefaultVariable。

英文:

I'm using @AppStorage along with a SwiftUI Toggle to manage a UserDefaults variable. My expectation is to perform some operations when the variable changes due to the Toggle interaction, so I need to execute some code in the willSet part of my @AppStorage variable.

However, when I directly bind the @AppStorage variable to the Toggle's isOn state, willSet doesn't trigger. In contrast, when I bind a @State variable to the Toggle and modify the @AppStorage variable when the @State variable changes, the code in willSet does get executed.

Why is this the case? Why does the internal modification of the @AppStorage variable by the Toggle seem to bypass my willSet? Is there a more elegant way to achieve my requirements?

Here's the sample code to illustrate the issue:

struct ContentView: View {
    @State var stateVariable = false
    @AppStorage("userDefaultVariable") var userDefaultVariable: Bool = false {
        willSet {
            print("userDefaultVariable will set, newValue: \(newValue)")
        }
    }
    var body: some View {
        VStack{
            Toggle("Directly modify userDefaultVariable",isOn: $userDefaultVariable)    
            Toggle("Indirectly modify userDefaultVariable",isOn: $stateVariable) 
                .onChange(of: stateVariable, perform: { value in
                    userDefaultVariable = value
                })
        }
    }
}

In this code, the "Directly modify userDefaultVariable" Toggle does not trigger the print statement in the willSet of userDefaultVariable. However, the "Indirectly modify userDefaultVariable" Toggle does trigger the print statement when it modifies the @State variable stateVariable, which in turn modifies userDefaultVariable.

答案1

得分: 1

属性包装器像 AppStorageState 会编译成包装器类型的存储属性,以及一个计算属性,用于获取和设置存储属性的 wrappedValue。还有一个以 $ 为前缀的计算属性,用于获取 projectedValue

@AppStorage("userDefaultVariable") var userDefaultVariable: Bool = false

转变为:

var _userDefaultVariable = AppStorage(wrappedValue: false, "userDefaultVariable")

var userDefaultVariable: Bool {
    get { _userDefaultVariable.wrappedValue }
    set { _userDefaultVariable.wrappedValue = newValue }
}

var $userDefaultVariable: Binding<Bool> {
    _userDefaultVariable.projectedValue
}

根据这里的帖子,当你添加 willSet 时,它会应用于计算属性 userDefaultVariable,而不是 _userDefaultVariable$userDefaultVariable。只要你不直接使用 userDefaultVariable = ... 来设置它,willSet 就不会触发。

为了进一步复杂化问题,Binding.wrappedValueAppStorage.wrappedValue 都有一个 nonmutating setter。

我认为在这里唯一的解决方案是创建自己的 Binding,并在 set: 参数的开头添加你想要的任何代码:

Toggle("Directly modify userDefaultVariable", isOn: Binding(get: { userDefaultVariable }, set: { newValue in
    // 在这里执行你的 willSet 操作...
    userDefaultVariable = newValue
}))

你可以将其重构为自己的属性包装器:

@propertyWrapper
struct Observed<Wrapped> {
    var wrappedValue: AppStorage<Wrapped>
    
    var willSet: (Wrapped) -> Void
    
    var projectedValue: Binding<Wrapped> {
        Binding {
            wrappedValue.wrappedValue
        } set: { newValue in
            willSet(newValue)
            wrappedValue.wrappedValue = newValue
        }
    }
}

用法:

struct ContentView: View {
    @Observed(willSet: { newValue in
        print("WillSet: \(newValue)") 
        // 进行其他操作...
    })
    @AppStorage("userDefaultVariable") var userDefaultVariable: Bool = false
    var body: some View {
        VStack{
            Toggle("Directly modify userDefaultVariable", isOn: $userDefaultVariable)
        }
    }
}
英文:

Property wrappers like AppStorage and State are compiled into a stored property of the wrapper type, and a computed property that gets and sets the wrappedValue of the stored property. There is also a $-prefixed computed property that gets the projectedValue.

@AppStorage(&quot;userDefaultVariable&quot;) var userDefaultVariable: Bool = false

becomes:

var _userDefaultVariable = AppStorage(wrappedValue: false, &quot;userDefaultVariable&quot;)

var userDefaultVariable: Bool {
    get { _userDefaultVariable.wrappedValue }
    set { _userDefaultVariable.wrappedValue = newValue }
}

var $userDefaultVariable: Binding&lt;Bool&gt; {
    _userDefaultVariable.projectedValue
}

According to this post here, when you add willSet, it is applied to the computed property userDefaultVariable, not _userDefaultVariable or $userDefaultVariable. So as long as you are not directly setting it with userDefaultVariable = ..., willSet is not triggered.

To further complicate the matter, Binding.wrappedValue and AppStorage.wrappedValue all have a nonmutating setter.

I think the only solution here is to make your own Binding and add whatever code you want at the start of the set: argument:

Toggle(&quot;Directly modify userDefaultVariable&quot;,isOn: Binding(get: { userDefaultVariable }, set: { newValue in
    // do your willSet things here...
    userDefaultVariable = newValue
}))

You can refactor this into your own property wrapper:

@propertyWrapper
struct Observed&lt;Wrapped&gt; {
    var wrappedValue: AppStorage&lt;Wrapped&gt;
    
    var willSet: (Wrapped) -&gt; Void
    
    var projectedValue: Binding&lt;Wrapped&gt; {
        Binding {
            wrappedValue.wrappedValue
        } set: { newValue in
            willSet(newValue)
            wrappedValue.wrappedValue = newValue
        }
    }
}

Usage:

struct ContentView: View {
    @Observed(willSet: { newValue in
        print(&quot;WillSet: \(newValue)&quot;) 
        // do other things...
    })
    @AppStorage(&quot;userDefaultVariable&quot;) var userDefaultVariable: Bool = false
    var body: some View {
        VStack{
            Toggle(&quot;Directly modify userDefaultVariable&quot;,isOn: $userDefaultVariable)
        }
    }
}

huangapple
  • 本文由 发表于 2023年7月13日 11:56:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/76675808.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定