Greetings, traveler!
SwiftUI’s fullScreenCover
modifier is a powerful tool for presenting views that take over the entire screen, such as modals, onboarding flows, or detailed content views. However, developers often want more control over the presentation animation—for example, how to make the full-screen cover fade in smoothly rather than relying on the default slide-up behavior.
In this article, we’ll walk through a solution that enables any custom transition for a full-screen cover using a custom view modifier. By the end, you’ll have a reusable tool to enhance your SwiftUI presentations with polished animations.

The Challenge with fullScreenCover
The fullScreenCover
modifier in SwiftUI is straightforward to use: you bind it to a boolean value, and when that value becomes true, the specified content is presented over the entire screen. By default, it animates with a slide-up transition. However, if you want a different effect—like a fade-in using an opacity transition—SwiftUI doesn’t provide a built-in way to customize this directly within the fullScreenCover modifier.
This limitation prompts us to get creative. We need a way to apply a custom transition and animation to the content of the full-screen cover while still leveraging the modifier’s full-screen presentation capabilities.
The Solution: A Custom Full-Screen Cover Extension
To solve this, we’ll extend SwiftUI’s View protocol with a customFullScreenCover
method. This method will use a ViewModifier
called CustomFullScreenCoverModifier
to handle the presentation logic, allowing us to specify both the transition and animation. Here’s the complete code:
import SwiftUI
public extension View {
func customFullScreenCover<Content: View>(
isPresented: Binding<Bool>,
transition: AnyTransition = .opacity,
animation: Animation = .easeInOut,
@ViewBuilder content: @escaping () -> Content
) -> some View {
modifier(
CustomFullScreenCoverModifier(
isPresented: isPresented,
transition: transition,
animation: animation,
presentedView: content
)
)
}
}
private struct CustomFullScreenCoverModifier<PresentedView: View>: ViewModifier {
@Binding var isPresented: Bool
let transition: AnyTransition
let animation: Animation
@ViewBuilder let presentedView: () -> PresentedView
@State private var isPresentedInternal = false
@State private var isShowContent = false
@Environment(\.scenePhase) private var scenePhase
@State private var isActive = true
func body(content: Content) -> some View {
content
.fullScreenCover(isPresented: $isPresentedInternal) {
Group {
if isShowContent {
presentedView()
.transition(transition)
.onDisappear {
guard scenePhase != .background, scenePhase != .inactive, isActive else { return }
isPresentedInternal = false
isPresented = false
}
}
}
.onAppear {
isShowContent = true
}
.presentationBackground(.clear)
}
.transaction {
$0.disablesAnimations = true
$0.animation = animation
}
.onChange(of: isPresented) { _, newValue in
if newValue {
isPresentedInternal = true
} else {
isShowContent = false
}
}
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .background, .inactive:
isActive = false
case .active:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isActive = true
}
@unknown default:
break
}
}
}
}
Breaking Down the Code
Let’s explore how this solution works step by step:
CustomFullScreenCoverModifier:
CustomFullScreenCoverModifier
manages the presentation logic using two@State
variables:isPresentedInternal
: Controls the underlyingfullScreenCover
presentation.isShowContent
: Determines when to display the content with the specified transition.
- Key Logic:
- The
fullScreenCover
modifier is bound toisPresentedInternal
. - Inside the full-screen cover, a
Group
conditionally shows thepresentedView
only whenisShowContent
istrue
. This allows the transition (e.g., fade-in) to apply to the content. - On appearance (
onAppear
),isShowContent
is set to true, triggering the transition. - On disappearance (
onDisappear
), bothisPresentedInternal
and the externalisPresented
are reset tofalse
. - The
presentationBackground
is set to.clear
to ensure a transparent background. - A transaction disables default animations for
fullScreenCover
and applies the custom animation. - The
onChange
handler syncs the externalisPresented
binding:- When
isPresented
becomestrue
,isPresentedInternal
is set totrue
to show the cover. - When
isPresented
becomesfalse
,isShowContent
is set tofalse
to trigger the dismissal transition.
- When
- Checking the
scenePhase
allows us to prevent dismissing when View disappears.
- The
This approach separates the full-screen cover’s presentation from the content’s transition, allowing us to animate the content as desired.
Example
Here’s an example of how to use customFullScreenCover
in your SwiftUI app:
struct ContentView: View {
@State private var isPresented = false
var body: some View {
Button("Show") {
isPresented = true
}
.buttonStyle(.borderedProminent)
.tint(.purple)
.shadow(radius: 2)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.orange)
.customFullScreenCover(isPresented: $isPresented) {
ChildView(isPresented: $isPresented)
}
}
}
struct ChildView: View {
@Binding var isPresented: Bool
var body: some View {
VStack {
Button("Dismiss") {
isPresented = false
}
.buttonStyle(.borderedProminent)
.tint(.orange)
.shadow(radius: 2)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.purple)
}
}
}
Conclusion
Adding custom effects to a full-screen cover presentation can make transitions more interesting. Try experimenting with different transitions (like .scale
or .asymmetric
) and animations to find the perfect fit for your app’s design. By the way, you can integrate this code into your project via this Swift Package.
If you enjoyed this article, please feel free to follow me on my social media: