IQListKit 7.0.2

IQListKit 7.0.2

Mohd Iftekhar Qurashi 维护。



IQListKit 7.0.2

  • 作者
  • Iftekhar Qurashi

IQListKit

基于模型的 UITableView/UICollectionView

[Insertion Sort] [Conference Video Feed] [Orthogonal Section] [Mountains] [User List]

Build Status

IQListKit 允许我们使用 UITableView/UICollectionView 而不必实现 dataSource。只需提供部分及其模型(包括单元格类型),它将自动处理其余操作,包括所有更改的动画。

针对 iOS13:感谢 Apple 为我们提供了 NSDiffableDataSourceSnapshot

需求

Platform iOS

语言 最低 iOS 目标 最低 Xcode 版本
IQListKit (1.1.0) Swift iOS 9.0 Xcode 11
IQListKit (4.0.0) Swift iOS 13.0 Xcode 14
IQListKit (5.0.0) Swift iOS 13.0 Xcode 14

支持的 Swift 版本

5.0 及以上

安装

使用 CocoaPods 安装

CocoaPods

IQListKit 通过 CocoaPods 提供。要安装它,只需将以下行添加到您的 Podfile 中:

pod 'IQListKit'

或者,您可以根据 Swift 支持 表格从 需求 选择您需要的版本。

pod 'IQListKit', '1.0.0'

使用源代码安装

Github tag

拖放 IQListKit 目录从示例项目到您的项目中

使用 Swift 包管理器安装

Swift 包管理器(SPM) 是苹果的依赖项管理工具。它现在在 Xcode 11 中受到支持,因此可以用于所有苹果 OS 类型的项目。它也可以与其他工具(如 CocoaPods 和 Carthage)一起使用。

要将 IQListKit 包安装到您的包中,请将 IQListKit 的引用和目标发布版本添加到 Package.swift 文件中的依赖项部分。

import PackageDescription

let package = Package(
    name: "YOUR_PROJECT_NAME",
    products: [],
    dependencies: [
        .package(url: "https://github.com/hackiftekhar/IQListKit.git", from: "1.0.0")
    ]
)

通过 Xcode 安装 IQListKit 包

如何使用 IQListKit?

如果您希望通过演示文稿来学习,请在此处下载演示文稿 PDF: 演示文稿 PDF

如果您想学习如何将其与现代集合视图布局一起使用,请在此处下载演示文稿 PDF: IQListKit 与现代集合视图

我们将使用一个简单的示例来学习 IQListKit。

假设我们需要在一个 UITableView 中显示用户列表,为此,我们有一个像这样的 用户模型

struct User {

    let id: Int       //A unique id of each user
    let name: String  //Name of the user
}

5 简单步骤

  1. 确保 Model(在我们的例子中是 User)遵从 Hashable 协议。
  2. 确保 Cell(在我们的例子中是 UserCell)遵从 IQModelableCell 协议,这强制具有 var model: Model? 属性。
  3. ModelCell UI 连接,例如设置标签文本、加载图像等。
  4. 在您的 ViewController 中创建 IQList 变量,并根据需要可选地配置设置。
  5. Model(在我们的例子中是 User 模型)与 Cell 类型(在我们的例子中是 UserCell)提供给 IQList,看看魔法吧🥳🎉🎉🎉.

步骤 1) 确保 Model(在我们的例子中是 User)遵从 Hashable 协议。

在深入实现之前,我们必须了解 Hashable 协议。

现在什么是可哈希的(Hashable)?我之前从未使用过它。

可哈希协议用于确定对象/变量的唯一性。技术上来说,可哈希是一种具有整数形式的hashValue,可以在不同类型之间进行比较的类型。

标准库中的许多类型都符合可哈希协议:String, Int, Float, Double和Bool值以及甚至是默认的可哈希的Set。为了确认可哈希协议,我们需要稍微修改一下我们的模型,如下所示:

//We have Int and String variables in the struct
//That's why we do not have to manually confirm the hashable protocol
//It will work out of the box by just adding the hashable protocol to the User struct
struct User: Hashable {

    let id: Int
    let name: String
}

但是如果我们想手动确认,我们必须实现func hash(into hasher: inout Hasher),并且最好我们还应该通过实现如下方式来确认Equatable协议:static func == (lhs: User, rhs: User) -> Bool

struct User: Hashable {

    func hash(into hasher: inout Hasher) {  //Manually Confirming to the Hashable protocol
        hasher.combine(id)
    }

