5

How can I handle scrollViewDidEndDecelerating and scrollViewDidEndDragging function in swiftUI

I want to set the position of the scrollView so that the selectedIndex is in the middle when the scrollView stops moving.

struct ContentView: View {
    
    @State private var imageList: [String] = ["Onur","Onur","Onur","Onur","Onur","Onur","Onur","Onur","Onur"]
    private var spacing: CGFloat = UIScreen.main.bounds.width / 2 - 100
    @State private var moffsetX: CGFloat = 0
    @State private var oldFffsetX: CGFloat = 0
    @State private var isScrolling: Bool = false
    @State private var selectedIndex: Int = 0
    
    func horizontalScrollView() -> some View {
        ScrollView(.horizontal){
            Spacer()
            HStack(spacing: spacing){
                Spacer()
                    .frame(width: 50)
                ForEach(imageList, id:\.self){ image in
                    Image(image)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 100,height: 100)
                        .clipShape(Circle())
                        .overlay(Circle().stroke(.blue,lineWidth: 4))
                }
                
                Spacer()
                    .frame(width: 50)
            }
            
            .overlay(GeometryReader{ geometry in
                Color.clear.onChange(of: geometry.frame(in: .global).minX) { value, newValue in
                    oldFffsetX = value
                    moffsetX = newValue
                    let index = Int(round(geometry.frame(in: .global).minX / (-1 * (100 + spacing))))
                    if index != selectedIndex{
                        if index < 0{
                            selectedIndex = 0
                        } else if index > imageList.count - 1{
                            selectedIndex = imageList.count - 1
                        } else{
                            selectedIndex = index
                        }
                    }
                    isScrolling = true
                }
            })
            
            Spacer()
        }
        
        
    }
}
3
  • I think this quite fits your expectation. Commented Dec 13, 2023 at 3:03
  • Are you reinventing scrollTargetBehaviour? Commented Dec 13, 2023 at 7:51
  • @Sweeper Will scrollTargetBehaviour be able to solve my problem? I don't have the intention to reinvent, I'm just looking for a solution to my problem. And I never used scrollTargetBehaviour I don't know how to use Commented Dec 13, 2023 at 22:36

2 Answers 2

1

I want to set the position of the scrollView so that the selectedIndex is in the middle when the scrollView stops moving.

That sounds like you are reinventing the viewAligned scroll target behaviour. This behaviour automatically aligns the scroll view's content offset with the frame of the views inside it.

Here is an example:

GeometryReader { geo in
    ScrollView(.horizontal) {
        // don't use UIScreen.main - use a geometry reader instead
        let spacing = geo.size.width / 2 - 100
        LazyHStack(spacing: spacing) {
            Spacer().frame(width: 50)
            // Here I replaced your images with green rectangles
            ForEach(0..<100) { i in
                Rectangle().fill(.green).frame(width: 100, height: 100)
            }
            Spacer().frame(width: 50)
        }
        .scrollTargetLayout() // <----
    }
    .scrollTargetBehavior(.viewAligned) // <----
}

You can track "which view is currently in the centre" using scrollPosition. This uses the IDs of the views. In the above example, ForEach gives its views the IDs 0 to 99, so I will declare:

@State var currentView = 0

In your code, ForEach uses the image names as IDs, so you can do:

// set this to the image name you initially want the scroll view to show
@State var currentView = "Onur"

Then, you can add

.scrollPosition(id: Binding($currentView), anchor: .center)

Now, whenever the user scrolls to another image, currentView will update to that image's ID.


For completeness: in general, when you want to detect the end of a scroll gesture, you would make a custom ScrollTargetBehaviour:

struct MyBehaviour: ScrollTargetBehavior {
    func updateTarget(_ target: inout ScrollTarget, context: TargetContext) {

    }
}

The updateTarget method will be called when the user ends dragging, as well as when the size of the scroll view changes. This is where you can update the scroll view's content offset, by adjusting target.rect.

Sign up to request clarification or add additional context in comments.

Comments

0

Add this PreferenceKey to track the vertical scroll offset of a View:

struct VerticalScrollOffsetKey: PreferenceKey {
    static var defaultValue = CGFloat.zero
    
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value += nextValue()
    }
}

Add this ViewModifier to allow tracking of a View's vertical offset and call a scrollPostionUpdate closure when scrolling has stopped:

extension View {
    
    func onScrollEnded(in coordinateSpace: CoordinateSpace, onScrollEnded: @escaping (CGFloat) -> Void) -> some View {
        modifier(OnVerticalScrollEnded(coordinateSpace: coordinateSpace, scrollPostionUpdate: onScrollEnded))
    }
}

final class OnVerticalScrollEndedOffsetTracker: ObservableObject {
    let scrollViewVerticalOffset = CurrentValueSubject<CGFloat, Never>(0)
    
    func updateOffset(_ offset: CGFloat) {
        scrollViewVerticalOffset.send(offset)
    }
}

struct OnVerticalScrollEnded: ViewModifier {
    let coordinateSpace: CoordinateSpace
    let scrollPostionUpdate: (CGFloat) -> Void
    @StateObject private var offsetTracker = OnVerticalScrollEndedOffsetTracker()
    
    func body(content: Content) -> some View {
        content
            .background(
                GeometryReader(content: { geometry in
                    Color.clear.preference(key: VerticalScrollOffsetKey.self, value: abs(geometry.frame(in: coordinateSpace).origin.y))
                })
            )
            .onPreferenceChange(VerticalScrollOffsetKey.self, perform: offsetTracker.updateOffset(_:))
            .onReceive(offsetTracker.scrollViewVerticalOffset.debounce(for: 0.1, scheduler: DispatchQueue.main).dropFirst(), perform: scrollPostionUpdate)
    }
}

Usage: Add the .onScrollEnded modifier to the content of the ScrollView and give the ScrollView a coordinateSpace name:

struct ScrollingEndedView: View {
    private let coordinateSpaceName = "scrollingEndedView_coordinateSpace"
    
    var body: some View {
        ScrollView {
            VStack {
                ForEach(0...100, id: \.self) { rowNum in
                    Text("Row \(rowNum)")
                        .frame(maxWidth: .infinity)
                        .padding(.vertical)
                        .background(Color.orange)
                }
            }
            .onScrollEnded(in: .named(coordinateSpaceName), onScrollEnded: updateScrollPosition(_:))
        }
        .coordinateSpace(name: coordinateSpaceName) // add the coordinateSpaceName to the ScrollView itself
    }
    
    private func updateScrollPosition(_ position: CGFloat) {
        print("scrolling ended @: \(position)")
    }
}

1 Comment

I did it but not worked for me

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.