@Bindable not triggering a call to updateViewController(_:context:) in UIViewControllerRepresentable

huangapple go评论72阅读模式
英文:

@Bindable not triggering a call to updateViewController(_:context:) in UIViewControllerRepresentable

问题

根据 Xcode 15 beta 4,下面的问题似乎已经得到修复。

我目前正在尝试使用 SwiftUI 状态观察的新 Observation 宏 beta 版本。我的数据模型是一个带有 @Observable 前缀的类:

import Observation
import SwiftUI
import UIKit

@Observable
class DataSource {
    var tapCount = 0

    init(tapCount: Int = 0) {
        self.tapCount = tapCount
    }
}

// 创建并嵌入 UIViewController 的包装器
struct VCR: UIViewControllerRepresentable {

    @Bindable var dataSource: DataSource
 
    func makeUIViewController(context: Context) -> VC {
        VC()
    }

    func updateUIViewController(_ uiViewController: VC, context: Context) {
        // 任何更新,我们想要发送到我们的 UIViewController,在这里执行
        print(#function)
        uiViewController.lbl.text = String(dataSource.tapCount)
    }
}

// SwiftUI 视图
struct ContentView: View {

    @State private var dataSource = DataSource()

    var body: some View {
        
        VStack {
          
            VCR(dataSource: dataSource)
                
            Text("Tap Count: \(dataSource.tapCount)")
            Button("Increment from SwiftUI") {
                dataSource.tapCount += 1
            }
        }
    }
}

拥有 DataSource 属性的我的 SwiftUI 视图声明如下:
@State dataSource = DataSource()

在符合 UIViewControllerRepresentable 的结构体中,我将对应的 Bindable 声明为 DataSource 属性如下:
@Bindable dataSource: DataSource

当 SwiftUI 视图使用符合 UIViewControllerRepresentable 的类型时,它初始化并传递 @State dataSource 属性,该属性由 SwiftUI 视图拥有和创建,并作为参数绑定到 @Bindable dataSource 属性。

问题是,当 SwiftUI 视图更新 tapCount 属性时,这不会触发 UIViewControllerRepresentable 中的 updateViewController(_:context:)

如果我在 UIViewControllerRepresentable 中存储一个 tapCount: Int 属性,并在 SwiftUI 视图中初始化 UIViewControllerRepresentable 时将 dataSource.tapCount 作为参数传入,那么当 dataSource.tapCount 更改时,将触发 updateViewController(_:context:)

但我不想传递属性,并在 UIViewControllerRepresentable 实例中存储它(而且再也不读取或写入它),只是为了使 Observation API 在 dataSource 的属性更新时触发更新方法。

它是否应该像这样工作,还是可能是一个 bug?我不确定,我已向 Apple 提交了反馈报告。它似乎不太可行以这种方式设置,或者说 Observation API 的功能方式是否正确。

我知道根据苹果关于新 Observation 宏 API 的文档,只有实际 读取 的属性才会引起状态更改。我的属性在拥有它的 SwiftUI 视图中被读取,并且通过 @Bindable 与之绑定,如上所述。

此外,如果我从 SwiftUI dataSource 属性中删除 @State 前缀(该属性拥有并创建 dataSource),并在 UIViewControllerRepresentable 中为 dataSource 属性添加 @State 前缀,那么一切都正常工作。但这似乎是对 Observation 宏 API 的滥用

使用较旧的(Combine)ObservableObject、@Published 和 @Observable 模式可以正常工作。但是根据苹果文档中的从 ObservableObject 协议迁移到 Observation 宏 API,这种迁移会破坏它。

对问题的根本原因有何想法?

Xcode 版本:15.0 beta (15A5160n),
iOS 版本:17.0,
Observable 宏,Beta

[编辑,2023-06-29,12:03]:我尝试了使用 UIViewRepresentable(不使用 @Bindable,因为它不需要),然而,问题仍然存在。在 Representable 中为属性添加 @State 前缀与我的预期行为非常契合。但正如我所指出的,我认为这是对 Observation 框架的滥用。

[编辑,2023-06-30,12:39]:而且有趣的是,如果使用这个解决方法(在 Representable 中对属性进行注释,并添加 @State dataSource: DataSource),如果将读取 tapCount 的 Text 包装在 SwiftUI 中(即读取数据的代码,因为只有读取的数据才会触发状态更改),那么即使是这个解决方法也不再起作用。因此,beta 版本存在太多问题,他们可能会在发布版中修复所有这些问题。

英文:

Edit, 2023-07-24
As per Xcode 15 beta 4, the issue below seems to have been fixed.

I am currently trying out the new Observation macro beta for SwiftUI state observation.
My data model is a class, prefixed with @Observable:

import Observation
import SwiftUI
import UIKit

@Observable
class DataSource {
    var tapCount = 0

    init(tapCount: Int = 0) {
        self.tapCount = tapCount
    }
}

// The wrapper that creates and embeds the UIViewController
struct VCR: UIViewControllerRepresentable {

    @Bindable var dataSource: DataSource
 
    func makeUIViewController(context: Context) -> VC {
        VC()
    }

