Building apps become more and more complex because more features and screens are added. SwiftUI helps to manage these advanced interfaces and user flows. One essential aspect of a great user experience is the effective use of presentation styles for displaying additional content or capturing user input. In this blog post, I will show you the different presentation styles in SwiftUI, including sheets, bottom sheets, and fullScreen presentations, showcasing how to create and manage these presentations to enhance your app’s functionality and user experience.
I am going to use a Shopping app as an example that I started developing for other tutorials about SwiftUI presentations like menus and popovers.
What is a modal view in SwiftUI?
A modal view is a view presented on top of another view, typically used for user input or displaying additional information. SwiftUI provides a powerful and flexible way to create modal presentations, such as sheets, popovers, alerts, or confirmation dialogs, to focus on important, narrowly scoped tasks within your app. To get an overview of all these presentation styles have a look at this overview of SwiftUI presentation styles.
What are SwiftUI sheets?
A sheet in SwiftUI is a presentation style that displays a new view on top of the current view. Sheets slide in from the bottom of the screen, which is why they are often referred to as bottom sheets. They cover the main content. SwiftUI has newer features to set the size of the sheet. If you for example what to create a half sheet and allow the user to see parts of the underlying content.
Example use cases for sheets are:
- Settings Menu: Present a sheet to allow users to access and modify app settings without leaving the main interface.
- Adding an item: In a to-do list app, use a sheet to display a form for users to add a new task without navigating away from the main task list.
- User profile editing: In a social media app, present a sheet for users to edit their profile information without leaving their profile view.
- Sharing options: Present a sheet with various sharing options when users want to share content from your app with others. This is done automatically with ShareLink.
- Info view: Show introduction and help documentation in a sheet. The user can dismiss the info view easily and go back.
Examples of presenting sheets in full screen mode are the following (fullScreenCover):
- Onboarding: Use fullScreenCover to present an onboarding flow that introduces users to the app’s features and functionality when they first open the app.
- Authentication: In apps requiring user authentication, use a fullScreenCover to present a login or sign-up screen, ensuring the user focuses on the authentication process before accessing the app’s content.
- Media viewer: In a gallery or media app, use fullScreenCover to display images or videos in a full-screen mode for an immersive viewing experience.
- Terms and conditions or privacy policy: When users need to review and accept terms and conditions or privacy policies, present them using fullScreenCover to ensure they focus on the content without distractions.
- In-app purchases or subscription plans: In apps offering in-app purchases or subscription plans, use fullScreenCover to present the available options and payment flow, ensuring users can concentrate on the purchasing process.
- Ads: Force the user to watch ads and only allow to continue once the ad has finished.
SwiftUI Example Project
I will use a shopping app as an example. It is a very basic implementation and uses the Fake Store API. It consists of 2 screens: one view where the list of all products is displayed and a detail view for the product.
Download the project files here: https://github.com/gahntpo/Sho…
How do I present a sheet in SwiftUI?
For my shopping app example I want to show a sheet where the user can select from a list of categories. You could use a popover, but my list of categories will eventually be quite long. I think it will better fit in a sheet. In the following image, you can see how the app should look like.
The main content of the app is ProductListView
inside a NavigationStack. I added the `Choose Category` button in the toolbar.
struct ContentView: View {
@StateObject var fetcher = ProductFetcher()
var body: some View {
NavigationStack(root: {
ProductListView(products: fetcher.products,
state: fetcher.state)
.toolbar {
ToolbarItem {
Button {
} label: {
Label("Choose Category", systemImage: "line.3.horizontal.decrease.circle")
}
}
}
.onAppear {
fetcher.load()
}
.navigationDestination(for: Product.self, destination: { product in
ProductDetailView(product: product)
})
})
}
}
In order to show the ProductCategoryListView
in a sheet, when the user taps on the `Choose Category` button, I first have to declare a state property and toggle it.
struct ContentView: View {
@State private var showCategorySelector: Bool = false
@StateObject var fetcher = ProductFetcher()
var body: some View {
...
.toolbar {
ToolbarItem {
Button {
showCategorySelector.toggle()
} label: {
Label("Choose Category", systemImage: "line.3.horizontal.decrease.circle")
}
}
}
}
}
I can then use the.sheet
modifier and bind the respective @State
property to the showCategorySelector
parameter:
struct ContentView: View {
@State private var showCategorySelector: Bool = false
@StateObject var fetcher = ProductFetcher()
var body: some View {
NavigationStack(root: {
...
})
.sheet(isPresented: $showCategorySelector) {
ProductCategoryListView(selectedCategory: $fetcher.selectedCategory)
}
}
}
Execute an action when the sheet closes
Now the user can select a category, but the product list does not update. When the sheet is closed, I want to update the product list and show only products in the selected category. You can use the argument onDismiss
to execute code when the sheet is dismissed. In the following example, I reset the products array and call load() which executes a fetch request to the Fake Store API:
.sheet(isPresented: $showCategorySelector, onDismiss: {
fetcher.products = []
fetcher.load()
}) {
ProductCategoryListView(selectedCategory: $fetcher.selectedCategory)
}
Can I use multiple sheets in SwiftUI?
Often you need to toggle multiple different views. In the shopping app example, I could add another sheet that shows the settings view.
struct ContentView: View {
@State private var showSettings: Bool = false
@State private var showCategorySelector: Bool = false
@StateObject var fetcher = ProductFetcher()
var body: some View {
NavigationStack(root: {
...
})
.sheet(isPresented: $showCategorySelector, onDismiss: {
fetcher.products = []
fetcher.load()
}) {
ProductCategoryListView(selectedCategory: $fetcher.selectedCategory)
}
.sheet(isPresented: $showSettings) {
SettingsView()
}
}
}
How to show sheets from items inside List and ForEach?
Sheet works also with a binding to the selected item. This is useful if you want to show sheets for items inside List
or ForEeach
. Here is a simple example:
struct ContentView: View {
@State private var products = [Product(id: 1,
title: "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
price: 9.95,
description: "Your perfect pack for everyday use and walks in the forest.",
category: "men's clothing",
image: "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg",
rating: Rating(rate: 3.9, count: 120)), ...]
@State private var selection: Product? = nil
var body: some View {
List(items, selection: $selection) { product in
ProductRow(product: product)
.tag(product)
}
.sheet(item: $selection,
onDismiss: { print("finished!") },
content: { ProductDetailView(product: $0)
})
}
}
How to present a full-screen modal view using fullScreenCover()?
To present a full-screen modal view using fullScreenCover(), add the .fullScreenCover modifier to the view you want to present from, provide an optional condition using the isPresented
binding, and supply the view you want to present. In the shopping app example, I have a detail view with a buy button. When the user I not logged in, I want to show a login screen in full screen:
struct ProductDetailView: View {
let product: Product
@State private var showAuthView = false
var body: some View {
VStack {
...
Button {
// check if logged in, otherwise
showAuthView.toggle()
} label: {
Label("Buy Now", systemImage: "cart")
.frame(maxWidth: .infinity)
.padding(5)
}
.buttonStyle(.borderedProminent)
.frame(maxHeight: .infinity)
}
}
.fullScreenCover(isPresented: $showAuthView) {
AuthenticationView()
}
}
}
Controlling the height of sheet with presentationDetents modifier to create a half sheet
So far, I should you the sheet and full-screen cover. Both cover the main view completely. But sometimes you want to show a half sheet and allow the user to see parts of the main content. In the product list example, I could show the category selections screen only in a half-bottom sheet. You can do this now with presentationDetents which is available for iOS 16+ and macOs 13+. The presentationDetents modifier needs to be placed inside the sheet. Here is how to add it to ProductCategoryListView:
struct ContentView: View {
@State private var showCategorySelector: Bool = false
@StateObject var fetcher = ProductFetcher()
var body: some View {
NavigationStack(root: {
...
})
.sheet(isPresented: $showCategorySelector, onDismiss: {
fetcher.products = []
fetcher.load()
}) {
ProductCategoryListView(selectedCategory: $fetcher.selectedCategory)
.presentationDetents([.medium])
}
}
}
The user can drag the sheet indicator to change the size of the sheet if you provide multiple sheet sizes. The configurations from the above image are done with:
.presentationDetents([.medium, .large, .fraction(0.8), .height(200)])
These are the configuration for detents:
- system provided: large and medium
- fraction: give the percentage of the current screen height
- height: give the absolute height in points
When the sheet is opened, it is presented with the smallest presentation size. If you want to set a specific size, you can use the binding to the selection property:
struct ContentView: View {
@State private var showCategorySelector: Bool = false
@StateObject var fetcher = ProductFetcher()
@State private var currentDetent = PresentationDetent.large
var body: some View {
NavigationStack(root: {
...
})
.sheet(isPresented: $showCategorySelector, onDismiss: {
fetcher.products = []
fetcher.load()
}) {
ProductCategoryListView(selectedCategory: $fetcher.selectedCategory)
.presentationDetents([.medium, .large, .fraction(0.8), .height(200)],
selection: $currentDetent)
}
}
}
This can also be used to programmatically adjust the size of the sheet. For example, you could choose the height depending on the currently selected category.
Prioritizing scrolling over resizing the sheet
ProductCategoryListView has a List inside. Both the List and the sheet react to a swipe up/down gesture. SwiftUI needs to decide who is going to react to the gesture. The default is that the sheet gets to gesture to resize. In my case, I want to allow the user to scroll inside the category list. This can be done with the newer presentationContentInteraction
.sheet(isPresented: $showCategorySelector) {
ProductCategoryListView(selectedCategory: $fetcher.selectedCategory)
.presentationDetents([.medium, .large, .fraction(0.8), .height(200)], selection: $currentDetent)
.presentationBackgroundInteraction(.enabled(upThrough: .height(200)))
.presentationContentInteraction(.scrolls)
}
presentationContentInteraction
takes either a value of resizes or scrolls.
Allow interaction with the view behind sheet with presentationBackgroundInteraction
Per default, when the sheet is open, you cannot interact with the view behind the sheet. With iOS 16 and macOS 13, you can now make it interactable. Here is an example that uses the new presentationBackgroundInteraction
modifier:
.sheet(isPresented: $showCategorySelector) {
ProductCategoryListView(selectedCategory: $fetcher.selectedCategory)
.presentationDetents([.medium, .large, .fraction(0.8), .height(200)], selection: $currentDetent)
.presentationBackgroundInteraction(.enabled)
}
This will always allow to interact with the below view. If you want to enable this only for certain sizes of the sheet, you can further customize presentationBackgroundInteraction like so:
.presentationBackgroundInteraction(.enabled(upThrough: .height(200)))
In the current shopping app example, it probably does not make sense. When the user taps on a product in the list, the detail view is opened.
How to customize the appearance of sheet?
With iOS 16+ and macOS 13+, you can now change the background color of the sheet. Here is an example to use presentationBackground with a gradient background:
.sheet(isPresented: $showSettings) {
SettingsView()
.presentationBackground(alignment: .bottom) {
LinearGradient(colors: [Color.pink, Color.purple], startPoint: .bottomLeading, endPoint: .topTrailing)
}
}
You could also use a material background to make the sheet semi-transparent:
.sheet(isPresented: $showSettings) {
SettingsView()
.presentationBackground(.thinMaterial)
}
Also available with iOS 16+ and macOS 13+, is presentationCornerRadius modifier. This changes the corner radius of the sheet:
.sheet(isPresented: $showSettings) {
SettingsView()
.presentationBackground(alignment: .bottom) {
LinearGradient(colors: [Color.pink, Color.purple], startPoint: .bottomLeading, endPoint: .topTrailing)
}
.presentationCornerRadius(50)
}
Dismiss Sheets and FullScreenCover
Sheet can be dismissed with a swipe-down gesture. If you want to prevent this, use the new interactiveDismissDisabled modifier:
.sheet(isPresented: $showSettings) {
SettingsView()
.interactiveDismissDisabled()
}
FullScreenCover does not allow swipe-down to dismiss. You have to add buttons to allow the user to dismiss sheets programmatically. Use the environment value dismiss:
struct ProductCategoryListView: View {
@StateObject private var categoryFetcher = ProductCategoryFetcher()
@Binding var selectedCategory: String?
@State private var searchText = ""
@Environment(\.dismiss) var dismiss
var categories: [String] {
guard !searchText.isEmpty else { return categoryFetcher.categories }
return categoryFetcher.categories.filter { category in
category.lowercased().contains(searchText.lowercased())
}
}
var body: some View {
NavigationView {
List(selection: $selectedCategory){
ForEach(categories, id: \.self) { category in
Text(category)
}
}
.navigationTitle("Select a Category")
.navigationBarTitleDisplayMode(.inline)
.toolbar(content: {
ToolbarItem {
Button {
dismiss()
} label: {
Label("Dismiss", systemImage: "xmark.circle.fill")
}
}
})
.searchable(text: $searchText)
.onAppear {
categoryFetcher.loadPoductCategories()
}
}
}
}
Dismiss works for all system presentations including sheet, fullscreenCover, or popover. Inside NavigationView, it will also pop the current view that means navigate back to the root view.
Conclusion
In summary, SwiftUI provides a versatile way to create modal views, such as sheets, fullScreenCovers, and more. Sheets are particularly useful for displaying additional content or prompting user input without navigating away from the main interface. This blog post explored various aspects of sheets in SwiftUI, including presenting sheets, using multiple sheets, controlling sheet height, customizing appearance, and dismissing sheets. By utilizing these techniques, developers can create dynamic and interactive user interfaces that enhance the overall user experience.
FAQ
What is the difference between a sheet to other presentation forms in SwiftUI like popover, alerts and confimation dialogs?
Sheets provide a more flexible presentation style that allows for greater customization and interaction, compared to other presentation forms like popovers, alerts, and confirmation dialogs, which serve more specific and focused purposes.
Sheets differ from other presentation forms in SwiftUI, such as popovers, alerts, and confirmation dialogs, in several ways:
- Popovers: A popover is a small, contextual view that appears next to the control that triggered it. Unlike sheets, popovers only cover a small portion of the screen, and their content is directly related to the triggering control. Popovers are commonly used on iPadOS and macOS, but they are adapted to sheets when used on iPhone devices.
- Alerts: Alerts are used to display important information or require user confirmation before proceeding with an action. They typically cover a small area in the center of the screen and include a title, message, and one or more buttons for user interaction. Unlike sheets, alerts interrupt the user’s workflow and demand immediate attention.
- Confirmation dialogs: Confirmation dialogs are similar to alerts in that they require user input before proceeding with an action. However, they can be more customizable and may include additional controls or views beyond just a title, message, and buttons. While sheets are used for more extensive tasks and allow for greater customization, confirmation dialogs focus on quickly gathering user input for a specific action.
How do I present a view in SwiftUI?
You can present a view in SwiftUI using presentation modifiers like .sheet, .fullScreenCover, or by creating a custom bottom sheet.
How do I create a modal in SwiftUI?
To create a modal in SwiftUI, use the .sheet modifier with a binding to manage the presentation state. Note that modals are deprecated in iOS 15 and it is recommended to use sheets instead.
How do I present a view to another view in SwiftUI?
To present a view on top of another view in SwiftUI, use presentation modifiers like .sheet or .fullScreenCover, or create a custom bottom sheet.
How do I present a sheet in SwiftUI?
To present a sheet in SwiftUI, attach the .sheet modifier to the view you want to present the sheet from, use a binding to manage the presentation state, and provide the view you want to present as a sheet.
What is fullScreenCover in SwiftUI?
fullScreenCover in SwiftUI is a presentation modifier that presents a view covering the entire screen, hiding the underlying view. It is useful for onboarding, login, or other situations where the user should focus on a single task.
How do I dismiss full screen cover in SwiftUI?
To dismiss a full screen cover in SwiftUI, use a binding to manage the presentation state and toggle the binding value when you want to dismiss the cover. You can also use the @Environment(.dismiss) property wrapper to dismiss the full screen cover programmatically.
Further Reading and Resources
- SwiftUI Popovers and Popup Menus: The Ultimate Guide
- How to Show SwiftUI Alerts with Buttons, Textfields and Error Messages
- SwiftUI in Action: A Deep Dive into Action Sheets and Confirmation Dialog
- Exploring Navigation in SwiftUI: A Deep Dive into NavigationView
- Better Navigation in SwiftUI with Navigation Stack
Hu, Is there a way to force a sheet to portrait mode only?
As fare as I know, SwiftUI itself doesn’t provide a built-in way to enforce orientation modes.
Anyway at all too display the tab bar above the sheet? Similar to Apples ‘Find My’ app. When sheet is present it blocks tab bar view, my sheet will always be present on my home view.