IQPullToRefresh

在 UIScrollView 子类上轻松实现下拉刷新和加载更多

Pull To Refresh & Load More Load More Custom Pull To Refresh 1 Custom Pull To Refresh 2

Build Status

IQPullToRefresh 是一个独立的库,可以插入到 UIScrollView 子类(如 UITableView/UICollectionView)中,以提供下拉刷新和加载更多功能,而无需任何麻烦。它还提供自定义机制,您可以使用该机制创建自己的自定义下拉刷新或自定义加载更多 UI。

要求

Platform iOS

语言 最低 iOS 目标版本 最低 Xcode 版本
IQPullToRefresh(1.0.0) Swift iOS 11.0 Xcode 11

Swift 版本支持

5.0 及以上

安装

使用 CocoaPods 安装

CocoaPods

IQPullToRefresh 可通过 CocoaPods 获得。要安装它,只需将以下行添加到您的 Podfile 中

pod 'IQPullToRefresh'

或者您可以根据 要求 中的 Swift 支持表选择您需要的版本

pod 'IQPullToRefresh', '1.0.0'

使用源代码安装

Github tag

IQPullToRefresh 目录从演示项目拖放到您的项目中

使用 Swift Package Manager 安装

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 包

深入了解之前您应该了解的事情

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
}

Refreshable 协议(用于下拉刷新功能)

它用于在触发刷新时获取回调,并且还负责通知是否已开始加载或已完成加载

    func refreshTriggered(type: IQPullToRefresh.RefreshType,
                          loadingBegin: @Sendable @escaping @MainActor (_ success: Bool) -> Void,
                          loadingFinished: @Sendable @escaping @MainActor (_ success: Bool) -> Void)

MoreLoadable 协议(用于加载更多功能)

它用于在触发加载更多时获取回调,并且还负责通知是否已开始加载或已完成加载

    func loadMoreTriggered(type: IQPullToRefresh.LoadMoreType,
                           loadingBegin: @Sendable @escaping @MainActor (_ success: Bool) -> Void,
                           loadingFinished: @Sendable @escaping @MainActor (_ 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: @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
            }
        })
    }
}

🤩 新的 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: @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
            }
        })
    }
}

🥳 您已经完成了添加下拉刷新和加载更多功能,而没有任何肮脏的💩和🐞错误代码 下拉刷新 & 加载更多

一个抽象的 IQPullToRefresh 包装类

大多数时候,下拉刷新和加载更多的需求是相同的,例如

IQRefreshAbstractWrapper 抽象类蓝图

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)
}

如何使用 IQRefreshAbstractWrapper

假设我们想获得许多用户的列表(假设 100 多个),但是每次用户下拉刷新时或如果用户滚动时,每次加载下一批 10 条记录。 我们需要创建一个 IQRefreshAbstractWrapper 类的子类

UsersStore 子类

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)
  }
}

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.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
  }
}

自定义下拉刷新或加载更多 UI

通过将 IQAnimatableRefresh 协议实现到您自己的 UIView 子类中,可以实现这一切。

IQAnimatableRefresh 协议要求

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