    static func == (lhs: User, rhs: User) -> Bool { //Manually confirming to the Equatable protocol
        lhs.id == rhs.id && lhs.name == rhs.name
    }

    let id: Int
    let name: String
}

现在让我们回到实现部分。要使用IQListKit,我们必须遵循几个步骤:

步骤2)确认Cell(在我们的情况下是UserCell)到IQModelableCell协议,这将强制具有一个var model: Model?属性。

什么是IQModelableCell协议?我们应该如何确认它?

IQModelableCell协议说,无论谁采用我,都必须暴露一个名为model的变量,它可以接受任何符合可哈希协议的类型。

假设我们有一个像这样的UserCell

class UserCell: UITableViewCell {

    @IBOutlet var labelName: UILabel!
}

我们可以通过暴露model命名的变量简单易行地以几种方式确认它。

方法 1:直接使用我们的 User 模型

class UserCell: UITableViewCell, IQModelableCell {

    @IBOutlet var labelName: UILabel!
    
    var model: User?  //Our User model confirms the Hashable protocol
}

方法 2:将我们的 User 模型重命名为通用的名称如 'Model'

class UserCell: UITableViewCell, IQModelableCell {

    @IBOutlet var labelName: UILabel!
    
    typealias Model = User  //typealiasing the User model to a common name

    var model: Model?  //Model is a typealias of User
}

方法 3:在每个单元格中创建一个可哈希的 struct(推荐)

这种方法是首选的,因为它将能够在模型中使用多个参数

class UserCell: UITableViewCell, IQModelableCell {

    @IBOutlet var labelName: UILabel!
    
    struct Model: Hashable {
        let user: User
        let canShowMenu: Bool //custom parameter which can be controlled from ViewControllers
        let paramter2: Int  //Another customized parameter
        ... and so on (if needed)
    }

    var model: Model?  //Our new Model struct confirms the Hashable protocol
}

步骤 3) 将 ModelCell UI 连接,例如设置标签文本、加载图片等。

为此,我们可以通过实现模型变量的 didSet 来轻松实现

class UserCell: UITableViewCell, IQModelableCell {

    @IBOutlet var labelName: UILabel!
    
    var model: User? {  //For simplicity, we'll be using the 1st method
        didSet {
            guard let model = model else {
                return
            }
        
            labelName.text = model.name
        }
    }
}

第4步)在你的ViewController中创建IQList变量,如有必要,可配置可选设置。

假设我们有一个类似于这样的UsersTableViewController

class UsersTableViewController: UITableViewController {

    private var users = [User]() //assuming the users array is loaded from somewhere e.g. API call response

    //...

    func loadDataFromAPI() {
    
        //Get users list from API
        APIClient.getUsersList({ [weak self] result in
        
            switch result {

                case .success(let users):
                    self?.users = users   //Updates the users array
                    self?.refreshUI()     //Refresh the data
            
                case .failure(let error):
                    //Handle error
            }
        }
    }
}

现在我们将创建一个IQList的实例,并提供模型列表和单元格类型。listView参数接受UITableView或UICollectionView。delegateDataSource参数是可选的,但如果我们想在单元格显示之前进行额外配置或希望在单元格被点击时获取回调,则更愿意使用。

class UsersTableViewController: UITableViewController {

    //...

    private lazy var list = IQList(listView: tableView, delegateDataSource: self)
    
    override func viewDidLoad() {
        super.viewDidLoad()

//        Optional configuration when there are no items to display
//        list.noItemImage = UIImage(named: "empty")
//        list.noItemTitle = "No Items"
//        list.noItemMessage = "Nothing to display here."
//        list.noItemAction(title: "Reload", target: self, action: #selector(refresh(_:)))
    }
}

extension UsersTableViewController: IQListViewDelegateDataSource {
}

第5步)将模型(在我们的示例中为User模型)和单元格类型(在我们的示例中为UserCell)提供给IQList,并见证魔法。🥳🎉🎉🎉.

让我们在名为refreshUI的独立函数中完成这件事。

class UsersTableViewController: UITableViewController {

    //...

