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:

preview

Recipe

Two things need to be put in place for this to work:

  1. Send a Notification from wherever in the app, with its payload object describing the view to be shown.
  2. Receive that notification in your app file, where a fullScreenCover has been attached to your ContentView.

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:

  1. Few @State vars that represent the modal state.
  2. onReceive to handle incoming notifications to present the modal.
  3. fullScreenCover to render the modal on top of ContentView.

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
    }
  })
}

Next Post Previous Post