(SwiftUI 变更检测)这段代码有啥问题?

Posted

技术标签:

【中文标题】(SwiftUI 变更检测)这段代码有啥问题?【英文标题】:(SwiftUI change detection) What is wrong with this piece of code?(SwiftUI 变更检测)这段代码有什么问题? 【发布时间】:2020-05-21 14:16:43 【问题描述】:

在调试我正在开发的应用程序的问题时,我设法将其缩小到这个最小的示例:

class RadioModel: ObservableObject 
    @Published var selected: Int = 0

struct RadioButton: View 
    let idx: Int
    @EnvironmentObject var radioModel: RadioModel
    var body: some View 
        Button(action: 
            self.radioModel.selected = self.idx
        , label: 
            if radioModel.selected == idx 
                Text("Button \(idx)").background(Color.yellow)
             else 
                Text("Button \(idx)")
            
        )
    

struct RadioListTest: View 
    @ObservedObject var radioModel = RadioModel()
    var body: some View 
        return VStack 
            Text("You selected: \(radioModel.selected)")
            RadioButton(idx: 0)
            RadioButton(idx: 1)
            RadioButton(idx: 2)
        .environmentObject(radioModel)
    

struct ContentView: View 
    @State var refreshDate = Date()
    func refresh() 
        print("Refreshing...")
        self.refreshDate = Date()
    
    var body: some View 
        VStack 
            Text("\(refreshDate)")
            HStack 
                Button(action: 
                    self.refresh()
                , label: 
                    Text("Refresh")
                )
                RadioListTest()
            
        
    

这段代码对我来说看起来很合理,尽管它存在一个特殊的错误:当我点击Refresh 按钮时,单选按钮停止工作。单选按钮不会刷新,并保留对旧 RadioModel 实例的引用,因此当我单击它们时,它们会更新它,而不是在 Refresh 之后创建的新按钮会导致构造新的 RadioListTest。我怀疑我使用EnvironmentObjects 的方式有问题,但我没有找到任何参考资料表明我所做的事情是错误的。我知道我可以通过各种强制刷新单选按钮的方式来解决这个特定问题,但我希望能够了解哪些情况需要强制刷新,我不能仅仅因为“更好安全胜于遗憾”,如果我每次修改都必须重新绘制所有内容,性能将是地狱。

编辑:澄清。在我看来很奇怪并且我想要解释的事情是:为什么在refresh 上重新创建了RadioListTest(连同一个新的RadioModel)并重新评估了它的body但是RadioButtons 被创建并且body 属性没有被评估,但是之前的body 被使用了。它们都只有一个视图模型作为状态,实际上是相同的视图模型,但一个是ObservedObject,另一个是EnvironmentObject。我怀疑这是对我正在做的EnvironmentObject 的滥用,但我找不到任何关于它为什么错误的参考

【问题讨论】:

您能再检查一下您的代码吗?它不可编译....错误:无法推断通用参数“内容” @Chris yeh 对不起,该参数必须被删除,它不再使用了 【参考方案1】:

