Search code examples
swiftuiswiftui-tabviewswiftui-animation

SwiftUI Matched Geometry Effect not working with multiple ForEach's


I am basically trying to recreate the photos app. In doing so, matched geometry effect should be the best way to recreate the animation that is used in the photos app when you click on an image/close it. However, on opening of an image it only does half of the animation. When closing the image the animation is only contained to the lazyvgrid individual image not the whole view. Also the first image of the gallery simply does not animate when closing.

Gallery view is made from a lazyvgrid and for each, full screen view is made of a tabview and for each.

Here is what it looks like:

Example

Main view:

struct ImageSelectorView: View {
    @EnvironmentObject var isvm: ImageSelectorViewModel
    @Namespace var namespace
    @State private var selectedImages: [SelectedImagesModel] = []
    @State private var selectedImageID: String = ""
    @State private var liveEventID: String = ""
    @State var showImageFSV: Bool = false
    @State var showPicker: Bool = false
    @Binding var liveEvent: [EventModel]
    public var pickerConfig: PHPickerConfiguration {
        var config = PHPickerConfiguration(photoLibrary: .shared())
        config.filter = .any(of: [.images, .livePhotos, .videos])
        config.selectionLimit = 10
        return config
    }
    private var gridItemLayout = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
    private let viewWidth: CGFloat = UIScreen.main.bounds.width
    private let viewHeight: CGFloat = UIScreen.main.bounds.height
    private let viewHPadding: CGFloat = 30
    
    init(liveEvent: Binding<[EventModel]>) {
        self._liveEvent = liveEvent
    }
    
    var body: some View {
        ZStack {
            Color.theme.background.ignoresSafeArea()
            VStack {
                ZStack {
                    ScrollView(.vertical, showsIndicators: true) {
                        LazyVGrid(columns: self.gridItemLayout, alignment: .center, spacing: 0.5) {
                            ForEach(self.liveEvent[0].eventImages.indices) { image in
                                GalleryImage(selectedImageID: self.$selectedImageID, showImageFSV: self.$showImageFSV, image: self.liveEvent[0].eventImages[image], namespace: self.namespace)
                            }
                        }
                    }
                    
                    if self.showImageFSV {
                        KFImagesFSV(eventImages: self.$liveEvent[0].eventImages, showImageFSV: self.$showImageFSV, selectedImageID: self.$selectedImageID, namespace: self.namespace)
                    }
                }
            }
        }
    }
}

Gallery Image View:

struct GalleryImage: View {
    @Binding var selectedImageID: String
    @Binding var showImageFSV: Bool
    public var image: EventImage
    public var namespace: Namespace.ID
    private let viewWidth: CGFloat = UIScreen.main.bounds.width
    private let viewHeight: CGFloat = UIScreen.main.bounds.height
    var body: some View {
        Button {
            DispatchQueue.main.async {
                withAnimation(.spring()) {
                    self.selectedImageID = image.id
                    if self.selectedImageID == image.id {
                        self.showImageFSV.toggle()
                    }
                }
            }
        } label: {
            KFImage(URL(string: image.url))
                .placeholder({
                    Image("topo")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                })
                .loadDiskFileSynchronously()
                .cacheMemoryOnly()
                .fade(duration: 0.2)
                .resizable()
                .matchedGeometryEffect(id: self.selectedImageID == image.id ? "" : image.id, in: self.namespace)
                .aspectRatio(contentMode: .fill)
                .frame(width: (self.viewWidth/2.9) - 3, height: (self.viewWidth/2.9) - 3)
                .clipped()
        }
    }
}

Image full screen view (tab view):

