
- section是按dataSource中的item的class來確定的,每個(gè)section有一個(gè)對(duì)應(yīng)的
MessageSectionController,需要遵守IGListSectionType協(xié)議。
2.func objects(for listAdapter: ListAdapter) -> [ListDiffable]返回的數(shù)據(jù)應(yīng)該是不可變的。
Getting Started
Diffing
內(nèi)置算法,可以發(fā)現(xiàn)新舊數(shù)據(jù)源之間的inserts, deletes, updates, moves操作。
需要遵守IGListDiffable協(xié)議,并實(shí)現(xiàn)diffIdentifier()和isEqual(toDiffableObject:)方法。
diffIdentifier()返回的標(biāo)識(shí)不要更改。
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都表示相同的唯一數(shù)據(jù),因?yàn)樗鼈児蚕硐嗤?code>primaryKey,但由于name不同,它們不相同。
IGListDiffable協(xié)議實(shí)現(xiàn):
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
}
}
算法會(huì)避免更新有相同primaryKey和name的User對(duì)象,即使它們是不同的實(shí)例!即使提供新的實(shí)例,您現(xiàn)在也可以避免在集合視圖中進(jìn)行不必要的UI更新。
isEqual(toDiffableObject :)返回false時(shí)會(huì)更新相應(yīng)cell.
Advanced Features
Working Range
IGListAdapter初始化時(shí)需要傳入workingRangeSize,該值是可見高度或?qū)挾鹊谋稊?shù),具體取決于滾動(dòng)方向。

