I am trying to replicate the view transition as seen in this video. My code uses a basic navigation link inside the context menu, but it doesn't do an identity transition. How can I get this behavior?
iMessage video: https://drive.google.com/file/d/12Grme6_mgeDB2a7kvnxFcyugsOiotkbz/view?usp=sharing
What I mean by identity transition (twitter, WhatsApp, and many other apps use this in context menus). When the context menu is presented the preview is an actual view (not interactive, and smaller scale) when you click on the preview itself, the preview expands to the full screen and it becomes a full view that is interactive. Its just like navigating to a view with a navigation link (you even have edge swipe ability) but its animated from the context menu preview.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach(0..<10) { index in
Text("Item \(index)")
.contextMenu {
NavigationLink(destination: DetailView(itemNumber: index)) {
Text("View Details")
Image(systemName: "info.circle")
}
Button(action: {
// Perform an action
}) {
Text("Delete")
Image(systemName: "trash")
}
}
}
}
.navigationTitle("Context Menu Example")
}
}
}
struct DetailView: View {
let itemNumber: Int
var body: some View {
VStack {
Text("Detail View for Item \(itemNumber)")
}
.navigationTitle("Detail View")
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
If I understand correctly, the main issue with your example code is that the NavigationLink
in the context menu does not work.
This can be fixed by using a NavigationStack
with a path
. NavigationView
is deprecated anyway, if your target is iOS 16 or later then there is no reason to be using a NavigationView
any more.
The updated example below shows it working. Some notes:
To add some variation to the detail views, four images are used. To see it working, you need to add 4 images to your asset catalog, named "image1" to "image4".
Navigation is performed programmatically by using a button which appends to the navigation path. In fact, I found that a NavigationLink
with a value
parameter also works when running on a simulator with iOS 18.0. So either technique can probably be used, but you might want to test on earlier iOS versions to be sure.
The transition from the context menu to the detail view is using .navigationTransition
with .zoom
style. This requires iOS 18. If your target is an earlier iOS version then just comment out the use of .matchedTransitionSource
and .navigationTransition
. The transition will then be the usual wipe-style navigation transition.
The transition works slightly differently if you attach .matchedTransitionSource
to the button in the context menu, instead of to the Text
item in the List
. You might like to experiment, to see which kind of animation you prefer. To see exactly what is happening, try running in a simulator with animations slowed.
I found it was not possible to add a tap gesture to the preview in the context menu. If you want to do this too, you could consider using a custom context menu. See this answer for an example of how it can be done (it was my answer).
struct ContentView: View {
enum NavDestination: Hashable {
case detail(itemNumber: Int)
}
@State private var navPath = [NavDestination]()
@Namespace private var ns
var body: some View {
NavigationStack(path: $navPath) {
List {
ForEach(0..<10) { index in
Text("Item \(index)")
.contextMenu {
Button("View Details", systemImage: "info.circle") {
navPath.append(NavDestination.detail(itemNumber: index))
}
// Alternative technique:
// NavigationLink(value: NavDestination.detail(itemNumber: index)) {
// Label("View Details", systemImage: "info.circle")
// }
Button("Delete", systemImage: "trash") {
// Perform an action
}
} preview: {
VStack {
Image("image\((index % 4) + 1)")
.resizable()
.scaledToFit()
.frame(idealWidth: 200)
Text("Preview \(index)")
}
.padding()
.background(Color(white: 0.8))
}
.matchedTransitionSource(id: index, in: ns)
}
}
.navigationTitle("Context Menu Example")
.navigationDestination(for: NavDestination.self) { dest in
switch dest {
case .detail(let itemNumber):
DetailView(itemNumber: itemNumber)
.navigationTransition(.zoom(sourceID: itemNumber, in: ns))
}
}
}
}
}
struct DetailView: View {
let itemNumber: Int
var body: some View {
VStack {
Text("Detail View for Item \(itemNumber)")
.font(.title2)
Image("image\((itemNumber % 4) + 1)")
.resizable()
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 20))
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationTitle("Detail View")
}
}