Pull to refresh functionality for any ScrollView in SwiftUI!

Overview

SwiftUIPullToRefresh

Pull to refresh is a common UI pattern, supported in UIKit via UIRefreshControl. (Un)surprisingly, it's also unavailable in SwiftUI.

This package contains a component - RefreshableScrollView - that enables this functionality with any ScrollView. It also doesn't rely on UIViewRepresentable. The end result will look like this:

in action

Recipe

Check out this recipe for in-depth description of the component and its code. Check out SwiftUIRecipes.com for more SwiftUI recipes!

Sample usage

struct TestView: View {
  @State private var now = Date()

  var body: some View {
     RefreshableScrollView(onRefresh: { done in
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
          self.now = Date()
          done()
        }
      }) {
        VStack {
          ForEach(1..<20) {
            Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)")
               .padding(.bottom, 10)
           }
         }.padding()
       }
     }
   }
}

Installation

This component is distrubuted as a Swift package.

Comments
  • Package won't build on Xcode 12.4 (12D4e), MacOS 10.15.7

    Package won't build on Xcode 12.4 (12D4e), MacOS 10.15.7

    I installed the current v1.1.0 package but have 13 Swift Compiler errors, as shown below. Please advise as to required Xcode and MacOS versions. We'd love to use this component!

    Screen Shot 2021-08-25 at 2 28 31 PM

    opened by testflyjets 8
  • Instead of callback, can I use a @State to control refresh action?

    Instead of callback, can I use a @State to control refresh action?

    Hi, thank you for your great implementation. How could I achieve things like using @State var loading = false to control when the refreshing action should be finish. Thanks

    opened by long-nguyen 4
  • crash in PreferenceKey.reduce(value:nextValue:) in conformance PositionPreferenceKey

    crash in PreferenceKey.reduce(value:nextValue:) in conformance PositionPreferenceKey

    Hey,

    thanks for creating this library! Super useful.

    We've been seeing a couple crashes with it since recently, and not a real clue what could be causing this. From the app usage logs we can see in our Crashlytics, we don't see a clear pattern yet. There's nothing from our own code directly involved in the crashes, all we can find so far is that its in PreferenceKey.reduce(value:nextValue:) from PositionPreferenceKey.

    Tapping in the dark here, so hoping you might have a clue. I'm myself not familiar enough yet with how PreferenceKeys are supposed to work.

    Cheers

    Crashed: com.apple.main-thread
    EXC_BREAKPOINT 0x00000001a6751acc
    
    Crashed: com.apple.main-thread
    0  libswiftCore.dylib             0x1a6751acc _assertionFailure(_:_:file:line:flags:) + 1532
    1  SwiftUI                        0x1a93f6b8c ViewCache.commitPlacedChildren(from:to:) + 2880
    2  SwiftUI                        0x1a90d7430 specialized IncrementalChildPlacements.updateValue() + 1480
    3  SwiftUI                        0x1a92b0664 partial apply for specialized implicit closure #2 in implicit closure #1 in closure #1 in closure #1 in Attribute.init<A>(_:) + 24
    4  AttributeGraph                 0x1cc2ae77c AG::Graph::UpdateStack::update() + 492
    5  AttributeGraph                 0x1cc2aebb4 AG::Graph::update_attribute(AG::data::ptr<AG::Node>, bool) + 332
    6  AttributeGraph                 0x1cc2b42fc AG::Graph::input_value_ref_slow(AG::data::ptr<AG::Node>, AG::AttributeID, unsigned int, AGSwiftMetadata const*, bool*, long) + 364
    7  AttributeGraph                 0x1cc2c609c AGGraphGetValue + 232
    8  SwiftUI                        0x1a93fadc0 IncrementalPreference.children.getter + 68
    9  SwiftUI                        0x1a93faf34 IncrementalPreference.value.getter + 252
    10 SwiftUI                        0x1a9336130 implicit closure #2 in implicit closure #1 in closure #1 in closure #1 in Attribute.init<A>(_:) + 252
    11 AttributeGraph                 0x1cc2ae77c AG::Graph::UpdateStack::update() + 492
    12 AttributeGraph                 0x1cc2aebb4 AG::Graph::update_attribute(AG::data::ptr<AG::Node>, bool) + 332
    13 AttributeGraph                 0x1cc2b42fc AG::Graph::input_value_ref_slow(AG::data::ptr<AG::Node>, AG::AttributeID, unsigned int, AGSwiftMetadata const*, bool*, long) + 364
    14 AttributeGraph                 0x1cc2c609c AGGraphGetValue + 232
    15 SwiftUI                        0x1a969d304 closure #1 in PreferenceCombiner.value.getter + 84
    16 UI                             0x1017a4a3c protocol witness for static PreferenceKey.reduce(value:nextValue:) in conformance PositionPreferenceKey + 4314073660 (<compiler-generated>:4314073660)
    17 SwiftUI                        0x1a969d24c PreferenceCombiner.value.getter + 488
    18 SwiftUI                        0x1a9336130 implicit closure #2 in implicit closure #1 in closure #1 in closure #1 in Attribute.init<A>(_:) + 252
    19 AttributeGraph                 0x1cc2ae77c AG::Graph::UpdateStack::update() + 492
    20 AttributeGraph                 0x1cc2aebb4 AG::Graph::update_attribute(AG::data::ptr<AG::Node>, bool) + 332
    21 AttributeGraph                 0x1cc2b42fc AG::Graph::input_value_ref_slow(AG::data::ptr<AG::Node>, AG::AttributeID, unsigned int, AGSwiftMetadata const*, bool*, long) + 364
    22 AttributeGraph                 0x1cc2c609c AGGraphGetValue + 232
    23 SwiftUI                        0x1a969d5ec closure #1 in PairPreferenceCombiner.value.getter + 84
    24 UI                             0x1017a4a3c protocol witness for static PreferenceKey.reduce(value:nextValue:) in conformance PositionPreferenceKey + 4314073660 (<compiler-generated>:4314073660)
    25 SwiftUI                        0x1a969d55c PairPreferenceCombiner.value.getter + 280
    26 SwiftUI                        0x1a9336130 implicit closure #2 in implicit closure #1 in closure #1 in closure #1 in Attribute.init<A>(_:) + 252
    27 AttributeGraph                 0x1cc2ae77c AG::Graph::UpdateStack::update() + 492
    28 AttributeGraph                 0x1cc2aebb4 AG::Graph::update_attribute(AG::data::ptr<AG::Node>, bool) + 332
    29 AttributeGraph                 0x1cc2b42fc AG::Graph::input_value_ref_slow(AG::data::ptr<AG::Node>, AG::AttributeID, unsigned int, AGSwiftMetadata const*, bool*, long) + 364
    30 AttributeGraph                 0x1cc2c609c AGGraphGetValue + 232
    31 SwiftUI                        0x1a9725ca0 PreferenceBinder.updateValue() + 360
    32 SwiftUI                        0x1a9453b00 partial apply for implicit closure #2 in implicit closure #1 in closure #1 in closure #1 in Attribute.init<A>(_:) + 32
    33 AttributeGraph                 0x1cc2ae77c AG::Graph::UpdateStack::update() + 492
    34 AttributeGraph                 0x1cc2aebb4 AG::Graph::update_attribute(AG::data::ptr<AG::Node>, bool) + 332
    35 AttributeGraph                 0x1cc2b7dc4 AG::Subgraph::update(unsigned int) + 884
    36 SwiftUI                        0x1a9b081c0 GraphHost.runTransaction() + 180
    37 SwiftUI                        0x1a9590170 ViewGraph.updateOutputs(at:) + 108
    38 SwiftUI                        0x1a9a546b8 closure #1 in ViewRendererHost.render(interval:updateDisplayList:) + 1508
    39 SwiftUI                        0x1a9a4ac0c ViewRendererHost.render(interval:updateDisplayList:) + 308
    40 SwiftUI                        0x1a9be6960 _UIHostingView.layoutSubviews() + 200
    41 SwiftUI                        0x1a9be6994 @objc _UIHostingView.layoutSubviews() + 28
    42 UIKitCore                      0x1a5ae4be4 -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 2576
    43 QuartzCore                     0x1a5f6b670 -[CALayer layoutSublayers] + 308
    44 QuartzCore                     0x1a5f6bb54 CA::Layer::layout_if_needed(CA::Transaction*) + 548
    45 QuartzCore                     0x1a5f8078c CA::Layer::layout_and_display_if_needed(CA::Transaction*) + 144
    46 QuartzCore                     0x1a5ec25e4 CA::Context::commit_transaction(CA::Transaction*, double, double*) + 500
    47 QuartzCore                     0x1a5eee7f4 CA::Transaction::commit() + 684
    48 QuartzCore                     0x1a5eefb20 CA::Transaction::observer_callback(__CFRunLoopObserver*, unsigned long, void*) + 96
    49 CoreFoundation                 0x1a2b2bc74 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 36
    50 CoreFoundation                 0x1a2b25d98 __CFRunLoopDoObservers + 572
    51 CoreFoundation                 0x1a2b26344 __CFRunLoopRun + 1052
    52 CoreFoundation                 0x1a2b259f4 CFRunLoopRunSpecific + 600
    53 GraphicsServices               0x1ba1f6734 GSEventRunModal + 164
    54 UIKitCore                      0x1a55a375c -[UIApplication _run] + 1072
    55 UIKitCore                      0x1a55a8fcc UIApplicationMain + 168
    56 Beams                          0x100d1d87c main + 11 (BeamsAnalyticsController.swift:11)
    57 libdyld.dylib                  0x1a27e1cf8 start + 4
    
    opened by dmrschmidt 3
  • `Conversion to Swift 5 is available` warning

    `Conversion to Swift 5 is available` warning

    When building a project inclusing this pod with Xcode 13.4.1, I get a Conversion to Swift 5 is available warning. Since the conversion assistant tells that there is No source changes necessary, maybe you can increase the Swift version in the .podspec file ?

    opened by sdc-78 2
  • Suggestion: Make the placeholder clear (or with configurable background color)

    Suggestion: Make the placeholder clear (or with configurable background color)

    I have an image (gradient) in the background of my app, and pull-to-refresh created a solid color block overlaying it.

    I fixed this by replacing foregroundColor(Color(UIColor.systemBackground)) for foregroundColor(Color.clear) at https://github.com/globulus/swiftui-pull-to-refresh/blob/main/Sources/SwiftUIPullToRefresh/SwiftUIPullToRefresh.swift#L108 but I had to fork the project to do this.

    It would be great if this could be configurable in a same way progress is (or, maybe just use the clear color as default?).

    Thank you for consideration!

    opened by tkafka 2
  • Haptic feedback triggers revamp

    Haptic feedback triggers revamp

    Updated haptic feedback:

    • shouldTrigger boolean passed on init to enable/disable haptic if needed
    • Trigger timings updated:
      • Changed .light for .medium and triggering only when the refresh is done
      • Added a .heavy when scrolling higher than threshold, intuitive to lift the finger knowing it'll reload.

    Suited my needs and felt like a good addition, seen this behavior pattern in a lot of places 😄

    opened by Tonbouy 1
  • "Pull to refresh" freeze/crash fixes.

    It works fine to perform one time "pull to refresh", but if you perform the "pull to refresh" twice quickly, the app will freeze for a while, and it will crash when you start it again.

    This issue behaves differently on different devices and OS versions, such as:

    There is a list of freeze/crash issues: iPhone 12, iOS 15.1

    List that works fine: iPhone 6s Plus, iOS 15.5

    opened by FoksWang 1
  • Issue on haptic feedback

    Issue on haptic feedback

    Actual:

    • User started to pull down the content view
    • User reached the threshold height
      • Notification Success Haptic Feedback(https://developer.apple.com/design/human-interface-guidelines/patterns/playing-haptics/images/success.mp4) is dispatched
    • Upon release, no haptic feedback.

    Suggested:

    • User started to pull down the content view
    • User reached the threshold height, no haptic feedback.
    • Upon release, dispatch Impact Light Haptic Feedback(https://developer.apple.com/design/human-interface-guidelines/patterns/playing-haptics/images/impact_light.mp4).

    I think the suggested implementation is more natural than the current ones.

    The current haptic feedback implementation best applies IF refresh action is immediately triggered upon reaching threshold height, rather than on release.

    • example: When using Safari Browser, perform pull down refresh. Upon reaching threshold, it immediately reloads the page along with Impact Light haptic feedback
    opened by dev-lcc 1
  • Fix: view jumping when state turn to loading

    Fix: view jumping when state turn to loading

    Thanks for this great library, it gives a workaround for pull to refresh on current scrollview. But I found the view would jumps when the state turn to loading, so I made a little change to avoid the view jumping happens.

    The main reason is when set state to loading, the loading view's offset and scroll view's alignment guide's value change instantly, which made the view jumps a little bit. So I calculate the current scrollview's content offset to make the movement more smoothly

    opened by Cookiezby 1
  • ScrollView not working with List

    ScrollView not working with List

    struct TestView: View {
      @State private var now = Date()
    
      var body: some View {
         RefreshableScrollView(onRefresh: { done in
            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
              self.now = Date()
              done()
            }
          }) {
            VStack {
                List {
                     ForEach(1..<20) {
                         Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)")
                         .padding(.bottom, 10)
                      }
                  }
              }.padding()
           }
         }
       }
    }
    
    opened by santicarmo31 1
  • Add haptic feedback & increase offset a bit to fix indicator being visible on certain iPad Pro models

    Add haptic feedback & increase offset a bit to fix indicator being visible on certain iPad Pro models

    1. Added haptic feedback to match native behaviour, feels way better!

    2. The current offset will show the indicator on some iPad Pro models, at least the 5th gen 12.9" Pro.

    image

    Increasing it slightly solves the issue 😄

    opened by jojost1 0
  • Scrolling too fast with scroll bar results in console error

    Scrolling too fast with scroll bar results in console error

    I started using this package a couple days ago, and it's pretty nice! However, I've encountered one issue.

    When I scroll normally by swiping my figure up and down, everything works fine. And when I scroll by holding down on the scroll bar and moving my figure up and down, this also works fine as long as I scroll slowly.

    However, if I hold on the scroll bar and move my figure up and down quickly, it results in many error messages in the console of the form Bound preference PositionPreferenceKey tried to update multiple times per frame.

    Based on this SO answer, the error message is probably due to how the GeometryReader and PositionPreferenceKey interact with each other. But I'm not sure exactly how to fix it.

    opened by mywristbands 3
  • Accidentally triggering the Pull to refresh action.

    Accidentally triggering the Pull to refresh action.

    If the scroll view has bouncing enabled it is very easy to accidentally trigger a pull to refresh action while scrolling the view up. The view will bounce using the momentum and velocity, reaching the threshold and thus triggering the action.

    I think it should only be triggered while a user is dragging the view down (still touching the screen with their finger).

    opened by darecki 5