    func refreshUI(animated: Bool = true) {

        //This is the actual method that reloads the data.
        //We could think it like a tableView.reloadData()
        //It does all the needed thing
        list.reloadData({

            //If we use multiple sections, then each section should be unique.
            //This should also confirm to hashable, so we can also provide a Int
            //like this `let section = IQSection(identifier: 1)`
            let section = IQSection(identifier: "first")
            
            //We can also provide the header/footer title, they are optional
            //let section = IQSection(identifier: "first",
            //                        header: "I'm header",
            //                        footer: "I'm footer")
            
            /*Or we an also provide custom header footer view and it's model, just like the cell,
              We just have to adopt IQModelableSupplementaryView protocol to SampleCollectionReusableView 
              And it's also havie exactly same requirement as cell (have a model property)
              However, you also need to register this using
              list.registerSupplementaryView(type: SampleCollectionReusableView.self,
                                         kind: UICollectionView.elementKindSectionHeader, registerType: .nib)
              and use it like below
            */
            //let section = IQSection(identifier: "first",
            //                        headerType: SampleCollectionReusableView.self,
            //                        headerModel: "This is my header text for Sample Collection model")
            
            list.append(section)

            //Telling to the list that the models should render in UserCell

            //If model created using Method 1 or Method 2
            list.append(UserCell.self, models: users, section: section) 
            
            /*
            If model created using Method 3
            var models = [UserCell.Model]()

            for user in users {
                models.append(.init(user: user))
            }
            list.append(UserCell.self, models: models, section: section)
            */

        //controls if the changes should animate or not while reloading
        }, animatingDifferences: animated, completion: nil)
    }
}

现在每次我们的用户数组发生变化时,我们将调用refreshUI()方法来重新加载tableView,这样就可以了。

🥳

默认包装类(IQListWrapper)

大多数情况下,我们都有相同的需求,在表格视图或收集视图中显示单节模型的列表。如果是这种情况,则可以使用IQListWrapper类轻松显示对象的单节列表。此类处理ViewController中的所有样板代码。

您只需要初始化listWrapper并提供表格视图和单元格类型,然后只需将模型传递给它,它就会在您的tableView和collectionView中刷新您的列表。

class MountainsViewController: UIViewController {

    //...

    private lazy var listWrapper = IQListWrapper(listView: userTableView,
                                                 type: UserCell.self,
                                                 registerType: .nib, delegateDataSource: self)
    
    //...
    
    func refreshUI(models: [User]) {
        listWrapper.setModels(models, animated: true)
    }
    //...
}

UITableView/UICollectionView 委托和数据源替代品

- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

IQListKit 是一个以模型驱动的框架,因此我们将处理 Cell 和模型而不是 IndexPath。IQListKit 提供了一些委托来在 Cell 显示之前修改 Cell 或根据它们的模型执行额外的配置。为此,我们可以实现类似于以下 IQList 的委托方法:

extension UsersTableViewController: IQListViewDelegateDataSource {

    func listView(_ listView: IQListView, modifyCell cell: IQListCell, at indexPath: IndexPath) {
        if let cell = cell as? UserCell { //Casting our cell as UserCell
            cell.delegate = self
            //Or additional work with the UserCell
            
            //🙂 Get the user object associated with the cell
            let user = cell.model

            //We discourage to use the indexPath variable to get the model object
            //😤 Don't do like this since we are model-driven list, not the indexPath driven list.
            //let user = users[indexPath.row]
        }
    }
}

- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)

别担心,我们会直接提供与单元格关联的用户模型。这很有趣!

extension UsersTableViewController: IQListViewDelegateDataSource {

    func listView(_ listView: IQListView, didSelect item: IQItem, at indexPath: IndexPath) {
        if let model = item.model as? UserCell.Model { //😍 We get the user model associated with the cell
            if let controller = UIStoryboard(name: "Main", bundle: nil)
            .instantiateViewController(identifier: "UserDetailViewController") as? UserDetailViewController {
                controller.user = model //If used Method 1 or Method 2
                //  controller.user = model.user  //If used method 3
                self.navigationController?.pushViewController(controller, animated: true)
            }
        }
    }
}

- func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat

- func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat

因为此方法主要根据单元格及其模型返回值,我们将这些配置移动到单元格中。这是 IQCellSizeProvider 协议的一部分,并且我们可以重写默认行为。

class UserCell: UITableViewCell, IQModelableCell {

    //...

    static func estimatedSize(for model: AnyHashable?, listView: IQListView) -> CGSize {
        return CGSize(width: listView.frame.width, height: 100)
    }

