Code Monkey home page Code Monkey logo

swiftui-pull-to-refresh's Introduction

SwiftUIPullToRefresh

Pull to refresh is a common UI pattern, supported in UIKit via UIRefreshControl. (Un)surprisingly, it's also unavailable in SwiftUI prior to version 3, and even then it's a bit lackluster.

This package contains a component - RefreshableScrollView - that enables this functionality with any ScrollView. It also doesn't rely on UIViewRepresentable, and works with any iOS version. The end result looks like this:

in action

Features

  • Works on any ScrollView.
  • Customizable progress indicator, with a default RefreshActivityIndicator spinner that works on any SwiftUI version.
  • Specify refresh operation and choose when it ends.
  • Support for Swift 5.5 async blocks.
  • Compatibility refreshCompat modifier to deliver a drop-in replacement for iOS 15 refreshable.
  • Built-in haptic feedback, just like regular List with refreshable has.
  • Additional optional customizations:
    • showsIndicators to allow for showing/hiding ScrollView indicators.
    • loadingViewBackgroundColor to specify the background color of the progress indicator.
    • threshold that indicates how much does the user how to pull before triggering refresh.

Installation

This component is distrubuted as a Swift package. Just add this URL to your package list:

https://github.com/globulus/swiftui-pull-to-refresh

You can also use CocoaPods:

pod 'SwiftUI-Pull-To-Refresh', '~> 1.1.9'

Sample usage

Bread & butter

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()
       }
     }
   }
}

Custom progress view

RefreshableScrollView(onRefresh: { done in
  DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    self.now = Date()
    done()
  }
},
progress: { state in // HERE
   if state == .waiting {
       Text("Pull me down...")
   } else if state == .primed {
       Text("Now release!")
   } else {
       Text("Working...")
   }
}) {
  VStack {
    ForEach(1..<20) {
      Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)")
         .padding(.bottom, 10)
     }
   }.padding()
}

Using async block

 RefreshableScrollView(action: { // HERE
     try? await Task.sleep(nanoseconds: 3_000_000_000)
     now = Date()
 }, progress: { state in
     RefreshActivityIndicator(isAnimating: state == .loading) {
         $0.hidesWhenStopped = false
     }
 }) {
    VStack {
      ForEach(1..<20) {
        Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)")
           .padding(.bottom, 10)
       }
     }.padding()
   }
 }

Compatibility mode

  VStack {
      ForEach(1..<20) {
      Text("\(Calendar.current.date(byAdding: .hour, value: $0, to: now)!)")
        .padding(.bottom, 10)
    }
  }
  .refreshableCompat { done in // HERE
      DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        self.now = Date()
        done()
      }
  } progress: { state in
      RefreshActivityIndicator(isAnimating: state == .loading) {
          $0.hidesWhenStopped = false
      }
  }

Recipe

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

Changelog

  • 1.1.9 - Reworked haptic feedback, added haptic feedback as optional.
  • 1.1.8 - Fixed crash when doing two pulls quickly in succession.
  • 1.1.7 - Updated haptic feedback. Increased Swift version for Podspec.
  • 1.1.6 - Fixed issue where content wouldn't swipe up while in refresh state.
  • 1.1.5 - Added smooth animation when loading pull is released.
  • 1.1.4 - Added threshold and loadingViewBackgroundColor customizations.
  • 1.1.3 - Add haptic feedback & increase offset a bit to fix indicator being visible on certain iPad Pro models.
  • 1.1.2 - Increase offset to fix UI bug occurring on iPhones without notch.
  • 1.1.1 - Added showsIndicators to allow for showing/hiding ScrollView indicators.
  • 1.1.0 - Added ability to specify custom progress view, iOS 15 support, async block support and compatibility mode.
  • 1.0.0 - Initial release.

swiftui-pull-to-refresh's People

Contributors

cookiezby avatar fokswang avatar globulus avatar gordan-glavas-codecons avatar jojost1 avatar rsvp-lawrencec avatar tonbouy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

swiftui-pull-to-refresh's Issues

Not smoothy pull down

Directly see attached video.

It feels snot very smoothy, especially on the first several tries. Is there any clue for the root cause?

Thanks

Kapture 2021-09-06 at 17 17 22

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

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

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!

Issue on haptic feedback

Actual:

Suggested:

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

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.

Issue with .large navigation bar titles

Thanks for this beautiful package but Unfortunately, it does not work properly with .large titles if I try to scroll:
Large Title does not collapse.

package version: 1.1.9
Xcode version: 14.2

`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 ?

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()
       }
     }
   }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.