Owner
Gordan Glavaš
Gordan Glavaš
Give pull-to-refresh & infinite scrolling to any UIScrollView with 1 line of code.

SVPullToRefresh + SVInfiniteScrolling These UIScrollView categories makes it super easy to add pull-to-refresh and infinite scrolling fonctionalities

Sam Vermette 4.9k Dec 1, 2022
SwiftUI ScrollView can pull to fresh

FreshScrollView SwiftUI ScrollView can pull to fresh One day, when I were writing a scrollview with a SwiftUI, I suddenly wanted to add a drop-down re

Weiha Wang 1 Dec 12, 2021
SwiftUI ScrollView can pull to fresh

FreshScrollView SwiftUI ScrollView can pull to fresh One day, when I were writing a scrollview with a SwiftUI, I suddenly wanted to add a drop-down re

Weiha Wang 0 Dec 8, 2021
PullToRefreshSwiftUI - Pull to refresh for SwiftUI

PullToRefreshSwiftUI Pull to refresh for SwiftUI. Usage with @State import Swift

Dmitry Kononchuk 1 Feb 28, 2022
✳️ SwiftUI Pull to Refresh (for iOS 13 and iOS 14) package.

Refreshable ✳️ SwiftUI Pull to Refresh (for iOS 13 and iOS 14) package. See complementary article at SwiftUI Pull to Refresh (for iOS 13 and iOS 14).