这行得通:(是的,我知道,你知道如何解决它,但我认为这是“正确”的方式。

问题是这一行:

struct RadioListTest: View 
    @ObservedObject var radioModel = RadioModel(). <<< problem

因为每次刷新 RadioListTest 视图时都会新创建 radioModel,所以只需在上面创建一个视图实例,不会在每次刷新时都创建它(或者您是否希望每次都创建它?!)

class RadioModel: ObservableObject 
    @Published var selected: Int = 0

    init() 
        print("init radiomodel")
    

struct RadioButton<Content: View>: View 
    let idx: Int
    @EnvironmentObject var radioModel: RadioModel
    var body: some View 
        Button(action: 
            self.radioModel.selected = self.idx
        , label: 
            if radioModel.selected == idx 
                Text("Button \(idx)").background(Color.yellow)
             else 
                Text("Button \(idx)")
            
        )
    

struct RadioListTest: View 
    @EnvironmentObject var radioModel: RadioModel

    var body: some View 
        return VStack 
            Text("You selected: \(radioModel.selected)")
            RadioButton<Text>(idx: 0)
            RadioButton<Text>(idx: 1)
            RadioButton<Text>(idx: 2)
        .environmentObject(radioModel)
    

struct ContentView: View 
    @ObservedObject var radioModel = RadioModel()

    @State var refreshDate = Date()
    func refresh() 
        print("Refreshing...")
        self.refreshDate = Date()
    
    var body: some View 
        VStack 
            Text("\(refreshDate)")
            HStack 
                Button(action: 
                    self.refresh()
                , label: 
                    Text("Refresh")
                )
                RadioListTest().environmentObject(radioModel)

            
        
    

【讨论】:

是的,如果所有状态都是全局的并且是单例的,那么双实例没有问题。但这不适用于更大的应用程序。如果有一个带有导航的复杂应用程序并且可能有大量RadioLists,我无法在根视图中初始化所有内容然后将其传递下去,我需要能够封装子视图状态。 想象一下代表文件系统导航的例子,每个单选按钮都是一个可以选择导航到的目录(有点像 MacOS“打开文件”对话框)。 我已经通过您的第一条评论理解了这一点;)我正在考虑另一种解决方案......但这不再是“第一个问题”......这是第二个问题;)跨度> 要明确:每次父项更改时都可以重新创建模型(并重置其状态),这不是我正在调查的问题。我希望按钮的body 在我更改与之关联的EnvironmentObject 实例时被重新评估(当refreshDate 更改时会发生这种情况),但它不会 问题从来不是“我如何才能完成这项工作”,而是“我做错了什么”。【参考方案2】:

这段代码有什么问题?

您的RadioListTest 子视图未在refresh() 上更新,因为它不依赖于更改的参数(在本例中为refreshDate),因此 SwiftUI 渲染引擎假定它等于先前创建的并且不对其执行任何操作:

HStack 
    Button(action: 
        self.refresh()
    , label: 
        Text("Refresh")
    )
    RadioListTest()     // << here !!

所以解决方案是让这个视图以某种方式依赖于更改的参数,当然如果需要的话,这里是固定变量

RadioListTest().id(refreshDate)

【讨论】:

RadioListTest 会更新,并且每次刷新父视图时都会调用其body。这也是RadioModel的两个实例的原因。您可以在方法initbody 上使用断点来检查。但是它的子视图没有更新,这就是导致错误的原因。 不知何故不添加显式 id 使 SwiftUI 将更改前后的 RadioButtons 识别为相同 - 因此即使它重新创建它也不会重新评估它们的 body 属性它们(因为它会重新评估调用其构造函数的RadioListTest 视图body 属性)。所以你建议我在每次视图属性更改时使用id 强制重新评估所有子视图,否则 swiftui 可能会决定只对它们进行部分重新评估(并创建不一致的状态)? @pqnet,不刷新子视图的原因相同 - 平等。它是 SwiftUI 的核心概念 - 如果它等于先前构造的视图,则不要刷新视图(视图只是值,因此与常规结构进行比较)。无论如何,如何解决它取决于您。 但从技术上讲,它们并不相等。他们有不同的radioModel 参考。这是EnvironmentObject 中的错误吗? @pqnet,不 - 从技术上讲,环境对象不是视图的一部分。 ))

以上是关于(SwiftUI 变更检测)这段代码有啥问题?的主要内容,如果未能解决你的问题,请参考以下文章

一行代码解决CoreData托管对象属性变更在SwiftUI中无动画效果的问题

一行代码解决CoreData托管对象属性变更在SwiftUI中无动画效果的问题

为啥这段代码在运行时会崩溃?它只是选项卡式视图中 SwiftUI 中的选择器

SwiftUI 中免费的可扩展 List 有啥要求?

Swiftui 有没有办法改变按钮的可点击区域

这段代码有啥问题? [关闭]