Ospina-Gonzalez

17 Feb, 2020

SwiftUI has no good way to present modals on iPad (question mark?)

SwiftUI has no good way to present modals on iPad (question mark?)

NOTE: For an update on this, read my new blog post http://piterwilson.com/blog/there-really-is-no-good-way-to-present-fullscreen-modals-in-swiftui

I'm looking to be proven wrong but I don't think I am. It would seem there’s really no good way to show a full screen modal on iPad using SwiftUI.

Even though I understand the use-case to be somewhat narrow, it is not completely unheard of. It's very common in a game for example.

Current state of affairs

At the time of writing, most resources online will do a variation of the following code as the way to present a modal using SwiftUI
.

BaseView.swift


struct BaseView: View {
    @State private var showModal = false
    var body: some View {
        VStack {
            Button(action: {
                self.showModal = true
            }, label: {
                Text("Open Modal")
                    .padding()
                    .overlay(
                        RoundedRectangle(cornerRadius: 5)
                            .stroke(Color.blue, lineWidth: 1)
                )
            })
        }.sheet(isPresented: $showModal){
            ModalView(closeAction: { self.showModal = false })
        }
    }
}

ModalView.swift


struct ModalView: View {
    var closeAction: (() -> Void) = {}
    var body: some View {
        ZStack {
            Color.blue.edgesIgnoringSafeArea(.all)
            VStack {
                Text("I am a modal.")
                    .font(.largeTitle)
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                    .padding()
                Button(action: {
                    self.closeAction()
                }, label: {
                    Text("OK, BYE!")
                        .foregroundColor(.white)
                        .padding()
                        .overlay(
                            RoundedRectangle(cornerRadius: 5)
                                .stroke(Color.white, lineWidth: 1)
                    )
                })
            }
        }
    }
}

Full example project here. https://github.com/piterwilson/SwiftUI-Modal-on-iPad/tree/master/IpadModalSwiftUISheet

This boils down to using the sheet(isPresented:) View modifier.

As the name implies, this is not a modal presentation. It kinda looks like a modal presentation on iPhone which is why this is the common approach. On iPad though, it looks much different.

Sheet presentation on iPad

This muddy state of affairs is somewhat expected since we learned that on iOS13 “sheet” presentation became the new default. However, on UIKit it is possible to use the modalPresentationStyle property to override the behavior in favor of .fullScreen when the situation calls for it. No such luck using SwiftUI.

If you google this problem hard enough you might come across some references to a modal View modifier but this is nowhere to be found in the official documentation. The references are obscure and date back to June 2019, this is right after SwiftUI was released and the API went trough A LOT of changes in that period.

So with this in mind, I went on a search of alternative methods to create this effect on the iPad.

UIKit + SwiftUI Hybrid approach

One way to achieve a full screen modal presentation on iPad is to wrap each View instance in a UIHostController and then use ObservableObject for the communication between SwiftUI and UIKit.

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

Full example here https://github.com/piterwilson/SwiftUI-Modal-on-iPad/tree/master/iPadModalHybrid

The code is so verbose that it really doesn’t fit the blog article format. Does it work? Yes perfectly, but it requires a LOT of boilerplate code. You will need to create one UIHostingController for each View you want to present and you will need at least one (but you will probably want two) ObservableObject to act as delegate and mediate the communication. This means you have to involve Combine into the mix!

Definitely not ideal.

PROS

  • Fulscreen modal presentation on iPad.
  • Ability to use UIKit presentation APIs. Storyboards, Nibs, Custom transitions etc. basically it works the same way you are already used to in UIKit because it IS UIKit.

CONS

  • Too much Boilerplate code.
  • Need to import Combine

SwiftUI conditional View

Another way to achieve a full screen modal presentation on iPad is to simply hide/show the "modal" view on top of the "base" view and use a conditional to control the visibility of the "modal".

It looks something like this:

BaseView.swift


struct BaseView: View {
    @State private var showModal = false
    var body: some View {
        ZStack {
            VStack {
                Button(action: {
                    self.showModal = true
                }, label: {
                    Text("Open Modal")
                        .padding()
                        .overlay(
                            RoundedRectangle(cornerRadius: 5)
                                .stroke(Color.blue, lineWidth: 1)
                    )
                })
            }
            if showModal {
                ModalView(closeAction: { self.showModal = false })
            }
        }
    }
}

PROS

  • Fulscreen modal presentation on iPad. Provided that your modal view has a layout that stretches to be full screen.
  • Full SwiftUI solution

CONS

  • Not really a modal presentation. The "base" view and all its view hierarchy are still present under the "modal". This can be a pain for example if you want to support accessibility.
  • You will have to add your own animated transitions.

So, in conclusion, what do we do here?

I imagine the 3rd option called here “SwiftUI conditional View” with all of its pitfalls is the better option and probably the one we are meant to use. It has the least amount of boilerplate to achieve the desired effect.

It does bug me how "unofficial" it feels. Like modals are no longer first-class citizens. It lends itself to lots of problems. I’m specially bothered by how supporting accessibility becomes a bit muddy. If the “base” view is still there, the accessibility tools will still how all the labels for those elements which are now under the “modal”. You will have to add some extra logic to hide those. It seems like there should be a better way.

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