Ospina-Gonzalez

27 Feb, 2020

SwiftUI's lack of customization possibilities for navigation

SwiftUI's lack of customization possibilities for navigation

At a fundamental level SwiftUI it provides very little customization possibilities for any kind of navigation action. Whether you want to present a new screen as part of your normal navigation flow or as a modal of some kind separate from your main context.

This is a conclusion reached after trying to solve the very simple problem of presenting a full-screen modal. By a full-screen modal I mean the equivalent of the following (very normal) presentation style we are used to in UIKit:

I do understand that SwiftUI is supposed to represent a paradigm shift from the imperative to the declarative. In the specific case of a navigation action, we tell the framework that we want go from one View representing a node in our App's flow to another but we are letting it decide HOW that happens.

I also understand that SwitfUI code is supposed to be platform agnostic and portable between iOS, MacOS, tvOS and WatchOS and this is perhaps why a full-screen modal presentation or a specific animated transition is not something that always applies.

Except this is one case where the HOW of how the navigation occurs is a subtlety that can carry a lot of meaning in the context of our Apps.

Beautiful, subtle animation throughout iOS builds a visual sense of connection between people and content onscreen. When used appropriately, animation can convey status, provide feedback, enhance the sense of direct manipulation, and help users visualize the results of their actions.

Quote from Animations, Human Interface Guidelines

This is why I do hope SwiftUI comes to a middle ground where we can influence or determine how navigation action presentation and transition is actually done even if limited to specific contexts, for example: If a new node in our flow is presented full-screen or not and what animation to use, if that is something that applies to a specific device.


Our specific example of a full-screen modal presentation (with customizable animation) remains unsolved using only SwiftUI. That being said, there are a number of workarounds, each one with their own problems that can be used if you want to achieve this today.

Without further ado I would like to present the alternatives that need to be employed to achieve a full-screen modal presentation at the time of writing using SwiftUI:


How to present full-screen modal in SwiftUI.

SwiftUI + UIKIt Hybrid

By far the best solution and it's a pity that at its core it means totally bypassing SwiftUI and reaching out to UIKit to solve the problem for us.

This solution was created by stackoverflow user Mojtaba Hosseini in this question and all credit goes to him.

One way to achieve a full-screen modal presentation is to create an EnvironmentKey that holds the value of UIApplication.shared.windows.first?.rootViewController and then access this from SwiftUI to call UIKit methods such as present(_,animated:completion:) or dismiss() on that UIViewController.

A small extension on UIViewController is needed to create a way for the UIViewController to present an arbitrary SwiftUI View.

By using this approach, one can leverage the existing presentation API's from UIKit while still having View instances made with SwiftUI.

This solution is good and it works like a charm for devices that support the UIKit framework but will not work at all for WatchKit or AppKit and this is the risk we run by bypassing SwiftUI.

PROS

  • Fulscreen modal presentation on iPad.
  • Ability to use UIKit presentation APIs.

CONS

  • Not very intuitive
  • Not full SwiftUI
  • Specific to UIKit meaning that porting to MacOS or WatchOS requires extra work.

SwiftUI Conditional View

I had already written about this approach on a previous article and its worth revisiting. To recap, this approach involves using a simple if statement to show or not our "modal" on top of our "base" View and it looks something like this:

This approach looks the part, but it has a few problems.

The biggest issue I have with it is that we are only cosmetically transitioning from one View to another. SwiftUI does not conceptually know that this is a totally new "Screen" or "node" in a navigation flow and as such it not officially a navigation action.

As a consequence, unless you manually hide the source/parent Screen all its view hierarchy will still be present creating optimization and accessibility issues.

Base screen elements still present

Accessibility Inspector let's us see that the entire view hierarchy from the base view is still present under the "modal"

We can solve the issue by presenting the "modal" instead of, rather than on top of the base view. We can also add a bit of animation while we are at it.

Solved. Right? Well... not really. This solution doesn't scale very well. Imagine having to write this piece of if statement for every View you want to present.

Lucky with SwitfUI we can use a few tricks to create a module out of this using a ViewModifier.

Which we can then apply like this, for a much more scalable solution:

Problem solved right? well ... no. This works really well until our base view is not the root view on screen. For example when it's presented inside a NavigationView

Oops

And this is why this solution only gets you so far. SwiftUI doesn't know that this is supposed to be a new screen or node in our app and it doesn't treat it as such. Because well, it really isn't.

I have anyway worked out this ViewModifier in this Git repository: https://github.com/piterwilson/SwiftUI-FullscreenModalViewModifier

Use it if it helps.

PROS

  • Fulscreen modal presentation on iPad.
  • Full SwiftUI solution

CONS

  • Not really a modal presentation(?)
  • You will have to add your own animated transition.
  • In general it ceases to be a full-screen modal when the modifier is added to a View that is not the root View in a given layout.

SwiftUI NavigationView + NavigationLink

It is possible to achieve the effect of a fullscreen modal by creating a NavigationLink inside a NavigationView on the iPad provided that:

  • .navigationViewStyle(StackNavigationViewStyle()) is applied to the NavigationView
  • The destination view uses .edgesIgnoringSafeArea(.all)
  • The destination view uses .navigationBarBackButtonHidden(true)

This will create a slide transition and it will be full-screen. At the time of writing it seems to be impossible to customize the transition of a NavigationView + NavigationLink presentation.

PROS

  • Full SwiftUI

CONS

  • Animation not customizable

Conclusion

To sum it up, when looked up close like this, it remain disappointing that transitions and presentations of different nodes in an App's navigation are so difficult to customize. This results in workarounds and cosmetic solutions which cause all kinds of problems.

SwiftUI is indeed a young framework and hopefully we will see some evolution in this respect.

Sources and Further reading

About

My name is Juan Carlos Ospina Gonzalez and I am an experienced iOS Developer with a strong background in Art and Design.

My education in Graphic Design and New Media combined with over 15 years of experience in Software Development gives me a good overview and insight in the creation of Digital products.

As a technical leader I can communicate effectively with stake holders and break down wishes and ideas into well-grounded feature definitions. I can gather technical requirements and provide well informed feedback during design iterations.

During development I enjoy planning architecture, setting up infrastructure for continuous integration and writing sane code supported by testing and code-review practices to ensure high quality software is delivered to the final phases of QA.

I enjoy working in a team of my peers where an atmosphere of constant research, collaboration and mutual mentoring ensure continuous personal and professional growth.

phone

small

medium

large