IGListKit学习笔记

作者: iamjjh | 来源:发表于2019-02-15 11:21 被阅读2次

    参考教程 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

    ![](https://img.haomeiwen.com/i1036329/97a3c2389b7fcbd1.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)![](https://img.haomeiwen.com/i1036329/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
    }
    

    相关文章

      网友评论

        本文标题:IGListKit学习笔记

        本文链接:https://www.haomeiwen.com/subject/pddpeqtx.html