nathantannar4 / Transmission

Bridges UIKit presentation APIs to a SwiftUI API so you can use presentation controllers, interactive transitions and more.
BSD 2-Clause "Simplified" License
378 stars 13 forks source link

How to do a hero animation back to a view that wasn't the original presenter? #14

Closed bryan1anderson closed 9 months ago

bryan1anderson commented 10 months ago

Hopefully this video clears up exactly what I'm trying to say.

For example, in the Photos app you would click on a photo, and then swipe left or right. Depending on which photo you have swiped to, transitioning back will change the original.

I mostly just stole the HeroMoveTransition from the Example project.

Can you think of the best way to do this? Normally in UIKit I'd of course be able to switch. But this is a little more complicated.

One of my thoughts was to store the PresentationLinkTransition as some sort of Observable model where I can change the active view..

But yeah wanted to see if you had any initial ideas

https://github.com/nathantannar4/Transmission/assets/7976521/9d6f7044-234c-42e2-927c-a70609ce7d71

nathantannar4 commented 10 months ago

Thanks for your support in using Transmission!

Can you think of the best way to do this?

Few thoughts. Even in UIKit this can be complicated depending on the source view. For example, the photo library in the Photos app is a scrollable view, so when dismissing the source content may also need to be scrolled into view.

Many apps handle this kind of interaction where if you scroll to a different photo in the expanded view, the dismissal animation doesn't perform a hero animation but rather a simple scale / dismiss animation.

One of my thoughts was to store the PresentationLinkTransition as some sort of Observable model where I can change the active view..

This is how I would approach building such an animation.

@State var selectedPhoto: Photo.ID?
@ObservedObject var viewModel: PhotosViewModel

CustomGridView {
    ForEach(viewModel.photos) { photo in 
        Button {
            withAnimation {
                selectedPhoto = photo.id
            }
        } label: {
            PhotoView(photo)
        }
        // Custom ViewModifier that inserts a UIViewRepresentable and then fires a closure in
        // updateUIView to add the uiView to as a *weak* reference in some view model the 
        // transition can use
        .sourceViewObserver { uiView in 
            viewModel.sourceViews[photo.id] = uiView
        }  
    }
}
.presentation(
    transition: .custom(PhotoTransition(selectedPhoto: $selectedPhoto, viewModel: viewModel)),
    isPresented: $selectedPhoto.isNotNil()
) { selectedPhoto in
    // Paging view that updates the selected photo, PhotoTransition uses binding to determine
    // source view for the hero animation
    PhotoPagingView(selectedPhoto: selectedPhoto, viewModel: viewModel)
}
.onChange(of: selectedPhoto) { [oldValue = selectedPhoto] newValue in
    if newValue == nil, oldValue != nil {
        // Optionally handle scrolling to the oldValue here
    }
}
bryan1anderson commented 10 months ago

Thanks for the quick response. It's an excellent and very needed package. I think I'm tracking.. It looks like your sourceViewObserver might work similarly to how you wired up PresentationLinkModifier? You put a uiview into content.background and then I'd just need to wire up a call back. I'm immediately terrified of this haha. Do views maintain a correct coordinate space that you can reliably use later? I guess they do or else your PresentationLinkModifier wouldn't work.. Darn I was hoping I was missing something super easy about this. You're correct it's not a trivial task. Luckily I'm not worried about scrolling.

nathantannar4 commented 10 months ago

Do views maintain a correct coordinate space that you can reliably use later?

Yes the UIView will maintain the correct coordinate space, which is why I recommend this instead of a GeometryReader.