IGListKit學(xué)習(xí)筆記

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

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

shayneann都表示相同的唯一數(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ì)避免更新有相同primaryKeynameUser對(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)方向。

image

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://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基于一個(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
}
  1. 為每個(gè)post派生一個(gè)唯一標(biāo)識(shí)符。由于單個(gè)帖子不應(yīng)該有相同的用戶名和時(shí)間戳組合,我們可以使用此作為唯一標(biāo)識(shí)符。
  2. 使用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,ImageCellActionCell創(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è)單一的模型和單元格。

ImageCellActionCell創(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)
}
  1. 就像object屬性一樣,collectionContext不應(yīng)該為空,但它是一個(gè)弱引用的對(duì)象,因此必須聲明為可選類型。再次,使用fatalError()來捕捉任何關(guān)鍵的失敗。
  2. UserViewModelActionViewModel高度皆為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)
  }

}
  1. 導(dǎo)入IGListKit。
  2. cell遵從ListBindable協(xié)議。
  3. 判斷視圖模型的類型。
  4. 使用SDWebImage下載圖片。

最后,修改其他3個(gè)cell中的代碼。

Displaying in the View Controller

最后一步是讓PostSectionController顯示在app的列表中。

返回ViewController.swift并在設(shè)置dataSourcecollectionView之前添加以下內(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。

為了將變化反映到模型,您需要在提供給ActionCellActionViewModel中使用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)被刪除。

具體討論可以參考 #240#409.

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
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容