Search code examples
swiftuiswiftui-form

How do you put a Form in a SwiftUI Modal?


I'm trying to create a modal containing a Form in SwiftUI. I'd prefer not to use action sheets in favor of a modal similar to the one pictured below. It seems as though SwiftUI Modals and Alerts aren't capable of such modals yet, is there a possible solution?

enter image description here


Solution

  • Two approaches

    Here are two examples of ways to create a custom modal.


    1. With .overlay()

    import SwiftUI
    
    struct ContentView: View { // your main view
        @State var showModal: Bool = false
    
        var body: some View {
            NavigationView {
                Button(action: {
                    self.showModal.toggle()
                }) {
                    HStack {
                        Image(systemName: "plus.circle.fill")
                            .imageScale(.large)
                        Text("Show Modal")
                    }
                }
                .navigationBarTitle("Welcome")
            }
            .navigationViewStyle(StackNavigationViewStyle())
            .overlay(ModalView(showModal: $showModal))
        }
    }
    
    struct ModalView: View { // draws a semi-transparent rectangle that contains the modal
        @Binding var showModal: Bool
    
        var body: some View {
            Group {
                if showModal {
                    Rectangle()
                        .foregroundColor(Color.black.opacity(0.5))
                        .edgesIgnoringSafeArea(.all)
                        .overlay(
                            GeometryReader { geometry in
                                RoundedRectangle(cornerRadius: 16)
                                    .foregroundColor(.white)
                                    .frame(width: min(geometry.size.width - 100, 300), height: min(geometry.size.height - 100, 200))
                                    .overlay(ModalContentView(showModal: self.$showModal))
                            }
                    )
                }
            }
        }
    }
    
    struct ModalContentView: View { // the real modal content
        @Binding var showModal: Bool
    
        var body: some View {
            VStack {
                Text("Modal Content")
    
                Button(action: {
                    self.showModal.toggle()
                }) {
                    HStack {
                        Image(systemName: "xmark.circle.fill")
                            .imageScale(.large)
                        Text("Close Modal")
                    }
                }
            }
        }
    }
    


    2. With ZStack

    struct ContentView: View {
        @State var showModal: Bool = false
    
        var body: some View {
            NavigationView {
                ZStack {
                    Button(action: {
                        withAnimation {
                            self.showModal.toggle()
                        }
                    }) {
                        HStack {
                            Image(systemName: "plus.circle.fill")
                                .imageScale(.large)
                            Text("Show Modal")
                        }
                    }
    
                    if showModal {
                        Rectangle() // the semi-transparent overlay
                            .foregroundColor(Color.black.opacity(0.5))
                            .edgesIgnoringSafeArea(.all)
    
                        GeometryReader { geometry in // the modal container
                            RoundedRectangle(cornerRadius: 16)
                                .foregroundColor(.white)
                                .frame(width: min(geometry.size.width - 100, 300), height: min(geometry.size.height - 100, 200))
                                .overlay(ModalContentView(showModal: self.$showModal))
                        }
                        .transition(.move(edge: .bottom))
    
                    }
                }
                .navigationBarTitle("Welcome")
    
            }
            .navigationViewStyle(StackNavigationViewStyle())
        }
    }
    
    struct ModalContentView: View {
        @Binding var showModal: Bool
    
        var body: some View {
            VStack {
                Text("Modal Content")
    
                Button(action: {
                    withAnimation {
                        self.showModal.toggle()
                    }
                }) {
                    HStack {
                        Image(systemName: "xmark.circle.fill")
                            .imageScale(.large)
                        Text("Close Modal")
                    }
                }
            }
        }
    }
    

    Maybe this second approach is even better. With this one, I also got the animation a kind of working.