SwiftUI missing modal presentation view modifier

Michał Ziobro
7 min readFeb 12, 2023

--

SwiftUI is a user interface framework provided by Apple that allows developers to build elegant and responsive apps for all Apple platforms with a simple and intuitive syntax. One of the most powerful features of SwiftUI is its ability to present views modally, which is an essential component for many iOS applications.

1. Native SwiftUI solutions

Firstly lets explore the available native apple modal presentation view modifiers in SwiftUI.

1.1 .sheet view modifier

The sheet modifier is used to present a view as a pop-up from the bottom of the screen. It provides a way to transition from one view to another by sliding the new view from the bottom of the screen. The sheet modifier is best used for modals that allow users to complete a task, such as setting preferences or making a selection.

Here’s an example of using the sheet modifier in SwiftUI:

struct ContentView: View {
@State private var showModal = false

var body: some View {
Button(action: {
self.showModal = true
}) {
Text("Show Modal")
}
.sheet(isPresented: $showModal) {
ModalView()
}
}
}

struct ModalView: View {
var body: some View {
Text("This is a modal")
}
}

1.2 .fullScreenCover view modifier

The fullScreenCover modifier is used to present a view as a full-screen modal. It provides a way to transition from one view to another by overlaying the new view on top of the current view. The fullScreenCover modifier is best used for modals that require the user's full attention, such as displaying detailed information or a sign-up form.

Here’s an example of using the fullScreenCover modifier in SwiftUI:

struct ContentView: View {
@State private var showModal = false

var body: some View {
Button(action: {
self.showModal = true
}) {
Text("Show Modal")
}
.fullScreenCover(isPresented: $showModal) {
ModalView()
}
}
}

struct ModalView: View {
var body: some View {
Text("This is a modal")
}
}

1.3 .alert view modifier

Alerts are modal presentations that display an important message or ask for a decision from the user. You can add up to two buttons to an alert and retrieve the result of the user’s selection.

To present an alert, you use the alert modifier on any view. You pass a closure that returns an Alert to the isPresented binding.

struct ContentView: View {
@State private var showAlert = false

var body: some View {
Button("Show Alert") {
self.showAlert.toggle()
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Important message"), message: Text("This is an alert"), dismissButton: .default(Text("OK")))
}
}
}

You can find more native view modifiers for presenting task-specific modals in iOS apps in Apple documentation https://developer.apple.com/documentation/swiftui/view-presentation

2. Custom solution .presentModal

As until now usage of .fullScreenCover and .sheet view modifiers in SwiftUI is a bit limited. We cannot customize them to present arbitrary modal views (or dialogs), to use custom presentation style or transition style. This was possible in UIKit with configuring modally presented view controler with UIModalPresentationStyle and UIModalTransitionStyle.

modally presented custom alert

3. UIKit background

In UIKit, modal presentations are used to present a view controller on top of the existing view hierarchy. Modal presentations are useful for tasks that require user attention, such as asking for input or confirming an action.

To present a view controller modally, you use the present(_:animated:completion:) method on a view controller. The presented view controller is referred to as the presentedViewController.

There are two properties that you can use to customize the appearance of a modal presentation: modalPresentationStyle and modalTransitionStyle.

modalPresentationStyle

modalPresentationStyle is an enumeration that defines the way the presented view controller is displayed on the screen. There are several values you can choose from, including:

  • fullScreen: The presented view controller covers the entire screen.
  • pageSheet: The presented view controller covers the screen and the navigation bar is translucent.
  • formSheet: The presented view controller is displayed as a form-sized modal view.
  • currentContext: The presented view controller inherits the presentation context of the presenting view controller.
  • overFullScreen: The presented view controller covers the entire screen and is displayed over the status bar.
  • overCurrentContext: The presented view controller is displayed over the current context.

Here’s an example of how you can present a view controller with a fullScreen presentation style:

let presentedVC = UIViewController()
presentedVC.modalPresentationStyle = .fullScreen
present(presentedVC, animated: true, completion: nil)