    static func size(for model: AnyHashable?, listView: IQListView) -> CGSize {

        if let model = model as? Model {
            var height: CGFloat = 100
            //...
            // return height based on the model
            return CGSize(width: listView.frame.width, height: height)
        }

        //Or return a constant height
        return CGSize(width: listView.frame.width, height: 100)

        //Or UITableView.automaticDimension for dynamic behaviour
//        return CGSize(width: listView.frame.width, height: UITableView.automaticDimension)
    }
}

- func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?

- func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?

此方法同样主要根据单元格及其模型返回值,我们将这些配置移动到单元格中。这是 IQCellActionsProvider 协议的一部分,并且我们可以重写默认行为。

class UserCell: UITableViewCell, IQModelableCell {

    //...

    @available(iOS 11.0, *)
    func leadingSwipeActions() -> [IQContextualAction]? {
        let action = IQContextualAction(style: .normal, title: "Hello Leading") { (action, completionHandler) in
            completionHandler(true)
            //Do your stuffs here
        }
        action.backgroundColor = UIColor.orange

        return [action]
    }

    func trailingSwipeActions() -> [IQContextualAction]? {

        let action1 = IQContextualAction(style: .normal, title: "Hello Trailing") { [weak self] (action, completionHandler) in
            completionHandler(true)
            guard let self = self, let user = self.model else {
                return
            }

            //Do your stuffs here
        }

        action.backgroundColor = UIColor.purple

        return [action]
    }
}

- func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration?

- func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating)

该方法也主要根据单元格及其模型返回值,我们将这些配置移动到了单元格中。这也是 IQCellActionsProvider 协议的一部分,并且我们可以覆盖默认行为。

class UserCell: UITableViewCell, IQModelableCell {

    //...

    @available(iOS 13.0, *)
    func contextMenuConfiguration() -> UIContextMenuConfiguration? {

        let contextMenuConfiguration = UIContextMenuConfiguration(identifier: nil,
                                                                  previewProvider: { () -> UIViewController? in
            let controller = UIStoryboard(name: "Main", bundle: nil)
            .instantiateViewController(identifier: "UserViewController") as? UserViewController
            controller?.user = self.model
            return controller
        }, actionProvider: { (actions) -> UIMenu? in

            var actions = [UIMenuElement]()
            let action = UIAction(title: "Hello Action") { _ in
                //Do your stuffs here
            }
            actions.append(action)

            return UIMenu(title: "Nested Menu", children: actions)
        })

        return contextMenuConfiguration
    }
    
    @available(iOS 13.0, *)
    func performPreviewAction(configuration: UIContextMenuConfiguration,
                              animator: UIContextMenuInteractionCommitAnimating) {
        if let previewViewController = animator.previewViewController, let parent = viewParentController {
            animator.addAnimations {
                (parent.navigationController ?? parent).show(previewViewController, sender: self)
            }
        }
    }
}

private extension UIView {
    var viewParentController: UIViewController? {
        var parentResponder: UIResponder? = self
        while let next = parentResponder?.next {
            if let viewController = next as? UIViewController {
                return viewController
            } else {  parentResponder = next  }
        }
        return nil
    }
}

其他有用的代理方法

extension UsersTableViewController: IQListViewDelegateDataSource {

    //...

    //Cell will about to display
    func listView(_ listView: IQListView, willDisplay cell: IQListCell, at indexPath: IndexPath)

    //Cell did end displaying
    func listView(_ listView: IQListView, didEndDisplaying cell: IQListCell, at indexPath: IndexPath)
    
    func listView(_ listView: IQListView, didDeselect item: IQItem, at indexPath: IndexPath)
    
    func listView(_ listView: IQListView, didHighlight item: IQItem, at indexPath: IndexPath)
    
    func listView(_ listView: IQListView, didUnhighlight item: IQItem, at indexPath: IndexPath)
    
    func listView(_ listView: IQListView, performPrimaryAction item: IQItem, at indexPath: IndexPath)
    
    func listView(_ listView: IQListView, modifySupplementaryElement view: UIView,
                  section: IQSection, kind: String, at indexPath: IndexPath)
                  
    func listView(_ listView: IQListView, willDisplaySupplementaryElement view: UIView,
                  section: IQSection, kind: String, at indexPath: IndexPath)
                 
    func listView(_ listView: IQListView, didEndDisplayingSupplementaryElement view: UIView,
                  section: IQSection, kind: String, at indexPath: IndexPath)
               
    func listView(_ listView: IQListView, willDisplayContextMenu configuration: UIContextMenuConfiguration,
                  animator: UIContextMenuInteractionAnimating?, item: IQItem, at indexPath: IndexPath)
                  
