IQPullToRefresh
基于 UIScrollView 子类的简单下拉刷新和加载更多处理
IQPullToRefresh 是一个独立的库,可以与 UITableViewDataSource/UICollectionViewDelegate 一起使用,为滚动视图提供下拉刷新和加载更多功能,无需任何麻烦。它还提供了自定义机制,您可以使用它创建自己的自定义下拉刷新或自定义加载更多 UI。
需求
库 | 语言 | 最低 iOS 目标 | 最低 Xcode 版本 |
---|---|---|---|
IQPullToRefresh(1.0.0) | Swift | iOS 11.0 | Xcode 11 |
Swift 版本支持
5.0 及以上
安装
使用CocoaPods安装
IQPullToRefresh 通过 CocoaPods 提供。要安装它,只需在您的 Podfile 中添加以下行
pod 'IQPullToRefresh'
或者您可以根据 Swift 支持表从 需求中选择您需要的版本
pod 'IQPullToRefresh', '1.0.0'
通过源代码安装
拖放 将 IQPullToRefresh
目录从演示项目拖放到您的项目中
使用Swift包管理器安装
Swift 包管理器(SPM)是 Apple 的依赖项管理工具。它现在支持 Xcode 11,因此在所有苹果操作系统类型的项目中都可以使用。它还可以与其他工具如 CocoaPods 和 Carthage 一起使用。
要将 IQPullToRefresh 包安装到您的包中,请将 IQPullToRefresh 的引用及其目标发布版本添加到 Package.swift
文件中的依赖部分。
import PackageDescription
let package = Package(
name: "YOUR_PROJECT_NAME",
products: [],
dependencies: [
.package(url: "https://github.com/hackiftekhar/IQPullToRefresh.git", from: "1.0.0")
]
)
通过 Xcode 安装 IQPullToRefresh 包
- 转到 文件 -> Swift 包 -> 添加包依赖...
- 然后在搜索框中搜索 https://github.com/hackiftekhar/IQPullToRefresh.git
- 并选择您想要的版本
在深入之前你应该了解的事
RefreshType (枚举)
enum RefreshType {
case manual // When we manually trigger the refresh
case refreshControl // When the refreshControl trigger the refresh
}
LoadMoreType (枚举)
enum LoadMoreType {
case manual // When we manually trigger the load more
case reachAtEnd // When the moreLoader trigger the load more
}
可刷新协议(用于下拉刷新功能)
该协议用于在触发刷新时进行回调,同时负责通知加载的开始或完成。
func refreshTriggered(type: IQPullToRefresh.RefreshType,
loadingBegin: @escaping (_ success: Bool) -> Void,
loadingFinished: @escaping (_ success: Bool) -> Void)
更多可加载协议(用于加载更多功能)
用于在触发加载更多时进行回调,并负责通知开始或结束加载
func loadMoreTriggered(type: IQPullToRefresh.LoadMoreType,
loadingBegin: @escaping (_ success: Bool) -> Void,
loadingFinished: @escaping (_ success: Bool) -> Void)
🤯 当前UsersViewController的加载更多逻辑🥴 🤦
方法1
class UsersViewController: UITableViewController {
var users = [User]()
private func getInitialUsers() { ... }
private func getMoreUsers() { ... }
private func refreshUI() { ... }
// Our Dirty 💩 logic to find load more condition, but this is not reliable to fulfil all edge cases
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if canLoadMore == true, loadMoreIndicatorView.isAnimating == false, (scrollView.isTracking == true || scrollView.isDecelerating == true) {
let bottomEdge = scrollView.contentOffset.y + scrollView.frame.height
let edgeToLoadMore = scrollView.contentSize.height - 100
if (bottomEdge >= edgeToLoadMore) {
getMoreUsers()
}
}
}
}
}
方法2
class UsersViewController: UITableViewController {
var users = [User]()
private func getInitialUsers() { ... }
private func getMoreUsers() { ... }
private func refreshUI() { ... }
// Our Dirty 💩 logic to find load more condition, or some peoples also use another logic to load more when last cell visible, but this also have it’s own limitations like don’t have users control when user can decide to load more.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if canLoadMore == true,
loadMoreIndicatorView.isAnimating == false,
(indexPath.row + 1) == users.count {
getMoreUsers()
}
}
}
🤩 新UsersViewController(下拉刷新)
class UsersViewController: UITableViewController {
lazy var refresher = IQPullToRefresh(scrollView: tableView, refresher: self, moreLoader: self)
override func viewDidLoad() {
super.viewDidLoad()
refresher.enablePullToRefresh = true
refresher.enableLoadMore = false
}
}
extension UsersViewController: Refreshable {
func refreshTriggered(type: IQPullToRefresh.RefreshType,
loadingBegin: @escaping (Bool) -> Void,
loadingFinished: @escaping (Bool) -> Void) {
loadingBegin(true)
let pageSize = 10
APIClient.users(page: 1, perPage: pageSize, completion: { [weak self] result in
loadingFinished(true)
switch result {
case .success(let models):
self.models = models
let gotAllRecords = models.count.isMultiple(of:pageSize)
self.refresher.enableLoadMore = models.count != 0 && gotAllRecords
self.refreshUI()
case .failure:
break
}
})
}
}
🤩 新UsersViewController(加载更多)
class UsersViewController: UITableViewController {
lazy var refresher = IQPullToRefresh(scrollView: tableView, refresher: self, moreLoader: self)
override func viewDidLoad() {
super.viewDidLoad()
refresher.enablePullToRefresh = true
refresher.enableLoadMore = false
}
}
extension UsersViewController: Refreshable, MoreLoadable {
func loadMoreTriggered(type: IQPullToRefresh.LoadMoreType,
loadingBegin: @escaping (Bool) -> Void,
loadingFinished: @escaping (Bool) -> Void) {
loadingBegin(true)
let pageSize = 10
let page = (models.count / pageSize) + 1
APIClient.users(page: page, perPage: pageSize, completion: { [weak self] result in
loadingFinished(true)
switch result {
case .success(let models):
self.models.append(contentsOf: models)
let gotAllRecords = models.count.isMultiple(of:pageSize)
self.refresher.enableLoadMore = models.count != 0 && gotAllRecords
self.refreshUI()
case .failure:
break
}
})
}
}
一个抽象的IQPullToRefresh包装器类
通常,下拉刷新和加载更多需求相似,如
- 在刷新时,使用api加载10条记录,页面索引为0(或页面数为1),页面大小为10
- 加载更多时,使用相同的API,通过page_index = 1(或page_no = 2)和page_size = 10来加载下一批10条记录。
- 保持从服务器获取的[Model]数组当前状态。例如,在加载更多成功后,将新记录添加到数组的末尾等。
- 一旦服务器没有更多记录,禁用加载更多功能。
IQRefreshAbstractWrapper 抽象类蓝图
IQRefreshAbstractWrapper 主要以最优化方式处理 IQPullToRefresh 代理函数。
open class IQRefreshAbstractWrapper<T: Decodable> {
public let pullToRefresh: IQPullToRefresh
public var pageOffset: Int
public var pageSize: Int
public var models: [T]
public var modelsUpdatedObserver: ((_ result: Swift.Result<[T], Error>) -> Void)?
public init(scrollView: UIScrollView,
pageOffset: Int, pageSize: Int,
modelsUpdatedObserver: ((_ result: Swift.Result<[T], Error>) -> Void)? = nil)
open func request(page: Int, size: Int, completion: @escaping (Result<[T], Error>) -> Void)
}
如何使用 IQRefreshAbstractWrapper
假设我们想要获取许多用户的列表(假设100+),但每次用户下拉刷新或滚动时,每次显示10条记录。我们需要创建 IQRefreshAbstractWrapper 类的子类
UsersStore 子类
class UsersStore: IQRefreshAbstractWrapper<User> {
// Override the request function and return users based on page and size, that’s it.
override func request(page: Int, size: Int, completion: @escaping (Result<[User], Error>) -> Void) {
APIClient.users(page: page, perPage: size, completion: completion)
}
}
UsersViewController 实现
您只需创建它的对象并观察 modelsUpdatedObserver,当模型列表更新时(无论是通过下拉刷新还是加载更多),您将在此处收到回调,并且您只需将这些模型与您的UI相关联即可。现在实现加载更多和下拉刷新这么简单。
class UsersViewModelController: UITableViewController {
private lazy var usersStore: UsersStore = UsersStore(scrollView: tableView, pageOffset: 1, pageSize: 10)
override func viewDidLoad() {
super.viewDidLoad()
// usersStore.pullToRefresh.enablePullToRefresh = false // You can always customize most of the things here
usersStore.modelsUpdatedObserver = { result in
switch result {
case .success:
self.refreshUI(animated: true)
case .failure:
break
}
}
}
func refreshUI(animated: Bool = true) {
// Access usersStore.models to get list of users
}
}
自定义下拉刷新或加载更多UI
这都可以通过将 IQAnimatableRefresh 协议实现到您自己的 UIView 子类中来实现。
IQAnimatableRefresh协议需求
- 采用此协议的类必须是 UIView 类
- 该类必须实现 2 个变量
var refreshHeight: CGFloat { get } // Height of your refresh view
var refreshState: IQAnimatableRefreshState { get set } //State handling
这些可以是
public enum IQAnimatableRefreshState: Equatable {
case unknown // Unknown state for initialization
case none // refreshControler is not active
case pulling(CGFloat) // Pulling the refreshControl
case eligible // Progress is completed but touch not released
case refreshing // Triggered refreshing
}
协议采用
class CustomPullToRefresh: UILabel, IQAnimatableRefresh {
var refreshHeight: CGFloat {
return 80
}
var refreshState: IQAnimatableRefreshState = .none {
didSet {
guard refreshState != oldValue else { return }
switch refreshState {
case .none:
alpha = 0
text = ""
case .pulling(let progress):
alpha = progress
text = "Pull to refresh"
case .eligible:
alpha = 1
text = "Release to refresh"
case .refreshing:
alpha = 1
text = "Loading"
}
}
}
...
}
自定义下拉刷新分配
class UsersViewController: UITableViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
...
let customPullToRefresh = CustomPullToRefresh()
refresher.refreshControl = customPullToRefresh
...
}
...
}
其他有用函数
- public var enablePullToRefresh: Bool //Enable/Disable Pull To refresh
- var isRefreshing: Bool { get } //Return true if refreshing in progress
- func refresh() //Manually trigger refresh
- public var refreshControl: IQAnimatableRefresh //Custom refreshControl
- public var enableLoadMore: Bool //Enable/Disable load more feature
- var isMoreLoading: Bool { get } //Return true if load more in progress
- func loadMore() //Manually trigger load more
- public var loadMoreControl: IQAnimatableRefresh //Custom loadMore
许可
依据MIT许可协议分发。
贡献
任何贡献都热烈的欢迎!您可以通过GitHub上的pull requests和issue来贡献。
作者
如果您想联系我,请发电子邮件至:[email protected]