272

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

I thought the following would work:

struct AmountView : View {

    @Binding var amount: Double   
    @State var includeDecimal = false

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

However, this gives me a compiler error as follows:

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

How do I implement a custom init method which takes in a Binding struct?

shim
  • 9,289
  • 12
  • 69
  • 108
keegan3d
  • 10,357
  • 9
  • 53
  • 77

7 Answers7

433

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)
aheze
  • 24,434
  • 8
  • 68
  • 125
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • Thanks! This helped a lot! I am getting a runtime error on `self.includeDecimal = round(self.amount)-self.amount > 0` of `Thread 1: Fatal error: Accessing State outside View.body` – keegan3d Jul 10 '19 at 19:37
  • Well, it kind of makes sense. `@State` variables should represent a source of truth. But in your case you are duplicating that truth, because the value of includeDecimal can be derived from your actual source of truth that is amount. You have two options: 1. You make includeDecimal a private var (no @State), or even better 2. You make it a computed property that derives its value from `amount`. This way, if amount changes, `includeDecimal` does too. You should declare it like this: `private var includeDecimal: Bool { return round(amount)-amount > 0 }` and remove the `self.includeDecimal = ...` – kontiki Jul 10 '19 at 20:05
  • Hmm, I need to be able to change `includeDecimal` so need it as a @State variable in the view. I really just want to initialize it with a starting value – keegan3d Jul 10 '19 at 21:23
  • I thought maybe it would work to remove the default value in the declaration `@State private var includeDecimal: Bool`, but then in my init I get the error: `Variable 'self.includeDecimal' used before being initialized` – keegan3d Jul 10 '19 at 21:24
  • Thanks, @kontiki for the awesome explanation. Each of your answer on the SwiftUI questions is with details explanation. Though I have a question for you how can you precisely say the time at which a particular concept is explained in a WWDC video? How many times have you watched them? – Let's_Create Jul 18 '19 at 05:36
  • 1
    @Let's_Create I watched them fully only once, but thanks god for the ***forwards*** button ;-) – kontiki Jul 18 '19 at 06:14
  • 4
    Really nice explanation, thanks. I think now the `.value` has been replaced with `.wrappedValue`, would be nice to update the answer and remove beta options. – user1046037 Dec 12 '19 at 05:12
  • Where is the underscore documented? I feel like I'm missing the documentation for SwiftUI... where is it hiding? It isn't here: https://developer.apple.com/documentation/swiftui/state – Diesel Jan 26 '20 at 03:11
  • Hi @Diesel, strictly speaking, the underscore is not really part of SwiftUI. It is part of "Swift Property Wrappers", which were added to the language at the same time (hence the confusion). I suggest you look into that if you want to expand your understanding. – kontiki Jan 26 '20 at 08:15
  • 2
    Ugh Swift is such a mess. What’s the June 2023 answer? – user1944491 Jun 14 '23 at 22:21
32

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

Simon Wang
  • 996
  • 7
  • 11
14

You said (in a comment) “I need to be able to change includeDecimal”. What does it mean to change includeDecimal? You apparently want to initialize it based on whether amount (at initialization time) is an integer. Okay. So what happens if includeDecimal is false and then later you change it to true? Are you going to somehow force amount to then be non-integer?

Anyway, you can't modify includeDecimal in init. But you can initialize it in init, like this:

struct ContentView : View {
    @Binding var amount: Double

    init(amount: Binding<Double>) {
        $amount = amount
        $$includeDecimal = State(initialValue: (round(amount.value) - amount.value) != 0)
    }

    @State private var includeDecimal: Bool

(Note that at some point the $$includeDecimal syntax will be changed to _includeDecimal.)

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
8

Since it's mid of 2020, let's recap:

As to @Binding amount

  1. _amount is only recommended to be used during initialization. And never assign like this way self.$amount = xxx during initialization

