all micro contact rss

SwiftUI and PopToRootController Workaround

I’m thoroughly enjoying using SwiftUI. But like any new API, there are limitations that can at times be maddening.

One issue I ran into while developing the UI for logging custom and favorite items in my RECaf watch app was the lack of any way in SwiftUI to pop back to the root of the navigation stack.

Here’s my scenario: You start with a list, showing items for Custom Entry and Favorites. Tap Favorites, and you are pushed to a list of your favorites. Tap one of the favorites, and you are presented with a confirmation screen, detailing what you are about to log. At the bottom are two buttons. One for completing the log, and the other for canceling.

Because SwiftUI has no function for getting back to the root, I had no way to pop the customer all the way back to the initial list after they canceled or logged. I was stuck hoping that Apple ends up adding this function sooner rather than later. I couldn’t ship this, knowing the customer would have to swipe right several times to get back to the home screen.

But then I remembered, my root SwiftUI view was inside a WKHostingController.

WKHostingController is just a WKInterfaceController that expects a SwiftUI body view. It still retains all the methods of WKInterfaceController, including awakeFromContext.

So what I did was set the hosting controller to listen for a notification indicating a reload was needed. Then, on my Cancel button and on my log confirmation button, I could post that same notification.

And sure enough, the navigation stack pops back to the root.

I’ve seen other workarounds that use more convoluted ways to get SwiftUI to go back one level in the hierarchy, or to dismiss a presented view. But for getting all the way down a long stack in one move, this is going to be my goto. At least until Apple gives us a proper way to pop to root.

Here’s the code for my hostingController:

import SwiftUI


final class NewEntryController: WKHostingController<NewEntry> {
    
    override func awake(withContext context: Any?) {
        NotificationCenter.default.addObserver(self, selector: #selector(reloadRoot(notification:)), name: Notification.Name(NotificationKeys.watchReloadNeeded), object: nil)
    }
    
    @objc func reloadRoot(notification: Notification) {
        self.popToRootController()
    }

    override var body: NewEntry {
        NewEntry()
    }

}