Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: How to implement a custom init with @Binding variables

Tags:

swift

swiftui

I'm working on a money input screen and need to implement a custom init to set a state variable based on the initialized amount.

I thought this would work, but I'm getting a compiler error of:

Cannot assign value of type 'Binding<Double>' to type 'Double'

struct AmountView : View {
    @Binding var amount: Double

    @State var includeDecimal = false

    init(amount: Binding<Double>) {
        self.amount = amount
        self.includeDecimal = round(amount)-amount > 0
    }
    ...
}
like image 331
keegan3d Avatar asked Oct 19 '22 19:10

keegan3d


People also ask

How do I pass a binding variable in SwiftUI?

Now we will create an @Binding variable. To do this we have to use the @Binding keyword before declaring the variable.

How do I create a binding value in SwiftUI?

In SwiftUI, you can create bindings in 2 ways: With the @Binding property wrapper, which creates a binding, but doesn't store it. With other property wrappers, like @State, which creates a binding, and also stores its value.

What is @binding in SwiftUI?

Binding is a property wrapper type that can read and write a value owned by a source of truth. We have several possible types of sources of truth in SwiftUI. It can be EnvironmentObject, ObservedObject or State. All these property wrappers provide a projected value, which is binding.

What does init do in SwiftUI?

By default, SwiftUI assumes that you don't want to localize stored strings, but if you do, you can first create a localized string key from the value, and initialize the text view with that. Using a key as input triggers the init(_:tableName:bundle:comment:) method instead.


2 Answers

Argh! You were so close. This is how you do it. You missed a dollar sign (beta 3) or underscore (beta 4), and either self in front of your amount property, or .value after the amount parameter. All these options work:

You'll see that I removed the @State in includeDecimal, check the explanation at the end.

This is using the property (put self in front of it):

struct AmountView : View {
    @Binding var amount: Double
    
    private var includeDecimal = false
    
    init(amount: Binding<Double>) {

        // self.$amount = amount // beta 3
        self._amount = amount // beta 4

        self.includeDecimal = round(self.amount)-self.amount > 0
    }
}

or using .value after (but without self, because you are using the passed parameter, not the struct's property):

struct AmountView : View {
    @Binding var amount: Double
    
    private var includeDecimal = false
    
    init(amount: Binding<Double>) {
        // self.$amount = amount // beta 3
        self._amount = amount // beta 4

        self.includeDecimal = round(amount.value)-amount.value > 0
    }
}

This is the same, but we use different names for the parameter (withAmount) and the property (amount), so you clearly see when you are using each.

struct AmountView : View {
    @Binding var amount: Double
    
    private var includeDecimal = false
    
    init(withAmount: Binding<Double>) {
        // self.$amount = withAmount // beta 3
        self._amount = withAmount // beta 4

        self.includeDecimal = round(self.amount)-self.amount > 0
    }
}
struct AmountView : View {
    @Binding var amount: Double
    
    private var includeDecimal = false
    
    init(withAmount: Binding<Double>) {
        // self.$amount = withAmount // beta 3
        self._amount = withAmount // beta 4

        self.includeDecimal = round(withAmount.value)-withAmount.value > 0
    }
}

Note that .value is not necessary with the property, thanks to the property wrapper (@Binding), which creates the accessors that makes the .value unnecessary. However, with the parameter, there is not such thing and you have to do it explicitly. If you would like to learn more about property wrappers, check the WWDC session 415 - Modern Swift API Design and jump to 23:12.

As you discovered, modifying the @State variable from the initilizer will throw the following error: Thread 1: Fatal error: Accessing State outside View.body. To avoid it, you should either remove the @State. Which makes sense because includeDecimal is not a source of truth. Its value is derived from amount. By removing @State, however, includeDecimal will not update if amount changes. To achieve that, the best option, is to define your includeDecimal as a computed property, so that its value is derived from the source of truth (amount). This way, whenever the amount changes, your includeDecimal does too. If your view depends on includeDecimal, it should update when it changes:

struct AmountView : View {
    @Binding var amount: Double
    
    private var includeDecimal: Bool {
        return round(amount)-amount > 0
    }
    
    init(withAmount: Binding<Double>) {
        self.$amount = withAmount
    }

    var body: some View { ... }
}

As indicated by rob mayoff, you can also use $$varName (beta 3), or _varName (beta4) to initialise a State variable:

// Beta 3:
$$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)

// Beta 4:
_includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
like image 371
kontiki Avatar answered Oct 22 '22 07:10

kontiki


You should use underscore to access the synthesized storage for the property wrapper itself.

In your case:

init(amount: Binding<Double>) {
    _amount = amount
    includeDecimal = round(amount)-amount > 0
}

Here is the quote from Apple document:

The compiler synthesizes storage for the instance of the wrapper type by prefixing the name of the wrapped property with an underscore (_)—for example, the wrapper for someProperty is stored as _someProperty. The synthesized storage for the wrapper has an access control level of private.

Link: https://docs.swift.org/swift-book/ReferenceManual/Attributes.html -> propertyWrapper section

like image 19
Simon Wang Avatar answered Oct 22 '22 09:10

Simon Wang