在 UIScrollView 子类上轻松实现下拉刷新和加载更多
IQPullToRefresh 是一个独立的库,可以插入到 UIScrollView 子类(如 UITableView/UICollectionView)中,以提供下拉刷新和加载更多功能,而无需任何麻烦。它还提供自定义机制,您可以使用该机制创建自己的自定义下拉刷新或自定义加载更多 UI。
库 | 语言 | 最低 iOS 目标版本 | 最低 Xcode 版本 |
---|---|---|---|
IQPullToRefresh(1.0.0) | Swift | iOS 11.0 | Xcode 11 |
5.0 及以上
IQPullToRefresh 可通过 CocoaPods 获得。要安装它,只需将以下行添加到您的 Podfile 中
pod 'IQPullToRefresh'
或者您可以根据 要求 中的 Swift 支持表选择您需要的版本
pod 'IQPullToRefresh', '1.0.0'
将 IQPullToRefresh
目录从演示项目拖放到您的项目中
Swift Package Manager(SPM) 是 Apple 的依赖管理工具。 现在 Xcode 11 中支持它。因此,它可以在所有 appleOS 类型的项目中使用。 它也可以与其他工具(如 CocoaPods 和 Carthage)一起使用。
要将 IQPullToRefresh 包安装到您的包中,请在 Package.swift
文件的 dependencies 部分添加对 IQPullToRefresh 的引用和目标发布版本
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 包
enum RefreshType {
case manual // When we manually trigger the refresh
case refreshControl // When the refreshControl trigger the refresh
}
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: @Sendable @escaping @MainActor (_ success: Bool) -> Void,
loadingFinished: @Sendable @escaping @MainActor (_ success: Bool) -> Void)
它用于在触发加载更多时获取回调,并且还负责通知是否已开始加载或已完成加载
func loadMoreTriggered(type: IQPullToRefresh.LoadMoreType,
loadingBegin: @Sendable @escaping @MainActor (_ success: Bool) -> Void,
loadingFinished: @Sendable @escaping @MainActor (_ success: Bool) -> Void)
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()
}
}
}
}
}
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()
}
}
}
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: @Sendable @escaping @MainActor (_ success: Bool) -> Void,
loadingFinished: @Sendable @escaping @MainActor (_ success: 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
}
})
}
}
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: @Sendable @escaping @MainActor (_ success: Bool) -> Void,
loadingFinished: @Sendable @escaping @MainActor (_ success: 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
}
})
}
}
🥳 您已经完成了添加下拉刷新和加载更多功能,而没有任何肮脏的💩和🐞错误代码
大多数时候,下拉刷新和加载更多的需求是相同的,例如
IQRefreshAbstractWrapper 主要以最优化方式处理 IQPullToRefresh 委托函数
open class IQRefreshAbstractWrapper<T> {
public let pullToRefresh: IQPullToRefresh
public var pageOffset: Int
public var pageSize: Int
public var models: [T]
public init(scrollView: UIScrollView,
pageOffset: Int, pageSize: Int)
public func addModelsUpdatedObserver(identifier: AnyHashable,
observer: (@Sendable @escaping @MainActor (_ result: Swift.Result<[T], Error>) -> Void))
public func addStateObserver(identifier: AnyHashable,
observer: (@Sendable @escaping @MainActor (_ result: RefreshingState) -> Void))
// Your subclass must override this function
open func request(page: Int, size: Int, completion: @Sendable @escaping @MainActor (Result<[T], Error>) -> Void)
}
假设我们想获得许多用户的列表(假设 100 多个),但是每次用户下拉刷新时或如果用户滚动时,每次加载下一批 10 条记录。 我们需要创建一个 IQRefreshAbstractWrapper 类的子类
class UsersStore: IQRefreshAbstractWrapper<User> {
// Override the request function and return users based on page and size, that’s it.
open func request(page: Int, size: Int, completion: @Sendable @escaping @MainActor (Result<[T], Error>) -> Void) {
APIClient.users(page: page, perPage: size, completion: completion)
}
}
您只需要创建它的对象并观察 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.addModelsUpdatedObserver(identifier: "\(Self.self)") { 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
}
}
通过将 IQAnimatableRefresh 协议实现到您自己的 UIView 子类中,可以实现这一切。
var refreshLength: CGFloat { get } // Height of your refresh view. Width in case of horizontal scroll
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 refreshLength: 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 上的拉取请求和问题进行贡献。
如果您希望与我联系,请给我发送电子邮件:hack.iftekhar@gmail.com