Update SwiftUI view with data received from WatchConnectivity

I'm using WatchConnectivity and the WCSessionDelegate to successfully receive user info from my iOS app to my watchOS extension. When new data is received, I want to update my SwiftUI view with the latest information. I have an @ObservedObject property in the HostingController which is set to the latest data when it arrives. However, this does not trigger a UI update. How can I fix this? I've posted the code below. Thank you!


import WatchKit
import SwiftUI
import WatchConnectivity
import Combine

class UserData: ObservableObject {
    @Published var account: Account = Account.getCurrentAccount()
}

class HostingController: WKHostingController<ContentView>, WCSessionDelegate  {
    @ObservedObject private var userData: UserData = UserData()
        
    override init() {
        super.init()
        
        if WCSession.isSupported() {
            print("WCSession supported")
            let session = WCSession.default
            session.delegate = self
            session.activate()
        }
    }
    
    override var body: ContentView {
        return ContentView(userData: userData)
    }
    
    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        
    }
    
    func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        guard let accountJSON = userInfo["main-account"] as? String else { return }
        
        let userDefaults = UserDefaults()
        userDefaults.set(accountJSON, forKey: "main-account")
        
        self.userData.account = Account.getCurrentAccount()

        print("\n\nReceived this: \(accountJSON) \n\n")
    }
}
Answered by Jim Dovey in 393348022

There are a couple of things you can do here.


First of all, you don't show what ContentView looks like. That's the view that needs to be updated, so that's what needs to use the @ObservedObject property wrapper. Note that WKHostingController is not a View, so SwiftUI isn't going to do anything automatically based on changes to its content. The ContentView, though, is a View, so if that contains an @ObservedObject property then everything ought to work.


The second option may be the better, though. Look at the interface definition for WKHostingController:


open class WKHostingController<Body> : WKInterfaceController where Body : View {

    /// The root `View` of the view hierarchy to display.
    open var body: Body { get }

    /// Invalidates the current `body` and triggers a body update during the
    /// next update cycle.
    public func setNeedsBodyUpdate()

    /// Update `body` immediately, if updates are pending.
    public func updateBodyIfNeeded()

    @objc override dynamic public init()
}


There's a method there called setNeedsBodyUpdate() which ought to do exactly what you need. Simply add a call to self.setNeedsBodyUpdate() inside your session(_:didReceiveUserInfo:) implementation, for instance at line 39 of your example, and you should see everything update properly.

Accepted Answer

There are a couple of things you can do here.


First of all, you don't show what ContentView looks like. That's the view that needs to be updated, so that's what needs to use the @ObservedObject property wrapper. Note that WKHostingController is not a View, so SwiftUI isn't going to do anything automatically based on changes to its content. The ContentView, though, is a View, so if that contains an @ObservedObject property then everything ought to work.


The second option may be the better, though. Look at the interface definition for WKHostingController:


open class WKHostingController<Body> : WKInterfaceController where Body : View {

    /// The root `View` of the view hierarchy to display.
    open var body: Body { get }

    /// Invalidates the current `body` and triggers a body update during the
    /// next update cycle.
    public func setNeedsBodyUpdate()

    /// Update `body` immediately, if updates are pending.
    public func updateBodyIfNeeded()

    @objc override dynamic public init()
}


There's a method there called setNeedsBodyUpdate() which ought to do exactly what you need. Simply add a call to self.setNeedsBodyUpdate() inside your session(_:didReceiveUserInfo:) implementation, for instance at line 39 of your example, and you should see everything update properly.

Thanks so much! setNeedsBodyUpdate() works.

@Jim Dovey, is there a way to call setNeedsBodyUpdate if you are using a single class as WCSessionDelegate for both iOS and watchOS? I have my session in a WatchSessionManager class, and use a environment object to update the view of my watch, but it wasn't updating. So, I tried to call HostingController().setNeedsBodyUpdate() and i got a EXCBADACCESS crash...code is below, is there a "right" was to do the updating from another class?

Code Block func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
DispatchQueue.main.async {
#if os(iOS)
let _ = WatchDataDistribution().messageReceivedByPhone(message: message)
#elseif os(watchOS)
let _ = WatchDataDistribution().messageReceivedByWatch(message: message)
HostingController().setNeedsBodyUpdate()
#endif
}
}


Update SwiftUI view with data received from WatchConnectivity
 
 
Q