gh123man/SwiftUI-LazyPager

Add support for matched geometry effect

Yonodactyl opened this issue · 6 comments

I have started using this package in a personal project of mine and tried to think of a way we might be able to use matchedGeometryEffect with the image.

Any hopes of getting this to include a feature like that, or maybe not on the roadmap yet? If I have some free time I can take a crack at getting it implemented

Hey!
I haven't considered this before, but after reading the docs it seems worth investigating.

I'm a bit busy at the moment, but can try to give it a look in the next week or 2. Otherwise, I am always very happy to accept PRs on the project!

Hey! Any progress on this? @gh123man did you ever look into this?

Hi all,

I took a good stab at this today. It's unfortunately far from trivial from what I can gather. This is not a lot of documentation on how to couple matchedGeometryEffect with UIKit components.

I have a feeling that some of the answers lie in this blog post.

I did some experimenting, and this code gets your "kind close" (can plug it into the sample project).
Note that this code isolates the root of the issue with UIViewControllerRepresentable and is not dependent on LazyPager.

struct SwiftUIViewWrapper<Content: View>: UIViewControllerRepresentable {
    @ViewBuilder var swiftUIView: Content
    
    func makeUIViewController(context: Context) -> UIHostingController<Content> {
        let hostingController = UIHostingController(rootView: swiftUIView)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        return hostingController
    }
    
    func updateUIViewController(_ uiViewController: UIHostingController<Content>, context: Context) {
    }
}

struct Experiment: View {
    @Namespace private var animation
    @State private var isFlipped = false
    
    var body: some View {
        VStack {
            if !isFlipped {
                Image("nora1")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .matchedGeometryEffect(id: "nora1", in: animation, isSource: true)
                    .frame(width: 150, height: 150)
            } else {
                SwiftUIViewWrapper {
                    Image("nora1")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .matchedGeometryEffect(id: "nora1", in: animation)
                }
            }
        }
        .onTapGesture {
            withAnimation {
                isFlipped.toggle()
            }
       }
    }
}

You will notice that the true case doesn't work - no animation.
But the false case works, and the image zooms out to the thumbnail.

At the end of the day, we should only have to care about the internal geometry of the loaded subview - which is a SwiftUI view and responds to events in the SwiftUI hierarchy. However we are missing something in UIViewControllerRepresentable to fully bridge the behaviors of matchedGeometryEffect.

I don't think I'm going to make much more progress on this in the short term, but I wanted to post what I learned. Maybe someone else can pick it up.

new.mov

(The animations are intentionally slowed down in the video)

I had some ideas an took another stab at this. I think it's 90% there but still has a few issues.
The good news - this is 100% a SwiftUI wrapper with no library modifications - so you can replicate this right now.

The bad news:

  1. Dismiss animation appears behind the other grid view elements
  2. Have to flip from the image to the pager after the animation is done. There is an API for this in iOS 17, but not lower :( (So I used a timer in this demo).

Here is the source

In order to avoid swapping views - we need to figure out a couple things:

  • how can we update the internal UIScrollView during a frame change animation? This causes the pages to be wrong while the animation is playing.
  • Setting the right source frame for the dismiss animation is difficult - I haven't figured it out yet.

Currently both of these are mitigated by switching between a static image and the pager once the animation is done playing.

@gh123man this looks really good!

Dismiss animation appears behind the other grid view elements

Have you tried using zIndex() to fix this?

Have you tried using zIndex() to fix this?

IIRC - yeah. I couldn't quite figure it out.
To be honest this isn't a feature I particularly require at the moment so hoping a contributor can pick up and run with it :)
Hopefully my sample code is a good starting point.

Eventually it would be nice to fully support scaling animations + pull to dismiss (except snap back to the grid instead of sliding off screen).