The Power of @State & @Binding in SwiftUI

Chances are that you have questions about all these @ annotations in SwiftUI like @State, @Binding, @ObservedObject, @StateObject and many others, what it means and when to use them appropriately.

If you are anything like me when I first found these things was like “what the hell are all these @ signs” and “how can I know when to use one instead of the other?”

Well, in this post I’ll dive into this new not-so-new way of using and referencing data in SwiftUI Apps.

SwiftUI Apps are driven by data state, and this states changes as things happen around.

This may be a tap of a user in an image or some counter going on behind the scenes that triggers some behavior.

Also we need to keep track of the different states our data models are in at different stages of the App Lifecycle and we need to make proper changes to other parts of the data or simply show that changes to the user.

To accomplish this we have some categories to reason about.

To start with let’s explore with this 2 ones:

@State & @Binding


Let’s begin with @State

The @State Property Wrapper allow us to define our “source of truth” to listen for State changes.

It works locally for an individual view and tracks it different states.

Take a look at this:

struct ContentView: View {
    @State private var isActive = false
    
    var body: some View {
        Rectangle()
            .frame(width: isActive ? 100 : 200, height: isActive ? 100 : 200)
            .onTapGesture {
                isActive.toggle()
            }
    }
}

In this simple example we define a @State var isActive, this way we set this value to become our source of truth.

From now on we can start to listen to this property and act accordingly to our needs.

In the example above when the user taps on the Rectangle it mutates (it changes) the value from the initial state, which is false, to a new state, which is true.

The .frame view modifier is listening to this @State Property Wrapper to know what should the width and height of the Rectangle be at any given point.

For readability it is common to use the Ternary Operator to read this state vars and instead of doing the if/else dance we convey it in a single line like this: “stateVar ? trueOption : falseOption”

The magic of SwiftUI Framework here is that it keeps listening to the different states and change the views accordingly to the state of our isActive @State variable.

Since this variables act only locally is important to set them as private and give it an initial value, to avoid potential mutations from external modules.

In this example we simply change the frame of the Rectangle when the user taps on it, nothing fancy, but you get the point, you can start adding many States variables as you wish and have a real complex view with all sorts of states.

Now imagine all of the behavior that you can introduce in this way.

Now it’s your turn, come on, just a few lines of code, and do not copy and paste! Type the code as you follow the examples, only this tip can help you become a much better developer over time.


Now let’s go with our second property: @Binding

Now imagine that we want a child view to take care of this Rectangle and listen to user events. Our current view now becomes the parent and needs to allow it’s child to mutate the state of our variable accordingly.

For this cases SwiftUI introduces @Binding, which establishes a two way street with your @State variable.

Let’s see how it works.

struct RectangleView: View {
    @Binding var isActive: Bool
    
    var body: some View {
        Rectangle()
            .onTapGesture {
                isActive.toggle()
            }
    }
}

Here we create a RectangleView which is responsible for displaying the Rectangle and to listen to user events, the user tap in this case, and when this happens it fires the .onTapGesture view modifier to toggle the “bound” state to its parent.

In turn, the parent view should be modified to look like this:

struct ContentView: View {
    @State private var isActive = false
    
    var body: some View {
        RectangleView(isActive: $isActive)
            .frame(width: isActive ? 200 : 100, height: isActive ? 200 : 100)
    }
}

Here the parent view (ContentView) passes the binding to it’s children but with one noticeable change, the dollar sign ($) which means, ok, I’ll let you mutate this state and I’ll change the view accordingly.

Now the parent view is responsible to act when it’s children sends a mutated state.

Again, the example is very basic but it shows you how you can start to do many different things with this pattern.

@State vs @Binding

There is no competition here, if you have only one view you can use just @State and you’re good to go.

But if your view is somehow complex you’ll need to start decoupling it with many sub-views.

Here is when the @Binding come into play and you set your “sources of truth@State variables in your parent view and let the child mutate them as necessary with @Binding.

This two Property Wrappers are essential tools for any iOS Developer to make interactive User Interfaces with ease.

In the next Post we’ll explore @ObservedObject and @StateObject the new in iOS 17 with Xcode 15, @Observable Macro, we will learn how to make our views react to Model changes.

That’s a wrap.

Thanks for reading! and don’t miss the next one!


Reference: https://developer.apple.com/documentation/swiftui/model-data