Search code examples
xcodeswiftuiswiftui-tabviewlazyvgrid

How to connect LazyVGrid cells with the corresponding full screen images in a TabView (Xcode 13, iOS15)


I'm having a hard time matching the correct grid cell when zooming out from the corresponding tab.

Just starting to learn and I should definitely go through a few more tutorials. If you want to lend a hand here, thank you in advance.

This is the code:

Models

struct Thumbnail: Identifiable {
    let id = UUID()
    var name: String
}

struct Wallpaper: Identifiable {
    var id = UUID()
    var name: String
}

let thumbnailSet = (1...50).map { Thumbnail(name: "iridisfera-thumbnail-\($0)") }
let wallpaperSet = (1...50).map { Wallpaper(name: "iridisfera-wallpaper-\($0)") }

Gallery (I removed my selectedTab experiments so you can directly insert your code)

struct GalleryView: View {
    @Namespace var namespace
    @State private var fullScreen: Int? = nil
    @State private var selectedTab = 0
    
    let columns = [GridItem(.flexible(), spacing: 2), GridItem(.flexible())]
    
    var body: some View {
        ZStack {
            // MARK: GRID VIEW
            ScrollView(showsIndicators: false) {
                LazyVGrid(columns: columns, spacing: 2) {
                    ForEach(thumbnailSet.indices) { index in
                        
                        let fullscreenIndex = fullScreen
                        if index == fullscreenIndex {
                            Color.clear
                        } else {
                            Image(thumbnailSet[index].name)
                                .resizable()
                                .aspectRatio(0.5, contentMode: .fit)
                                .matchedGeometryEffect(id: index, in: namespace)
                                .onTapGesture {
                                    withAnimation(.interpolatingSpring(mass: 0.2, stiffness: 34, damping: 4)) {fullScreen = index}
                                }
                        }
                    }
                }
            }
            .ignoresSafeArea()
            
            // MARK: FULL SCREEN VIEW
            if let fullscreenIndex = fullScreen {
                TabView(selection: $selectedTab) {
                    ForEach(wallpaperSet.indices) { index in
                        
                        Image(wallpaperSet[index].name)
                            .resizable()
                            .ignoresSafeArea()
                            .scaledToFill()
                    }
                }
                .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
                .matchedGeometryEffect(id: fullscreenIndex, in: namespace)
                .ignoresSafeArea()
                .zIndex(1)
                .onTapGesture {
                    withAnimation(.interpolatingSpring(mass: 0.1, stiffness: 28, damping: 4)) {fullScreen = nil}
                }
            }
        }
    }
}

Solution

  • This code shows the general approach. The TabView has to be in a separate struct, so it can be initialized with the already selected tab. The images in TabView need to have .tag() to identify them for the selection.
    It does not work perfectly yet, as your images are not identifiable, but it should give you the direction. Working with the indices alone is unsafe, so you should put them in an Identifiable struct and select by the id.

    struct GalleryView: View {
        
        @Namespace var namespace
        @State private var fullScreen: Int? = nil
        
        let columns = [GridItem(.flexible(), spacing: 2), GridItem(.flexible())]
        
        var body: some View {
            ZStack {
                // MARK: GRID VIEW
                ScrollView(showsIndicators: false) {
                    LazyVGrid(columns: columns, spacing: 2) {
                        ForEach(thumbnailSet.indices) { index in
                            
                            if index == fullScreen {
                                Color.clear
                            } else {
                                Image(thumbnailSet[index].name)
                                    .resizable()
                                    .aspectRatio(1, contentMode: .fill)
                                    .matchedGeometryEffect(id: index, in: namespace)
                                    .onTapGesture {
                                        withAnimation {
                                            fullScreen = index
                                        }
                                    }
                            }
                        }
                    }
                }
                .ignoresSafeArea()
                
                // MARK: FULL SCREEN VIEW
                if fullScreen != nil {
                    FullscreenTabView(selectedImage: $fullScreen, ns: namespace)
                }
            }
        }
    }
    
    
    struct FullscreenTabView: View {
        
        @Binding var selectedImage: Int?
        var ns: Namespace.ID
        
        init(selectedImage: Binding<Int?>, ns: Namespace.ID) {
            self._selectedImage = selectedImage
            self.ns = ns
            // initialize selctedTab to selectedImage
            self._selectedTab = State(initialValue: selectedImage.wrappedValue ?? 0)
        }
        
        @State private var selectedTab: Int
        
        var body: some View {
            TabView(selection: $selectedTab) {
                
                ForEach(wallpaperSet.indices) { index in
                    
                    Image(wallpaperSet[index].name)
                        .resizable()
                        .tag(index) // << the images in TabView need tags to identify
                        .ignoresSafeArea()
    
                        .matchedGeometryEffect(id: index == selectedTab ? selectedTab : 0,
                                               in: ns, isSource: true)
                                    
                        .onTapGesture {
                            withAnimation {
                                selectedImage = nil
                            }
                        }
                }
            }
            .ignoresSafeArea()
            .tabViewStyle(.page(indexDisplayMode: .never) )
        }
    }