Skip to content

Instantly share code, notes, and snippets.

@liamnichols
Last active June 24, 2024 10:38
Show Gist options
  • Save liamnichols/06bdf45a971b14dc0e118a86f96ca32f to your computer and use it in GitHub Desktop.
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
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