Ferrakkem Bhuiyan
Ferrakkem Bhuiyan

Reputation: 2783

SwiftUI refreshable (Pull to refresh) is not working at ScrollView

var body: some View {
    NavigationView {
        ZStack {
            Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all)
            ScrollView(showsIndicators: false) {
                if #available(iOS 15.0, *) {
                    VStack {
                        if self.viewModel.forms.isEmpty && !self.viewModel.isLoading {
                            Text("No Forms Assigned")
                                .foregroundColor(Color.gray)
                                .padding(.vertical, 20)
                                .accessibility(label: Text("No forms assigned"))
                        }
                        if self.viewModel.isLoading {
                            ActivityIndicator(isAnimating: .constant(true), style: .large).padding(.top, 20)
                        }
                        noFormsScrollView
                        Spacer(minLength: 16).accessibility(hidden: true)
                    }
                    .refreshable {
                        self.refreshData()
                    }
                } else {
                    // Fallback on earlier versions
                }
            }
        }.navigationBarTitle("Forms", displayMode: .inline)
    }
}

I am trying to add pull to refresh on my ScrollView But it's not not working. I am wondering, what i am doing wrong.

Upvotes: 5

Views: 20778

Answers (5)

Patryk Węgrzyński
Patryk Węgrzyński

Reputation: 1

Some time ago I've created this small package. It's pretty simple to use just create ScrollView and inside of it add ScrollViewRefresher {}. I hope it helps.

Here is a sample showing how to use it:

struct Content: View{
var body: some View{
    ScrollView{
        VStack{
            ScrollViewRefresher(action: action)
            ForEach(0..<10, id: \.self){item in
                Text("Item: \(item)")
            }
        }
    }
}

func action() async{
    //TODO: Implement async action that will refresh the view, update refreshing once finished         
    do{             
        // Delay the task to simulate API call or some other action             
        try? await Task.sleep(for: .seconds(2))      
    } catch {             
        // TODO: Handle error   
        print(error.localizedDescription)         
    }
}

}

https://github.com/wegosh/ScrollViewRefresher - sample usage in repo description

Upvotes: 0

cagy
cagy

Reputation: 41

in my project I needed some functions from UIScrollView, so I created this. It allows you to read scrollview offset, dragging end and define on refresh action. It's little bit hacky.. It extract UIScrollView from SwiftUI ScrollView:

import SwiftUI

/// Extracting UIScrollview from SwiftUI ScrollView for monitoring offset and velocity and refreshing
public struct ScrollDetector: UIViewRepresentable {
    public init(
        onScroll: @escaping (CGFloat) -> Void,
        onDraggingEnd: @escaping (CGFloat, CGFloat) -> Void,
        onRefresh: @escaping () -> Void
    ) {
        self.onScroll = onScroll
        self.onDraggingEnd = onDraggingEnd
        self.onRefresh = onRefresh
    }

    /// ScrollView Delegate Methods
    public class Coordinator: NSObject, UIScrollViewDelegate {
        init(parent: ScrollDetector) {
            self.parent = parent
        }

        public func scrollViewDidScroll(_ scrollView: UIScrollView) {
            parent.onScroll(scrollView.contentOffset.y)
        }

        public func scrollViewWillEndDragging(
            _: UIScrollView,
            withVelocity velocity: CGPoint,
            targetContentOffset: UnsafeMutablePointer<CGPoint>
        ) {
            parent.onDraggingEnd(targetContentOffset.pointee.y, velocity.y)
        }

        public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
            let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView.panGestureRecognizer.view)
            parent.onDraggingEnd(scrollView.contentOffset.y, velocity.y)
        }

        @objc func handleRefresh() {
            parent.onRefresh()

            refreshControl?.endRefreshing()
        }

        var parent: ScrollDetector

        /// One time Delegate Initialization
        var isDelegateAdded: Bool = false
        var refreshControl: UIRefreshControl?
    }

    public var onScroll: (CGFloat) -> Void
    /// Offset, Velocity
    public var onDraggingEnd: (CGFloat, CGFloat) -> Void
    public var onRefresh: () -> Void

    public func makeCoordinator() -> Coordinator {
        return Coordinator(parent: self)
    }

    public func makeUIView(context _: Context) -> UIView {
        return UIView()
    }

    public func updateUIView(_ uiView: UIView, context: Context) {
        DispatchQueue.main.async {
            /// Adding Delegate for only one time
            /// uiView - Background
            /// .superview = background {}
            /// .superview = VStack {}
            /// .superview = ScrollView {}
            if let scrollview = uiView.superview?.superview?.superview as? UIScrollView, !context.coordinator.isDelegateAdded {
                /// Adding Delegate
                scrollview.delegate = context.coordinator
                context.coordinator.isDelegateAdded = true

                /// Adding refresh control
                let refreshControl = UIRefreshControl()
                refreshControl.addTarget(context.coordinator, action: #selector(Coordinator.handleRefresh), for: .valueChanged)
                scrollview.refreshControl = refreshControl
                context.coordinator.refreshControl = refreshControl
            }
        }
    }
}

How to use it in SwiftUI:

ScrollView(.vertical, showsIndicators: false) {
    VStack {
       // content here
    }
    .background {
         ScrollDetector(onScroll: { _ in }, onDraggingEnd: { _, _ in }, onRefresh: {
             print("Refresh action")
          })
    }
}

Upvotes: 1

Adrian
Adrian

Reputation: 555

Looks like in iOS 16+ you could do:

ScrollView {
    // view body
}
.refreshable {
    // refresh action
}

source: https://developer.apple.com/documentation/swiftui/refreshaction https://developer.apple.com/forums/thread/707510

The sources do say that this thing is 15+, but it does not seem to work on 15, even though it does not complain at compile time.

Upvotes: 9

Jishnu Raj T
Jishnu Raj T

Reputation: 2457

Complete SwiftUI Example

Simply create RefreshableScrollView in your project

public struct RefreshableScrollView<Content: View>: View {
    var content: Content
    var onRefresh: () -> Void

    public init(content: @escaping () -> Content, onRefresh: @escaping () -> Void) {
        self.content = content()
        self.onRefresh = onRefresh
    }

    public var body: some View {
        List {
            content
                .listRowSeparatorTint(.clear)
                .listRowBackground(Color.clear)
                .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
        }
        .listStyle(.plain)
        .refreshable {
            onRefresh()
        }
    }
}

then use RefreshableScrollView anywhere in your project

example:

RefreshableScrollView{
    // Your content
} onRefresh: {
    // do something you want
}

Upvotes: 0

Randall
Randall

Reputation: 733

Pull to refresh is only automatically available on List when using refreshable. To make any other view refreshable, you will need to write your own handler for that.

This StackOverflow example shows how to make pull to refresh work on a ScrollView.

Upvotes: 4

Related Questions