Lory Huz
Lory Huz

Reputation: 1508

scrollTo conflict with scrollTargetBehavior?

I try to replicate the snapchat camera filters scroll in SwiftUI.

When I scroll manually it's working well thanks to new iOS 17 scroll API, it's centering on each circles. But on tap it's not working, scrollTo method don't keep the center of the circles, I tried many anchors but can't make it work for all circles. Sometimes it's work for the last circles but not the first ones.

Like you can see below, on the beginning of the gif I'm scrolling witg gesture (working), then I tap (not working anymore).

enter image description here

If I remove scrollTargetBehavior(.viewAligned) and scrollTargetLayout the scrollTo is working.

Here is a simplified code to reproduce the issue:


import SwiftUI

@main
struct scrollApp: App {
    var body: some Scene {
        WindowGroup {
            GeometryReader(content: { outerProxy in
                ScrollViewReader(content: { proxy in
                    ScrollView(.horizontal, showsIndicators: false) {
                        LazyHStack(spacing: 0) {
                            ForEach(0...10, id: \.self) { index in
                                Circle()
                                    .frame(width: 100, height: 100)
                                    .foregroundStyle(.primary)
                                    .id(index)
                                    .onTapGesture {
                                        print("tap \(index)")
                                        withAnimation {
                                            proxy.scrollTo(index, anchor: .center)
                                        }
                                    }
                            }
                        }
                        .padding(.horizontal, outerProxy.size.width * 0.5 - 100/2)
                        .scrollTargetLayout()
                    }
                    .scrollTargetBehavior(.viewAligned)
                    .overlay {
                        Circle()
                            .stroke(.red, lineWidth: 10)
                            .frame(width: 100, height: 100)
                    }
                })
            })
            
        }
    }
}

Only pure SwiftUI solutions will be validated, I already make it works on UIKit.

Upvotes: 2

Views: 579

Answers (1)

britafilter
britafilter

Reputation: 11

I encountered the same problem and was able to solve it by doing the following:

  • Remove the ScrollViewReader.
  • Use .scrollPosition(id: Binding<(Hashable)?>, anchor: UnitPoint?) to programatically set the scroll position.
  • Replace LazyHStack with HStack (not sure why but it won't work with LazyHStack).
  • Apply .containerRelativeFrame(.horizontal, count: 3, span: 1, spacing: 0) to the circle within your ForEach. This dynamically spaces the circles so that 3 circles evenly fill up the width of the ScrollView. You can change this number but make sure it's an odd number to ensure there is always a circle in the center.
  • Apply a GeometryReader after the .containerRelativeFrame to get its size so that the padding applied to the HStack can be properly calculated.
struct ContentView: View {
    @State private var selectedIndex: Int?
    @State private var childSize: CGSize = .zero

    var body: some View {
        GeometryReader(content: { outerProxy in
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 0) {
                    ForEach(0...10, id: \.self) { index in
                        Circle()
                            .frame(width: 100, height: 100)
                            .foregroundStyle(.primary)
                            .id(index)
                            .onTapGesture {
                                print("tap \(index)")
                                withAnimation {
                                    selectedIndex = index
                                }
                            }
                            .containerRelativeFrame(.horizontal, count: 3, span: 1, spacing: 0)
                            .overlay(
                                GeometryReader { geo in
                                    Color.clear
                                        .onAppear {
                                            childSize = geo.size
                                        }
                                }
                            )
                    }
                }
                .padding(.horizontal, outerProxy.size.width * 0.5 - childSize.width * 0.5)
                .scrollTargetLayout()
            }
            .scrollPosition(id: $selectedIndex, anchor: .center)
            .scrollTargetBehavior(.viewAligned)
            .overlay {
                Circle()
                    .stroke(.red, lineWidth: 10)
                    .frame(width: 100, height: 100)
            }
        })
    }
}

Upvotes: 0

Related Questions