- section是按dataSource中的item的class来确定的,每个section有一个对应的
MessageSectionController
,需要遵守IGListSectionType
协议。
2.func objects(for listAdapter: ListAdapter) -> [ListDiffable]
返回的数据应该是不可变的。
Getting Started
Diffing
内置算法,可以发现新旧数据源之间的inserts, deletes, updates, moves操作。
需要遵守IGListDiffable
协议,并实现diffIdentifier()
和isEqual(toDiffableObject:)
方法。
diffIdentifier()
返回的标识不要更改。
class User {
let primaryKey: Int
let name: String
// implementation, etc
}
let shayne = User(primaryKey: 2, name: "Shayne")
let ann = User(primaryKey: 2, name: "Ann")
shayne
和ann
都表示相同的唯一数据,因为它们共享相同的primaryKey
,但由于name
不同,它们不相同。
IGListDiffable
协议实现:
extension User: IGListDiffable {
func diffIdentifier() -> NSObjectProtocol {
return primaryKey
}
func isEqual(toDiffableObject object: Any?) -> Bool {
if let object = object as? User {
return name == object.name
}
return false
}
}
算法会避免更新有相同primaryKey
和name
的User
对象,即使它们是不同的实例!即使提供新的实例,您现在也可以避免在集合视图中进行不必要的UI更新。
isEqual(toDiffableObject :)
返回false
时会更新相应cell.
Advanced Features
Working Range
IGListAdapter
初始化时需要传入workingRangeSize
,该值是可见高度或宽度的倍数,具体取决于滚动方向。
IGListDiffable and Equality
实例需要遵守IGListDiffable
协议,并实现diffIdentifier()
和isEqual(toDiffableObject:)
方法。
diffIdentifier()
用来确定数据的唯一性(类似数据库中的主键),isEqual(toDiffableObject:)
用来判断是否相等。
IGListDiffable bare minimum
- (id<NSObject>)diffIdentifier {
return self;
}
- (BOOL)isEqualToDiffableObject:(id<IGListDiffable>)object {
return [self isEqual:object];
}
Writing better Equality methods
- 如果重写了
-isEqual:
,必须重写-hash
。详情参考:article by Mike Ash - 首先比较指针。
- 比较对象值时,请在
-isEqual:
之前检查nil。举个栗子,[nil isEqual:nil]
返回的是NO
。 - 总是先比较开销最低的值。比如
[self.array isEqual:other.array] && self.intVal == other.intVal
是浪费的,应该先比较intVal
.
举个栗子:
声明:
@interface User : NSObject
@property NSInteger identifier;
@property NSString *name;
@property NSArray *posts;
@end
实现:
@implementation User
- (NSUInteger)hash {
return self.identifier;
}
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[User class]]) {
return NO;
}
User *right = object;
return self.identifier == right.identifier
&& (self.name == right.name || [self.name isEqual:right.name])
&& (self.posts == right.posts || [self.posts isEqualToArray:right.posts]);
}
@end
个人总结,之所以数据模型需要实现IGListDiffable
协议,目的是对内存不一致的模型进行比对,所以想正确对数据源进行update操作,应该是重新创建相应的数据模型进行覆盖。
Modeling and Binding
- 将设计规范转换为顶级模型和视图模型
- 使用
ListBindingSectionController
进行动画单向单元更新 - Cell-to-controller的动作处理和代理
- 通过本地数据变更更新UI
Getting Started
下载示例工程,打开ModelingAndBinding-Starter/ModelingAndBinding.xcworkspace。
IGListKit
基于一个模型对应一个section controller的理念。本设计中的所有cell都与服务器传递的一个顶级post对象相关联。
你们需要创建一个包含所有这些cell需要的信息的Post
对象。
一个常见的错误是为单个cell创建单个模型和section controller。在这个例子中,由于顶级对象包含用户,图像,动作和评论模型的混合搭配,因此会造成非常混乱的体系结构。
Creating Models
在工程中创建Post.swift文件:
import IGListKit
final class Post: ListDiffable {
// 1
let username: String
let timestamp: String
let imageURL: URL
let likes: Int
let comments: [Comment]
// 2
init(username: String, timestamp: String, imageURL: URL, likes: Int, comments: [Comment]) {
self.username = username
self.timestamp = timestamp
self.imageURL = imageURL
self.likes = likes
self.comments = comments
}
}
总是把值声明为let是最好的做法,它们不能再被改变。
由于IGListKit与Objective-C兼容,所以您的类必须是有初始化方法。
现在在Post
中添加ListDiffable
协议的实现:
// MARK: ListDiffable
func diffIdentifier() -> NSObjectProtocol {
// 1
return (username + timestamp) as NSObjectProtocol
}
// 2
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
return true
}
- 为每个post派生一个唯一标识符。由于单个帖子不应该有相同的用户名和时间戳组合,我们可以使用此作为唯一标识符。
- 使用
ListBindingSectionController
的核心要求是,如果两个模型具有相同的diffIdentifier
,则它们必须相等,以便section controller可以比较视图模型。
View Models
创建Comment.swift文件,并实现Comment
模型:
final class Comment: ListDiffable {
let username: String
let text: String
init(username: String, text: String) {
self.username = username
self.text = text
}
func diffIdentifier() -> NSObjectProtocol {
return (username + text) as NSObjectProtocol
}
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
return true
}
}
在Post
中使用Comment
数组:每篇文章都有一些动态的评论,每个cell上展示一条评论。
当你使用ListBindingSectionController
时,你需要为UserCell
,ImageCell
和ActionCell
创建模型,你需要接受一点新的理念。
一个绑定的section controller几乎就像一个迷你的IGListKit。它需要一个视图模型数组,并将其转换为配置的cell。养成为
ListBindingSectionController
实例中的每个单元格类型创建新模型的习惯。
考虑到这一点,让我们从UserCell
的模型开始:
创建UserViewModel.swift文件:
import IGListKit
final class UserViewModel: ListDiffable {
let username: String
let timestamp: String
init(username: String, timestamp: String) {
self.username = username
self.timestamp = timestamp
}
// MARK: ListDiffable
func diffIdentifier() -> NSObjectProtocol {
// 1
return "user" as NSObjectProtocol
}
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
// 2
guard let object = object as? UserViewModel else { return false }
return username == object.username
&& timestamp == object.timestamp
}
}
由于每个帖子只有一个UserViewModel,所以你可以硬编码一个标识符。这将只强制使用一个单一的模型和单元格。
为ImageCell
和ActionCell
创建视图模型,参考代码位于示例工程中
Using ListBindingSectionController
您现在有以下视图模型,它们都可以从每个Post
对象派生:
- UserViewModel
- ImageViewModel
- ActionViewModel
- Comment
创建PostSectionController.swift文件并添加以下代码:
final class PostSectionController: ListBindingSectionController<Post>,
ListBindingSectionControllerDataSource {
override init() {
super.init()
dataSource = self
}
}
注意你继承了ListBindingSectionController <Post>。这将声明您的节控制器接收Post
模型。这样就不用对model做特殊处理。
数据源根据协议,需要实现3个方法:
- 返回顶层模型的视图模型数组(Post)
- 返回给定视图模型的大小
- 为给定的视图模型返回一个cell
首先关注Post
到视图模型的转换:
// MARK: ListBindingSectionControllerDataSource
func sectionController(
_ sectionController: ListBindingSectionController<ListDiffable>,
viewModelsFor object: Any
) -> [ListDiffable] {
// 1
guard let object = object as? Post else { fatalError() }
// 2
let results: [ListDiffable] = [
UserViewModel(username: object.username, timestamp: object.timestamp),
ImageViewModel(url: object.imageURL),
ActionViewModel(likes: object.likes)
]
// 3
return results + object.comments
}
接下来添加所需的API以返回每个视图模型的大小:
func sectionController(
_ sectionController: ListBindingSectionController<ListDiffable>,
sizeForViewModel viewModel: Any,
at index: Int
) -> CGSize {
// 1
guard let width = collectionContext?.containerSize.width else { fatalError() }
// 2
let height: CGFloat
switch viewModel {
case is ImageViewModel: height = 250
case is Comment: height = 35
// 3
default: height = 55
}
return CGSize(width: width, height: height)
}
- 就像
object
属性一样,collectionContext
不应该为空,但它是一个弱引用的对象,因此必须声明为可选类型。再次,使用fatalError()
来捕捉任何关键的失败。 -
UserViewModel
和ActionViewModel
高度皆为55.
最后实现返回cell的API。
cell是在Main.storyboard中定义的。可以点击每个cell来查看其标识符。
func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, cellForViewModel viewModel: Any, at index: Int) -> UICollectionViewCell {
let identifier: String
switch viewModel {
case is ImageViewModel: identifier = "image"
case is Comment: identifier = "comment"
case is UserViewModel: identifier = "user"
default: identifier = "action"
}
guard let cell = collectionContext?.dequeueReusableCellFromStoryboard(withIdentifier: identifier, for: self, at: index) else { fatalError() }
return cell
}
Binding Models to Cells
现在,由PostSectionController
来创建视图模型,尺寸和单元格。使用ListBindingSectionController
的最后一部分是让cell接收他们分配的视图模型并进行自我配置。
ListBindingSectionController
将自动绑定视图模型到每个遵守ListBindable
协议的cell。
修改 ImageCell.swift 中的代码:
import UIKit
import SDWebImage
// 1
import IGListKit
// 2
final class ImageCell: UICollectionViewCell, ListBindable {
@IBOutlet weak var imageView: UIImageView!
// MARK: ListBindable
func bindViewModel(_ viewModel: Any) {
// 3
guard let viewModel = viewModel as? ImageViewModel else { return }
// 4
imageView.sd_setImage(with: viewModel.url)
}
}
- 导入
IGListKit
。 - cell遵从
ListBindable
协议。 - 判断视图模型的类型。
- 使用
SDWebImage
下载图片。
最后,修改其他3个cell中的代码。
Displaying in the View Controller
最后一步是让PostSectionController
显示在app的列表中。
返回ViewController.swift
并在设置dataSource
或collectionView
之前添加以下内容到viewDidLoad()
中:
data.append(Post(
username: "@janedoe",
timestamp: "15min",
imageURL: URL(string: "https://placekitten.com/g/375/250")!,
likes: 384,
comments: [
Comment(username: "@ryan", text: "this is beautiful!"),
Comment(username: "@jsq", text: "😱"),
Comment(username: "@caitlin", text: "#blessed"),
]
))
最后,修改listAdapter(_, sectionControllerFor object:)
:
func listAdapter(
_ listAdapter: ListAdapter,
sectionControllerFor object: Any
) -> ListSectionController {
return PostSectionController()
}
通常你会根据
object
的类型返回不同的ListSectionController
,但是因为现在只有Post
对象,只返回一个新的PostSectionController
是安全的。
运行工程,看看效果。
Handling Cell Actions
点击ActionCell
上的❤️
按钮,将事件转发到PostSectionController
:
在 ActionCell.swift 中添加以下协议:
protocol ActionCellDelegate: class {
func didTapHeart(cell: ActionCell)
}
在ActionCell
中添加新的delegate变量:
weak var delegate: ActionCellDelegate? = nil
重写awakeFromNib()
,为❤️
按钮添加target-action:
override func awakeFromNib() {
super.awakeFromNib()
likeButton.addTarget(self, action: #selector(ActionCell.onHeart), for: .touchUpInside)
}
func onHeart() {
delegate?.didTapHeart(cell: self)
}
修改PostSectionController.swift中的cellForViewModel:
方法。在方法最后添加以下代码:
if let cell = cell as? ActionCell {
cell.delegate = self
}
实现cell的代理方法:
final class PostSectionController: ListBindingSectionController<Post>,
ListBindingSectionControllerDataSource,
ActionCellDelegate {
//...
// MARK: ActionCellDelegate
func didTapHeart(cell: ActionCell) {
print("like")
}
Local Mutations
每次有人点击❤️
按钮,都需要在Post
上添加一个新的like。但是,所有的模型都是用let声明的,因为不可变的模型是一个更安全的设计。但是,如果一切都是不可变的,我们如何改变like的计数呢?
PostSectionController
是处理和存储变量的理想场所。打开PostSectionController.swift
并添加以下变量:
var localLikes: Int? = nil
在代理方法didTapHeart(cell:)
中添加以下代码:
func didTapHeart(cell: ActionCell) {
// 1
localLikes = (localLikes ?? object?.likes ?? 0) + 1
// 2
update(animated: true)
}
调用ListBindingSectionController
上的update(animated:,completion:)
API来刷新屏幕上的cell。
为了将变化反映到模型,您需要在提供给ActionCell
的ActionViewModel
中使用localLikes
。
在PostSectionController.swift
中,找到cellForViewModel:
API并将ActionViewModel
初始化相关代码更改为以下内容:
ActionViewModel(likes: localLikes ?? object.likes)
Working with UICollectionView
本指南提供了有关如何使用UICollectionView和IGListKit的详细信息。
Background
2.x之前的版本的IGListKit
中,包含UICollectionView
的子类IGListCollectionView
。3.0版本之后,IGListCollectionView
已经被删除。
Methods to avoid
IGListKit
的主要目的之一是为UICollectionView
执行最佳的批量更新。因此,客户端应该从不在UICollectionView
上调用任何涉及重新加载,插入,删除或更新cell和index paths的API。作为替代,使用IGListAdapter
提供的API。你也应该避免设置 collection view的数据源和代理,因为这也是IGListAdapter
的责任。
避免调用以下方法:
- (void)performBatchUpdates:(void (^)(void))updates
completion:(void (^)(BOOL))completion;
- (void)reloadData;
- (void)reloadSections:(NSIndexSet *)sections;
- (void)insertSections:(NSIndexSet *)sections;
- (void)deleteSections:(NSIndexSet *)sections;
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection;
- (void)insertItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (void)reloadItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (void)deleteItemsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths;
- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath;
- (void)setDelegate:(id<UICollectionViewDelegate>)delegate;
- (void)setDataSource:(id<UICollectionViewDataSource>)dataSource;
- (void)setBackgroundView:(UIView *)backgroundView;
Performance
在iOS 10中,引入了新的单元预取API。在Instagram上,启用此功能会显著降低滚动性能。我们建议将isPrefetchingEnabled
设置为NO
(在Swift中为false
)。请注意,默认值是true
。
您可以使用UIAppearance
在进行全局设置:
if ([[UICollectionView class] instancesRespondToSelector:@selector(setPrefetchingEnabled:)]) {
[[UICollectionView appearance] setPrefetchingEnabled:NO];
}
if #available(iOS 10, *) {
UICollectionView.appearance().isPrefetchingEnabled = false
}