Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using SwiftUI's matchedGeometryEffect in complex UI

In my app, I want to create a 'hero' animation between a card and a full screen overlay, which matchedGeometryEffect seems suited for. However, no matter what I try, I can't get the animation to work as expected and it doesn't look at all like the usual matchedGeometryEffect animations. Here's what it looks like so far. This is what I currently have: (apologies for the tons of code, but it's necessary since for a trivially simple view, it works fine)

Something.swift

struct Something: Identifiable {
    let id = UUID()
    let image: Image
}

ContentView.swift

struct ContentView: View {
    @Namespace var namespace
    
    let items: [Something] = [
        Image("a"), Image("b")
    ].map { Something(image: $0 )}
    
    @State var selectedItem: Something?

    var body: some View {
        ZStack {
            VStack {
                ScrollView {
                    VStack(alignment: .leading) {
                        ForEach(items) { item in
                            CardView(
                                image: item.image,
                                namespace: namespace,
                                isSource: self.selectedItem == nil,
                                id: item.id
                            )
                            .background(Color.white)
                            .contentShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
                            .zIndex(1)
                            .onTapGesture {
                                withAnimation(.spring()) {
                                    self.selectedItem = item
                                }
                            }
                        }
                    }
                }
            }
            .overlay(EmptyView())
            
            if let item = selectedItem {
                EventView(
                    image: item.image
                ) {
                    self.selectedItem = nil
                }
                .matchedGeometryEffect(id: item.id, in: namespace, isSource: false)
                .zIndex(2)
            }
        }
        .animation(.spring())
        .transition(.scale)
    }
}

CardView.swift

struct CardView: View {
    let image: Image
    let namespace: Namespace.ID
    let isSource: Bool
    let id: UUID

    var body: some View {
        VStack(alignment: .leading) {
            ZStack(alignment: .bottomTrailing) {
                image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(height: 225)
                    .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
                    .matchedGeometryEffect(id: id, in: namespace, isSource: isSource)
            }
        }
    }
}

EventView.swift

struct EventView: View {
    let image: Image
    let onDismiss: () -> Void
        
    var body: some View {
        image
            .resizable()
            .aspectRatio(contentMode: .fill)
            .edgesIgnoringSafeArea(.all)
            .onTapGesture(perform: onDismiss)
    }
}

Any suggestions on what to add or change to get it to work properly would be super appreciated, thanks!!

like image 212
Richard Robinson Avatar asked Apr 16 '26 11:04

Richard Robinson


1 Answers

What I did for the same requirement was to add properties: .position to the matchedGeometryEffect. Then you need to specify "how" you transition from one view (say, a thumbnail card view) to another (say, a full screen card view). This is accomplished through custom transitions, such as this:

extension AnyTransition
    {
    // This transition will pass a value (0.0 - 1.0), indicating how much of the
    // transition has passed. To communicate with the view, it will
    // use the custom environment key .modalTransitionPercent
    // it will also make sure the transitioning view is not faded in or out and it
    // stays visible at all times.
    static var modal: AnyTransition
        {
        AnyTransition.modifier(active: ThumbnailExpandedModifier(pct: 0), identity: ThumbnailExpandedModifier(pct: 1))
        }

    struct ThumbnailExpandedModifier: AnimatableModifier
        {
        var pct: CGFloat
        
        var animatableData: CGFloat
            {
            get { pct }
            set { pct = newValue }
            }

        func body(content: Content) -> some View
            {
            return content
                .environment(\.modalTransitionPercent, pct)
                .opacity(1)
            }
        }
  }

extension EnvironmentValues
    {
    var modalTransitionPercent: CGFloat
        {
        get { return self[ModalTransitionKey.self] }
        set { self[ModalTransitionKey.self] = newValue }
        }
    }

public struct ModalTransitionKey: EnvironmentKey
    {
    public static let defaultValue: CGFloat = 0
    }
like image 163
Gene Loparco Avatar answered Apr 18 '26 00:04

Gene Loparco



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!