当多个视图中有属性需要共享时,可能会出现以下代码示例的问题。这时就需要 @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
(不再需要 @State
):@Binding var counter: Int
@State
关键词:@State private var counterBlue = 0
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 var isPresented: Bool
// 绑定属性需要传入时,其类型为 Binding<>
init (project: Project, isPresented: Binding<Bool>) {
// 绑定属性的初始化前面要加下划线
_isPresented = isPresented
}
@Binding
主要是标记 A 视图上的某些状态,由另一个 B 视图拥有,并且 B 视图对底层数据具有读写访问权限@Binding
<aside>
💡 例如,有一个 @State
属性,用于存储布尔值 、字符串数组等,并且您想要传递它们;但它不使用 @Observable
宏,因此我们不能使用 @Bindable
。相反,我们使用 @Binding
,这让我们可以在多个地方共享该值。
</aside>
<aside>
💡 当您想要创建自定义用户界面组件时,@Binding
变得极其重要。从本质上讲,UI 组件只是 SwiftUI 视图,就像其他所有组件一样,但 @Binding
是它们的独特之处。它们可能具有本地的 @State
属性,但也可能具备公开的 @Binding
属性。
</aside>
当子视图和主视图属于分别的两个文件时:
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 ) ,人为补了一个固定的数值。
Binding<Int>
,加上.constant()
的目的就是将一个普通值(例如整数或字符串)转换为Binding
类型的值。Binding
用于建立对状态(State)的双向绑定,使视图能够读取和修改状态的值。当你在预览中使用.constant()
时,实际上是在创建一个与某个特定值相关联的虚拟绑定。如果你的子视图需要接受一个@Binding
参数,但在预览中你不想与具体的状态绑定,而只是想提供一个静态的值,那么就可以使用来创建一个虚拟绑定,将静态值传递给子视图。.constant()
实际上是一个视图修饰符,用于创建绑定,但不需要实际的状态。我们可以很好地绑定本地 @State
属性,因为 @State
会自动创建双向绑定,我们通过 $
语法即可进行绑定,如 $name
即使这些属性是属于某个类 class 里的,也可以使用 @State
标记,只是需要在类名前加上 @Observable
关键词即可
但当需要跨文件中传递【类 class】数据时,如果没有在 B 视图中使用 @State
创建该类的实例,只是从 A 视图接收该类的实例。则意味着在 B 视图中,无法使用双向绑定,这是一个问题:
这时就要用到 @Bindable
包装器。由于 class 使用了 @Observable
宏,这意味着 SwiftUI 能够监视此数据的更改。而 @Bindable
属性包装器所做的,就是为我们创建缺少的绑定。它生成能与 @Observable
宏一起使用的双向绑定,而无需使用 @State
创建本地数据。
当需要为添加了 @Observable
关键词的类,在B页面创建双向绑定时,请使用 @Bindable
。具体做法如下:
@State
创建该类的实例,因此该视图可实现绑定;A 视图只要加 @State
,不需要加 @Bindable
@Bindable
关键词// 类必须是加上 @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
类拥有的属性创建绑定。@Bindable
仅限于遵循 Observable
协议的类。Observable
对象的最简单方法是使用 @Observable
宏。这两个属性包装器共同存在,以实现强大的数据共享行为。<aside> 💡 在 iOS 17 中,使用 @Observable 宏来创建 Observable 对象,但它不像 ObservableObject 那样能以相同的方式创建绑定。相反,我们可以使用 @Bindable 属性包装器,标记持有 Observable 类实例的属性,从而支持代码创建到类实例属性的绑定。
</aside>