{"id":16835227,"url":"https://github.com/hackiftekhar/iqpulltorefresh","last_synced_at":"2025-03-22T04:30:50.833Z","repository":{"id":56915165,"uuid":"337107441","full_name":"hackiftekhar/IQPullToRefresh","owner":"hackiftekhar","description":"Easy Pull to refresh and Load more handling on a UIScrollView subclass","archived":false,"fork":false,"pushed_at":"2024-11-08T06:36:45.000Z","size":2192,"stargazers_count":18,"open_issues_count":0,"forks_count":6,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-18T08:11:17.517Z","etag":null,"topics":["loadmore","loadmoreandrefresh","pulltorefresh","uicollectionview","uitableview"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hackiftekhar.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-08T14:44:42.000Z","updated_at":"2025-02-27T09:36:49.000Z","dependencies_parsed_at":"2023-02-12T02:46:13.575Z","dependency_job_id":"abc4b112-dd12-4d68-a15f-0f8b1a77a67f","html_url":"https://github.com/hackiftekhar/IQPullToRefresh","commit_stats":{"total_commits":22,"total_committers":2,"mean_commits":11.0,"dds":"0.045454545454545414","last_synced_commit":"67ea1550e19364056ebdc2e2179930c0fff30dfa"},"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hackiftekhar%2FIQPullToRefresh","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hackiftekhar%2FIQPullToRefresh/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hackiftekhar%2FIQPullToRefresh/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hackiftekhar%2FIQPullToRefresh/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hackiftekhar","download_url":"https://codeload.github.com/hackiftekhar/IQPullToRefresh/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244907420,"owners_count":20529850,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["loadmore","loadmoreandrefresh","pulltorefresh","uicollectionview","uitableview"],"created_at":"2024-10-13T12:09:24.277Z","updated_at":"2025-03-22T04:30:50.123Z","avatar_url":"https://github.com/hackiftekhar.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"IQPullToRefresh\n==========================\nEasy Pull to refresh and Load more handling on a UIScrollView subclass\n\n\n![Pull To Refresh \u0026 Load More](https://raw.githubusercontent.com/hackiftekhar/IQPullToRefresh/master/Documents/pull_to_refresh_load_more.gif)\n![Load More](https://raw.githubusercontent.com/hackiftekhar/IQPullToRefresh/master/Documents/load_more.gif)\n![Custom Pull To Refresh 1](https://raw.githubusercontent.com/hackiftekhar/IQPullToRefresh/master/Documents/custom_pull_to_refresh1.gif)\n![Custom Pull To Refresh 2](https://raw.githubusercontent.com/hackiftekhar/IQPullToRefresh/master/Documents/custom_pull_to_refresh2.gif)\n\n[![Build Status](https://travis-ci.org/hackiftekhar/IQPullToRefresh.svg)](https://travis-ci.org/hackiftekhar/IQPullToRefresh)\n\nIQPullToRefresh is a standalone library which can be plugged with UIScrollView subclasses like UITableView/UICollectionView to provide pull-to-refresh and load-more feature without any hassle.\nIt also Provide customization mechanism using which you can create your own custom pull-to-refresh or custom load-more UI.\n\n## Requirements\n[![Platform iOS](https://img.shields.io/badge/Platform-iOS-blue.svg?style=fla)]()\n\n| Library                | Language | Minimum iOS Target | Minimum Xcode Version |\n|------------------------|----------|--------------------|-----------------------|\n| IQPullToRefresh(1.0.0) | Swift    | iOS 11.0           | Xcode 11              |\n\n#### Swift versions support\n5.0 and above\n\nInstallation\n==========================\n\n#### Installation with CocoaPods\n\n[![CocoaPods](https://img.shields.io/cocoapods/v/IQListKit.svg)](http://cocoadocs.org/docsets/IQListKit)\n\nIQPullToRefresh is available through [CocoaPods](http://cocoapods.org). To install\nit, simply add the following line to your Podfile:\n\n```ruby\npod 'IQPullToRefresh'\n```\n\n*Or you can choose the version you need based on the Swift support table from [Requirements](README.md#requirements)*\n\n```ruby\npod 'IQPullToRefresh', '1.0.0'\n```\n\n#### Installation with Source Code\n\n[![Github tag](https://img.shields.io/github/tag/hackiftekhar/IQListKit.svg)]()\n\n***Drag and drop*** `IQPullToRefresh` directory from demo project to your project\n\n#### Installation with Swift Package Manager\n\n[Swift Package Manager(SPM)](https://swift.org/package-manager/) is Apple's dependency manager tool. It is now supported in Xcode 11. So it can be used in all appleOS types of projects. It can be used alongside other tools like CocoaPods and Carthage as well. \n\nTo install IQPullToRefresh package into your packages, add a reference to IQPullToRefresh and a targeting release version in the dependencies section in `Package.swift` file:\n\n```swift\nimport PackageDescription\n\nlet package = Package(\n    name: \"YOUR_PROJECT_NAME\",\n    products: [],\n    dependencies: [\n        .package(url: \"https://github.com/hackiftekhar/IQPullToRefresh.git\", from: \"1.0.0\")\n    ]\n)\n```\n\nTo install IQPullToRefresh package via Xcode\n\n * Go to File -\u003e Swift Packages -\u003e Add Package Dependency...\n * Then search for https://github.com/hackiftekhar/IQPullToRefresh.git\n * And choose the version you would like\n\nThings you should understand before going into deep\n==========================\n\n#### RefreshType (Enumeration)\n```swift\nenum RefreshType {\n   case manual  // When we manually trigger the refresh\n   case refreshControl  // When the refreshControl trigger the refresh\n}\n```\n   \n#### LoadMoreType (Enumeration)\n```swift\nenum LoadMoreType {\n    case manual // When we manually trigger the load more\n    case reachAtEnd // When the moreLoader trigger the load more\n}\n```\n\n#### Refreshable protocol (For Pull-To-Refresh feature)\nIt is used to get callback when refresh is triggered and also responsible to inform if loading has begin or loading has finished\n```swift\n    func refreshTriggered(type: IQPullToRefresh.RefreshType,\n                          loadingBegin: @Sendable @escaping @MainActor (_ success: Bool) -\u003e Void,\n                          loadingFinished: @Sendable @escaping @MainActor (_ success: Bool) -\u003e Void)\n```\n\n#### MoreLoadable protocol (For Load-More feature)\nIt is used to get callback when load more is triggered and also responsible to inform if loading has begin or loading has finished\n\n```swift\n    func loadMoreTriggered(type: IQPullToRefresh.LoadMoreType,\n                           loadingBegin: @Sendable @escaping @MainActor (_ success: Bool) -\u003e Void,\n                           loadingFinished: @Sendable @escaping @MainActor (_ success: Bool) -\u003e Void)\n```\n\n🤯 Current UsersViewController logic for load more 🥴 🤦\n==========================\n#### Approach 1\n```swift\nclass UsersViewController: UITableViewController {\n    var users = [User]()\n    private func getInitialUsers() { ... }\n    private func getMoreUsers() { ... }\n    private func refreshUI() { ... }\n\n    // Our Dirty 💩 logic to find load more condition, but this is not reliable to fulfil all edge cases\n    override func scrollViewDidScroll(_ scrollView: UIScrollView) {            \n            if canLoadMore == true, loadMoreIndicatorView.isAnimating == false, (scrollView.isTracking == true || scrollView.isDecelerating == true) {\n                let bottomEdge = scrollView.contentOffset.y + scrollView.frame.height\n                let edgeToLoadMore = scrollView.contentSize.height - 100\n                if (bottomEdge \u003e= edgeToLoadMore) {\n                    getMoreUsers()\n                }\n            }\n        }\n    }\n}\n```\n#### Approach 2\n```swift\nclass UsersViewController: UITableViewController {\n    var users = [User]()\n    private func getInitialUsers() { ... }\n    private func getMoreUsers() { ... }\n    private func refreshUI() { ... }\n\n    // 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.\n\n    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {\n        if canLoadMore == true,\nloadMoreIndicatorView.isAnimating == false,\n(indexPath.row + 1) == users.count {\n            getMoreUsers()\n        }\n    }\n}\n```\n\n🤩 New UsersViewController (Pull To Refresh)\n==========================\n```swift\nclass UsersViewController: UITableViewController {\n    lazy var refresher = IQPullToRefresh(scrollView: tableView, refresher: self, moreLoader: self)\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        refresher.enablePullToRefresh = true\n        refresher.enableLoadMore = false\n    }\n}\nextension UsersViewController: Refreshable {\n    func refreshTriggered(type: IQPullToRefresh.RefreshType,\n                          loadingBegin: @Sendable @escaping @MainActor (_ success: Bool) -\u003e Void,\n                          loadingFinished: @Sendable @escaping @MainActor (_ success: Bool) -\u003e Void) {\n    loadingBegin(true)\n    let pageSize = 10\n\n    APIClient.users(page: 1, perPage: pageSize, completion: { [weak self] result in\n            loadingFinished(true)\n\n            switch result {\n            case .success(let models):\n                self.models = models\n                let gotAllRecords = models.count.isMultiple(of:pageSize)\n                self.refresher.enableLoadMore = models.count != 0 \u0026\u0026 gotAllRecords\n                self.refreshUI()\n            case .failure:\n                break\n            }\n        })\n    }\n}\n```\n\n🤩 New UsersViewController (Load More)\n==========================\n```swift\nclass UsersViewController: UITableViewController {\n    lazy var refresher = IQPullToRefresh(scrollView: tableView, refresher: self, moreLoader: self)\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        refresher.enablePullToRefresh = true\n        refresher.enableLoadMore = false\n    }\n}\nextension UsersViewController: Refreshable, MoreLoadable {\n    func loadMoreTriggered(type: IQPullToRefresh.LoadMoreType,\n                           loadingBegin: @Sendable @escaping @MainActor (_ success: Bool) -\u003e Void,\n                           loadingFinished: @Sendable @escaping @MainActor (_ success: Bool) -\u003e Void) {\n    loadingBegin(true)\n    let pageSize = 10\n    let page = (models.count / pageSize) + 1\n\n    APIClient.users(page: page, perPage: pageSize, completion: { [weak self] result in\n            loadingFinished(true)\n\n            switch result {\n            case .success(let models):\nself.models.append(contentsOf: models)\n                let gotAllRecords = models.count.isMultiple(of:pageSize)\n                self.refresher.enableLoadMore = models.count != 0 \u0026\u0026 gotAllRecords\n                self.refreshUI()\n            case .failure:\n                break\n            }\n        })\n    }\n}\n```\n\n🥳\nYou have done adding pull-to-refresh and load more without any dirty 💩 \u0026 🐞 buggy code\n![Pull To Refresh \u0026 Load More](https://raw.githubusercontent.com/hackiftekhar/IQPullToRefresh/master/Documents/pull_to_refresh_load_more.gif)\n\n\nAn abstract IQPullToRefresh wrapper class \n==========================\nMost of the time, the pull to refresh and load more erquirements are same like\n\n- On pull to refresh, load 10 records using an api with page_index = 0 (or page_no = 1) and page_size = 10\n- On load more, load next batch of 10 records using same api with page_index = 1\t(or page_no = 2)and page_size = 10\n- Keep current state of [Model] array which comes from servers. Example like on load more success then add new records at the end of array etc.\n- Once server don’t have more records, disable load more feature.\n\n## IQRefreshAbstractWrapper abstract class blueprint\nThe IQRefreshAbstractWrapper mainly handles IQPullToRefresh delegate functions in most optimized way\n```swift\nopen class IQRefreshAbstractWrapper\u003cT\u003e {\n\n    public let pullToRefresh: IQPullToRefresh\n    public var pageOffset: Int\n    public var pageSize: Int\n    public var models: [T]\n\n    public init(scrollView: UIScrollView,\n                     pageOffset: Int, pageSize: Int)\n\n    public func addModelsUpdatedObserver(identifier: AnyHashable,\n                                         observer: (@Sendable @escaping @MainActor (_ result: Swift.Result\u003c[T], Error\u003e) -\u003e Void))\n    public func addStateObserver(identifier: AnyHashable,\n                                 observer: (@Sendable @escaping @MainActor (_ result: RefreshingState) -\u003e Void))\n\n    // Your subclass must override this function\n    open func request(page: Int, size: Int, completion: @Sendable @escaping @MainActor (Result\u003c[T], Error\u003e) -\u003e Void)\n}\n```\n\n### How to use IQRefreshAbstractWrapper\nLet’s assume, we would like to get list of many users (assume 100+), but 10 record each time when user pull to refresh or if user scroll then next 10 batch with load more. We'll need to create a subclass of IQRefreshAbstractWrapper class\n\n#### UsersStore subclass\n```swift\nclass UsersStore: IQRefreshAbstractWrapper\u003cUser\u003e {\n\n\t// Override the request function and return users based on page and size, that’s it.\n    open func request(page: Int, size: Int, completion: @Sendable @escaping @MainActor (Result\u003c[T], Error\u003e) -\u003e Void) {\n\t    APIClient.users(page: page, perPage: size, completion: completion)\n  }\n}\n```\n\n#### UsersViewController Implementation\nYou just need to create it's object and observe the modelsUpdatedObserver, when models list get's updated with either pull to refresh or load more, you'll get a callback here and you just need to connect those models with your UI now. It's this much simple to implement load more and pull to refresh now.\n\n```swift\nclass UsersViewModelController: UITableViewController {\n\n  private lazy var usersStore: UsersStore = UsersStore(scrollView: tableView, pageOffset: 1, pageSize: 10)\n\n  override func viewDidLoad() {\n    super.viewDidLoad()\n\n        // usersStore.pullToRefresh.enablePullToRefresh = false\t// You can always customize most of the things here\n        usersStore.addModelsUpdatedObserver(identifier: \"\\(Self.self)\") { result in\n            switch result {\n            case .success:\n                self.refreshUI(animated: true)\n            case .failure:\n                break\n            }\n        }\n  }\n\n  func refreshUI(animated: Bool = true) {\n\t  // Access usersStore.models to get list of users\n  }\n}\n```\n\nCustom Pull To Refresh or Load more UI\n==========================\nThis is all possible with implementing IQAnimatableRefresh protocol to your own UIView's subclasses.\n\n### IQAnimatableRefresh protocol Requirement\n\n- The class who adopt it must be a UIView\n- The class must implement 2 variables\n\n```swift\nvar refreshLength: CGFloat { get }\t// Height of your refresh view. Width in case of horizontal scroll \nvar refreshState: IQAnimatableRefreshState { get set } //State handling\n```\n\nThis can be \n```swift\npublic enum IQAnimatableRefreshState: Equatable {\n    case unknown            // Unknown state for initialization\n    case none               // refreshControler is not active\n    case pulling(CGFloat)   // Pulling the refreshControl\n    case eligible           // Progress is completed but touch not released\n    case refreshing         // Triggered refreshing\n}\n```\n\n#### Protocol Adoption\n```swift\nclass CustomPullToRefresh: UILabel, IQAnimatableRefresh {\n    var refreshLength: CGFloat {\n        return 80\n    }\n    var refreshState: IQAnimatableRefreshState = .none {\n        didSet {\n            guard refreshState != oldValue else { return }\n            switch refreshState {\n            case .none:\n                    alpha = 0\n                    text = \"\"\n            case .pulling(let progress):\n                    alpha = progress\n                    text = \"Pull to refresh\"\n            case .eligible:\n                    alpha = 1\n                    text = \"Release to refresh\"\n            case .refreshing:\n                    alpha = 1\n                    text = \"Loading\"\n            }\n        }\n    }\n    ...\n}\n```\n\n#### Assigning custom pull to refresh\n```swift\nclass UsersViewController: UITableViewController {\n    ...\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        ...\n  let customPullToRefresh = CustomPullToRefresh()\n        refresher.refreshControl = customPullToRefresh\n        ...\n    }\n    ...\n}\n```\n\n\nOther useful functions\n==========================\n\n```swift\n- public var enablePullToRefresh: Bool //Enable/Disable Pull To refresh\n- var isRefreshing: Bool { get } //Return true if refreshing in progress\n- func refresh() //Manually trigger refresh\n- public var refreshControl: IQAnimatableRefresh //Custom refreshControl\n\n- public var enableLoadMore: Bool //Enable/Disable load more feature\n- var isMoreLoading: Bool { get } //Return true if load more in progress\n- func loadMore() //Manually trigger load more\n- public var loadMoreControl: IQAnimatableRefresh //Custom loadMore\n```\n\nLICENSE\n---\nDistributed under the MIT License.\n\nContributions\n---\nAny contribution is more than welcome! You can contribute through pull requests and issues on GitHub.\n\nAuthor\n---\nIf you wish to contact me, email me: hack.iftekhar@gmail.com\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhackiftekhar%2Fiqpulltorefresh","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhackiftekhar%2Fiqpulltorefresh","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhackiftekhar%2Fiqpulltorefresh/lists"}