@Binding

Binding 的用途

当多个视图中有属性需要共享时,可能会出现以下代码示例的问题。这时就需要 @Binding 属性包装器。它可以让 A 视图与另一个 B 视图共享简单的 @State 属性,它们都指向相同的数据(Int、String…等)。例如,当抽出子视图后,有些属性存在于子视图中,而不在主视图 ContentView ;但子视图又是在主视图里实作的。那子视图的属性和 主界面之间的数据如何绑定呢?这时就要用到 @Binding

// 例如:以下主视图的 Text 元素不会随着按钮控制的布尔值,而发生变化
// 因为这里类似定义了一种单向数据流:ContentView 有一个布尔值 rememberMe,用于创建 PushButton(按钮具有由 ContentView 提供的初始值)。但一旦创建了按钮,按钮就会接管对该值的控制:它会在按钮内部将 isOn 属性在 true 或 false 之间切换,但不会将该更改传递回 ContentView 中的 rememberMe。于是现在有两个数据来源: ContentView 存储一个值, PushButton 存储另一个值。

//【主视图】
struct ContentView: View {
    @State private var rememberMe = false
    var body: some View {
        VStack {
            PushButton(title: "Remember Me", isOn: rememberMe)
            Text(rememberMe ? "On" : "Off")
        }
    }
}

//【子视图】
struct PushButton: View {
    let title: String
    @State var isOn: Bool
    var body: some View {
        Button(title) { 
	        isOn.toggle() 
	      }
        .shadow(radius: isOn ? 0 : 5)
    }
}

// 这时就需要 @Binding :它允许我们在 PushButton 和使用它的对象之间创建双向连接,这样当一个值发生变化时,另一个值也会变化。

Binding 的设置

  1. 在 B 视图希望绑定的属性前,添加关键词 @Binding(不再需要 @State):@Binding var counter: Int
  2. 在 A 视图声明一个同类型的属性,并添加 @State 关键词:@State private var counterBlue = 0
  3. 在 A 视图创建 B 视图的地方,初始化参数处,添加 $ 符号将两者进行绑定:CounterButton(counter: $counterBlue)

counterBlue 之前添加美元符号,表示传递的是“绑定”本身,而不是其中的布尔值。

// 主视图
struct ContentView: View {
    @State private var counterBlue = 0
    @State private var counterGreen = 0
    @State private var counterRed = 0
    var body: some View {
        VStack {
            Text("\\(counterBlue + counterGreen + counterRed)")
            HStack {
								//同个View类型的3个实例, 分别对应3个变量
                CounterButton(counter: $counterBlue, color: .blue)
                CounterButton(counter: $counterGreen, color: .green)
                CounterButton(counter: $counterRed, color: .red)
            }
        }
    }
}

// 子视图:按钮视图
struct CounterButton: View {
    @Binding var counter: Int
    var color: Color
    var body: some View {
        Button(action: {
            counter += 1
        }) {
            Circle()
                .overlay(
                    Text("\\(counter)")
                )
        }
    }
}

Binding 的初始化

@Binding var isPresented: Bool

// 绑定属性需要传入时,其类型为 Binding<>
init (project: Project, isPresented: Binding<Bool>) {
        // 绑定属性的初始化前面要加下划线
        _isPresented = isPresented
}

Binding 服务的是值类型

<aside> 💡 例如,有一个 @State 属性,用于存储布尔值 、字符串数组等,并且您想要传递它们;但它不使用 @Observable 宏,因此我们不能使用 @Bindable 。相反,我们使用 @Binding ,这让我们可以在多个地方共享该值。

</aside>

<aside> 💡 当您想要创建自定义用户界面组件时,@Binding 变得极其重要。从本质上讲,UI 组件只是 SwiftUI 视图,就像其他所有组件一样,但 @Binding 是它们的独特之处。它们可能具有本地的 @State 属性,但也可能具备公开的 @Binding 属性。

</aside>

Binding 视图的预览

当子视图和主视图属于分别的两个文件时:

struct ChildView: View {
    @Binding var value: Int
    var body: some View {
        Text("Value: \\(value)")
    }
}

//在以下代码中,ChildView是单独的一个文件,它后面是要跟主视图参数进行绑定的。
//这种情况在contentView中预览是没问题的,但是在ChildView文件里预览就会报错,因为参数缺失。

struct ChildView_Previews: PreviewProvider {
    static var previews: some View {
        ChildView(value:.constant(42))
    }
}

解决办法是在预览代码中,给实例化的对象补上参数,例如上面的value: .constant( 4 ) ,人为补了一个固定的数值。


@Bindable

Bindable 的用途

Bindable 的设置

当需要为添加了 @Observable 关键词的类,在B页面创建双向绑定时,请使用 @Bindable 。具体做法如下:

  1. 在 A 视图中使用 @State 创建该类的实例,因此该视图可实现绑定;A 视图只要加 @State ,不需要加 @Bindable
  2. 在 B 视图里如果希望对该类的属性使用双向绑定,则先声明相同的参数,然后在其前面添加 @Bindable 关键词
  3. 完成后即可在此页面创建【双向绑定】
// 类必须是加上 @Observable 的
@Observable
class Order : Codable {...}

// A 视图:实作 @observable 类的实例(因为是在这里实作的,要加 @State)
@State private var order = Order()

// B 视图:加上 Bindable 属性,等待传入,自己不实作该类的实例
// 加上后即可使用双向绑定
@Bindable var order: Order
......
TextField("Zip", text: $order.userAddress.zip)

Bindable 服务 @Observable 类

<aside> 💡 在 iOS 17 中,使用 @Observable 宏来创建 Observable 对象,但它不像 ObservableObject 那样能以相同的方式创建绑定。相反,我们可以使用 @Bindable 属性包装器,标记持有 Observable 类实例的属性,从而支持代码创建到类实例属性的绑定。

</aside>