modalTransitionStyle

modalTransitionStyle is an enumeration that defines the animation that is used when the presented view controller is shown or dismissed. There are several values you can choose from, including:

  • coverVertical: The view controller slides up from the bottom.
  • flipHorizontal: The view controller flips over horizontally.
  • crossDissolve: The view controller dissolves into the background.
  • partialCurl: The view controller curls up to reveal the underlying view controller.

Here’s an example of how you can present a view controller with a crossDissolve transition style:

let presentedVC = UIViewController()
presentedVC.modalTransitionStyle = .crossDissolve
present(presentedVC, animated: true, completion: nil)

You can also combine both modalPresentationStyle and modalTransitionStyle to create a custom modal presentation. For example:

let presentedVC = UIViewController()
presentedVC.modalPresentationStyle = .fullScreen
presentedVC.modalTransitionStyle = .crossDissolve
present(presentedVC, animated: true, completion: nil)

In this example, the presented view controller is displayed as a full-screen view and animates into view with a cross-dissolve transition.

4. ModalPresenter library

ModalPresenter is a Swift package (also available as Pod) that makes it easier to present modal views in your SwiftUI iOS applications. With ModalPresenter, you can create and present modal views with just a few lines of code, and the library provides several customization options so you can create modal views that fit your needs.

Git Repo: https://github.com/michzio/ModalPresenter.

To use it in your project here are example code snippets:

struct ContentView: View {

@State private var showModel = false

var body: some View {
ZStack {
Button("Show modal") {
showModel = true
}
}
.presentModal(isPresented: $showModel) {
Text("Modal")
}
}
}
   struct ContentView: View {

@State private var presentedItem: Item?

var body: some View {
ZStack {
Button("Show modal") {
presentedItem = .init(id: "Modal text")
}
}
.presentModal(item: $presentedItem) { item in
VStack {
Text(item.id)

Button("Show new modal") {
presentedItem = .init(id: "New modal text")
}
}
}
}
}

5. ModalPresenter implementation

5.1 presentModal viewModifier

The presentModal view modifier is an extension on the View type, and it provides two variants: one with a closure for onDismiss, and one with a Binding for dismissAction.

public extension View {
func presentModal<Content>(
isPresented: Binding<Bool>,
configuration: ModalPresenterConfiguration = .init(),
onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping () -> Content
) -> some View where Content : View {
modifier(ModalPresenterViewModifier(isPresented: isPresented, configuration: configuration, onDismiss: onDismiss, content: content))
}

func presentModal<Content, T>(
isPresented: Binding<Bool>,
configuration: ModalPresenterConfiguration = .init(),
dismissAction: Binding<IdentifiableAction?>,
@ViewBuilder content: @escaping () -> Content
) -> some View where Content : View {
let onDismiss = {
guard let action = dismissAction.wrappedValue else { return }
action()
dismissAction.wrappedValue = nil
}
return presentModal(isPresented: isPresented, configuration: configuration, onDismiss: onDismiss, content: content)
}
}

presentModal view modifier takes the following parameters:

  • isPresented: A Binding to a Bool that indicates whether the modal view should be shown or dismissed.
  • configuration: A ModalPresenterConfiguration that allows you to customize the appearance and behavior of the modal view.
  • onDismiss: A closure that is called when the modal view is dismissed.
  • content: A closure that returns the content of the modal view.

5.2 ModalPresenterViewModifier implementation

The ModalPresenterViewModifier uses the ModalPresenter struct to wrap the content of the modal and apply the desired configuration. The modifier also includes a coordinator class that is used to keep a reference to the UIKit view controller that presents the modal. This coordinator is needed to ensure that the modal can be dismissed properly when the view disappears.

The ModalPresenterViewModifier uses the background modifier to apply the ModalPresenter to the content view. It also uses the onDisappear modifier to dismiss the modal when the view disappears. This is done by checking if the modal is presented and if so, dismissing it using the Presentation.dismiss function.

