9

When I use a List view I can easily add the refreshable modifier to trigger refresh logic. My question is how to achieve the same when using a LazyVStack instead.

I have the following code:

struct TestListView: View { var body: some View { Text("the list view") // WORKS: // VStack { // List { // ForEach(0..<10) { n in // Text("N = \(n)") // } // } // .refreshable { // // } // } // DOES NOT SHOW REFRESH CONTROL: ScrollView { LazyVStack { ForEach(0..<10) { n in Text("N = \(n)") } } } .refreshable { } } } 

How can I get the pull to refresh behavior in the LazyVStack case?

2

4 Answers 4

13

For iOS 16+

Now SwiftUI Added the .refreshable modifier to ScrollView.

Just use it the way you do with List

ScrollView { LazyVStack { // Loop and add View } } .refreshable { refreshLogic() } 

Here is the documentation reference

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) extension View { /// Marks this view as refreshable. public func refreshable(action: @escaping @Sendable () async -> Void) -> some View } 
Sign up to request clarification or add additional context in comments.

4 Comments

what's the iOS requirement on it? is it iOS 16+ only?
iOS 15. will update the answer
refreshable itself is available since iOS 15, but speaking of the availability on ScrollView, it seems since iOS 16. JFYI.
Also this is not possible on watchOS currently, even if you use List or ScrollView. Seems to be an intentional decision from Apple
12

Actually it is possible, and I'd say, I understand Apple's idea - they give default built-in behavior for heavy List, but leave lightweight ScrollView just prepared so we could customise it in whatever way we need.

So here is a demo of solution (tested with Xcode 13.4 / iOS 15.5)

demo

Main part:

struct ContentView: View { var body: some View { ScrollView { RefreshableView() } .refreshable { // << injects environment value !! await fetchSomething() } } } struct RefreshableView: View { @Environment(\.refresh) private var refresh // << refreshable injected !! @State private var isRefreshing = false var body: some View { VStack { if isRefreshing { MyProgress() .transition(.scale) } // ... .onPreferenceChange(ViewOffsetKey.self) { if $0 < -80 && !isRefreshing { // << any creteria we want !! isRefreshing = true Task { await refresh?() // << call refreshable !! await MainActor.run { isRefreshing = false } } } } 

Complete test module is here

1 Comment

it doesn't work in case of fast scroll to top, as it will refresh the view.
7

Based on Asperi answer:

import SwiftUI struct ContentView: View { var body: some View { ScrollView { RefreshableView { RoundedRectangle(cornerRadius: 20) .fill(.red).frame(height: 100).padding() .overlay(Text("Button")) .foregroundColor(.white) } } .refreshable { // << injects environment value !! await fetchSomething() } } func fetchSomething() async { // demo, assume we update something long here try? await Task.sleep(nanoseconds: 2 * 1_000_000_000) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } struct RefreshableView<Content: View>: View { var content: () -> Content @Environment(\.refresh) private var refresh // << refreshable injected !! @State private var isRefreshing = false var body: some View { VStack { if isRefreshing { MyProgress() // ProgressView() ?? - no, it's boring :) .transition(.scale) } content() } .animation(.default, value: isRefreshing) .background(GeometryReader { // detect Pull-to-refresh Color.clear.preference(key: ViewOffsetKey.self, value: -$0.frame(in: .global).origin.y) }) .onPreferenceChange(ViewOffsetKey.self) { if $0 < -80 && !isRefreshing { // << any creteria we want !! isRefreshing = true Task { await refresh?() // << call refreshable !! await MainActor.run { isRefreshing = false } } } } } } struct MyProgress: View { @State private var isProgress = false var body: some View { HStack{ ForEach(0...4, id: \.self){index in Circle() .frame(width:10,height:10) .foregroundColor(.red) .scaleEffect(self.isProgress ? 1:0.01) .animation(self.isProgress ? Animation.linear(duration:0.6).repeatForever().delay(0.2*Double(index)) : .default , value: isProgress) } } .onAppear { isProgress = true } .padding() } } public struct ViewOffsetKey: PreferenceKey { public typealias Value = CGFloat public static var defaultValue = CGFloat.zero public static func reduce(value: inout Value, nextValue: () -> Value) { value += nextValue() } } 

Comments

-4

Simply create file 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 LaztVStack{} } onRefresh: { // do something you want } 

1 Comment

This is a List not a ScrollView as @zumum was asking for

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.