Mohd Iftekhar Qurashi 欢迎通过 LFX Mentorship 支持 IQListKit。如果您觉得这个 package 有用,请考虑支持它。
当使用 Xcode 项目时
当使用 Swift Package Manager manifest 文件时
选择一个 package 版本
7.1.0
master
模型驱动的 UITableView/UICollectionView
模型驱动的 UITableView/UICollectionView
IQListKit 允许我们使用 UITableView/UICollectionView 而无需实现 dataSource。只需提供 section 和它们的模型以及 cell 类型,它将处理其余的事情,包括所有更改的动画。
对于 iOS13:感谢 Apple 提供的 NSDiffableDataSourceSnapshot
库 | 语言 | 最低 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 |
5.7 及以上
IQListKit 可通过 CocoaPods 获得。要安装它,只需将以下行添加到您的 Podfile
pod 'IQListKit'
或者您可以根据 要求 中的 Swift 支持表选择您需要的版本
pod 'IQListKit', '1.0.0'
拖放 IQListKit
目录从演示项目到你的项目
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 包
- 转到 File -> Swift Packages -> Add Package Dependency...
- 然后搜索 https://github.com/hackiftekhar/IQListKit.git
- 并选择您想要的版本
如果您希望通过演示文稿学习使用方法,请在此处下载演示文稿 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
}
- 确认
Model
(在我们的例子中是User
)符合Hashable
协议。 - 确认
Cell
(在我们的例子中是UserCell
)符合IQModelableCell
协议,该协议强制要求有一个var model: Model?
属性。 - 将
Model
与Cell
UI 连接起来,例如设置标签文本、加载图像等。 - 在您的
ViewController
中创建IQList
变量,并根据需要有选择地配置可选设置。 - 将
Models
(在我们的例子中是User
模型)与 Cell 类型(在我们的例子中是UserCell
)提供给 IQList,并看到奇迹 🥳🎉🎉🎉。
在深入实现之前,我们必须了解 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,我们必须遵循几个步骤
IQModelableCell 协议表示,任何采用我的人都必须公开一个名为 model 的变量,并且它可以是任何符合 Hashable 的类型。
假设我们有 UserCell 如下所示
class UserCell: UITableViewCell {
@IBOutlet var labelName: UILabel!
}
我们可以通过公开一个名为 model 的变量来轻松地确认它,有几种方法。
class UserCell: UITableViewCell, IQModelableCell {
@IBOutlet var labelName: UILabel!
var model: User? //Our User model confirms the Hashable protocol
}
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
}
这种方法是首选的,因为它将能够在模型中使用多个参数
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
}
为此,我们可以通过实现 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
}
}
}
假设我们有一个 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 {
}
让我们在一个单独的名为 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,就是这样。
🥳
在大多数情况下,我们的需求相同,即在表格视图或集合视图中显示单个 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)
}
//...
}
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]
}
}
}
啊,不用担心。我们将直接为您提供与单元格关联的用户模型。这很有趣!
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)
}
}
}
}
由于此方法主要根据单元格及其模型返回值,因此我们已将这些配置移至单元格。这是 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)
}
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 }
}
}
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,我们必须将单元格标识符设置为与其类名完全相同。如果我们使用 UICollectionView,那么我们还必须使用 list.registerCell(type: UserCell.self, registerType: .storyboard) 方法手动注册我们的单元格,因为对于 UICollectionView,无法检测单元格是否是在故事板中创建的。
您不会相信 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