50

I want to click a button and then present a new view like present modally in UIKit enter image description here

I have already seen "How to present a new view using sheets", but I don't want to attach it to the main view as a modal sheet.

And I don't want to use NavigationLink, because I don't want a new view and old view have a navigation relationship.

Thanks for your help...

6
  • Why you don't want to attach it to the main view as a modal sheet? It is a standard method even in UIKit. Do you have any special reason? Commented Nov 20, 2019 at 22:21
  • I try to explain my thoughts... If there is anything wrong, please correct me. Commented Nov 21, 2019 at 8:32
  • 1
    The Apps have 3 view, 1: Login Page 2: TableView Page 3: TableDetail Page, TableView page and TableDetail page is navigation relation. After login will present to TableView page, TableView page has no any relationship with login page after login Commented Nov 21, 2019 at 8:46
  • So you need it to be fullscreen right? Commented Nov 21, 2019 at 8:50
  • ys! i want fullscreen Commented Nov 21, 2019 at 8:53

5 Answers 5

66

To show a modal (iOS 13 style)

You just need a simple sheet with the ability to dismiss itself:

struct ModalView: View { @Binding var presentedAsModal: Bool var body: some View { Button("dismiss") { self.presentedAsModal = false } } } 

And present it like:

struct ContentView: View { @State var presentingModal = false var body: some View { Button("Present") { self.presentingModal = true } .sheet(isPresented: $presentingModal) { ModalView(presentedAsModal: self.$presentingModal) } } } 

Note that I passed the presentingModal to the modal so you can dismiss it from the modal itself, but you can get rid of it.


To make it REALLY present fullscreen (Not just visually)

You need to access to the ViewController. So you need some helper containers and environment stuff:

struct ViewControllerHolder { weak var value: UIViewController? } struct ViewControllerKey: EnvironmentKey { static var defaultValue: ViewControllerHolder { return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController) } } extension EnvironmentValues { var viewController: UIViewController? { get { return self[ViewControllerKey.self].value } set { self[ViewControllerKey.self].value = newValue } } } 

Then you should use implement this extension:

extension UIViewController { func present<Content: View>(style: UIModalPresentationStyle = .automatic, @ViewBuilder builder: () -> Content) { let toPresent = UIHostingController(rootView: AnyView(EmptyView())) toPresent.modalPresentationStyle = style toPresent.rootView = AnyView( builder() .environment(\.viewController, toPresent) ) NotificationCenter.default.addObserver(forName: Notification.Name(rawValue: "dismissModal"), object: nil, queue: nil) { [weak toPresent] _ in toPresent?.dismiss(animated: true, completion: nil) } self.present(toPresent, animated: true, completion: nil) } } 

Finally

you can make it fullscreen like:

