SwiftUI Hero Animations with NavigationTransition
Replicating the App Store Hero Animation
We all know and love the App Store’s hero animation - it’s great for visually rich UIs, such as the App Store’s Today view, the Apple TV app, the Explore stream in AirBnB, and many other apps.
With SwiftUI’s NavigationTransition
protocol, introduced in iOS 18, implementing a hero animation is a matter of just three lines of code.
In this article, you will learn how to implement a hero animation that looks similar to the one in the App Store’s Today view. Achieving this look and feel requires more than just three lines of code, so we will also look into making this a reusable SwiftUI component.
Zooming from zero to hero
Let’s imagine we’re working on an app that allows users to browse gradients. The following snippet displays a list of meshed gradients. The user can navigate to a details screen by tapping on any of the cards.
This drill-down navigation is common for many iOS apps, and has been around since the very first version of iOS. It’s well suited for navigating from list items to their details (like in the Contacts app). However, for our beautiful meshed gradients (or a movie poster, a product card, or any other visually rich views), it doesn’t feel snazzy enough.
Thanks to SwiftUI’s new NavigationTransition
protocol and its associated view modifiers, we can make this more appealing with just a few lines of code:
There are three main steps to implementing a hero animation:
- Define a namespace to allow SwiftUI to track the identity of the source and target view across the animation.
- Add the
matchedTransitionSource
view modifier to the view you want to transition from, and specify the namespace you defined in the first step, and anid
for the view within that namespace. - On the target view, use the
navigationTransition
view modifier, and specify the animnation you want to use. At the moment, the only animations available are .automatic
(which is the default slide-over animation), and.zoom
, which enables the hero animation. When using.zoom
, you need to specify the same namespace andid
you used on the source view.
Adjusting the appearance of the source view
The design of the source view leaves a bit to be desired (even though we’re using a gradient…), but thankfully there is an overloaded version of the . matchedTransitionSource
view modifier that we can use to adjust the appearance of the source view.
Inside the configuation
closure of the .matchedTransformationSource
view modified, we can use the source
parameter to adjust the appearance of the source view.
It’s worth noting that source
does not conform to View
and is not a direct representation of the source view itself. Instead, it conforms to the EmptyMatchedTransitionSourceConfiguration
protocol.
This means we can only control the shadow, background color, and clip shape of the source view. It’s also not possible to wrap the source view inside another view, as the return type of the closure is MatchedTransitionSourceConfiguration
.
It’s surprising that the clipShape
modifier only supprts RoundedRectangle
- so if you were hoping to use UnevenRoundedRectangle
for some fancy asymtrical rounded corners, you might need to file a feedback with Apple to get supports for this in one of the next betas.
But even with the limited number of view modifiers, we can achieve a much more appealing visual appearance of the source view.
Adding a dismiss button
If you compare the details view with the details screen of the App Store’s Today section, you will notice that the App Store details screen doesn’t have a back button - instead, the user can dismiss the screen by either tapping on the dismiss button in the top right corner, or by pulling down on the screen.
Let’s implement the dismiss button first.
SwiftUI’s zoom
animation can transition from any view to any other view, so we can create a wrapper view that hides the nvigatino bar and overlays the GradientView
with a dismiss button.
To make the code easier to read and more maintainable, it’s a good idea to create a separate view for this. This will also allow us to use the Dismiss
action from the detail view’s environment.
By wrapping the content in a ZStack
, we can place the dismiss button on top of the main content. We can now get rid of the navigation back button and the status bar, and finally use ignoresSafeArea()
to allow the content to take up the entire space of the screen.
We can now use the DismissableView
at the call site:
Dragging down to dismiss
In the App Store Today section, users can also drag down the details view to dismiss it. To implement this feature, we can make use of SwiftUI’s new onScrollGeometryChange
view modifier. It provides an elegant way to trigger actions based on changes of a scroll view’s geometry.
In our case, we want to dismiss the view when the user pulls the view down for a certain amount.
onScrollGeometryChange
takes three parameters: the first one specifies the type we want to map scroll geometry changes to. We want to detect whether or not we should dismiss the view, so this is a Bool
.
The second parameter is a closure in which we map scroll geometry changes to the type we specified in the first parameter. Here, we return true
if the user drags the view down for more than 50 pixels.
The final parameter is a clousre that will be called when the value returned from the transform
closure changes. It receives both the old and new value. In our case, we just need the new value - if it’s true
, we know that the user has dragged the view down beyond the tear-off point, and we can dismiss the view.
Scaling down
To implement the scaling effect of the App Store’s Today details view, we can apply what we’ve learned in the previous section.
This time, we want to translate the scroll geometry changes to a scaleFactor
of type CGFloat
so we can scale the entire view accordingly. In addition, we change the opacity of the dissmiss button in response to the amount the user drags down.
Disable highlighting the source view
The final tweak is to get rid of the highlight SwiftUI applies to the source view when the user taps it. To achieve this, we can define a new ButtonStyle
that doesn’t implement any highlighting:
Apply the style on the NavigationLink
like so:
Conclusion
SwiftUI 16 makes it easier than ever before to implement hero animations that allow us to implement UIs that delight our users. With just three lines of code, we were able to add a transition that looks similar to the App Store’s Today view hero animation. And with a little bit of extra effort, we were able to improve the user experience even more.
If you’re interested in learning more about building custom SwiftUI components, check out my interactive tutorial Building Reusable SwiftUI Components, and don’t forget to follow me on Twitter - I regularly post about SwiftUI, Firebase, AI, and other topics.
Improve your app's UX with SwiftUI's task view modifier
Mastering the art of the pause
Styling SwiftUI Views
How does view styling work?
Previewing Stateful SwiftUI Views
Interactive Previews for your SwiftUI views
Asynchronous programming with SwiftUI and Combine
The Future of Combine and async/await
Building a Custom Combine Operator for Exponential Backoff
Make your Combine code reusable