Presenters in MvvmCross: Controlling the Back Stack

In my last presenter post I showed how you can use presentation hints to tell the presenter to clear the view stack prior to showing the next view. That is probably the most common custom navigation pattern I've seen used in MvvmCross apps, but I also wanted to note another similar pattern I've used as well.

When I'm mapping out the navigation paths through my apps, I try to be very conscious of the state of the back stack at any given time. Often navigation through an app is pretty linear which makes this a non-issue, but it's also easy to come across scenarios that are more problematic. Using our apps as an example, once you have a basket in progress the app will show you a persistent shortcut in the top bar that will bring you back to it from anywhere in the app. Once you do so, you end up on a screen that lets you perform some different actions, one of which is the option to return to the menu to add more items. During the initial flow through the app this would simply mean going back one screen in the stack, but if the user just jumped here from somewhere else, that view doesn't exist on the stack.

To solve this we introduced the presentation concept of navigating "back or in place". This tells the presenter to start by checking the back stack for the existence of a view of the same type we're navigating to. If found, it rewinds the stack back to that view. If not found, it pops the current view, then creates and navigates to the destination view. This allows us to map out any paths we want through the app without needing to sorry about showing the other views first.

Navigating with this mode is handled the same way shown in the last post:

var presentationBundle = new MvxBundle(new Dictionary<string, string> { { "NavigationMode", "BackOrInPlace" } });

ShowViewModel<MyViewModelType>(presentationBundle: presentationBundle);  

iOS

Here's what the implementation on iOS might look like:

public override void Show(MvxViewModelRequest request)  
{
    if (request.PresentationValues != null)
    {
        if (request.PresentationValues.ContainsKey("NavigationMode") && request.PresentationValues["NavigationMode"] == "BackOrInPlace")
        {
            var nextViewController = (UIViewController)_viewCreator.Value.CreateView(request);
            var existingViewController = MasterNavigationController.ViewControllers.FirstOrDefault(vc => vc.GetType() == nextViewController.GetType() && vc != CurrentTopViewController);

            if (existingViewController != null)
            {
                MasterNavigationController.PopToViewController(existingViewController, true);
            }
            else
            {
                var transition = new CATransition
                {
                    Duration = 0.3,
                    Type = CAAnimation.TransitionPush,
                    Subtype = CAAnimation.TransitionFade
                };

                MasterNavigationController.PopViewControllerAnimated(false);
                MasterNavigationController.View.Layer.AddAnimation (transition, null);
                MasterNavigationController.PushViewController(nextViewController, false);
            }

            return;
        }
    }

    base.Show(request);
}

In cases where the target view isn't already in the stack, this will provide a little fade transition into the new view, rather than making it look like a normal navigation push. I find this to be a nice visual cue to the user on what happened, and avoid the expectation that the previous view is still there on the back stack.

Android

The implementation looks pretty similar on Android, except that again we're using fragments to give us finer control over the stack:

public override void Show(MvxViewModelRequest request)  
{
    if (vmRequest.PresentationValues != null)
    {
        if (request.PresentationValues.ContainsKey("NavigationMode") && request.PresentationValues["NavigationMode"] == "BackOrInPlace")
        {
            var hasFragmentTypeInStack = 
                Enumerable.Range(0, _fragmentManager.BackStackEntryCount - 1)
                          .Reverse()
                          .Any(index => _fragmentManager.GetBackStackEntryAt(index).Name == fragmentType.Name);

            if (hasFragmentTypeInStack)
            {
                while (CurrentFragment.GetType() != fragmentType)
                    _fragmentManager.PopBackStackImmediate();

                return;
            }

            _fragmentManager.PopBackStackImmediate();
        }
    }

    // ...
}

This is just one example of what you might want to do with the back stack in your apps. I encourage you to think about the state of the stack as you build up navigation flows through your apps, in order to provide the most sensible experience possible for users.

comments powered by Disqus
Navigation