Reputation: 121
I have a SwiftUI View that presents and updates data originating in an @ObservedObject. The code sample below works as expected UNLESS the @ObservedObject happens to be a subclass of UIViewController. In this case, data updates are made to the @ObservedObject but they do not trigger a reload of the View content as expected.
Here is the SwiftUI View:
struct ContentView : View {
@ObservedObject var scene: SceneViewController
var body: some View {
VStack {
Text("The time is: \(scene.currentSimTimestamp.description(with: .current))")
Button(action: {self.scene.currentSimTimestamp = Date()},
label: {Text("Update")})
}
}
}
And here is the @ObservedObject:
class SceneViewController: UIViewController, ObservableObject {
@Published var currentSimTimestamp: Date = Date()
}
Pressing the "Update" button will cause the value stored in scene.currentSimTimestamp to update, HOWEVER ContentView will not reload (the screen won't update to reflect the data change).
Changing class SceneViewController: UIViewController, ObservableObject {
to class SceneViewController: ObservableObject {
will cause the update to display as expected.
It seems this may be a bug, as the Apple documentation and videos that I have seen seem to suggest that any Class can adopt ObservableObject, and indeed there is no compiler issue generated. But am I missing something?
(Added sample SceneDelegate code to reproduce the sample code into a project, below)
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let sceneController: SceneViewController = SceneViewController()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView(scene: sceneController)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
...
Upvotes: 0
Views: 4219
Reputation: 3438
2 more ways of getting notified:
The simplest is just invoking the publisher (AFAIK scene.currentSimTimestamp
does not have to be @Published
):
Button(action: {
self.scene.currentSimTimestamp = Date()
self.scene.objectWillChange.send()
},
label: {Text("Update")})
Slightly more involved but IMHO slightly cleaner the Combine way: https://stackoverflow.com/a/60126962/301790
Upvotes: 1
Reputation: 121
As a workaround, I have found that I can pull out the @Published property wrapper from the View Controller, and move it to a new (non UIViewController) ObservableObject class that exists just for the purpose of publishing this property. With this as the only change, it functions as expected. Obviously, the workaround is clunky, but it does allow data owned by an existing UIViewController to be utilized as expected within a SwiftUI View. Here's the update:
class NewClass: ObservableObject {
@Published var timestamp: Date = Date()
}
class SceneViewController: UIViewController {
var currentSimTimestamp: NewClass()
override func viewDidLoad() {
super.viewDidLoad()
// Shown here with our SwiftUI view as a child in our ViewController's hierarchy
let contentView = ContentView(newClass: currentSimTimestamp)
let contentViewController = UIHostingController(rootView: contentView)
addChild(contentViewController)
view.addSubview(contentViewController.view)
...
}
}
struct ContentView : View {
@ObservedObject var newClass: NewClass
var body: some View {
VStack {
Text("The time is: \(newClass.timestamp.description(with: .current))")
Button(action: {self.newClass.timestamp = Date()},
label: {Text("Update")})
}
}
}
Upvotes: 1