struct KFImagesFSV: View {
    @Binding var eventImages: [EventImage]
    @Binding var showImageFSV: Bool
    @Binding var selectedImageID: String
    public var namespace: Namespace.ID
    private let viewWidth: CGFloat = UIScreen.main.bounds.width
    private let viewHeight: CGFloat = UIScreen.main.bounds.height
    private let viewHPadding: CGFloat = 30
    var body: some View {
        ZStack {
            TabView(selection: self.$selectedImageID) {
                ForEach(self.eventImages.indices) { image in
                    KFImage(URL(string: self.eventImages[image].url))
                        .placeholder({
                            Image("topo")
                                .resizable()
                                .aspectRatio(contentMode: .fill)
                        })
                        .loadDiskFileSynchronously()
                        .cacheMemoryOnly()
                        .fade(duration: 0.2)
                        .resizable()
                        .tag(self.eventImages[image].id)
                        .matchedGeometryEffect(id: self.selectedImageID == self.eventImages[image].id ? self.eventImages[image].id : "", in: self.namespace)
                        .aspectRatio(contentMode: .fit)
                        .frame(width: self.viewWidth, height: self.viewHeight)
                }
            }
            .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
        }
    }
}

Solution

  • This is how far I got. The zoom out from FullScreenView to GalleryView works. the only thing that doesn't, is a clean zoom in into the TabView. I suppose this is because of the wrapping by TabView.

    enter image description here

    struct ImageStruct: Identifiable {
        let id = UUID()
        var image: String = ""
    }
    
    let imagesArray = [
        ImageStruct(image: "image1"),
        ImageStruct(image: "image2"),
        ImageStruct(image: "image3"),
        ImageStruct(image: "image4"),
        ImageStruct(image: "image5"),
        ImageStruct(image: "image6"),
        ImageStruct(image: "image7"),
        ImageStruct(image: "image8")
    ]
    
    
    
    struct ContentView: View {
        
        @Namespace var ns
        @State private var selectedImage: UUID?
        
        var body: some View {
    //        ZStack {
                if selectedImage == nil {
                    GalleryView(selectedImage: $selectedImage, ns: ns)
                } else {
                    FullScreenView(selectedImage: $selectedImage, ns: ns)
                }
    //        }
        }
    }
    
    
    struct GalleryView: View {
        
        @Binding var selectedImage: UUID?
        var ns: Namespace.ID
        
        private let columns = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
        
        var body: some View {
            VStack {
                ScrollView(.vertical, showsIndicators: true) {
                    
                    LazyVGrid(columns: columns) {
                        
                        ForEach(imagesArray) { image in
                            
                            Color.clear.overlay(
                                Image(image.image)
                                    .resizable()
                                    .aspectRatio(contentMode: .fill)
                                    .matchedGeometryEffect(id: image.id, in: ns, isSource: true)
                            )
                                .clipped()
                                .aspectRatio(1, contentMode: .fit)
                            
                                .onTapGesture {
                                    withAnimation {
                                        selectedImage = image.id
                                    }
                                }
                        }
                    }
                }
            }
        }
    }
    
    
    struct FullScreenView: View {
        
        @Binding var selectedImage: UUID?
        var ns: Namespace.ID
        
        init(selectedImage: Binding<UUID?>, ns: Namespace.ID) {
            print(selectedImage)
            self._selectedImage = selectedImage
            self.ns = ns
            // initialize selctedTab to selectedImage
            self._selectedTab = State(initialValue: selectedImage.wrappedValue ?? UUID())
        }
        
        
        @State private var selectedTab: UUID
        
        var body: some View {
            
            TabView(selection: $selectedTab) {
                
                ForEach(imagesArray) { image in
                    
                    Image(image.image)
                        .resizable()
                        .scaledToFit()
                        // ternary applying effect only for selected tab
                        .matchedGeometryEffect(id: image.id == selectedTab ? selectedTab : UUID(),
                                               in: ns, isSource: true)
                    
                        .tag(image.id)
                    
                        .onTapGesture {
                            withAnimation {
                                selectedImage = nil
                            }
                        }
                }
            }
            .tabViewStyle(.page)
        }
    }