swiftswiftuiz-indexgeometryreaderswiftui-scrollview

Update zIndex based on scroll position in ScrollView


This code shows a cover-flow-style view where you can scroll through the images in a Scroll View, while the images itself come from an array. The spacing is -100 to let the other pictures not in the middle of the screen to appear behind the current picture. Whilst this is working well on the left hand side, on the right hand side of the current image, the other images in the ScrollView are intersecting:enter image description here (paper plane is in the middle and download symbols are in front of it)

Using zIndex I would like to dynamically update the view when the user is scrolling through the gallery. I tried using GeometryReader with @State var selectedItemInd but that didn't quite work for me as the view didn't update. I believe every time the user scrolls, I would not only need to know what item of is in the middle position, but also, where all the other items are.

import SwiftUI
import MusicKit

struct ContentView: View {
#if false
    let images = ["Ghost - Single", "Burre - Single", "Butterfly 3001", "Tension (Deluxe)", "Shreya Ghoshal_ Best of Me", "Life - EP", "Liebeskummer lohnt sich nicht"]
#else
    let images = ["externaldrive.fill.badge.checkmark", "square.and.arrow.up.fill", "square.and.arrow.up.circle", "folder.circle", "paperplane.fill", "square.and.arrow.down", "square.and.arrow.down.fill", "doc.badge.gearshape", "square.and.arrow.up.on.square.fill"]
    let colours = [Color.red, Color.blue, Color.green, Color.yellow, Color.orange, Color.purple, Color.pink, Color.cyan, Color.indigo]
#endif
    var itemWidthHeight: CGFloat = CGFloat(200)
    @StateObject var albums = MusicStorage.shared
    @StateObject var authorizationObject = AuthorizationHandler.shared
//    @State var selectedItemInd: Int = 0
    
    var body: some View {
        VStack{
            GeometryReader { geo in
                ScrollView(.horizontal, showsIndicators: false){
                    HStack(spacing: -100) {
                        ForEach(0..<images.count) { s in
                            Image(systemName: images[s])
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .frame(width: itemWidthHeight, height: itemWidthHeight)
                                .background(colours[s])
                                .visualEffect { effect, proxy in
                                    let frame = proxy.frame(in: .scrollView(axis: .horizontal))
                                    let distance = frame.minX
//                                    print(distance)
                                    
                                    return effect
                                    //only rotate at left or right swipe, not both at once
                                    // distance to left is negative, distance to right is positive value
                                        .rotation3DEffect(Angle(degrees: distance < 0 ? min(-distance * 0.3, 40) : 0), axis: (x: 0.0, y: 1.0, z: 0.0))
                                        .rotation3DEffect(Angle(degrees: distance > 0 ? max(-distance * 0.3, -40) : 0), axis: (x: 0.0, y: 1.0, z: 0.0))
                                }
//                                .frame(width: itemWidthHeight, height: itemWidthHeight, alignment: .center)
                        }
                    }
                    .scrollTargetLayout()
                }
                .frame(width: geo.size.width, height: geo.size.height, alignment: .center)
                .contentMargins((geo.size.width / 2) - itemWidthHeight / 2) // scroll view item should be in middle position
                .scrollTargetBehavior(.viewAligned(limitBehavior: .automatic))
            }
        }
        .onAppear() {
            albums.loadAlbums()
        }
        // Sheet is presented if authorization failed (init)
        .sheet(isPresented: $authorizationObject.isSheetPresented) {
            AuthorizationSheet(musicAuthorizationStatus: $authorizationObject.isAuthorizedForMusicKit)
                .interactiveDismissDisabled()
        }
    }
}


#Preview {
    ContentView()
}

But maybe I'm wrong and there's a simple solution. Any ideas?


Solution

  • The main problem is that .zIndex is not supported as a VisualEffect. So the modifier .visualEffect cannot be used to apply a z-index.

    As a workaround, you can compute the scrolled distance for a view based on the index position of the view and the amount that the ScrollView has been scrolled. The modifier .onScrollGeometryChange can be used to measure the amount that the ScrollView has been scrolled.

    Here is the updated example to show it working:

    @State private var scrollOffset = CGFloat.zero
    
    GeometryReader { geo in
        ScrollView(.horizontal, showsIndicators: false){
            HStack(spacing: -100) {
                ForEach(Array(images.enumerated()), id: \.offset) { s, image in
                    let distance = (CGFloat(s) * (itemWidthHeight - 100)) - scrollOffset
                    let degrees = max(-40, min(40, -distance * 0.3))
                    Image(systemName: image)
                        .resizable()
                        .scaledToFit()
                        .frame(width: itemWidthHeight, height: itemWidthHeight)
                        .background(colours[s])
                        .rotation3DEffect(.degrees(degrees), axis: (x: 0.0, y: 1.0, z: 0.0))
                        .zIndex(max(0, 1000.0 - abs(distance)))
                }
            }
            .scrollTargetLayout()
        }
        .onScrollGeometryChange(for: CGFloat.self) { proxy in
            proxy.contentOffset.x + proxy.contentInsets.leading
        } action: { _, newOffset in
            scrollOffset = newOffset
        }
        .frame(width: geo.size.width, height: geo.size.height)
        .contentMargins((geo.size.width / 2) - itemWidthHeight / 2) // scroll view item should be in middle position
        .scrollTargetBehavior(.viewAligned(limitBehavior: .automatic))
    }
    

    Animation