How do you drag to refresh a grid view in swiftui? I know you can do it with List view with refreshable modifier in iOS 15, but how can you do it with a LazyVGrid? How would you do it in either List or Grid view pre iOS 15? I pretty new at swiftui. I attached a gif showing what Im trying to achieve.
Asked
Active
Viewed 1,993 times
5
-
Welcome to SO - Please take the [tour](https://stackoverflow.com/tour) and read [How to Ask](https://stackoverflow.com/help/how-to-ask) to improve, edit and format your questions. Without a [Minimal Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example) it is impossible to help you troubleshoot your attempt. – lorem ipsum Dec 07 '21 at 18:46
-
1Does this answer your question? [Pull down to refresh data in SwiftUI](https://stackoverflow.com/questions/56493660/pull-down-to-refresh-data-in-swiftui) – jnpdx Dec 07 '21 at 19:58
1 Answers
4
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)
}
}

MegaManX
- 8,766
- 12
- 51
- 83
-
1This answer helped me a lot! I did find an issue with it though. you should not use `abs(offset)` because this causes an infinite loop when you scroll down as well as up. It should just be offset. Also for me I needed a bigger offset than 50 otherwise my views would default to refresh, but that might just be a weird edge case. – AlexW.H.B. Apr 24 '22 at 18:08
-
2One thing I noticed in the refined variant is that it runs the refresh when I pull down to the trigger offset. But usually refresh should happen after user scroll to the trigger offset and then after releasing the drag. (Like in twitter). I updated the `onPreferenceChange` to achieve that. `if !needsRefresh && offset < Constants.refreshTriggerOffset { withAnimation { needsRefresh = true } } else if needsRefresh && offset == 0 { onRefresh() }` – Lakshith Nishshanke Oct 08 '22 at 03:51