Pull down to refresh data in SwiftUI

here is a simple, small and pure SwiftUI solution i made in order to add pull to refresh functionality to a ScrollView.

struct PullToRefresh: View {
    
    var coordinateSpaceName: String
    var onRefresh: ()->Void
    
    @State var needRefresh: Bool = false
    
    var body: some View {
        GeometryReader { geo in
            if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
                Spacer()
                    .onAppear {
                        needRefresh = true
                    }
            } else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
                Spacer()
                    .onAppear {
                        if needRefresh {
                            needRefresh = false
                            onRefresh()
                        }
                    }
            }
            HStack {
                Spacer()
                if needRefresh {
                    ProgressView()
                } else {
                    Text("⬇️")
                }
                Spacer()
            }
        }.padding(.top, -50)
    }
}

To use it it's simple, just add it at the top of your ScrollView and give it the coordinate space of the ScrollView :

ScrollView {
    PullToRefresh(coordinateSpaceName: "pullToRefresh") {
        // do your stuff when pulled
    }
    
    Text("Some view...")
}.coordinateSpace(name: "pullToRefresh")

I needed the same thing for an app I'm playing around with, and it looks like the SwiftUI API does not include a refresh control capability for ScrollViews at this time.

Over time, the API will develop and rectify these sorts of situations, but the general fallback for missing functionality in SwiftUI will always be implementing a struct that implements UIViewRepresentable. Here's a quick and dirty one for UIScrollView with a refresh control.

struct LegacyScrollView : UIViewRepresentable {
    // any data state, if needed

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UIScrollView {
        let control = UIScrollView()
        control.refreshControl = UIRefreshControl()
        control.refreshControl?.addTarget(context.coordinator, action:
            #selector(Coordinator.handleRefreshControl),
                                          for: .valueChanged)

        // Simply to give some content to see in the app
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 30))
        label.text = "Scroll View Content"
        control.addSubview(label)

        return control
    }


    func updateUIView(_ uiView: UIScrollView, context: Context) {
        // code to update scroll view from view state, if needed
    }

    class Coordinator: NSObject {
        var control: LegacyScrollView

        init(_ control: LegacyScrollView) {
            self.control = control
        }

        @objc func handleRefreshControl(sender: UIRefreshControl) {
            // handle the refresh event

            sender.endRefreshing()
        }
    }
}

But of course, you can't use any SwiftUI components in your scroll view without wrapping them in a UIHostingController and dropping them in makeUIView, rather than putting them in a LegacyScrollView() { // views here }.


from iOS 15+

NavigationView {
    List(1..<100) { row in
     Text("Row \(row)")
    }
    .refreshable {
         print("write your pull to refresh logic here")
    }
}

for more details: Apple Doc