Geri Borbás 21 Dec 2, 2022
Elastic pull to refresh for iOS developed in Swift

DGElasticPullToRefresh Elastic pull to refresh compontent developed in Swift Inspired by this Dribbble post: Pull Down to Refresh by Hoang Nguyen Tuto

Danil Gontovnik 3.7k Jan 3, 2023
Animated "Pull To Refresh" Library for UIScrollView.

PullToBounce Animated "Pull To Refresh" Library for UIScrollView. You can add animated "pull to refresh" action to your UIScrollView, UITableView and

Takuya Okamoto 1.9k Dec 5, 2022
A pull-down-to-refresh control for iOS that plays pong, originally created for the MHacks III iOS app

BOZPongRefreshControl A pull-down-to-refresh control for iOS that plays pong Installation It's on CocoaPods! Put pod 'BOZPongRefreshControl' in your P

Ben Oztalay 885 Dec 12, 2022
Fully customizable pull-to-refresh control inspired by Storehouse iOS app

CBStoreHouseRefreshControl What is it? A fully customizable pull-to-refresh control for iOS inspired by Storehouse iOS app ![screenshot1] (https://s3.

Suyu Zhang 4k Jan 6, 2023
Customizable pull-to-refresh control,written in pure Swift.

What is it This project is heavily inspired by CBStoreHouseRefreshControl which is Objective-C implemented. SurfingRefreshControl provides you a chanc

Peiwei 55 Aug 21, 2022
One gesture, many actions. An evolution of Pull to Refresh.

MNTPullToReact MNTPullToReact is an extended evolution of the famous Pull to Refresh interaction. The main idea comes from a unique question: can the

Mention 777 Nov 20, 2022
Play BreakOut while loading - A playable pull to refresh view using SpriteKit

BreakOutToRefresh Play BreakOut while loading - A playable pull to refresh view using SpriteKit BreakOutToRefresh uses SpriteKit to add a playable min

Dominik Hauser 2.5k Dec 29, 2022
An easy way to use pull-to-refresh.

MJRefresh An easy way to use pull-to-refresh ?? ✍??Release Notes: more details Contents New Features Dynamic i18n Switching SPM Supported Swift Chaini

M了个J 13.7k Jan 6, 2023
GIFRefreshControl is a pull to refresh that supports GIF images as track animations.

GIFRefreshControl GIFRefreshControl is a pull to refresh that supports GIF images as track animations. Installation You have multiple choices here: Co

Kevin Delannoy 163 Oct 4, 2022
Pull-to-refresh animation in UICollectionView with a sticky header flow layout, written in Swift :large_orange_diamond:

ReplaceAnimation Implementation of Zee Young's Dribbble animation (https://dribbble.com/shots/2067564-Replace) Info I really liked Zee Young's animati

Alex Türk 957 Sep 13, 2022
Custom animated pull-to-refresh that can be easily added to UIScrollView

PullToMakeSoup Custom animated pull-to-refresh that can be easily added to UIScrollView Check this article on our blog to know more details about anim

Yalantis 1.9k Dec 17, 2022
ESPullToRefresh is an easy-to-use component that give pull-to-refresh and infinite-scrolling implemention for developers.

ESPullToRefresh is an easy-to-use component that give pull-to-refresh and infinite-scrolling implemention for developers.

Vincent Li 1.7k Jan 8, 2023
An easy way to use pull-to-refresh

CRRefresh an easy way to use pull-to-refresh, If you want to customize its UI style, you just need conform the specified protocol. We will not regular

CRAnimation 957 Dec 13, 2022
Animated, customizable, and flexible pull-to-refresh framework for faster and easier iOS development.

KafkaRefresh Animated, customizable, and flexible pull-to-refresh framework for faster and easier iOS development. Report bug · Request feature · 中文文档

H. H. Hsiang 1.2k Dec 11, 2022