Understanding @Binding in Swiftui

Difference between @Binding and Binding Type in Swift

You're correct that @Binding is a property wrapper. More specifically, it's a DynamicProperty, which means that it has the ability to notify its parent View to trigger a render when its value changes.

@Binding does this while maintaining a fairly transparent interface to the underlying Bool. For example, in a View where you had @Binding var hidden: Bool defined as a property, you could write hidden = false or Text(hidden ? "hidden" : "visible"), just as if hidden were a regular Bool. If you want to get access to the underlying Binding<Bool>, you can use $:

$hidden //<-- Binding<Bool>

In your second example, Binding<Bool> is not "type casting", but rather "type annotation" -- by writing var hidden: Binding<Bool>, you're telling the compiler that hidden is Binding<Bool>. Because it's Binding<Bool> and not just a Bool (and not a @Binding), you can't treat it like you would if it were just a Bool. For example, hidden = false will not work with Binding<Bool>. Instead, to access the underlying Bool value, you can use its .wrappedValue property: hidden.wrappedValue = false.

The two are are very similar, but different in a couple of important ways (like those detailed above). In practical terms:

  • If you're using a binding as a property on a View, you'll likely end up using @Binding.
  • If you're the binding it outside of a view (and thus don't have use of the DynamicProperty aspect), you'll likely use Binding<Bool> (technically, nothing is stopping you from using @Binding outside of a View, but it's a semantically odd decision).

SwiftUI – @State vs @Binding

SwiftUI is a declarative Component-Oriented framework. You have to forget about MVC where you have controllers mediating between view and model. SwiftUI uses diffing algorithm to understand changes and update only corresponding views.

@State

  • A State property is connected to the view. A State property is permanently being read by the view. That means that every time the @State property gets changed/updated, the view gets re-rendered and eventually displays the content depending on the @State's data.
  • State is accessible only to a particular view.
  • Simple properties like strings, integers and booleans belongs to a single view - mark as private.
  • All the fields marked as State are stored in special separated memory, where only corresponded view can access and update them.

@Binding

  • BindableObject protocol, which requires a didChange property. It makes possible to use it inside Environment and rebuild view as soon as it changes.
  • The didChange property should be a Publisher, which is a part of a new Apple’s Reactive framework called Combine.
  • The main goal of Publisher is to notify all subscribers when something changes. As soon as new values appear, SwiftUI will rebuild Views.

@EnvironmentObject

  • It is a part of feature called Environment. You can populate your Environment with all needed service classes and then access them from any view inside that Environment.
  • @EnvironmentObject is accessible for every view inside the Environment.
  • @EnvironmentObject Properties created elsewhere such as shared data. App crashes if it is missing.
  • The Environment is the right way of Dependency Injection with SwiftUI.

SwiftUI: Preventing binding value from passing back up?

In Secondary use:

@State var secondTime: String 

and in ContentView use:

Secondary(secondTime: time)

not $time

EDIT1:

If you want to click the button in ContentView to change both views,
but Secondary only changes itself, then try this approach:

struct Secondary: View {
@Binding var secondTime: String
@State var localTime: String = ""

var body: some View {
Text("Secondary time is \(localTime)") // <--- here
.onChange(of: secondTime) { newval in // <--- here
localTime = newval // <--- here
}
Button("Change time again from Secondary View") {
localTime = "3 oclock " + String(Int.random(in: 1..<100)) // <-- to show changes
}
}
}

struct ContentView: View {
@State var time: String = ""

var body: some View {
VStack (spacing: 55) {
Text("ContentView it is: \(time)")
Secondary(secondTime: $time)
Button("Change time") {
time = "2 oclock " + String(Int.random(in: 1..<100)) // <-- to show changes
}
}
}
}

How to optionally pass in a Binding in SwiftUI?

The main problem with what you want to achieve is that when the index is handled by the parent your View needs a @Binding to it, but when it handles the index itself it needs @State. There are two possible solutions.

If the view can ignore the index property when it doesn't have one:

struct ReusableView: View {

@Binding var index: Int?

init(_ index: Binding<Int?>) {
self._index = index
}

init() {
self._index = .constant(nil)
}

var body: some View {
VStack {
index.map { Text("The index is \($0)") }
}
}
}

The advantage is that it is very straightforward - just two initializers, but you cannot change the value of the index when it is handled by ResusableView itself (its a constant).

If the view cannot ignore the index property when it doesn't have one:

struct ReusableView: View {

private var content: AnyView

init(_ index: Binding<Int>? = nil) {
if let index = index {
self.content = AnyView(DependentView(index: index))
} else {
self.content = AnyView(IndependentView())
}
}

var body: some View {
content
}

private struct DependentView: View {

@Binding var index: Int

var body: some View {
Text("The index is \(index)")
}
}

private struct IndependentView: View {

@State private var index: Int = 0

var body: some View {
Text("The index is \(index)")
}
}

}

The clear advantage is that you have a view that can either be bound to a value or manage it as its own State. As you can see ReusableView is just a wrapper around two different views one managing its own @State and one being bound to @State of its parent view.

How does this SwiftUI binding + state example manage to work without re-invoking body?

On State change SwiftUI rendering engine at first checks for equality of views inside body and, if some of them not equal, calls body to rebuild, but only those non-equal views. In your case no one view depends (as value) on text value (Binding is like a reference - it is the same), so nothing to rebuild at this level. But inside WrappedText it is detected that Text with new text is not equal to one with old text, so body of WrappedText is called to re-render this part.

This is declared rendering optimisation of SwiftUI - by checking & validating exact changed view by equality.

By default this mechanism works by View struct properties, but we can be involved in it by confirming our view to Eqatable protocol and marking it .equatable() modifier to give some more complicated logic for detecting if View should be (or not be) re-rendered.



Related Topics



Leave a reply



Submit