struct ModalPresenterViewModifier<ModalContent: View>: ViewModifier {

final class Coordinator: NSObject {
weak var uiViewController: UIViewController?
}

@State private var coordinator = Coordinator()
@Binding private var isPresented: Bool

private let configuration: ModalPresenterConfiguration
private let onDismiss: (() -> Void)?
private let modalContent: () -> ModalContent

init(
isPresented: Binding<Bool>,
configuration: ModalPresenterConfiguration = .init(),
onDismiss: (() -> Void)?,
content: @escaping () -> ModalContent
) {
...
}

func body(content: Content) -> some View {
content
.background(
ModalPresenter(isPresented: $isPresented, configuration: configuration, onDismiss: onDismiss, content: modalContent)
.introspectViewController { viewController in
coordinator.uiViewController = viewController
}
)
.onDisappear {
guard isPresented else { return }
isPresented = false

guard let uiViewController = coordinator.uiViewController else { return }
DispatchQueue.main.throttle(interval: 0.1, context: uiViewController) {
Presentation.dismiss(
from: uiViewController,
context: configuration.context,
animated: configuration.animated,
completion: { onDismiss?() }
)
}
}

}
}

5.3 ModalPresenter wrapper around UIKit implementation

ModalPresenter uses a UIViewControllerRepresentable to present the modal view and is customizable through the ModalPresenterConfiguration structure.

struct ModalPresenter<Content>: UIViewControllerRepresentable where Content: View {

@Binding private var isPresented: Bool
@State private var presented: UIViewController? = nil

private let configuration: ModalPresenterConfiguration

private let content: () -> Content
private let onDismiss: (() -> Void)?

init(
isPresented: Binding<Bool>,
configuration: ModalPresenterConfiguration = .init(),
onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping () -> Content
) {
self.configuration = configuration
self.onDismiss = onDismiss
self.content = content

_isPresented = isPresented
}

func makeUIViewController(context: UIViewControllerRepresentableContext<ModalPresenter>) -> UIViewController {
UIViewController()
}

func updateUIViewController(_ uiViewController: UIViewController, context _: UIViewControllerRepresentableContext<ModalPresenter>) {
if isPresented {
if let presented = presented {
(presented as? UIHostingController)?.rootView = content()
} else {
let hc = ModalHostingController(rootView: content())
hc.modalPresentationStyle = configuration.modalPresentationStyle
hc.modalTransitionStyle = configuration.modalTransitionStyle
hc.view.backgroundColor = .clear
hc.dismissHandler = {
self.isPresented = false
}

configuration.configurePresentedViewController(hc)

DispatchQueue.main.throttle(interval: 1.0, context: uiViewController) {
Presentation.present(
modal: hc,
from: uiViewController,
context: configuration.context,
animated: configuration.animated) {
presented = hc
}
}
}

} else {
guard presented != nil else { return }
DispatchQueue.main.throttle(interval: 0.1, context: uiViewController) {
self.presented?.dismiss(animated: configuration.animated, completion: {
self.presented = nil
self.onDismiss?()
})
}
}
}
}

The ModalPresenter has the following properties:

  • isPresented: A binding that indicates whether the modal view is presented or not.
  • presented: A state property that holds the reference to the presented UIViewController.
  • configuration: A configuration structure that holds the properties to configure the modal view.
  • content: A closure that returns the SwiftUI view that will be presented as a modal view.
  • onDismiss: A closure that will be called when the modal view is dismissed.

In the updateUIViewController method, the logic to present and dismiss the modal view is defined. If isPresented is true, the modal view is presented. If the presented property is not nil, the content of the modal view is updated. If the presented property is nil, a new modal view is created and presented.

When isPresented is false, the modal view is dismissed and the onDismiss closure is called.

6. Code repository

Full code repository can be found on my github ModalPresenter.

Generated by OpenAI’s language model ChatGPT. (https://openai.com/)

--

--

No responses yet