I got no idea why my code breaks Xcode, it does not throw an error or warning, but also it get hanged for no reason! What I am doing wrong there?
Xcode Version 13.2.1 (13C100)
struct ContentView: View {
@State private var activeValue: CGFloat = 1.0 {
didSet(oldValue) {
didSetFunction(totalValue: 10, oldValue: oldValue, value: &activeValue)
}
}
var body: some View {
Button(" ") { activeValue = 1.0 }
}
}
func didSetFunction<T>(totalValue: T, oldValue: T, value: inout T) where T: Numeric & Comparable {
if (oldValue != value) {
if (value >= totalValue) { value = totalValue }
else if (value < 0) { value = 0 }
}
}
CodePudding user response:
You have an infinite recursion and consequently a stack overflow:
7 SwiftApp 0x10a15c6ba ContentView.activeValue.setter 234
8 SwiftApp 0x10a15c2d5 ContentView.activeValue.didset 229 (ContentView.swift:15)
9 SwiftApp 0x10a15c6d9 ContentView.activeValue.setter 265
10 SwiftApp 0x10a15c2d5 ContentView.activeValue.didset 229 (ContentView.swift:15)
11 SwiftApp 0x10a15c6d9 ContentView.activeValue.setter 265
12 SwiftApp 0x10a15c2d5 ContentView.activeValue.didset 229 (ContentView.swift:15)
13 SwiftApp 0x10a15c6d9 ContentView.activeValue.setter 265
14 SwiftApp 0x10a15c2d5 ContentView.activeValue.didset 229 (ContentView.swift:15)
... (and so on)
Your activeValue
setter calls a function that changes (through the inout
parameter) the value of activeValue
— which triggers the setter, which calls the function, which changes the value, which triggers the setter, which calle\s the function, which changes the value... Loop-loop-loop-kaboom.
CodePudding user response:
This is actually a pretty interesting question because, as you wrote in a comment on @matt's answer, “it has no issue using same method for didSet of a custom struct, how ever using my method in view combined with State wrapper make issue!”
So, what's happening in your code, where you use @State
? Let's de-sugar it to figure it out. We'll go through two phases of de-sugaring.
First, let's rewrite your code to remove the inout
parameter. In theory, Swift implements an inout
parameter by passing a copy of the value into the receiving function, and then when the function returns, it assigns the possibly-modified parameter value back to its original storage. (In practice it may use a more efficient implementation, but the effect must be equivalent.) We'll make that explicit by returning the possibly-modified value instead of using inout
:
struct ContentView: View {
@State private var activeValue: CGFloat = 1.0 {
didSet(oldValue) {
activeValue = update(totalValue: 10, oldValue: oldValue, value: activeValue)
}
}
var body: some View {
Button(" ") { activeValue = 1.0 }
}
}
func update<T>(totalValue: T, oldValue: T, value: T) -> T
where T: Numeric & Comparable {
guard oldValue != value else { return value }
return max(0, min(value, totalValue))
}
Now the assignment back to activeValue
is explicit.
Next, we'll de-sugar the use of the @State
property wrapper. When you use a property wrapper like @State
that has a projectedValue
property, Swift converts your property into three properties: one stored property and two computed properties. They look like this:
struct ContentView: View {
private var _activeValue: State<CGFloat> = State(wrappedValue: 1.0)
private var $activeValue: Binding<CGFloat> { _activeValue.projectedValue }
private var activeValue: CGFloat {
get { _activeValue.wrappedValue }
nonmutating set {
let oldValue = _activeValue.wrappedValue
_activeValue.wrappedValue = newValue
// THIS IS YOUR didSet CODE.
activeValue = update(totalValue: 10, oldValue: oldValue, value: activeValue)
}
}
var body: some View {
Button(" ") { activeValue = 1.0 }
}
}
Now we can see why your @State
-using code has infinite recursion. The activeValue
setter calls update
and assigns the result to activeValue
. Since activeValue
is a computed property, the only way to perform this assignment is by calling the activeValue
setter again—from inside the activeValue
setter!
Let's compare that with a version that doesn't use @State
:
struct ContentView: View {
private var activeValue: CGFloat = 1.0 {
didSet(oldValue) {
activeValue = update(totalValue: 10, oldValue: oldValue, value: activeValue)
}
}
var body: some View {
Button(" ") {
var me = self
me.activeValue = 1.0
}
}
}
In this version, the assignment to activeValue
happens directly within the didSet
observer of the activeValue
stored property. When a didSet
observer of a stored property assigns to its own property, Swift does not call the willSet
and didSet
observers of that property again. So there is no recursion.
(The calls to willSet
and didSet
are only suppressed if the assignment happens directly inside the body of the didSet
observer. If didSet
calls out to a function that then performs the assignment, that will recursively call didSet
.)