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:
(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?
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))
}