ScrollView is jumping when release dragging in SwiftUI

Viewed 473

I'm using Introspect to apply pull to refresh on my ScrollView.

When I pull down to refresh the scroll view jumps back.

I know that this is a common problem that answered already but with SwiftUI and Introspect I don't know where to put the finger.

I know that it can be fixed with scrollDidEndDragging but I can't implement it here.

Does any one have an idea how to solve this issue with this given code?

The code:

private struct PullToRefresh: UIViewRepresentable {
    
    @Binding var isShowing: Bool
    let onRefresh: () -> Void
    
    public init(isShowing: Binding<Bool>, onRefresh: @escaping () -> Void) {
        _isShowing = isShowing
        self.onRefresh = onRefresh
    }
    
    public class Coordinator {
        let onRefresh: () -> Void
        let isShowing: Binding<Bool>
        
        init(onRefresh: @escaping () -> Void, isShowing: Binding<Bool>) {
            self.onRefresh = onRefresh
            self.isShowing = isShowing
        }
        
        @objc func onValueChanged() {
            isShowing.wrappedValue = true
            onRefresh()
        }
    }
    
    public func makeUIView(context: UIViewRepresentableContext<PullToRefresh>) -> UIView {
        let view = UIView(frame: .zero)
        view.isHidden = true
        view.isUserInteractionEnabled = false
        return view
    }
    
    private func scrollView(entry: UIView) -> UIScrollView? {
        
        if let scrollView = Introspect.findAncestor(ofType: UIScrollView.self, from: entry) {
            return scrollView
        }
        
        guard let viewHost = Introspect.findViewHost(from: entry) else {
            return nil
        }
        
        return Introspect.previousSibling(containing: UIScrollView.self, from: viewHost)
    }
    
    public func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PullToRefresh>) {
        
        DispatchQueue.main.asyncAfter(deadline: .now()) {
            
            guard let scrollView = self.scrollView(entry: uiView) else {
                return
            }
            
            if let refreshControl = scrollView.refreshControl {
                if self.isShowing {
                    refreshControl.beginRefreshing()
                } else {
                    refreshControl.endRefreshing()
                }
                return
            }
            
            let refreshControl = UIRefreshControl()
            refreshControl.addTarget(context.coordinator, action: #selector(Coordinator.onValueChanged), for: .valueChanged)
            scrollView.refreshControl = refreshControl
            
        }
    }
    
    public func makeCoordinator() -> Coordinator {
        return Coordinator(onRefresh: onRefresh, isShowing: $isShowing)
    }
}

Usage:

extension View {
    public func pullToRefresh(isShowing: Binding<Bool>, onRefresh: @escaping () -> Void) -> some View {
        return overlay(
            PullToRefresh(isShowing: isShowing, onRefresh: onRefresh)
                .frame(width: 0, height: 0)
            , alignment: .center)
    }
}
0 Answers
Related