turbolinks/turbolinks-ios

`Turbolinks.visit(location.href, { action: "replace" })` from root view of nav stack has strange behaviour

henrik opened this issue · 8 comments

We have a tab bar controller where each tab has a stack of navigation controllers.

When we did Turbolinks.visit(location.href, { action: "replace" }) on the root view of a navigation controller (the first page of the nav stack), the page was replaced with no animation (like a "replace") but there was a "< Back" type button on the top left (like we had done "advance").

When we showed the same page as a non-root-view (that is, we linked to it from a root view), then the same visit command did the right thing.

This is my theory: turbolinks-ios interprets "replace" as "dismiss the current controller, then push another without animation". But when the current controller is the root controller, it can't be dismissed, so we get this strange behaviour.

We worked around the issue by instead posting a message from the web to the app with JS, and had the app do session.reload().

Hi @henrik

Are you using the same Session Delegate for each tabs navigation stack or do they have their own individual ones?

Hi @gearoidoceallaigh! Each tab has its own individual Session and Session delegate.

The Turbolinks framework doesn't handle navigation at all, so the replace should be happening somewhere in your app's code where you handle the session:didProposeVisitToURL. What code are you using at that point to do the replace?

Aah, thank you. Yes, we used the code from the demo app, which does a pop and push. popViewController(animated:) is documented as doing nothing if we're at root, so it seems our theory was correct. We're currently experimenting with replacing the entire nav stack instead if we're at root.

We require something similar in one of our apps and have implemented the following to basically "reset" the navigation stack. It feels a little hacky but works well (production tested).

func popToRootAndVisit(_ visitable: NuaViewController) {
        var topViewController = self.topUINavigationController()
        
        topViewController?.purgeViewControllers(except: visitable)
        topViewController?.pushViewController(visitable, animated: false)
        
        SessionHandler.sharedInstance.session.visit(visitable)
    }
extension UINavigationController {
   
    func purgeViewControllers(except: UIViewController) {
        for viewController in (self.viewControllers) {
            if viewController != except {
                self.viewControllers.remove(at: (self.viewControllers.index(of: viewController))!)
            }
        }
    }    
}

For reference, our experiment is based on an isAtRoot property:

   var isAtRoot: Bool {
       get {
           return viewControllers.count == 1
       }
   }

And then code like:

         case .Replace:
            if isAtRoot {
                // Show the old page's title until the new web page loads and sets its title. Otherwise we get a flash of empty title.
                // TODO: We should probably assign the entire navigationItem.           
                pageViewController.navigationItem.title = topViewController?.navigationItem.title

                setViewControllers([ pageViewController ], animated: false)
            } else {
                popViewController(animated: false)
                pushViewController(pageViewController, animated: false)
            }

Still working on it, so not sure where we'll end up.

Your way of purging the VC stack is much nicer - I think I'll steal this :)

I will close this issue since it's not a problem with turbolinks-ios as such. Would perhaps be nice if it was documented or implemented in a more robust way in the demo app (assuming others also use it as a reference), but I'm not in a position to PR that right now myself. If anyone else wants to, feel free :)