Presenting modal views

Adding the “Place Order” Button

Great! What’s left is to insert one last Section containing a Button for the user to submit his order.

So, let’s create another Section containing a Button view (performing a temporary dummy action).

Section {
    Button(action: {
        print("Order placed.")
    }) {
        Text("Place Order")
    }
}

However, we want to disable the button until the user has filled in all address fields.

Button(action: {
    print("Order placed.")
}) {
    Text("Place Order")
}
    .disabled(!firstName.isEmpty && !lastName.isEmpty && !street.isEmpty && !city.isEmpty && !zip.isEmpty ? false : true)

Once the user taps the “Order”-Button, we would like to ask him again explicitly whether he wants to place the order bindingly.

For this purpose, we can use a so-called Confirmation dialog, which is displayed to the user as an alert sheet.

For this we add another State “showConfirmationDialog” to our OrderForm.

@State var showConfirmationDialog = false

We want to show the Confirmation dialog as soon as the user taps the “Order”-Button.

Button(action: {
    showConfirmationDialog = true
}) {
    Text("Place Order")
}

Now we add the .confirmationDialog modifier to our “Order”-Button and specify the displayed content.

.confirmationDialog("Are you sure?", isPresented: $showConfirmationDialog, titleVisibility: Visibility.visible) {
    Button(action: {print("Order placed")}) {
        Text("Place Order Now!")
    }
}

Let’s see if this works by running a Live preview:

Displaying the Form as a modal view 

We want to show the OrderForm we created whenever the user selects a certain food from the DetailView of our Food Delivery App and taps on the “Order”-Button. When that happens, we want to present our OrderForm as a modal view. A modal view is a temporary view that prevents the user from interacting with the overlaid UI until a certain action is done to exit the modal view. In our case, the DetailView should be overlayed until the user places an order or cancels the process. 

To present the OrderForm when the user taps the “Order”-Button, we have to go to our DetailView and add a State property for keeping track of when the modal view should be shown.

@State var showOrderSheet = false

To add a modal view, we have to append the .sheet modifier that presents the wrapped OrderForm when the bound State gets assigned to true.⠀

List(filterData(foodDataSet: foodData, by: currentCategory)) { food in
    DetailRow(food: food)
}
    //...
    .sheet(isPresented: $showOrderSheet, content: {
    OrderForm()
    })

We want to toggle our showOrderSheet State from the DetailRow view where our “Order”-Button is. Therefore, we have to declare a Binding inside the DetailRow struct.

@Binding var showOrderSheet: Bool

Our DetailRow_Previews struct needs to know what value should be assigned to the Binding. We can use the .constant method for this. Only for the DetailView‘s preview, we want to simulate that the showOrderSheet Binding is false, regardless of the related State of the DetailView.

struct DetailRow_Previews: PreviewProvider {
    static var previews: some View {
        DetailRow(food: foodData[0], showOrderSheet: .constant(false))
            .previewLayout(.sizeThatFits)
    }
}

But inside our DetailView’s List, we have to initialize the DetailRow while binding it to the showOrderSheet State. 

List(filterData(foodDataSet: foodData, by: currentCategory)) { food in
    DetailRow(food: food, showOrderSheet: $showOrderSheet)
}

Now, we can toggle the showOrderSheet State through this Binding by using the Order Button’s action closure.

Button(action: {
    showOrderSheet = true
}) {
    Text("ORDER")
        .foregroundColor(.white)
}

Okay, let’s start a Live preview of our DetailView and see if that works. When we tap on the “Order”-Button, the DetailRow’s Button assigns the showOrderSheet State of the DetailView through its Binding to true. This causes the DetailView’s body to rerender, which leads to presenting our OrderForm as a modal view. 

When we swipe down the modal view, SwiftUI automatically assigns the showOrderSheet State to false again which causes the OrderForm to dismiss!

We also want to add a cancel button for dismissing the OrderForm.

Adding a navigation bar item

We want to present this cancel button as a navigation bar item. Therefore, we have to wrap our OrderForm’s Form into a NavigationView.

NavigationView {
    Form {
        //...
    }
}

Next, we add a navigation bar by using the .navigationTitle modifier.

NavigationView {
    Form {
        //...
    }
        .navigationTitle("Your Order")
}

To add a Button to our navigation bar, we have to append another modifier below our .navigationTitle called .navigationBarItems. By using the “leading” argument of this modifier, we can add a view to the left side. By using the “trailing” argument we can add a view to the right side of the navigation bar.

We want our cancel button to be on the left side of the navigation bar, so we use the “leading” argument.

.navigationBarItems(leading:
    //Button goes here
)

But before we add the cancel button, we need to be able to access the showSheetModifier State of the DetailView for manually dismissing the OrderForm modal view. We can do this by adding a Binding to the OrderForm.

@Binding var showOrderSheet: Bool

Again, we use the .constant method inside our OrderForm_Previews struct for telling the preview of this particular view that it should always simulate the case that the showOrderSheet property is assigned false.

struct OrderForm_Previews: PreviewProvider {
    static var previews: some View {
        OrderForm(showOrderSheet: .constant(false))
    }
}

But in our DetailView, we initialize the OrderForm by binding it to the showOrderSheet State of the DetailView!

.sheet(isPresented: $showOrderSheet, content: {
    OrderForm(showOrderSheet: $showOrderSheet)
})

Now, we can use the Binding in our OrderForm to add a Button as the leading navigation bar item that assigns the showSheetModifier to false when tapping on it, which causes the OrderForm to dismiss.

.navigationBarItems(leading:
    Button(action: {
        showOrderSheet = false
    }) {
        Text("Cancel")
})

Great! Let’s try running the app in the regular simulator or run the DetailView in a Live preview.

When we click on the “Order”-Button, the showOrderSheet State of the DetailView gets assigned to true which causes the OrderForm to be displayed as an overlaid modal view.

We can now interact with the Form, fill in the information, and work with the controls. When we are finished, we can tap on the “Place Order” button.

We can cancel the order process by tapping on the cancel navigation bar button, which assigns the DetailView’s showOrderSheet State to false again through the OrderForm’s binding. Alternatively, we can swipe down the modal view, which automatically resets the showOrderSheet State and therefore dismisses the OrderForm.

5 replies on “Presenting modal views”

Hello, I don’t know if it’s correct or not, but when I play in any area of the DetailRow (outside the Button), the Button code is executed, and it changes the value of the variable and displays the sheet.

In the downloaded project it also works the same way.
It is correct, or it should only show the sheet when the button is tapped.

Thank you

Hi Pablo,

you are right that is the intended behaviour. When inserting a Button into a List’s row, the whole row gets “touchable”. If you want to limit the “touchable” area to the actual Button you need to apply the following modifier to it

.buttonStyle(BorderlessButtonStyle())

Leave a Reply

Your email address will not be published. Required fields are marked *