Home > other >  Unknown hanging when I use Numeric in Swift
Unknown hanging when I use Numeric in Swift

Time:02-01

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.)

  •  Tags:  
  • Related