我们经常需要把一些值从一个View传递给另一个子View,以便在子View中显示相关内容。
那么我们就会用到@Binding来进行传递。
@Binding介绍
@Binding
是SwiftUI的一个属性包装器,用于将一个值绑定到视图状态中。它通常用于在不同视图之间共享数据。
例如,假设您在一个ParentView
中定义了一个isToggled
的布尔值,并将其传递给一个名为ChildView
的子视图。您可以使用@Binding
将isToggled
值绑定到ChildView
的Toggle
视图中,如下所示:
struct ParentView: View {
@State private var isToggled = false
var body: some View {
ChildView(isToggled: $isToggled)
}
}
struct ChildView: View {
@Binding var isToggled: Bool
var body: some View {
Toggle(isOn: $isToggled) {
Text("Toggle")
}
}
}
在上面的示例中,我们使用@Binding
将isToggled
值绑定到ChildView
的Toggle
视图中。当用户在Toggle
中切换状态时,isToggled
的值将发生变化,并且视图将自动重新渲染以反映这种变化。由于isToggled
是被@Binding
包装的,所以当它的值发生变化时,它会更新ParentView
中的isToggled
值。
因此,@Binding
的主要功能是在不同视图之间共享数据,并确保它们之间的状态保持一致。当您在一个视图中修改@Binding
值时,它会自动更新与之绑定的其他视图中的值。
使用@Binding传递普通String/Int/Float等简单变量没有问题,通过值绑定到ChildView到,并且在ChildView变化时,会更新ParentView。
但是结构却不同!!
用@Binding传递结构的实例代码
struct testPlant: Identifiable {
let id = UUID()
var name: String
}
class testGreenhouseData: ObservableObject {
@Published var greenhouses: [[testPlant]] = []
init() {
let greenhouse1 = [testPlant(name: "Plant 1"), testPlant(name: "Plant 2")]
let greenhouse2 = [testPlant(name: "Plant 3"), testPlant(name: "Plant 4")]
greenhouses = [greenhouse1, greenhouse2]
}
}
struct testFullScreeView: View {
@StateObject var greenhouseData = testGreenhouseData()
@State var houseID = 0
@State var selectedPlantId = 0
@State var showPlantDetail = false
var body: some View {
NavigationView {
List {
ForEach(Array(greenhouseData.greenhouses.enumerated()), id: \.offset) { sectionIndex, section in
Section(header: Text("Greenhouse \(sectionIndex + 1)")) {
ForEach(section.indices) { plantIndex in
Button(action: {
self.houseID = sectionIndex
self.selectedPlantId = plantIndex
self.showPlantDetail = true
}) {
Text(section[plantIndex].name)
}
}
}
}
}
.navigationTitle("My Greenhouses")
.fullScreenCover(isPresented: $showPlantDetail) {
testMyPlantDetailsView(myPlant: $greenhouseData.greenhouses[self.houseID][self.selectedPlantId], showPlantDetail:$showPlantDetail)
.onDisappear {
houseID = 0
selectedPlantId = 0
}
}
}
}
}
struct testMyPlantDetailsView: View {
@Binding var myPlant: testPlant
@Binding var showPlantDetail: Bool
var body: some View {
VStack {
Text("Plant details")
Text("Name: \(myPlant.name)")
Button("Close") {
self.showPlantDetail = false
}
.padding()
}
.navigationBarTitle(Text("Plant Details"), displayMode: .inline)
}
}
这个代码是ParentView
通过
testMyPlantDetailsView(myPlant: $greenhouseData.greenhouses[self.houseID][self.selectedPlantId],showPlantDetail:$showPlantDetail)
传递一个testPlant结构给testMyPlantDetailsView,结果是每一次点击List任何一项链接,都是显示:Name: Plant 1
这是什么鬼!!!!我就想是不是:
Button(action: {
self.houseID = sectionIndex
self.selectedPlantId = plantIndex
self.showPlantDetail = true
})
赋值有问题?
我加了三个print,
Button(action: {
self.houseID = sectionIndex
print("\(self.houseID)")
self.selectedPlantId = plantIndex
print("\(self.houseID)")
self.showPlantDetail = true
print("\(self.houseID)")
})
再观察这个变量是否成功被赋值,结果是有赋值成功!
那我就再看看在调用
testMyPlantDetailsView(myPlant: $greenhouseData.greenhouses[self.houseID][self.selectedPlantId],showPlantDetail:$showPlantDetail)时,是不是有问题?
我加了一个判断:
if self.houseID == 1,self.selectedPlantId == 1 {
testMyPlantDetailsView(myPlant: $greenhouseData.greenhouses[self.houseID][self.selectedPlantId],showPlantDetail:$showPlantDetail)
}else
{
Text("fail!")
}
再直接点击第二行第二项,保证self.houseID = 1,self.selectedPlantId == 1,但实际怎么都进入不了testMyPlantDetailsView,而是每次显示Text("fail!")
我就换了一个思路,在testMyPlantDetailsView结构中增加两个结构
@Binding var houseID:Int
@Binding var selectedPlantId:Int
结果发现houseID,selectedPlantId都可以传递进入testMyPlantDetailsView,但myPlant就是不行(主要指第一次进入的值不管怎么点击任何列表项,都是Name: plant1)
直接问了chatGPT
它的回复是:
greenhouseData
是在testFullScreeView
中创建的,然后被传递给testMyPlantDetailsView
。在testFullScreeView
中,greenhouseData
的初始值是由testGreenhouseData
类的构造函数设置的:
init() {
let greenhouse1 = [testPlant(name: "Plant 1"), testPlant(name: "Plant 2")]
let greenhouse2 = [testPlant(name: "Plant 3"), testPlant(name: "Plant 4")]
greenhouses = [greenhouse1, greenhouse2]
}
在testFullScreeView
中,当您点击列表中的植物时,您将houseID
和selectedPlantId
分别设置为所选植物所在的温室和在子数组中的索引。然后,您将这些值传递给testMyPlantDetailsView
,并将greenhouseData.greenhouses[self.houseID][self.selectedPlantId]
绑定到myPlant
属性上。
在testMyPlantDetailsView
中,如果您使用myPlant.name
来获取植物的名称,它将始终返回绑定到myPlant
属性的初始testPlant
对象的名称(即“Plant 1”)。这是因为在testMyPlantDetailsView
中,greenhouseData
是一个新的testGreenhouseData
实例,它与在testFullScreeView
中创建的greenhouseData
不同。
但是,当您使用greenhouseData.greenhouses[houseID][selectedPlantId].name
获取植物名称时,它将返回所选温室和植物的名称,这是因为它直接从testFullScreeView
中传递的greenhouseData
实例中获取数据。
还没解释清楚,再问
greenhouseData是一个新的testGreenhouseData实例,它与在testFullScreeView中创建的greenhouseData不同。---这句话解释详细一起,具体原理是什么?
chatGPT回复:
当您在
testFullScreeView
中创建testGreenhouseData
的实例时,它将被分配给greenhouseData
属性,并在整个视图层次结构中传递。在
testFullScreeView
中使用@StateObject
修饰符来创建greenhouseData
属性,这意味着它是一个被观察的对象,并且它只会在第一次访问时创建。在之后的访问中,将返回同一实例。因此,在testFullScreeView
中创建的greenhouseData
实例将在整个视图层次结构中共享。当您在
testMyPlantDetailsView
中访问greenhouseData
属性时,由于您没有将greenhouseData
实例传递给它,系统会创建一个新的testGreenhouseData
实例。这是因为
@StateObject
修饰符会自动为您处理创建和管理观察对象的过程,但它只在当前视图中创建观察对象。因此,在testMyPlantDetailsView
中,您需要手动传递与在testFullScreeView
中创建的greenhouseData
相同的实例,以便能够访问相同的数据。简而言之,当您使用
@StateObject
修饰符创建观察对象时,该对象将在整个视图层次结构中共享。但是,如果您在不同的视图中使用@StateObject
来创建相同类型的对象,则会创建多个实例。如果您需要在多个视图中使用相同的观察对象,则应该手动将其传递给这些视图,以确保它们都使用相同的数据。因此,在
testMyPlantDetailsView
中手动创建一个新的testGreenhouseData
实例,而不是使用在testFullScreeView
中创建的实例,导致第一次访问greenhouseData.greenhouses[houseID][selectedPlantId].name
时出现问题。因为greenhouseData
是一个新的实例,它的值与在testFullScreeView
中创建的实例不同。
这里在childView(testMyPlantDetailsView)
中 因为没有直接传递greenhouseData,导致
创建一个新的testGreenhouseData
实例。因此第一次访问的myPlant.name,永远都是初始化的0值!
真相大白!
但是!!
我还是要问
但为什么新实例时,不是有同步[houseID][selectedPlantId]给它吗?为什么还是初始值呢
chatGPT回复
如果在
testMyPlantDetailsView
中,您手动创建了一个新的testGreenhouseData
实例,而没有将之前在testFullScreeView
中创建的testGreenhouseData
实例传递给它,那么这两个实例是不同的对象。虽然它们都是testGreenhouseData
类的实例,但它们在内存中位于不同的位置,并且它们的属性值也不同。因此,在第一次访问
greenhouseData.greenhouses[houseID][selectedPlantId].name
时,由于您创建了一个新的testGreenhouseData
实例,它将返回在构造函数中指定的初始值,即“Plant 1”,而不是之前在testFullScreeView
中创建的值。如果您想要在
testMyPlantDetailsView
中使用之前在testFullScreeView
中创建的testGreenhouseData
实例,您需要将它作为参数传递给testMyPlantDetailsView
,或者使用@EnvironmentObject
修饰符将其绑定到环境变量中。这样,您就可以在testMyPlantDetailsView
中访问相同的对象,而不是创建新的实例。
那么我们就可以使用chatGPT的建议修改代码:
在testFullScreeView
中:
struct testFullScreeView: View {
@StateObject var greenhouseData = testGreenhouseData()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: testMyPlantDetailsView()) {
Text("Go to My Plant Details")
}
}
}
.environmentObject(greenhouseData) // 将greenhouseData绑定到环境变量中
}
}
在testMyPlantDetailsView
中:
struct testMyPlantDetailsView: View {
@EnvironmentObject var greenhouseData: testGreenhouseData // 从环境变量中获取greenhouseData
var body: some View {
VStack {
Text(greenhouseData.greenhouses[houseID][selectedPlantId].name) // 访问相同的greenhouseData对象
}
}
}
使用@EnvironmentObject可以实现,为什么不能使用@StateObject实现 ?
再问
在testMyPlantDetailsView中不用@EnvironmentObject,而使用@StateObject var greenhouseData = testGreenhouseData()也可以吗?
chatGPT 回复
使用
@StateObject var greenhouseData = testGreenhouseData()
在testMyPlantDetailsView
中也可以工作,但这样会创建一个新的testGreenhouseData
实例,而不是使用之前在testFullScreeView
中创建的实例。如果您想在整个应用程序中共享和使用相同的
testGreenhouseData
实例,那么最好的方法是使用@EnvironmentObject
将其绑定到环境变量中,并在需要访问它的视图中使用@EnvironmentObject
来获取它。这样可以确保在整个应用程序中使用相同的实例,并且当testGreenhouseData
实例的状态发生更改时,所有使用该实例的视图都会自动更新。如果您只想在
testMyPlantDetailsView
中使用testGreenhouseData
实例,并且不需要在其他视图中共享它,那么使用@StateObject
也可以工作。但是请注意,这将导致在testMyPlantDetailsView
中创建一个新的testGreenhouseData
实例,而不是使用之前在testFullScreeView
中创建的实例。这可能会导致应用程序中出现多个不同的testGreenhouseData
实例,从而导致状态混乱或错误。因此,如果您需要在整个应用程序中共享和使用相同的testGreenhouseData
实例,最好使用@EnvironmentObject
将其绑定到环境变量中。如果您只需要在局部视图中使用testGreenhouseData
实例,并且不需要在全局范围共享它,那么使用@StateObject
可能是更好的选择。
至此我们就应该了解解决多层结构传递的方法:
1、@Binding传递结构需要把父结构一起传递。而不是通过子结构或结构成员传递
2、或者使用@EnvironmentObject读取父结构,再通过@Binding 传递结构成员索引(简单变量,Int/String), 读取父结构的子成员了。
注意:在ParentView中用.environmentObject(StructData) // 将StructData绑定到环境变量中,再在childView
原理解释:
1、当您将一个String
或Int
类型的值传递给一个@Binding
成员时,它不会被实例化,因为这些类型都是值类型,而不是引用类型。值类型在传递时会被复制,而不是通过引用传递。
例如,当您将一个String
类型的值传递给一个@Binding
成员时,它会被复制到视图状态中,并且当该值发生变化时,视图状态也会相应地更新。在这个过程中,SwiftUI并没有创建一个新的对象,而是将原始值的副本传递给了@Binding
成员。
2、当您将一个自定义类型的对象,例如testPlant
对象,传递给一个@Binding
成员时,它可能会被实例化,因为自定义类型是引用类型,而不是值类型。当您将一个自定义对象传递给@Binding
成员时,SwiftUI会尝试在视图状态中创建一个新的对象,以反映您的修改。如果您在使用@Binding
成员时没有正确地处理对象的状态,那么可能会出现新实例化的情况。
3、@Binding
是SwiftUI的一个属性包装器,用于将一个值绑定到视图状态中。它通常用于在不同视图之间共享数据。
4、@State
是SwiftUI的一个属性包装器,用于将一个值绑定到视图状态中。它通常用于在单个视图中管理数据。
5、@ObservedObject
是SwiftUI的一个属性包装器,用于将一个对象绑定到视图状态中。它通常用于在多个视图之间共享数据。
与@Binding
和@State
不同,@ObservedObject
的作用范围可以跨越多个视图,因此它通常用于在多个视图之间共享数据和状态。同时,被@ObservedObject
包装的对象必须遵循ObservableObject
协议,并且其中至少有一个带有@Published
属性包装器的属性。这样才能确保当对象的属性发生变化时,视图能够自动重新渲染以反映这种变化。
@StateObject
通常用于以下情况:
- 在视图中创建一个对象,并将其绑定到视图的状态中。
- 在需要访问该对象的任何子视图中使用
@EnvironmentObject
属性包装器来获取该对象。
6、@EnvironmentObject
是SwiftUI的一个属性包装器,用于将一个对象绑定到环境变量中。这意味着该对象可以在整个应用程序中被访问和共享,而不需要手动将它传递给每个视图。当该对象的状态发生变化时,所有使用它的视图都会自动更新。
@EnvironmentObject
通常用于以下情况:
- 在应用程序的顶层视图中创建一个全局对象,并将其绑定到环境变量中。
- 在需要访问该对象的任何视图中使用
@EnvironmentObject
属性包装器来获取该对象。