Present View from Anywhere in SwiftUI
Reading time: 2 min
This recipe shows how to present a custom view modally from anywhere in the SwiftUI app. Normally, presenting a view on top of another one forces you do define the transition in the parent view. This solution, however, makes it so that the segue is defined in just a single place and is always presented on top of current view.
The end result (which admittedly doesn't fully convey what's going on) looks like this:
Recipe
Two things need to be put in place for this to work:
- Send a
Notification
from wherever in the app, with its payload object describing the view to be shown. - Receive that notification in your app file, where a
fullScreenCover
has been attached to yourContentView
.
First, add the name for the new notification:
public extension Notification.Name {
static let showModal = Notification.Name("showModal")
}
Then, define the model for the custom modal view. This will be sent in the notification itself and translated into the contents of the modal. We also want to be able to define a dismiss callback, as well as a presentation binding in order to be able to manually dismiss the modal from within the custom view:
struct ModalData {
let onDismiss: (() -> Void)?
let content: (Binding<Bool>) -> AnyView
init<Content: View>(onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping (Binding<Bool>) -> Content) {
self.onDismiss = onDismiss
self.content = { AnyView(content($0)) }
}
static let empty = ModalData { _ in EmptyView() }
}
Next, open your app file. It's the one that contains a struct that inherits from App
. It should roughly look like this:
@main
struct MyAwesomeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Now, extend it with:
- Few
@State
vars that represent the modal state. onReceive
to handle incoming notifications to present the modal.fullScreenCover
to render the modal on top ofContentView
.
Here's what it should look like in the end:
@main
struct MyAwesomeApp: App {
@State private var showModal = false
@State private var modalData = ModalData.empty
var body: some Scene {
WindowGroup {
ContentView()
.onReceive(NotificationCenter.default.publisher(for: .showModal)) { notif in
if let data = notif.object as? ModalData {
modalData = data
showModal = true
}
}
.fullScreenCover(isPresented: $showModal,
onDismiss: modalData.onDismiss) {
modalData.content($showModal)
}
}
}
}
And that's actually it! Now, just send the notification from anywhere in your app to make the magic happen:
Button("Show modal") {
NotificationCenter.default.post(name: .showModal,
object: ModalData(onDismiss: {
print("Modal dismissed")
}) { isPresented in
Text("I'm shown on top of everything!")
Button("Dismiss modal") {
isPresented.wrappedValue = false
}
})
}