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.
@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()
}
}
@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:

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.
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
.
@EnvironmentObject
is particularly useful when you want to share data across multiple views in your app. For example, you might use @EnvironmentObject
to bind a UserSettings
object to your app's main view, like this:
class UserSettings: ObservableObject {
@Published var theme: String = "light"
@Published var fontSize: Double = 16
}
struct ContentView: View {
@EnvironmentObject var userSettings: UserSettings
var body: some View {
VStack {
if userSettings.theme == "light" {
Text("Light mode")
} else {
Text("Dark mode")
}
Text("Font size: \(userSettings.fontSize)")
}
}
}
You can pass the @EnvironmentObject
down to child views using the environmentObject(_:)
modifier. For example:
struct ChildView: View {
@EnvironmentObject var userSettings: UserSettings
var body: some View {
Text("Font size: \(userSettings.fontSize)")
}
}
struct ContentView: View {
@ObservedObject var userSettings: UserSettings
var body: some View {
VStack {
if userSettings.theme == "light" {
Text("Light mode")
} else {
Text("Dark mode")
}
ChildView()
}.environmentObject(userSettings)
}
}
However, I would suggest not using @EnvironmentObject
or only using it on a small scale, as it introduces global dependencies and makes the app's architecture messier. I covered modular architecture principles in one of my previous posts:

Best practices for using data binding in SwiftUI
Here are a few best practices for using data binding in SwiftUI:
- Use
@State
for simple values that are specific to a single view. - Use
@ObservedObject
for complex objects that you need to share with other parts of your app. - Use
@EnvironmentObject
sparingly, as it can introduce global dependencies and make your app's architecture messier. Only use it when you need to share data across a small portion of your app and there is no cleaner way to do it. - Use
@Binding
to create custom views with data binding. This allows you to reuse your views and keep your code more modular. - Organize and structure your data bindings in a logical and easy-to-maintain way. Use modular architecture principles to break your app into smaller, more manageable pieces.
By following these best practices, you can create dynamic and responsive user interfaces in SwiftUI that stay in sync with your data and are easy to maintain and test.
Tips for debugging and testing your data bindings
Data binding can be a powerful tool, but it can also be a source of bugs and issues if you're not careful. Here are a few tips for debugging and testing your data bindings:
- Make sure that your classes conform to the
ObservableObject
protocol and use the@Published
property wrapper for any properties that you want to bind to your user interface. - Check that you use
@State
for value type objects and@ObservedObject
for reference type. - Check that
@Published
properties are value types - Use the debugging tools in Xcode to identify and fix issues in your data bindings. You can use the debugger to inspect the values of your bound properties and step through your code to see how the data is flowing through your app.
- Use Xcode's preview feature to test your layouts and behaviors in real-time as you build your app. This can be a great way to catch issues with your data bindings early on and ensure that your user interface is working as expected.
- Consider using unit tests to validate your data bindings and ensure that they are working correctly. You can use the
XCTest
framework to write tests that verify the values of your bound properties and check that your user interface is behaving as expected.
Conclusion
Data binding is a powerful tool that allows you to create dynamic and responsive user interfaces that stay in sync with your data.
As you continue to develop your app, remember to keep your data bindings organized and well-structured to ensure that your app is easy to maintain and test. Use modular architecture principles to break your app into smaller, more manageable pieces, and be sure to test your data bindings thoroughly to catch any bugs or issues before you release your app.
With these tips in mind, you'll be well on your way to creating amazing apps with SwiftUI and data binding. Thanks for reading!
See also


