Exploring changes in the binding mechanisms
- With iOS 17, iPadOS 17, macOS 14, tvOS 17, watchOS 10, and visionOS 1.0, SwiftUI bindings have been simplified. To support these changes, you should perform the following tips on any existing code you want to modernize:
- Rather than using
ObservableObject
andStateObject
, it is now possible to use the Observation pattern (https://developer.apple.com/documentation/observation/). If you already have code that was written for previous versions of the operating systems, you should replace classes inheriting fromObservableObject
andStateObject
with classes marked with the@
Observable
macro.You should perform this change incrementally; it is not wise to change all your code base in one go. Notice that the
@Observable
macro can only be applied to classes, not value types such as enums and structs.The properties of an
@Observable
class, if they are visible, are all observable. They don’t need any more to be marked as@Published
; the accessibility of the@Observable
class will instead determine which of the properties are observable. - If you don’t want a specific property to be tracked, you can use the
@ObservationIgnored
macro to mark that property to be ignored. - In SwiftUI, when a bound variable changes, the views that depend on that will get re-rendered on the screen. Due to performance improvements, now the view depending on that value will get re-evaluated only if the affected view actually needs to be redrawn and is affected by this change; if the bound variable change does not affect the view, the redraw of that view won’t get triggered.
- Rather than using
Thankfully, these changes in binding mechanisms lead to a reduction of complexity in programming. Now, rather than having to deal with six different choices, to be used in different situations, we have just three: @State
, @Bindable
, and @Environment
.
Let’s look at these in greater detail:
@State
is unchanged and is still used to bind a variable to the view that contains it. As a best practice,@State
variables should be declared private, to make it apparent that they are used to bind the private state, that is, local, belonging to a view, and not exposed outside it.
Additional reading
@State
is explained in full in the developer documentation: https://developer.apple.com/documentation/swiftui/state/.
@Bindable
is instead used to mark those properties of an@Observable
class that you want to bind bi-directionally, that is, they are mutable. As before, you will need to mark each name of these properties with a dollar sign ($
) prefix when you reference them. You can use@Bindable
on the properties of an@
Observable
object.- The main difference between
@Bindable
and the old@Binding
is that while@Binding
can only refer to an external variable to the view,@Bindable
can reference any property in an@Observable
, including any bindings to a SwiftData data model, global variables, properties that exist outside of SwiftUI types, or even local variables.
Additional reading
@Bindable
is explained in full in the developer documentation: https://developer.apple.com/documentation/swiftui/bindable#.
@Environment
now works both for values that need to be bound globally, whether these are defined by the user, which previously requiredEnvironmentObject
, and if they are provided by the system (the oldEnvironment
).
Additional reading
You will find a complete explanation of @Environment
here: https://developer.apple.com/documentation/swiftui/environment.
StateObject
was used before iOS 17 to preserve the state of an observable object when a view gets re-rendered. Achieving this with @Observable
requires ensuring that the same instance of the @Observable
class is used whenever the view is re-rendered.
The following code example shows the use of @Bindable
:
import SwiftUI @Observable class Illumination { var isOn: Bool = false } struct MyView: View { @Bindable var lightBackground: Illumination var body: some View { VStack{ Spacer() Toggle(isOn: $lightBackground.isOn) { Text("Lights") }.fixedSize() } } } struct ContentView: View { @State private var light = Illumination() var body: some View { VStack { MyView(lightBackground: light) if light.isOn { Text("On") } else { Text("off") .colorInvert() } Divider() Text("Example of a bindable var") .foregroundStyle(light.isOn ? .primary : Color.white) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(light.isOn ? Color.white : Color.blue) } } #Preview { ContentView() }
You can declare a variable inside the view as @Bindable
. A variable, to be @Bindable
, needs to conform to Sendable
and Identifiable
. The result is shown in the following screenshot:
Figure 14.1 – An example of @Bindable
In the next section, I will explain how a data model is created in SwiftData.