Last active
June 24, 2024 10:38
-
-
Save liamnichols/06bdf45a971b14dc0e118a86f96ca32f to your computer and use it in GitHub Desktop.
A UIRefreshControl subclass that you can start animating before adding to the view hierarchy without worrying about broken animations and what not
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
/// A subclass that improves the usability of `UIRefreshControl` by: | |
/// | |
/// 1. Providing the `isRefreshing` boolean binding. | |
/// 2. Deferring calls to `beginRefreshing()` until the time of appearance to avoid glitches. | |
/// 3. Automatically scrolling the parent scrollView to reveal the refresh control when scrolled to the top. | |
/// | |
/// When using this subclass, you should not use ``beginRefreshing()`` or ``endRefreshing()`` and instead set the ``isRefreshing`` property to show and hide the refresh control. | |
/// | |
/// You can call ``isRefreshing`` at any point after initialisation. If the control is not yet in the view hierarchy, the underlying call to ``beginRefreshing()`` will be deferred until the correct time. | |
class RefreshControl: UIRefreshControl { | |
private var shouldBeginRefreshingOnAppear = false | |
/// Indicates if the control is currently showing the refresh animation, or if it will show the animation once the receiver appears on screen. | |
/// | |
/// Set this property to show or hide the refresh control. | |
override var isRefreshing: Bool { | |
get { | |
shouldBeginRefreshingOnAppear || super.isRefreshing | |
} | |
set { | |
guard isRefreshing != newValue else { return } | |
if newValue && window == nil { | |
shouldBeginRefreshingOnAppear = true | |
} else if !newValue && shouldBeginRefreshingOnAppear { | |
shouldBeginRefreshingOnAppear = false | |
} | |
if newValue && !shouldBeginRefreshingOnAppear && !super.isRefreshing { | |
beginRefreshingAndRevealIfNeeded(true) | |
} else if !newValue && super.isRefreshing { | |
super.endRefreshing() | |
} | |
} | |
} | |
override func didMoveToWindow() { | |
super.didMoveToWindow() | |
if window != nil && shouldBeginRefreshingOnAppear && !super.isRefreshing { | |
shouldBeginRefreshingOnAppear = false | |
UIView.animate(withDuration: 0, animations: { }, completion: { _ in | |
self.beginRefreshingAndRevealIfNeeded(false) | |
}) | |
} | |
} | |
private func beginRefreshingAndRevealIfNeeded(_ animated: Bool) { | |
let wasAtTop = if let scrollView = superview as? UIScrollView { | |
scrollView.contentOffset.y == -scrollView.adjustedContentInset.top | |
} else { | |
false | |
} | |
super.beginRefreshing() | |
if wasAtTop, let scrollView = superview as? UIScrollView { | |
scrollView.setContentOffset( | |
CGPoint(x: scrollView.contentOffset.x, y: -scrollView.adjustedContentInset.top), | |
animated: animated | |
) | |
} | |
} | |
@available(*, unavailable, message: "Use the `isRefreshing` property instead") | |
override func beginRefreshing() { | |
super.beginRefreshing() | |
} | |
@available(*, unavailable, message: "Use the `isRefreshing` property instead") | |
override func endRefreshing() { | |
super.endRefreshing() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment