swiftswiftui

Context Menu Identity view transition Swiftui


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()
       }
   }
}

Solution

  • 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:

    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")
        }
    }
    

    Animation