A SwiftUI scroll view that supports pull to refresh

RefreshableScrollView

In SwiftUI, it is currently only possible to use pull to refresh on List views. This package provides a custom scroll view that is refreshable, so that you can pull to refresh any SwiftUI view you desire. It responds to the .refreshable() modifier, so you use it in the exact same way as you would refresh a List.

It also supports passing in a custom UIRefreshControl, so that you may have loaders with a custom look.

How does it work?

This view works by wrapping UIScrollView internally and putting your SwiftUI view in a UIHostingController within the scroll view.

Examples

A full sample project with various refreshable views is available at: https://github.com/Q42/RefreshableScrollViewDemo

Basic example

struct CustomView: View {
  var body: some View {
    RefreshableScrollView {
      Text("Hello, World!")
    }
    .refreshable {
      do {
        // Sleep for 2 seconds
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
      } catch {}
    }
  }
}

Lazy VStack

struct StackView: View {
  struct Item: Identifiable {
    let id: Int
  }
  @State var items: [Item] = Array(1...100).map(Item.init)
  @State var count = 100

  var body: some View {
    RefreshableScrollView {
      LazyVStack(alignment: .leading) {
        ForEach(items) { item in
          Text("Row \(item.id)")
        }
      }
    }
    .refreshable {
      do {
        // Sleep for 2 seconds
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
      } catch {}
      
      count += 1
      items.insert(Item(id: count), at: 0)
    }
  }
}

Lazy VGrid

struct GridView: View {
  let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)

  var body: some View {
    RefreshableScrollView {
      LazyVGrid(columns: columns) {
        ForEach((0...79), id: \.self) {
          let codepoint = $0 + 0x1f600
          let codepointString = String(format: "%02X", codepoint)
          Text("\(codepointString)")
          let emoji = String(Character(UnicodeScalar(codepoint)!))
          Text("\(emoji)")
        }
      }.font(.largeTitle)
    }
    .refreshable {
      do {
        // Sleep for 2 seconds
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
      } catch {}
    }
  }
}

GitHub

View Github