IGListKit学习笔记

参考教程 IGListKit Tutorial: Better UICollectionViews
英文版
中文版

flowchart.png
  1. 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")

shayneann都表示相同的唯一数据,因为它们共享相同的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
  }
}

算法会避免更新有相同primaryKeynameUser对象,即使它们是不同的实例!即使提供新的实例,您现在也可以避免在集合视图中进行不必要的UI更新。

isEqual(toDiffableObject :)返回false时会更新相应cell.

Advanced Features

Working Range

IGListAdapter初始化时需要传入workingRangeSize,该值是可见高度或宽度的倍数,具体取决于滚动方向。

image

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

![](http://upload-images.jianshu.io/upload_images/1036329-97a3c2389b7fcbd1.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![](http://upload-images.jianshu.io/upload_images/1036329-97a3c2389b7fcbd1.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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
}
  1. 为每个post派生一个唯一标识符。由于单个帖子不应该有相同的用户名和时间戳组合,我们可以使用此作为唯一标识符。
  2. 使用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时,你需要为UserCellImageCellActionCell创建模型,你需要接受一点新的理念。

一个绑定的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,所以你可以硬编码一个标识符。这将只强制使用一个单一的模型和单元格。

ImageCellActionCell创建视图模型,参考代码位于示例工程中

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)
}
  1. 就像object属性一样,collectionContext不应该为空,但它是一个弱引用的对象,因此必须声明为可选类型。再次,使用fatalError()来捕捉任何关键的失败。
  2. UserViewModelActionViewModel高度皆为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)
  }

}
  1. 导入IGListKit
  2. cell遵从ListBindable协议。
  3. 判断视图模型的类型。
  4. 使用SDWebImage下载图片。

最后,修改其他3个cell中的代码。

Displaying in the View Controller

最后一步是让PostSectionController显示在app的列表中。

返回ViewController.swift并在设置dataSourcecollectionView之前添加以下内容到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。

为了将变化反映到模型,您需要在提供给ActionCellActionViewModel中使用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已经被删除。

具体讨论可以参考 #240#409.

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
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容