Data Binding in SwiftUI: Tips, Tricks, and Best Practices

Want to create dynamic and responsive user interfaces in SwiftUI? Data binding is the key! In this tutorial, I'll show you how to use @State, @ObservedObject, @EnvironmentObject, and @Binding to keep your user interface in sync with your data

Alex Dremov

Are you building an app with SwiftUI and wondering how to manage your app's state? Data binding is a powerful tool that can help you build dynamic and responsive interfaces.

In this tutorial, we'll explore how to use @State, @ObservedObject, and @EnvironmentObject.

What is data binding in SwiftUI?

Data binding connects UI element to a piece of data in your app. When the data changes, the UI element automatically updates to reflect the new value, and when the user interacts with the element, the data updates to reflect the new input.

SwiftUI provides several tools for data binding: @State, @ObservedObject, and @EnvironmentObject. These tools allow you to bind values, objects, and even global objects to your user interface.

How to use @State to bind a simple value to your user interface

@State is a property wrapper that allows you to bind a simple value, like a string or an integer, to your user interface.

💥
Strictly, @State can be used to bind value-type objects only. So, any struct also can be binded using @State.

To use @State, you first define a property with the @State wrapper, and then use the property in your user interface as a usual. For example, here's how you might use @State to bind a string to a text field:

struct ContentView: View {
    @State private var name: String = ""
    
    var body: some View {
        VStack {
            TextField("Enter your name", text: $name)
            Text("Hello, \(name)!")
        }
    }
}

You may notice that $name is used. It allows to access projectedValue of the wrapper. In case of @State it is Binding<Type>.

Now, whenever name is changed, the UI updates automatically. And when the user modifies the text field, variable data gets updated too.

Using @Binding

@Binding is used when you want to bind a value or object that is owned by a different view.

To use @Binding, you first define a property with the @Binding wrapper, and then pass the binding to another view as an argument. The other view can then use the binding to read and write the data from the original view.

struct CustomTextField: View {
    @Binding var text: String
    
    var body: some View {
        HStack {
            Image(systemName: "person.circle")
            TextField("Enter your name", text: $text)
        }
        .padding()
    }
}

struct ContentView: View {
    @State private var name: String = ""
    
    var body: some View {
        VStack {
            CustomTextField(text: $name)
            Text("Hello, \(name)!")
        }
    }
}

You also can pass binding in init using direct access to property wrapper through underscore.

struct CustomTextField: View {
    @Binding var text: String
    
    init(text: Binding<String>) {
        self._text = text
    }
    
    var body: some View {
        HStack {
            Image(systemName: "person.circle")
            TextField("Enter your name", text: $text)
        }
        .padding()
    }
}
💥
You can view @Binding as a channel that gets value from the source and sets value to the source. It does not own an object.

Therefore, @Binding is great for the view decomposition as it allows to inject dependencies to subviews.

Read more about modular app architecture with SwiftUI in my previous post:

iOS App As a Microservice. Using SwiftUI in Modular App
The modular architecture is excellent. But how to implement it effectively with SwiftUI? From its core, SwiftUI is state-driven, and it can be tricky to modularize an app and define exact responsibility borders.

How to use @ObservedObject to bind a class to your user interface

@ObservedObject allows you to bind a class to your user interface. The class must conform to the ObservableObject protocol and use the @Published property wrapper for any properties that you want to bind to your user interface. When the object's @Published properties change, the user interface updates.

Here's an example of how you might use @ObservedObject to bind a User object to a form:

class User: ObservableObject {
    @Published var name: String = ""
    @Published var email: String = ""
    
    var someUntrackedValue = ""
}

struct ContentView: View {
    @ObservedObject private var user = User()
    
    var body: some View {
        VStack {
            TextField("Enter your name", text: $user.name)
            TextField("Enter your email", text: $user.email)
            Text("Hello, \(user.name)!")
        }
    }
}

In this example, the user property is bound to the text fields using the $user.name and $user.email syntax. When the user types in the text fields, the name and email properties of the User object update to reflect the new input, and the Text view updates to show the new value.

💥
Mind that if you publish a reference type in ObservableObject, then changes inside it will not be propagated.

How to use @EnvironmentObject to bind a global object to your user interface

EnvironmentObject allows you to bind a global object. The object must conform to the ObservableObject protocol the same way as with @ObservedObject.

Share

Subscribe to Alex Dremov

Get the email newsletter and receive valuable tips to bump up your professional skills