Reputation: 410
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.
Upvotes: 2
Views: 505
Reputation: 30746
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.
Upvotes: 1