  2. amount.wrappedValue and amount.projectedValue are not frequently used, but you can see cases like

@Environment(\.presentationMode) var presentationMode

self.presentationMode.wrappedValue.dismiss()
  1. A common use case of @binding is:
@Binding var showFavorited: Bool

Toggle(isOn: $showFavorited) {
    Text("Change filter")
}
LiangWang
  • 8,038
  • 8
  • 41
  • 54
4

The accepted answer is one way but there is another way too

struct AmountView : View {
var amount: Binding<Double>
  
init(withAmount: Binding<Double>) {
    self.amount = withAmount
}

var body: some View { ... }
}

You remove the @Binding and make it a var of type Binding The tricky part is while updating this var. You need to update it's property called wrapped value. eg

 amount.wrappedValue = 1.5 // or
 amount.wrappedValue.toggle()
   
arthas
  • 680
  • 1
  • 8
  • 16
  • Actually, if you want to instantiate private properties that are neither Binding nor state during init (like observers), this is the only way to do it. Otherwise, I get an error saying that the init is private – Frederic Adda Mar 13 '23 at 13:35
4

You can achieve this either with static function or with custom init.

import SwiftUI
import PlaygroundSupport

struct AmountView: View {
    @Binding var amount: Double
    @State var includeDecimal: Bool
    var body: some View {
        Text("The amount is \(amount). \n Decimals  \(includeDecimal ? "included" : "excluded")")
    }
}

extension AmountView {
    static func create(amount: Binding<Double>) -> Self {
        AmountView(amount: amount, includeDecimal: round(amount.wrappedValue) - amount.wrappedValue > 0)
    }
    init(amount: Binding<Double>) {
        _amount = amount
        includeDecimal = round(amount.wrappedValue) - amount.wrappedValue > 0
    }
}
struct ContentView: View {
    @State var amount1 = 5.2
    @State var amount2 = 5.6
    var body: some View {
        AmountView.create(amount: $amount1)
        AmountView(amount: $amount2)
    }
}

PlaygroundPage.current.setLiveView(ContentView())

Actually you don't need custom init here at all since the logic could be easily moved to .onAppear unless you need to explicitly set initial state externally.

struct AmountView: View {
    @Binding var amount: Double
    @State private var includeDecimal = true
    
    var body: some View {
        Text("The amount is \(amount, specifier: includeDecimal ? "%.3f" : "%.0f")")
        Toggle("Include decimal", isOn: $includeDecimal)
            .onAppear {
                includeDecimal = round(amount) - amount > 0
            }
    }
}

This way you keep your @State private and initialized internally as documentation suggests.

Don’t initialize a state property of a view at the point in the view hierarchy where you instantiate the view, because this can conflict with the storage management that SwiftUI provides. To avoid this, always declare state as private, and place it in the highest view in the view hierarchy that needs access to the value

.

Paul B
  • 3,989
  • 33
  • 46
3

State:

To manages the storage of any property you declare as a state. When the state value changes, the view invalidates its appearance and recomputes the body and You should only access a state property from inside the view’s body, or from methods called.

Note: To pass a state property to another view in the view hierarchy, use the variable name with the $ prefix operator.

struct ContentView: View {
    @State private var isSmile : Bool = false
    var body: some View {
        VStack{
            Text(isSmile ? "" : "").font(.custom("Arial", size: 120))
            Toggle(isOn: $isSmile, label: {
                    Text("State")
                }).fixedSize()
        }
    }
}

enter image description here

Binding:

The parent view declares a property to hold the isSmile state, using the State property wrapper to indicate that this property is the value’s source of deferent view.

struct ContentView: View {
    @State private var isSmile : Bool = false
    var body: some View {
        VStack{
            Text(isSmile ? "" : "").font(.custom("Arial", size: 120))
            SwitchView(isSmile: $isSmile)
        }
    }
}

Use a binding to create a two-way connection between a property that stores data, and a view that displays and changes the data.

struct SwitchView: View {
    @Binding var isSmile : Bool
    var body: some View {
        VStack{
                Toggle(isOn: $isSmile, label: {
                    Text("Binding")
                }).fixedSize()
        }
    }
}

enter image description here

Nazmul Hasan
  • 10,130
  • 7
  • 50
  • 73
  • 1
    It's a nice explanation what binding is, but the question is how to set the binding value at the beginning from outside the view. In your example imagen I integrate the SwitchView and from outside I want to define the start value of isSmile from the outside. SwitchView(isSmile: true) dosn't work, how to achieve this is the original question – Carmen Jan 21 '22 at 13:31