When working with SwiftUI, you are familiar with the sheet(isPresented:onDismiss:content:)
modifier for presenting views. It is a handy way to display content in a modal-style overlay, and it comes with a built-in swipe gesture to dismiss the view.
But what if you want to use a fullScreenCover(isPresented:onDismiss:content:)
instead? Unfortunately, it does not have that same swipe-to-dismiss functionality out of the box. With a little workaround, we can add it ourselves using DragGesture
!
DragGesture
You can use a DragGesture
that calculates the difference between the starting and ending position of the swipe to swipe down on the full-screen view. If the swipe distance exceeds a certain threshold (say 150 points), then you can programmatically dismiss the fullScreenCover
.
Here is a simple example of how you might set this up:
struct SampleView: View {
@State private var showCoverView = false
var body: some View {
Button("PRESENT") {
showCoverView.toggle()
}
.fullScreenCover(isPresented: $showCoverView) {
CoverView()
}
}
}
struct CoverView: View {
/// Use @Environment(\.presentationMode) private var presentationMode for iOS 14 and below
@Environment(\.dismiss) private var dismiss
var body: some View {
Button("DISMISS") {
/// Use presentationMode.wrappedValue.dismiss() for iOS 14 and below
dismiss()
}
.gesture(
DragGesture().onEnded { value in
if value.location.y - value.startLocation.y > 150 {
/// Use presentationMode.wrappedValue.dismiss() for iOS 14 and below
dismiss()
}
}
)
}
}
The key is the gesture modifier applied in CoverView
. This adds a DragGesture
that tracks the user's swipe. In the onEnded
closure, it checks if the height of the swipe translation exceeds 150 points. If so, it calls dismiss()
to close the fullScreenCover
.
Example from Chroma Game
Let us take a closer look at a real-world example in my Chroma Game app.
In the HomeView
, the view model contains an enum called GameModeDestination
that determines which color mode to present - either RGB or HSB. The fullScreenCover
modifier uses this enum to decide which view to display.
struct HomeView: View {
@EnvironmentObject var viewModel: HomeViewModel
var body: some View {
VStack {
Text("MAIN VIEW IMPLEMENTATION")
/// Stripped implementation of Home View
Button("PRESENT RGB") {
viewModel.gameDestination = .rgb
}
}
.fullScreenCover(
item: $viewModel.gameDestination,
onDismiss: viewModel.didDismiss) { item in
switch item {
case .rgb: RGBView(viewModel: viewModel.new)
case .hsb: HSBView(viewModel: viewModel.new)
}
}
}
}
The presented view (either RGBView or HSBView) is wrapped in a ContainerView
. This ContainerView
displays a dismiss button at the top, similar to the now playing screen in the Apple Music app. The button uses a custom DismissButton
view:
public struct DismissButton: View {
var action: () -> ()
public init(_ action: @escaping () -> ()) {
self.action = action
}
public var body: some View {
Button(action: action) {
RoundedRectangle(cornerRadius: 16)
.fill(Color.gray)
.frame(width: 50, height: 5)
}
}
}
The ContainerView
itself applies the DragGesture
to enable the swipe-to-dismiss interaction:
struct ContainerView: View {
@Environment(\.presentationMode) var presentation
@ObservedObject var viewModel: MainViewModel
var body: some View {
VStack {
DismissButton { dismiss() }
Text("IMPLEMENTATION")
}
.gesture(
DragGesture().onEnded { value in
if value.location.y - value.startLocation.y > 150 {
dismiss()
}
}
)
}
private func dismiss() {
viewModel.invalidateTimer()
viewModel.dismiss()
presentation.wrappedValue.dismiss()
}
}
When the user swipes down on the ContainerView
and the swipe distance exceeds 150 points, the dismiss()
function is called. This function updates the view model, invalidates any active timers, and then uses the presentationMode
environment variable to dismiss the view.
Conclusion
And that's all there is to it! With just a few lines of code, you can tweak the fullScreenCover
experience to support the type of swipe-to-dismiss interaction that many users have come to expect from modal views.
Give it a try in your apps and let me know how it goes! I wouldd love to see the creative ways you put this technique to use. Happy coding!