IGListDiffable and Equality
實(shí)例需要遵守IGListDiffable協(xié)議,并實(shí)現(xiàn)diffIdentifier()和isEqual(toDiffableObject:)方法。
diffIdentifier()用來確定數(shù)據(jù)的唯一性(類似數(shù)據(jù)庫中的主鍵),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 - 首先比較指針。
- 比較對(duì)象值時(shí),請(qǐng)?jiān)?code>-isEqual:之前檢查nil。舉個(gè)栗子,
[nil isEqual:nil]返回的是NO。 - 總是先比較開銷最低的值。比如
[self.array isEqual:other.array] && self.intVal == other.intVal是浪費(fèi)的,應(yīng)該先比較intVal.
舉個(gè)栗子:
聲明:
@interface User : NSObject
@property NSInteger identifier;
@property NSString *name;
@property NSArray *posts;
@end
實(shí)現(xiàn):
@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
個(gè)人總結(jié),之所以數(shù)據(jù)模型需要實(shí)現(xiàn)IGListDiffable協(xié)議,目的是對(duì)內(nèi)存不一致的模型進(jìn)行比對(duì),所以想正確對(duì)數(shù)據(jù)源進(jìn)行update操作,應(yīng)該是重新創(chuàng)建相應(yīng)的數(shù)據(jù)模型進(jìn)行覆蓋。
Modeling and Binding
- 將設(shè)計(jì)規(guī)范轉(zhuǎn)換為頂級(jí)模型和視圖模型
- 使用
ListBindingSectionController進(jìn)行動(dòng)畫單向單元更新 - Cell-to-controller的動(dòng)作處理和代理
- 通過本地?cái)?shù)據(jù)變更更新UI
Getting Started
下載示例工程,打開ModelingAndBinding-Starter/ModelingAndBinding.xcworkspace。
](http://ww1.sinaimg.cn/large/721c0e13ly1fnsuc8bbxfj20yy0n8go4.jpg)
IGListKit基于一個(gè)模型對(duì)應(yīng)一個(gè)section controller的理念。本設(shè)計(jì)中的所有cell都與服務(wù)器傳遞的一個(gè)頂級(jí)post對(duì)象相關(guān)聯(lián)。
你們需要?jiǎng)?chuàng)建一個(gè)包含所有這些cell需要的信息的Post對(duì)象。
一個(gè)常見的錯(cuò)誤是為單個(gè)cell創(chuàng)建單個(gè)模型和section controller。在這個(gè)例子中,由于頂級(jí)對(duì)象包含用戶,圖像,動(dòng)作和評(píng)論模型的混合搭配,因此會(huì)造成非?;靵y的體系結(jié)構(gòu)。
Creating Models
在工程中創(chuàng)建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兼容,所以您的類必須是有初始化方法。
現(xiàn)在在Post中添加ListDiffable協(xié)議的實(shí)現(xiàn):
// MARK: ListDiffable
func diffIdentifier() -> NSObjectProtocol {
// 1
return (username + timestamp) as NSObjectProtocol
}
// 2
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
return true
}
- 為每個(gè)post派生一個(gè)唯一標(biāo)識(shí)符。由于單個(gè)帖子不應(yīng)該有相同的用戶名和時(shí)間戳組合,我們可以使用此作為唯一標(biāo)識(shí)符。
- 使用
ListBindingSectionController的核心要求是,如果兩個(gè)模型具有相同的diffIdentifier,則它們必須相等,以便section controller可以比較視圖模型。
View Models
創(chuàng)建Comment.swift文件,并實(shí)現(xiàn)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數(shù)組:每篇文章都有一些動(dòng)態(tài)的評(píng)論,每個(gè)cell上展示一條評(píng)論。
當(dāng)你使用ListBindingSectionController時(shí),你需要為UserCell,ImageCell和ActionCell創(chuàng)建模型,你需要接受一點(diǎn)新的理念。
一個(gè)綁定的section controller幾乎就像一個(gè)迷你的IGListKit。它需要一個(gè)視圖模型數(shù)組,并將其轉(zhuǎn)換為配置的cell。養(yǎng)成為
ListBindingSectionController實(shí)例中的每個(gè)單元格類型創(chuàng)建新模型的習(xí)慣。
考慮到這一點(diǎn),讓我們從UserCell的模型開始:
創(chuàng)建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
}
}
由于每個(gè)帖子只有一個(gè)UserViewModel,所以你可以硬編碼一個(gè)標(biāo)識(shí)符。這將只強(qiáng)制使用一個(gè)單一的模型和單元格。
為ImageCell和ActionCell創(chuàng)建視圖模型,參考代碼位于示例工程中
Using ListBindingSectionController
您現(xiàn)在有以下視圖模型,它們都可以從每個(gè)Post對(duì)象派生:
- UserViewModel
- ImageViewModel
- ActionViewModel
- Comment
創(chuàng)建PostSectionController.swift文件并添加以下代碼:
final class PostSectionController: ListBindingSectionController<Post>,
ListBindingSectionControllerDataSource {
override init() {
super.init()
dataSource = self
}
}
注意你繼承了ListBindingSectionController <Post>。這將聲明您的節(jié)控制器接收Post模型。這樣就不用對(duì)model做特殊處理。
數(shù)據(jù)源根據(jù)協(xié)議,需要實(shí)現(xiàn)3個(gè)方法:
- 返回頂層模型的視圖模型數(shù)組(Post)
- 返回給定視圖模型的大小
- 為給定的視圖模型返回一個(gè)cell
首先關(guān)注Post到視圖模型的轉(zhuǎn)換:
// 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以返回每個(gè)視圖模型的大?。?/p>
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不應(yīng)該為空,但它是一個(gè)弱引用的對(duì)象,因此必須聲明為可選類型。再次,使用fatalError()來捕捉任何關(guān)鍵的失敗。 -
UserViewModel和ActionViewModel高度皆為55.
最后實(shí)現(xiàn)返回cell的API。
cell是在Main.storyboard中定義的。可以點(diǎn)擊每個(gè)cell來查看其標(biāo)識(shí)符。
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
現(xiàn)在,由PostSectionController來創(chuàng)建視圖模型,尺寸和單元格。使用ListBindingSectionController的最后一部分是讓cell接收他們分配的視圖模型并進(jìn)行自我配置。
ListBindingSectionController將自動(dòng)綁定視圖模型到每個(gè)遵守ListBindable協(xié)議的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)
}
}
- 導(dǎo)入
IGListKit。 - cell遵從
ListBindable協(xié)議。 - 判斷視圖模型的類型。
- 使用
SDWebImage下載圖片。
最后,修改其他3個(gè)cell中的代碼。
Displaying in the View Controller
最后一步是讓PostSectionController顯示在app的列表中。
返回ViewController.swift并在設(shè)置dataSource或collectionView之前添加以下內(nèi)容到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()
}
通常你會(huì)根據(jù)
object的類型返回不同的ListSectionController,但是因?yàn)楝F(xiàn)在只有Post對(duì)象,只返回一個(gè)新的PostSectionController是安全的。
運(yùn)行工程,看看效果。
Handling Cell Actions
點(diǎn)擊ActionCell上的??按鈕,將事件轉(zhuǎn)發(fā)到PostSectionController:
在 ActionCell.swift 中添加以下協(xié)議:
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
}
實(shí)現(xiàn)cell的代理方法:
final class PostSectionController: ListBindingSectionController<Post>,
ListBindingSectionControllerDataSource,
ActionCellDelegate {
//...
// MARK: ActionCellDelegate
func didTapHeart(cell: ActionCell) {
print("like")
}
?
Local Mutations
每次有人點(diǎn)擊??按鈕,都需要在Post上添加一個(gè)新的like。但是,所有的模型都是用let聲明的,因?yàn)椴豢勺兊哪P褪且粋€(gè)更安全的設(shè)計(jì)。但是,如果一切都是不可變的,我們?nèi)绾胃淖僱ike的計(jì)數(shù)呢?
PostSectionController是處理和存儲(chǔ)變量的理想場所。打開PostSectionController.swift并添加以下變量:
var localLikes: Int? = nil
在代理方法didTapHeart(cell:)中添加以下代碼:
func didTapHeart(cell: ActionCell) {
// 1
localLikes = (localLikes ?? object?.likes ?? 0) + 1
// 2
update(animated: true)
}
調(diào)用ListBindingSectionController上的update(animated:,completion:)API來刷新屏幕上的cell。
為了將變化反映到模型,您需要在提供給ActionCell的ActionViewModel中使用localLikes。
在PostSectionController.swift中,找到cellForViewModel:API并將ActionViewModel初始化相關(guān)代碼更改為以下內(nèi)容:
ActionViewModel(likes: localLikes ?? object.likes)
Working with UICollectionView
本指南提供了有關(guān)如何使用UICollectionView和IGListKit的詳細(xì)信息。
Background
2.x之前的版本的IGListKit中,包含UICollectionView的子類IGListCollectionView。3.0版本之后,IGListCollectionView已經(jīng)被刪除。
Methods to avoid
IGListKit的主要目的之一是為UICollectionView執(zhí)行最佳的批量更新。因此,客戶端應(yīng)該從不在UICollectionView上調(diào)用任何涉及重新加載,插入,刪除或更新cell和index paths的API。作為替代,使用IGListAdapter提供的API。你也應(yīng)該避免設(shè)置 collection view的數(shù)據(jù)源和代理,因?yàn)檫@也是IGListAdapter的責(zé)任。
避免調(diào)用以下方法:
?
- (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中,引入了新的單元預(yù)取API。在Instagram上,啟用此功能會(huì)顯著降低滾動(dòng)性能。我們建議將isPrefetchingEnabled設(shè)置為NO(在Swift中為false)。請(qǐng)注意,默認(rèn)值是true。
您可以使用UIAppearance在進(jìn)行全局設(shè)置:
if ([[UICollectionView class] instancesRespondToSelector:@selector(setPrefetchingEnabled:)]) {
[[UICollectionView appearance] setPrefetchingEnabled:NO];
}
if #available(iOS 10, *) {
UICollectionView.appearance().isPrefetchingEnabled = false
}