The Swift Package Index logo.Swift Package Index

追踪 Swift 6 严格并发检查对数据竞争安全性的采用情况。有多少个 Package 为 Swift 6 做好准备

当使用 Xcode 项目时

当使用 Swift Package Manager manifest 文件时

选择一个 package 版本

7.1.0

master


模型驱动的 UITableView/UICollectionView

Mohd Iftekhar Qurashi 欢迎通过 LFX Mentorship 支持 IQListKit。如果您觉得这个 package 有用,请考虑支持它。




IQListKit

模型驱动的 UITableView/UICollectionView

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

Build Status

IQListKit 允许我们使用 UITableView/UICollectionView 而无需实现 dataSource。只需提供 section 和它们的模型以及 cell 类型,它将处理其余的事情,包括所有更改的动画。

对于 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.7 及以上

安装

使用 CocoaPods 安装

CocoaPods

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

pod 'IQListKit'

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

pod 'IQListKit', '1.0.0'

使用源代码安装

Github tag

拖放 IQListKit 目录从演示项目到你的项目

使用 Swift Package Manager 安装

Swift Package Manager(SPM) 是 Apple 的依赖管理工具。它现在在 Xcode 11 中受支持。因此它可以在所有 appleOS 类型的项目中使用。它也可以与其他工具(如 CocoaPods 和 Carthage)一起使用。

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

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

如果您希望了解如何将其与 Modern Collection View 布局一起使用,那么您可以在此处下载演示文稿 PDF:带有 Modern Collection View 的 IQListKit

我们将通过一个简单的例子来学习 IQListKit。

假设我们必须在 UITableView 中显示一个用户列表,为此,我们有 User Model,如下所示

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. Models(在我们的例子中是 User 模型)与 Cell 类型(在我们的例子中是 UserCell)提供给 IQList,并看到奇迹 🥳🎉🎉🎉。

步骤 1)确认 Model(在我们的例子中是 User)符合 Hashable 协议。

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

什么是 Hashable?我以前从未使用过。

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

标准库中的许多类型都符合 Hashable:String、Int、Float、Double 和 Bool 值,甚至 Set 默认也是 hashable 的。为了确认 Hashable 协议,我们必须稍微修改我们的模型,如下所示

//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),最好我们还应该通过实现 static func == (lhs: User, rhs: User) -> Bool 来确认 Equatable 协议,如下所示

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 的变量,并且它可以是任何符合 Hashable 的类型。

假设我们有 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:在每个单元格中创建一个 Hashable 结构体(首选)

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

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 连接起来,例如设置标签文本、加载图像等。

为此,我们可以通过实现 model 变量的 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)将 Models(在我们的例子中是 User 模型)与 Cell 类型(在我们的例子中是 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({ [users] builder in

            //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 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")
            
            builder.append([section])

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

            //If model created using Method 1 or Method 2
            builder.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))
            }
            builder.append(UserCell.self, models: models, section: section)
            */

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

现在,每当我们的 users 数组更改时,我们将调用 refreshUI() 方法来重新加载 tableView,就是这样。

🥳

默认包装类 (IQListWrapper)

在大多数情况下,我们的需求相同,即在表格视图或集合视图中显示单个 section 的模型列表。如果属于这种情况,则可以使用 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 提供了一些委托来修改单元格或在单元格显示之前根据其模型进行额外的配置。为此,我们可以实现 IQList 的委托方法,如下所示:-

extension UsersTableViewController: IQListViewDelegateDataSource {

    func listView(_ listView: IQListView, modifyCell cell: some IQModelableCell, 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: Model, listView: IQListView) -> CGSize? {
        return CGSize(width: listView.frame.width, height: 100)
    }

    static func size(for model: Model, 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 {

    //...

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

        return [action]
    }

    func trailingSwipeActions() -> [UIContextualAction]? {

        let action1 = UIContextualAction(style: .normal, title: "Hello Trailing") { [weak self] (_, _, 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 {

    //...

    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
    }
    
    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 display
    func listView(_ listView: IQListView, willDisplay cell: some IQModelableCell, at indexPath: IndexPath)
    func listView(_ listView: IQListView, didEndDisplaying cell: some IQModelableCell, at indexPath: IndexPath)
    
    func listView(_ listView: IQListView, didSelect item: IQItem, 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: some IQModelableSupplementaryView,
                  section: IQSection, kind: String, at indexPath: IndexPath)
    func listView(_ listView: IQListView, willDisplaySupplementaryElement view: some IQModelableSupplementaryView,
                  section: IQSection, kind: String, at indexPath: IndexPath)
    func listView(_ listView: IQListView, didEndDisplayingSupplementaryElement view: some IQModelableSupplementaryView,
                  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) -> (any IQModelableSupplementaryView)?

    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 {

    //...

    struct Model: Hashable, IQReorderableModel, IQSelectableModel {

        // IQReorderableModel
        var canMove: Bool { false }
        var canEdit: Bool { false }
        var editingStyle: UITableViewCell.EditingStyle { .none }

        // IQSelectableModel
        var isHighlightable: Bool { true }
        var isDeselectable: Bool { true }
        var canPerformPrimaryAction: Bool { true }
    }
}

其他有用的 IQModelableCell 方法

class UserCell: UITableViewCell, IQModelableCell {

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

解决方法

IQListKit!😠 为什么你不加载我在故事板中创建的单元格?

好吧。如果我们在故事板中创建单元格,那么要使用 IQListKit,我们必须将单元格标识符设置为与其类名完全相同。如果我们使用 UICollectionView,那么我们还必须使用 list.registerCell(type: UserCell.self, registerType: .storyboard) 方法手动注册我们的单元格,因为对于 UICollectionView,无法检测单元格是否是在故事板中创建的。

我有一个大型数据集,list.reloadData 方法需要时间来为更改添加动画效果 😟。我能做什么?

您不会相信 reloadData 方法已经在单独的 后台线程 中运行😍。但是如果您愿意,可以显示一个 loadingIndicator。再次感谢 Apple 提供的 NSDiffableDataSourceSnapshot。UITableView/UICollectionView 将在主线程中重新加载。请参考以下代码:-

class UsersTableViewController: UITableViewController {

    //...

    func refreshUI(animated: Bool = true) {

        //Show loading indicator
        loadingIndicator.startAnimating()

        self.list.reloadData({ [users] builder in

            let section = IQSection(identifier: "section")
            builder.append(section)

            builder.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 上的 pull request 和 issue 进行贡献。

作者

如果您想联系我,请给我发电子邮件:hack.iftekhar@gmail.com