struct ContentView: View { @Environment(\.viewController) private var viewControllerHolder: UIViewController? var body: some View { Button("Login") { self.viewControllerHolder?.present(style: .fullScreen) { Text("Main") // Or any other view you like // uncomment and add the below button for dismissing the modal // Button("Cancel") { // NotificationCenter.default.post(name: Notification.Name(rawValue: "dismissModal"), object: nil) // } } } } } 
Sign up to request clarification or add additional context in comments.

11 Comments

I get this error at the environment property wrapper: Cannot convert value of type 'Environment<UIViewController?>' to specified type 'UIViewController'
It should be handled by default, but try adding ? at the end of the line there. @jsbeginnerNodeJS
I get this error in the console: ``` Warning: Attempt to present <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fafd2641d30> on <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fafd2611bd0> whose view is not in the window hierarchy!```
how do you dismiss it?
sorry if i sound ignorant.. but why do we have to jump thorough so many hoops just to present a view modally?? doesn't it feel like a step backwards... SwiftUI just feels so incomplete.. what do you think? what am I missing here??
|
31

For iOS 14 and Xcode 12:

struct ContentView: View { @State private var isPresented = false var body: some View { Button("Show Modal with full screen") { self.isPresented.toggle() } .fullScreenCover(isPresented: $isPresented, content: FullScreenModalView.init) } } struct FullScreenModalView: View { @Environment(\.presentationMode) var presentationMode var body: some View { VStack { Text("This is a modal view") } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.red) .edgesIgnoringSafeArea(.all) .onTapGesture { presentationMode.wrappedValue.dismiss() } } } 

See also: How to present a full screen modal view using fullScreenCover()

Comments

4

Disclaimer: Below is not really like a "native modal", neither behave nor look&feel, but if anyone would need a custom transition of one view over other, making active only top one, the following approach might be helpful.

So, if you expect something like the following

custom SwiftUI modal

Here is a simple code for demo the approach (of corse animation & transition parameters can be changed by wish)

struct ModalView : View { @Binding var activeModal: Bool var body : some View { VStack { Button(action: { withAnimation(.easeInOut(duration: 0.3)) { self.activeModal = false } }) { Text("Hide modal") } Text("Modal View") } .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) .background(Color.green) } } struct MainView : View { @Binding var activeModal: Bool var body : some View { VStack { Button(action: { withAnimation(.easeInOut(duration: 0.3)) { self.activeModal = true } }) { Text("Show modal") } Text("Main View") } .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center) .background(Color.yellow) } } struct ModalContainer: View { @State var showingModal = false var body: some View { ZStack { MainView(activeModal: $showingModal) .allowsHitTesting(!showingModal) .disabled(showingModal) if showingModal { ModalView(activeModal: $showingModal) .transition(.move(edge: .bottom)) .zIndex(1) } } } } 

Comments

2

Based on @Mojtaba Hosseini answer. Tested with iOS 16.4

struct PresentEnvironmentKey: EnvironmentKey { static let defaultValue: PresentAction = .init() } extension EnvironmentValues { var present: PresentAction { get { self[PresentEnvironmentKey.self] } } } struct PresentAction { func callAsFunction<Content: View>(style: UIModalPresentationStyle = .automatic, @ViewBuilder builder: () -> Content) { UIApplication.topViewController()?.present(style: style, builder: builder) } } extension UIApplication { static func topViewController() -> UIViewController? { let keyWindow = UIApplication.shared .connectedScenes.compactMap { ($0 as? UIWindowScene)?.keyWindow }.last if var topController = keyWindow?.rootViewController { while let presentedViewController = topController.presentedViewController { topController = presentedViewController } return topController } return keyWindow?.rootViewController } } extension UIViewController { func present<Content: View>(style: UIModalPresentationStyle = .automatic, @ViewBuilder builder: () -> Content) { let toPresent = UIHostingController(rootView: AnyView(EmptyView())) toPresent.modalPresentationStyle = style toPresent.rootView = AnyView( builder() ) self.present(toPresent, animated: true, completion: nil) } } 

Usage:

struct ContentView: View { @Environment(\.present) var present var body: some View { Button("FullScreenCover") { present(style: .fullScreen) { YellowView() } } } } struct YellowView: View { @Environment(\.dismiss) var dismiss var body: some View { Color.yellow .onTapGesture { dismiss() } } } 

In case you need access to a topViewController from SwiftUI View:

 extension EnvironmentValues { var topViewController: UIViewController? { get { UIApplication.topViewController() } } } 

Comments

-1

Here is a simple one way - forward views. It's very straight forward.

 struct ChildView: View{ private let colors: [Color] = [.red, .yellow,.green,.white] @Binding var index : Int var body: some View { let next = (self.index+1) % MyContainer.totalChildren return ZStack{ colors[self.index % colors.count] Button("myNextView \(next) ", action: { withAnimation{ self.index = next } } )}.transition(.asymmetric(insertion: .move(edge: .trailing) , removal: .move(edge: .leading) )) } } struct MyContainer: View { static var totalChildren = 10 @State private var value: Int = 0 var body: some View { HStack{ ForEach(0..<(Self.totalChildren) ) { index in Group{ if index == self.value { ChildView(index: self.$value) }} } } } } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.