How to duplicate the animation and transition of Safari Tabs?

I am creating an in-app browser and want to support multiple “tabs”. As a starting point I have attempted to re-create the same UI/UX that safari has, a “grid” of “cards”.

I am trying to figure out how to duplicate the smooth scaling transition when you tap on the “card” in Safari and it scales/grows it to full size.

I can’t seem to make a smooth scaling of the card so that the web view’s content stays static as it scales from card to full size. Currently the website will keep rendering as the view grows and treats the webView’s content as if the view port is being resized.

Here is some of the code:

struct CardGridView: View {
    @Namespace var animation
    @State var selectedIndex: Int?
    @State var viewModels: [CardGridViewModel]

    private let gridItemLayout = [GridItem(.flexible()), GridItem(.flexible())]

    var body: some View {
        NavigationStack {
            ZStack {
                ScrollView {
                    LazyVGrid(columns: gridItemLayout, spacing: 0) {
                        ForEach(Array(viewModels.enumerated()), id: \.offset) { index, viewModel in
                            CardView(title: viewModel.title, color: viewModel.color)
                            .matchedGeometryEffect(id: index, in: animation)
                            .frame(width: 175, height: 280)
                            .padding()
                            .scaleEffect(selectedIndex == nil || selectedIndex == index ? 1 : 0.75) // Other cards will scale down slightly while selected card grows
                            .onTapGesture {
                                withAnimation {
                                    selectedIndex = index
                                }
                            }
                            .overlay { // Card's close button
                                VStack {
                                    HStack(spacing: 0) {
                                        Spacer()
                                        Button {
                                            removeCard(at: index)
                                        } label: {
                                            Image(systemName: "x.circle.fill")
                                                .font(.system(size: 20))
                                                .tint(.primary)
                                        }
                                        .padding(.top, 4)
                                        .padding(.trailing, 4)
                                    }
                                    Spacer()
                                }
                                .padding()
                            }
                        }
                    }
                }
                .onAppear {
                    selectedIndex = 0 // Default to DetailView of first item in array
                }
                .navigationTitle("Card View")
                .navigationBarTitleDisplayMode(.inline)

                if let selectedIndex { // Show DetailView once a card is selected
                    DetailView(title: viewModels[selectedIndex].title, color: viewModels[selectedIndex].color)
                        .matchedGeometryEffect(id: selectedIndex, in: animation)
                        .frame(maxWidth: .infinity, maxHeight: .infinity)
                        .ignoresSafeArea()
                }
            }
        }
    }
}

Leave a Comment