    func listView(_ listView: IQListView, willEndContextMenuInteraction configuration: UIContextMenuConfiguration,
                  animator: UIContextMenuInteractionAnimating?, item: IQItem, at indexPath: IndexPath)
}

其他有用的数据源方法

extension UsersTableViewController: IQListViewDelegateDataSource {

    //...

     //Return the size of an Item, for tableView the size.height will only be effective
    func listView(_ listView: IQListView, size item: IQItem, at indexPath: IndexPath) -> CGSize?

    //Return the custom header or footer View of section (or item in collection view)
    func listView(_ listView: IQListView, supplementaryElementFor section: IQSection,
                  kind: String, at indexPath: IndexPath) -> UIView?

    func sectionIndexTitles(_ listView: IQListView) -> [String]?
    
    func listView(_ listView: IQListView, prefetch items: [IQItem], at indexPaths: [IndexPath])
    
    func listView(_ listView: IQListView, cancelPrefetch items: [IQItem], at indexPaths: [IndexPath])
    
    func listView(_ listView: IQListView, canMove item: IQItem, at indexPath: IndexPath) -> Bool
    
    func listView(_ listView: IQListView, move sourceItem: IQItem,
                  at sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
                  
    func listView(_ listView: IQListView, canEdit item: IQItem, at indexPath: IndexPath) -> Bool
    
    func listView(_ listView: IQListView, commit item: IQItem,
                  style: UITableViewCell.EditingStyle, at indexPath: IndexPath)
}

其他有用的IQModelableCell属性

class UserCell: UITableViewCell, IQModelableCell {

    //...

    var isHighlightable: Bool { //IQSelectableCell protocol
        return true
    }

    var isSelectable: Bool {    //IQSelectableCell protocol
        return false
    }
    
    var isDeselectable: Bool {   //IQSelectableCell protocol
        return false
    }
    
    var canPerformPrimaryAction: Bool { //IQSelectableCell protocol
        return false
    }
    
    var canMove: Bool {         //IQReorderableCell protocol
        return false
    }

    var canEdit: Bool {         //IQReorderableCell protocol
        return false
    }
    
    var editingStyle: UITableViewCell.EditingStyle {    //IQReorderableCell protocol
        return .none
    }
}

其他有用的IQModelableCell方法

class UserCell: UITableViewCell, IQModelableCell {

    //...
    
    // IQViewSizeProvider protocol
    static func indentationLevel(for model: AnyHashable?, listView: IQListView) -> Int {
        return 1
    }
    
    //IQCellActionsProvider protocol
    func contextMenuPreviewView(configuration: UIContextMenuConfiguration) -> UIView? {
        return viewToBePreview
    }
}

解决方案

IQListKit!😠为什么你不会加载我在 storyboard 中创建的 cell 呢?

嗯,如果我们是在 storyboard 中创建 cell,那么要使用 IQListKit,我们必须确保 cell 的标识符与其类名完全相同。如果我们使用 UICollectionView,我们还需要使用 list.registerCell(type: UserCell.self, registerType: .storyboard) 方法手动注册我们的 cell,因为在 UICollectionView 中,无法检测 cell 是否是在 storyboard 中创建的。

我有大量数据集,并且 list.reloadData 方法更新变化时耗时,我该怎么办?😟。我能做什么?

您可能不知道,performUpdtes 方法是 后台线程安全😍。我们可以在后台调用它,并显示一个加载指示器。在完成处理程序中,我们可以隐藏加载指示器。在底层,更改计算将在后台执行。再次感谢苹果公司为 NSDiffableDataSourceSnapshot。UITableView/UICollectionView 将在主线程中重新加载。请参考以下代码:

class UsersTableViewController: UITableViewController {

    //...

    func refreshUI(animated: Bool = true) {

        //Show loading indicator
        loadingIndicator.startAnimating()

        //Reload data in background
        DispatchQueue.global().async {

            self.list.reloadData({

                let section = IQSection(identifier: "first")
                self.list.append(section)

                self.list.append(UserCell.self, models: users, section: section)

            }, animatingDifferences: animated, completion: {
                //Hide loading indicator since the completion will be called in main thread
                self.loadingIndicator.stopAnimating()
            })
        }
    }
}

许可证

基于 MIT 许可证分发。

贡献

任何贡献都受欢迎!您可以通过 GitHub 上的拉动请求和问题来进行贡献。

作者

如果您想联系我,请发送电子邮件至: [email protected]