A modern, lightweight Swift library that provides pull-to-refresh and load-more functionality for UIScrollView and all its subclasses. Built with performance and customization in mind.
- ๐ Pull-to-refresh - Smooth pull down gesture to refresh content
- ๐ฑ Load more - Infinite scrolling with automatic load more detection
- ๐จ Highly customizable - Easy to implement custom animations and styles
- ๐ Performance optimized - Minimal memory footprint and smooth animations
- ๐ฆ Easy integration - Simple API with sensible defaults
- ๐ง Universal support - Works with UIScrollView, UITableView, UICollectionView, and UITextView
- Xcode 14.0 or later
- iOS 15.0 or later
- Swift 5.9 or later
Add Refreshable to your project using Xcode:
- File โ Add Package Dependencies
- Enter package URL:
https://github.com/hoangtaiki/Refreshable.git - Select version rule and add to your target
Or add to your Package.swift:
dependencies: [
.package(url: "https://github.com/hoangtaiki/Refreshable.git", from: "2.0.0"),
]Add to your Podfile:
platform :ios, '15.0'
use_frameworks!
target 'YourApp' do
pod 'Refreshable', '~> 2.0.0'
endimport Refreshable
class ViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Add pull-to-refresh with default animation
tableView.addPullToRefresh { [weak self] in
self?.refreshData()
}
}
private func refreshData() {
// Simulate network request
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
// Update your data source
self.tableView.reloadData()
// Stop the refresh animation
self.tableView.stopPullToRefresh()
}
}
}// Add load more functionality
tableView.addLoadMore { [weak self] in
self?.loadMoreData()
}
private func loadMoreData() {
// Load additional data
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
// Append new data to your data source
// Stop the load more animation
self.tableView.stopLoadMore()
// Disable load more when no more data available
if noMoreData {
self.tableView.setLoadMoreEnabled(false)
}
}
}Create a custom animator by conforming to PullToRefreshDelegate:
class CustomRefreshAnimator: UIView, PullToRefreshDelegate {
private let imageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
addSubview(imageView)
imageView.image = UIImage(named: "refresh_icon")
// Setup constraints...
}
func pullToRefresh(_ view: PullToRefreshView, stateDidChange state: PullToRefreshState) {
switch state {
case .idle:
// Reset state
break
case .pullToRefresh:
// User is pulling
imageView.transform = .identity
case .releaseToRefresh:
// Ready to refresh
UIView.animate(withDuration: 0.2) {
self.imageView.transform = CGAffineTransform(rotationAngle: .pi)
}
case .loading:
// Currently refreshing
break
}
}
func pullToRefreshAnimationDidStart(_ view: PullToRefreshView) {
// Start your custom animation
let rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation.toValue = NSNumber(value: Double.pi * 2)
rotation.duration = 1.0
rotation.repeatCount = Float.infinity
imageView.layer.add(rotation, forKey: "rotationAnimation")
}
func pullToRefreshAnimationDidEnd(_ view: PullToRefreshView) {
// End your custom animation
imageView.layer.removeAllAnimations()
imageView.transform = .identity
}
}
// Use the custom animator
let customAnimator = CustomRefreshAnimator()
tableView.addPullToRefresh(withAnimator: customAnimator, height: 60) {
// Refresh action
}// Start refresh programmatically
tableView.startPullToRefresh()
// Check load more status
if tableView.isLoadMoreEnabled() {
// Load more is currently enabled
}
// Disable load more temporarily
tableView.setLoadMoreEnabled(false)tableView.addPullToRefresh { [weak self] in
Task {
await self?.refreshDataAsync()
await MainActor.run {
self?.tableView.stopPullToRefresh()
}
}
}
private func refreshDataAsync() async {
// Your async data loading logic
}import Combine
private var cancellables = Set<AnyCancellable>()
tableView.addPullToRefresh { [weak self] in
self?.dataService.refreshData()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in
self?.tableView.stopPullToRefresh()
},
receiveValue: { data in
// Update UI with new data
}
)
.store(in: &self.cancellables)
}LoadMorableprotocol has been renamed toLoadMoreDelegate- Improved access control - some internal APIs are no longer public
- Enhanced documentation with DocC support
We welcome contributions! Please see our Contributing Guide for details.
- Clone the repository
- Open
Package.swiftin Xcode - Run tests:
โ+U - Run the demo: Open
RefreshableDemo/RefreshableDemo.xcodeproj
Refreshable is available under the MIT license. See the LICENSE file for more information.
- ๐ง Email: duchoang.vp@gmail.com
- ๐ Issues: GitHub Issues
- ๐ฌ Discussions: GitHub Discussions
Made with โค๏ธ by Hoangtaiki
Weโre glad youโre interested in Refreshable, and weโd love to see where you take it. If you have suggestions or bug reports, feel free to send pull request or create new issue.
Thanks, and please do take it for a joyride!