In SwiftUI, the flows between data model and views are bidirectional automatically. That means, if a variable changes, the corresponding view changes too. The same is true on the reverse side.
@State
This mechanism is called binding. For variables within the same view, you can use @State
property wrapper.
import SwiftUI
struct ContentView: View {
@State var isEditing = true
var body: some View {
VStack {
if isEditing {
Text("Editing")
} else {
Text("Done")
}
}.onTapGesture {
isEditing.toggle()
}.frame(width: 200, height: 200, alignment: .center)
.font(.title)
.padding()
}
}
When the code runs, the app shows a text with "Editing". If you click the text, the isEditing
is toggled and app shows a text with "Done".
@Binding
@State is used only its own view. If you need to pass @State to another view, the destination view should not use @State, but @Binding to get the @State variable.
import SwiftUI
struct AnotherSwiftUIView: View {
@State var isEditing = true
var body: some View {
ContentView(isEditing: $isEditing)
}
}
import SwiftUI
struct ContentView: View {
@Binding var isEditing:Bool
var body: some View {
VStack {
if isEditing {
Text("Editing")
} else {
Text("Done")
}
}.onTapGesture {
isEditing.toggle()
}.frame(width: 200, height: 200, alignment: .center)
.font(.title)
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(isEditing: .constant(true))
}
}
For @Binding variable, you must use
.constant(true)
as a parameter. Also there is a$
prefix for @State as a @Binding variable.
@Binding in Closure
Many times, we may need binding in closure. For example, doing things when List is selected. However, you will find that @Binding doesn't work in parameters in closure.
import SwiftUI
struct AnotherSwiftUIView: View {
@State var bools:[Bool] = Array(repeating: true, count: 10)
var body: some View {
List(bools, id: \.self) { isEditing in
ContentView(isEditing: $isEditing) // Cannot find '$isEditing' in scope
}
}
}
That is because the parameter in closure cannot be used as @Binding.
New Trick
import SwiftUI
struct AnotherSwiftUIView: View {
@State var bools:[Bool] = Array(repeating: true, count: 10)
var body: some View {
List(bools.indices, id: \.self) { idx in
ContentView(isEditing: $bools[idx])
}
}
}
We use Array.indices and idx to get the element from the struct property, this works with @Binding.
App Crash Issue When Delete Data With Nested Bindings
@Binding is useful. But you also need to take good card of it. Some times, it leads crashes. For example,
import SwiftUI
struct ContentView: View {
static let deleteNum = Notification.Name("deleteNum")
@Binding var num:Int
var body: some View {
HStack {
Text("\(num)")
Spacer()
Button("Delete", action:delete)
}
}
private func delete() {
NotificationCenter.default.post(name: ContentView.deleteNum, object: self, userInfo: ["number":num])
}
}
import SwiftUI
struct AnotherSwiftUIView: View {
let pub = NotificationCenter.default.publisher(for: ContentView.deleteNum)
@State var array = [1,2,3,4,5]
var body: some View {
List(array.indices, id:\.self) { idx in
ContentView(num: $array[idx])
}.onReceive(pub, perform: { noti in
if let info = noti.userInfo,
let num = info["number"] as? Int,
let index = array.firstIndex(of: num) {
array.remove(at: index)
}
})
}
}
struct AnotherSwiftUIView_Previews: PreviewProvider {
static var previews: some View {
AnotherSwiftUIView()
}
}
The app will crash when you click the "Delete" button. The issue happens because the
array
is changed but thearray.indices
are not changed accordingly in the app.
We need to double check the index.
import SwiftUI
struct AnotherSwiftUIView: View {
let pub = NotificationCenter.default.publisher(for: ContentView.deleteNum)
@State var array = [1,2,3,4,5]
@State var indexSet = IndexSet()
var body: some View {
List(array.indices, id:\.self) { idx in
Safe($array, index: idx) { binding in
ContentView(num: binding)
}
}.onReceive(pub, perform: { noti in
if let info = noti.userInfo,
let num = info["number"] as? Int,
let index = array.firstIndex(of: num) {
array.remove(at: index)
}
})
}
}
struct AnotherSwiftUIView_Previews: PreviewProvider {
static var previews: some View {
AnotherSwiftUIView()
}
}
struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View {
typealias BoundElement = Binding<T.Element>
private let binding: BoundElement
private let content: (BoundElement) -> C
init(_ binding: Binding<T>, index: T.Index, @ViewBuilder content: @escaping (BoundElement) -> C) {
self.content = content
self.binding = .init(get: { binding.wrappedValue[index] },
set: { binding.wrappedValue[index] = $0 })
}
var body: some View {
content(binding)
}
}