    func updateUIViewController(_ uiViewController: VC, context: Context) {
        // Any updates, we want to send to our UIViewController, do them here
        print(#function)
        uiViewController.lbl.text = String(dataSource.tapCount)
    }
}

// The SwiftUI View
struct ContentView: View {

    @State private var dataSource = DataSource()

    var body: some View {
        
        VStack {
          
            VCR(dataSource: dataSource)
                
            Text("Tap Count: \(dataSource.tapCount)")
            Button("Increment from SwiftUI") {
                dataSource.tapCount += 1
            }
        }
    }
}

My SwiftUI View, which owns the DataSource property, declares it as such:
@State dataSource = DataSource()

In the struct conforming to UIViewControllerRepresentable, I declare the corresponding Bindable to the DataSource property as such:
@Bindable dataSource: DataSource

When the SwiftUI View will use the type that conforms to UIViewControllerRepresentable, it inits it and passes in the @State dataSource property, owned and created by the SwiftUI View, as an argument, to be bound to the @Bindable dataSource property.

The problem is, when the SwiftUI View updates the tapCount property, this will not trigger the updateViewController(_:context:) in UIViewControllerRepresentable.

If I store a property for tapCount: Int in the UIViewControllerRepresentable, and pass in dataSource.tapCount as an argument when I init the UIViewControllerRepresentable in the SwiftUI View, then that WILL trigger the updateViewController(_:context:) when dataSource.tapCount is changed.

But I don't want to pass in a property, and storing it in the UIViewControllerRepresentable instance(and never again read or write it) just so that the Observation API triggers the update method when the property in the dataSource is updated.

Is it supposed to work like that, or is it likely a bug? I am not sure, and I did file a feedback report to Apple. It just does not seem feasible to set it up like that, or the way the Observation API is supposed to function.

I am aware that only properties that are actually read will cause a state change, according to the Apple documentation on the new Observation macro API. My property is read in the SwiftUI View, which owns it, and it has a binding to it via @Bindable, as noted above.

What's more, if I remove the @State prefix from the SwiftUI dataSource property(which owns and creates the dataSource) and instead, prefix the dataSource property in the UIViewControllerRepresentable, with @State, then it all works fine. But that seems like an abuse of the Observation macro API.

Using the older(Combine) ObservableObject, @Published and @Observable pattern works as expected. But the migration to the Observation macro API, as per the Apple documentation, breaks that.

Any ideas on the root cause of the issue?

Xcode version: 15.0 beta (15A5160n),
iOS 17.0,
Observable macro, Beta

Many thanks

[Edit, 2023-06-29, 12:03]:
I tested it with UIViewRepresentable(without @Bindable, because it is not needed), however, the same problem persists. Prefixing the property in the Representable with @State, works great with my expected behaviour. But as noted I consider that an abuse of the Observation framework.

[Edit, 2023-06-30, 12:39]:
And here is the funky part, with the workaround in place(annotating the the property in the Representable with @State dataSource: DataSource), if you wrap the Text that reads the tapCount in SwiftUI(i.e. the piece of code that reads the data, because only read data will trigger a state change), in a GeometryReader, then even the workaround will not work anymore. So the beta is just too buggy, and they will likely fix all of this for the release.

答案1

得分: 1

Edit2: 我认为这个问题现在已经修复了,在 Xcode v15b4 中使用 let dataSource: DataSource,当 tapcount 改变时会调用 update。

Edit: @State var dataSource: DataSource 是一种临时解决方法,直到修复该问题,因为 @State 不应该是必需的,甚至在没有 init 的情况下使用它都是不正确的。

所以你不需要 @Bindable,因为在 representable 中,你不会创建任何 SwiftUI 视图来传递绑定。但是,我测试了即使只有 var dataSource: DataSourceupdateUIViewController,当 tapCount 被设置时也不会被调用,所以读取依赖关系没有被配置。我猜想他们可能还没有实现它,因为传递整个对象而不仅仅是需要的数据在这种情况下并不常见,但我确定他们最终会实现的。也许在 UIViewRepresentable 中测试一下,如果在那里可以工作,那么可能他们只是忘记了 UIViewControllerRepresentable

顺便说一下,如果你的视图使用丰富的模型对象,与简单类型相比,预览性会降低,所以 @Binding var tapCount: Int 无论如何都是更好的选项。

英文:

Edit2: I think this is fixed now, in Xcode v15b4 with let dataSource: DataSource, update is called when tapcount changes.

Edit: @State var dataSource: DataSource is a workaround until it is fixed because @State shouldn't be necessary and it isn't even correct to use it without an init.

So you don't need @Bindable because in the representable you won't be creating any SwiftUI Views to pass bindings to. However, I tested even with just var dataSource: DataSource and updateUIViewController isn't called when tapCount is set either so the read dependency is not being configured. I would guess they just haven't implemented it yet because it is not common to pass in a whole object rather than just the data needed but I'm sure they will eventually. Maybe check if it works in UIViewRepresentable, if it works there then it is possible they just forgot about UIViewControllerRepresentable.

Fyi your Views are less preview-able when they take rich model objects compared with simple types so @Binding var tapCount: Int would be the better option anyway.

huangapple
  • 本文由 发表于 2023年6月29日 00:34:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/76575122.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定