SwiftUI missing modal presentation view modifier
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.
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
: ABinding
to aBool
that indicates whether the modal view should be shown or dismissed.configuration
: AModalPresenterConfiguration
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/)