SwiftUI @Binding传递结构数组或多层结构的子成员变量时,第一次值总是错误的原因分析,搞清@Binding@State@ObservedObject@EnvironmentObject

我们经常需要把一些值从一个View传递给另一个子View,以便在子View中显示相关内容。

那么我们就会用到@Binding来进行传递。

@Binding介绍

@Binding是SwiftUI的一个属性包装器,用于将一个值绑定到视图状态中。它通常用于在不同视图之间共享数据。

例如,假设您在一个ParentView中定义了一个isToggled的布尔值,并将其传递给一个名为ChildView的子视图。您可以使用@BindingisToggled值绑定到ChildViewToggle视图中,如下所示:

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")
        }
    }
}

在上面的示例中,我们使用@BindingisToggled值绑定到ChildViewToggle视图中。当用户在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中,当您点击列表中的植物时,您将houseIDselectedPlantId分别设置为所选植物所在的温室和在子数组中的索引。然后,您将这些值传递给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、当您将一个StringInt类型的值传递给一个@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属性包装器来获取该对象。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【课程特点】1、231节大容量课程:包含了SwiftUI的大部分知识点,详细讲解SwiftUI的方方面面;2、15个超级精彩的实例:包含美食、理财、健身、教育、电商务等各行业的App实例;3、创新的教学模式:手把手教您SwiftUI用户界面开发技术,一看就懂,一学就会;4、贴心的操作提示:让您的眼睛始终处于操作的焦点位置,不用再满屏找光标;5、语言简洁精练:瞄准问题的核心所在,减少对思维的干扰,并节省您宝贵的间;6、视频短小精悍:即方便于您的学习和记忆,也方便日后对功能的检索;7、齐全的学习资料:提供所有课程的源码,在Xcode 11 + iOS 13环境下测试通过; 更好的应用,更少的代码!SwiftUI是苹果主推的下一代用户界面搭建技术,具有声明式语法、实生成界面预览等特性,可以为苹果手机、苹果平板、苹果电脑、苹果电视、苹果手表五个平台搭建统一的用户界面。SwiftUI是一种创新、简单的iOS开发中的界面布局方案,可以通过Swift语言的强大功能,在所有的Apple平台上快速构建用户界面。 仅使用一组工具和API为任何Apple设备构建用户界面。SwiftUI具有易于阅读和自然编写的声明式Swift语法,可与新的Xcode设计工具无缝协作,使您的代码和设计**同步。自动支持动态类型、暗黑模式、本地化和可访问性,意味着您的**行SwiftUI代码已经是您编写过的非常强大的UI代码了。 

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值