Pull Down to Refresh Data in Swiftui

Pull down to refresh data in SwiftUI

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 }.

How can you Drag to refresh a Grid View (LazyVGrid) in Swiftui?

Here is the code LazyVStack:

import SwiftUI

struct PullToRefreshSwiftUI: View {
@Binding private var needRefresh: Bool
private let coordinateSpaceName: String
private let onRefresh: () -> Void

init(needRefresh: Binding<Bool>, coordinateSpaceName: String, onRefresh: @escaping () -> Void) {
self._needRefresh = needRefresh
self.coordinateSpaceName = coordinateSpaceName
self.onRefresh = onRefresh
}

var body: some View {
HStack(alignment: .center) {
if needRefresh {
VStack {
Spacer()
ProgressView()
Spacer()
}
.frame(height: 100)
}
}
.background(GeometryReader {
Color.clear.preference(key: ScrollViewOffsetPreferenceKey.self,
value: $0.frame(in: .named(coordinateSpaceName)).origin.y)
})
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in
guard !needRefresh else { return }
if abs(offset) > 50 {
needRefresh = true
onRefresh()
}
}
}
}


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

}

And here is typical usage:

struct ContentView: View {
@State private var refresh: Bool = false
@State private var itemList: [Int] = {
var array = [Int]()
(0..<40).forEach { value in
array.append(value)
}
return array
}()

var body: some View {
ScrollView {
PullToRefreshSwiftUI(needRefresh: $refresh,
coordinateSpaceName: "pullToRefresh") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation { refresh = false }
}
}
LazyVStack {
ForEach(itemList, id: \.self) { item in
HStack {
Spacer()
Text("\(item)")
Spacer()
}
}
}
}
.coordinateSpace(name: "pullToRefresh")
}
}

This can be easily adapted for LazyVGrid, just replace LazyVStack.

EDIT:
Here is more refined variant:

struct PullToRefresh: View {

private enum Constants {
static let refreshTriggerOffset = CGFloat(-140)
}

@Binding private var needsRefresh: Bool
private let coordinateSpaceName: String
private let onRefresh: () -> Void

init(needsRefresh: Binding<Bool>, coordinateSpaceName: String, onRefresh: @escaping () -> Void) {
self._needsRefresh = needsRefresh
self.coordinateSpaceName = coordinateSpaceName
self.onRefresh = onRefresh
}

var body: some View {
HStack(alignment: .center) {
if needsRefresh {
VStack {
Spacer()
ProgressView()
Spacer()
}
.frame(height: 60)
}
}
.background(GeometryReader {
Color.clear.preference(key: ScrollViewOffsetPreferenceKey.self,
value: -$0.frame(in: .named(coordinateSpaceName)).origin.y)
})
.onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in
guard !needsRefresh, offset < Constants.refreshTriggerOffset else { return }
withAnimation { needsRefresh = true }
onRefresh()
}
}
}


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


private enum Constants {
static let coordinateSpaceName = "PullToRefreshScrollView"
}

struct PullToRefreshScrollView<Content: View>: View {
@Binding private var needsRefresh: Bool
private let onRefresh: () -> Void
private let content: () -> Content

init(needsRefresh: Binding<Bool>,
onRefresh: @escaping () -> Void,
@ViewBuilder content: @escaping () -> Content) {
self._needsRefresh = needsRefresh
self.onRefresh = onRefresh
self.content = content
}

var body: some View {
ScrollView {
PullToRefresh(needsRefresh: $needsRefresh,
coordinateSpaceName: Constants.coordinateSpaceName,
onRefresh: onRefresh)
content()
}
.coordinateSpace(name: Constants.coordinateSpaceName)
}
}

SwiftUI Generic Pull to refresh view

Here is possible approach (compiled with Xcode 11.4)

Usage:

CustomScrollView(width: 100, height: 100, 
viewModel: HomeViewMode()) {
HomeView()
}

Generic type:

protocol CustomViewModel {
func loadPackages()
}


// It is used generic ViewBuilder pattern for content
struct CustomScrollView<Content: View, VM: CustomViewModel> : UIViewRepresentable {
var width : CGFloat
var height : CGFloat

let viewModel: VM
let content: () -> Content

init(width: CGFloat, height: CGFloat, viewModel: VM, @ViewBuilder content: @escaping () -> Content) {
self.width = width
self.height = height
self.viewModel = viewModel
self.content = content
}

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

let childView = UIHostingController(rootView: content())

childView.view.frame = CGRect(x: 0, y: 0, width: width, height: height)

control.addSubview(childView.view)
return control
}

func updateUIView(_ uiView: UIScrollView, context: Context) { }

class Coordinator: NSObject {
var control: CustomScrollView<Content, VM>
var viewModel: VM

init(_ control: CustomScrollView, viewModel: VM) {
self.control = control
self.viewModel = viewModel
}

@objc func handleRefreshControl(sender: UIRefreshControl) {
sender.endRefreshing()
viewModel.loadPackages()
}
}
}

How to use pull to refresh in Swift?

Pull to refresh is built in iOS. You could do this in swift like

let refreshControl = UIRefreshControl()

override func viewDidLoad() {
super.viewDidLoad()

refreshControl.attributedTitle = NSAttributedString(string: "Pull to refresh")
refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged)
tableView.addSubview(refreshControl) // not required when using UITableViewController
}

@objc func refresh(_ sender: AnyObject) {
// Code to refresh table view
}

At some point you could end refreshing.

refreshControl.endRefreshing()


Related Topics



Leave a reply



Submit