IglistKit介绍

  虽然在iOS开发中有很多很好用的列表控件,性能和API都很好用,对于简单无变化或者变化较为简单的列表cell是可以满足开发需求的,但是对于复杂的列表,就会出现不足,常见的reloadData时的闪烁和github中文官网网页performBatchUpdates时手动维护ugithubpdate人头攒动的读音r的较大难度和易crash,由此出现了针数组c语言对复杂列表的三方库IglistKit,它是 Instagram 的一个算法导论数据驱动的 UICollectionVigithub中文官网网页ew 框架,为了构建快速和可扩展的列表。另外,它有助于你在 app 结束对于大量视图控制器的使用。

iOS数据驱动IglistKit及StackedSectionController弃用替代

iOS数据驱动IglistKit及StackedSectionController弃用替代

  从iglist的结构图中可以看出,其中引入了adapter,它可以被算法所属的控制器所持有,同时对于传入的不同的data描述成不同的sectionControlgit命令ler,同一个sectionController又可以描述成相同的cell或者不同的cell,adapt算法分析的目的是er是变化(新增、删除、更新)开始的地方,sectionController就是变化适配的地方,它的强大之处在于其中的diff算法gitlab本篇暂不讨论其中的算法实现和差异更新,侧重如何使用和注意事项!

IglistKit基本使用(StackedSectionCgithub官网ontr算法工程师oller)

iOS数据驱动IglistKit及StackedSectionController弃用替代
  让我们来看一个例子,来源于/post/684490… 如上图所示,对于一人体肠道结构示意图个列表的一个cell可以拆分为github人体肠道结构示意图「红框(usernfo)」和「绿框(userCogiticomfort轮胎ntent)」堆积而成,在iglistKit~3.4.0可以通过StackedSectionController实现,每一个部算法的时间复杂度取决于分都会对应一个sectionController,每一个sectionController又对应了一个自己的cel算法的特征l和闰土刺猹model,个人信息和评论分别由自己的sectionController和cell显示,步骤如下:

1、布局collectinView和Adapter

  在普通的ViewController中创建showObjgiticomfort轮胎ects用来存需要展示的数据,创建collectionView和adapter,注意在实现ListAdapterDataSource协议时,返回需要显示的数据源、sectionController和空态,由上图可知,我们需要两个sectionController(info和content)

import UIKit
import IGListKit
class FirstViewController: UIViewController {
  //显示的数据
  var showObjects:[ListDiffable] = [ListDiffable]()
    //collectionView
  var collectionView: UICollectionView = {
    let flow = UICollectionViewFlowLayout()
    let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: flow)
    return collectionView
  }()
  //adapter
  lazy var adapter:ListAdapter = {
    let adapter = ListAdapter(updater: ListAdapterUpdater(), viewController: self)
    return adapter
  }()
  override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(collectionView)
    adapter.collectionView = collectionView
    adapter.dataSource = self
    collectionView.frame = view.bounds
    collectionView.showsVerticalScrollIndicator = false
    collectionView.showsHorizontalScrollIndicator = false
    do {
            //从data1中拿到解析成Feed模型的数据源数组
      let data = try JsonTool.decode([Feed].self, jsonfileName: "data1")
      self.showObjects.append(contentsOf: data)
      adapter.performUpdates(animated: true, completion: nil)
    } catch {
      print("decode failure")
    }
  }
}
extension FirstViewController:ListAdapterDataSource {
    //collectionView中要显示的数据
  func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
    return showObjects
  }
    //数据对应的sectionController,注意这里使用的是ListStackedSectionController用栈的方式压入了UserInfoSectionController和UserContentSectionController
  func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
    let stack = ListStackedSectionController(sectionControllers: [UserInfoSectionController(),UserContentSectionController()])
    stack.inset = UIEdgeInsets(top: 5, left: 0, bottom: 0, right: 0)
    return stack
  }
  func emptyView(for listAdapter: ListAdapter) -> UIView? {
    return nil
  }
}

2、布局UserInfoSectionController

创建UserInfoSectionController继承自ListSectionController,实现它的三数组词个方法

class UserInfoSectionController: ListSectionController {
  //包含用户信息(头像和姓名)的object
  var obj: Feed!
  //数据模型
  lazy var userInfoModel: UserInfoCellModel = {
    let userInfoModel = UserInfoCellModel(avatar: URL(string: obj.avatar), userName: obj.userName)
    return userInfoModel
  }()
  override func numberOfItems() -> Int {
    return 1
  }
  override func sizeForItem(at index: Int) -> CGSize {
    let width = collectionContext?.containerSize(for: self).width
    return CGSize(width:width!, height: 60);
  }
  override func cellForItem(at index: Int) -> UICollectionViewCell {
        //这里返回的就是UserInfoCell
    guard let cell = collectionContext?.dequeueReusableCell(withNibName: UserInfoCell.cellIdentifier, bundle: nil, for: self, at: index) as? UserInfoCell else {fatalError()}
    cell.bindViewModel(UserInfoCellModel as Any)
    return cell
  }
}

3、布局UserInfoCell

自定义一个UICollectionView算法的时间复杂度取决于Cell用xib描述,并且遵守ListBindable协议,实现bindViewModel协议方法

class UserInfoCell: UICollectionViewCell {
  @IBOutlet weak var avaterImageView: UIImageView!
  @IBOutlet weak var showAndHiddenBtn: UIButton!
  @IBOutlet weak var nameLabel: UILabel!
  override func awakeFromNib() {
    super.awakeFromNib()
    self.avaterImageView.layer.cornerRadius = 20
    self.backgroundColor = UIColor.yellow
  }
}
extension UserInfoCell: ListBindable {
  func bindViewModel(_ viewModel: Any) {
    guard let viewModel = viewModel as? UserInfoCellModel else { return }
    avaterImageView.backgroundColor = UIColor.cyan
    nameLabel.text = viewModel.userName
  }
}

4、布局UserInfoCellModel

class UserInfoCellModel {
  //头像
  var avatar: URL?
  //昵称
  var userName: String = ""
  //初始化构造函数
  init(avatar: URL?, userName:String){
    self.avatar = avatar
    self.userName = userName
  }
}
extension UserInfoCellModel: ListDiffable {
  func diffIdentifier() -> NSObjectProtocol {
    return "UserInfo" as NSObjectProtocol
  }
  func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
    if self === object {
      return true
    }
    guard let obj = object as? UserInfoCellModel else { return false}
    return userName == obj.userName
  }
}

5、布局UserContentSectionController

class UserContentSectionController: ListSectionController {
  var obj: Feed!
  var expand: Bool = false
  //数据模型
  lazy var userContentModel: UserContentCellModel = {
    let userContentModel = UserContentCellModel(contentStr: obj.content ?? "")
    return userContentModel
  }()
  override func numberOfItems() -> Int {
    if obj.content?.isEmpty ?? true {
      return 0
    }
    return 1
  }
  override func sizeForItem(at index: Int) -> CGSize {
    guard let content = obj.content else { return CGSize.zero }
    let width: CGFloat! = collectionContext?.containerSize(for: self).width
    let height: CGFloat = expand ? UserContentCell.lineHeight() : UserContentCell.height(for: content, limitWidth: width)
    return CGSize(width: width, height: height + 5)
  }
  override func cellForItem(at index: Int) -> UICollectionViewCell {
    guard let cell = collectionContext?.dequeueReusableCell(withNibName: UserContentCell.cellIdentifier, bundle: nil, for: self, at: index) as? UserContentCell else { fatalError() }
    cell.bindViewModel(userContentCellModel as Any)
    return cell
  }
 
  override func didUpdate(to object: Any) {
    self.obj = object as? Feed
  }
 
  override func didSelectItem(at index: Int) {
    expand.toggle()
    UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.6, options: [], animations: {
      self.collectionContext?.invalidateLayout(for: self, completion: nil)
    }, completion: nil)
  }
}

6、布局UserCo数组c语言ntentCell

class UserContentCell: UICollectionViewCell {
  @IBOutlet weak var label: UILabel!
  override func awakeFromNib() {
    super.awakeFromNib()
  }
   // 计算content收起时的高度
 static func lineHeight() -> CGFloat {
    return UIFont.systemFont(ofSize: 16).lineHeight
  }
   // 计算content展开时的高度
 static func height(for text: NSString,limitwidth: CGFloat) -> CGFloat {
    let font = UIFont.systemFont(ofSize: 16)
    let size: CGSize = CGSize(width: limitwidth - 20, height: CGFloat.greatestFiniteMagnitude)
    let rect = text.boundingRect(with: size, options: [.usesFontLeading,.usesLineFragmentOrigin], attributes: [NSAttributedString.Key.font:font], context: nil)
    return ceil(rect.height)
  }
}
extension UserContentCell: ListBindable {
  func bindViewModel(_ viewModel: Any) {
    guard let vm = viewModel as? UserContentCellModel else { return }
    self.label.text = vm
  }
}

7、布局UserContentCelgithub直播平台永久回家lModel

class UserContentCellModel {
  //内容
  var content: String = ""
  init(contentStr: String) {
    self.content = contentStr
  }
}
extension UserContentModel: ListDiffable {
  func diffIdentifier() -> NSObjectProtocol {
    return "UserContentCellModel" as NSObjectProtocol
  }
 
  func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
    if self === object {
      return true
    }
    guard let userContent = object as? UserContentCellModel else {return false}
    return content == userContent.content
  }
}

github中文社区 至此使用StackedSectionController就已经实现了滑动列表的显示功能了,其实UserContentCellModel完全没要存在,因为它接收的就是一个github中文社区string,在Us人体触电的极限电流是多少erContentCell中的bindViewMo数组和链表的区别del中可以直接改用guard let vm = viewModel让天秤倒追的星座 as? String else { retGitHuburn }在UserContentSectionController中也就不需要UserCon闰土刺猹tentCellModel模型了,cell.bindViewModel(userContentC算法ellModel as Any)也可以直github接改为RTCcell.bindViewModel(obj.content as Any),为了对比说明和后面的ListBindingSectionController说明,暂时添加了,到此为止我相信你已经get到了iglist的基本使用了,注意点有:

  • StackedSectionController(section1, secti数组on2,..数组词..)中是github是什么顺序要求的,section1会先于人头攒动的近义词section2去展示
  • sectionController需要定义Git一个模型属性在方法didUpdate(to object: Any)中转化接收objects(for listAdapter: ListAdapter) -> [ListDiffable]传过来的模型数据数人头攒动的读音组的每一项
  • 如需使用模型数据中的某几个字段,可以进行自己封装成对应的cell数据模型,明确cell模型的标识(diffIdentifier)和算法的五个特性更新条件(isEqual),并且在外部进行绑定cell.bindV算法的特征iewModel()

IglistKit基本使用(ListBindingSectionController)

iOS数据驱动IglistKit及StackedSectionController弃用替代
  在iglistKit~4.0.0中已经弃用了StackedSectionController,头文件中也没有对应的初始化方法了,弃用的相关github.com/Instagram/I肉跳测吉凶
  对数组指针于StackedSectionController来说,它的diff是算法设计与分析section级别的,一个cell对应一个section,每个sectionController的numberOfItem() -> Int保持默认值1,以此实现section到c算法ell的1:1关系,数组词来模拟cell级别的更新。其实很多场景下对于多cell一个section的场景,只需算法设计与分析要维护cell的diff,不需要繁琐配置一个cell对应一个section,这个时人体肠道结构示意图候就可以使用ListBindingSectionController来实现。
  如上图所示是同样的需求,这时候我只需要创建一个sectionController继承ListBindingSectionController并且满足ListBindingSectionControllerDataS算法的时间复杂度是指什么ource协议,在对应的协议方法中维护需要的model和展示对应model的cell即可,为了后面说明相关的update、insert和remove刷新,在这里的section和cell也添加了对应算法的时间复杂度取决于的操作和逻辑代码。

布局UserInfoVgithub中文官网网页iewModel和userContntViewM算法是什么odel

  info和content的model几乎没算法有什么变化,这里就不重复啰嗦了

布局Sgithub开放私库ectionContro数组排序ller

class FirstSectionViewController: ListBindingSectionController<Feed> {
  var expand: Bool = true
  var updateHandler: ((_ feed: UserInfoViewModel) -> Void)?
  var deleteAvatarHandler: ((_ feed: UserInfoViewModel) -> Void)?
  var insertHandler: ((_ feed: UserInfoViewModel) -> Void)?
  override init() {
   super.init()
   dataSource = self
  }
}
extension FirstSectionViewController: ListBindingSectionControllerDataSource {
  /*
     相当于数据源,只是这里对数据源做了model的区分,实际上装的是一个区分了info和content的模型数组
     就像这样[infomodel, contentmodel]
    */
    func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, viewModelsFor object: Any) -> [ListDiffable] {
    guard let feedViewModel = object as? Feed else {return []}
    var viewModels = [ListDiffable]()
    if !feedViewModel.userName.isEmpty {
      viewModels.append(UserInfoViewModel(name: feedViewModel.userName, avatar: feedViewModel.avatar, feedID: feedViewModel.feedId))
    }
    if let contentStr = feedViewModel.content, !contentStr.isEmpty {
      viewModels.append(userContntViewModel(uerContent: contentStr))
    }
    return viewModels
  }
    //这里就会根据不同的model差异化创建不同的cell进行展示对应的modle数据
  func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, cellForViewModel viewModel: Any, at index: Int) -> UICollectionViewCell & ListBindable {
    switch viewModel {
    case is UserInfoViewModel:
      guard let infoModel = viewModel as? UserInfoViewModel else {fatalError()}
      let cell = collectionContext?.dequeueReusableCell(withNibName: UserInfoCell.cellIdentifier, bundle: nil, for: self, at: index) as! UserInfoCell
     
      //content展开和收起
      cell.onclickArrow = {[weak self] cell in
        guard let self = self else { return }
        self.expand.toggle()
        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.6, options: [], animations: {
          self.collectionContext?.invalidateLayout(for: self, completion: nil)
        }, completion: nil)
      }
      //delete
      cell.deleteAction = {[weak self] deleteCell in
        guard let self = self else { return }
        self.deleteAvatarHandler?(infoModel)
      }
      //update
      cell.updateAction = {[weak self] updateCell in
        guard let self = self else { return }
        self.updateHandler?(infoModel)
      }
      //insert
      cell.addAction = {[weak self] insertCell in
        guard let self = self else { return }
        self.insertHandler?(infoModel)
      }
      return cell
    case is userContntViewModel:
      let cell = collectionContext?.dequeueReusableCell(withNibName: UserContentInfo.cellIdentifier, bundle: nil, for: self, at: index) as! UserContentInfo
      return cell
    default:
      return collectionContext?.dequeueReusableCell(of: EmptyCollectionViewCell.self, for: self, at: index) as! EmptyCollectionViewCell
    }
  }
    //确定section的size
  func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, sizeForViewModel viewModel: Any, at index: Int) -> CGSize {
    let width = collectionContext?.containerSize(for: self).width
    switch viewModel {
    case is UserInfoViewModel:
      return CGSize(width: width!, height: 60)
    case is userContntViewModel:
      if let contentModel = viewModel as? userContntViewModel, let content = contentModel.content {
        let height: CGFloat = expand ? UserContentInfo.lineHeight() : UserContentInfo.height(for: content, limitWidth: width!)
        return CGSize(width: width!, height: height + 5)
      }
    default:
      return CGSize(width: 0.0, height: 0.0)
    }
    return CGSize(width: 0.0, height: 0.0)
  }
}

布局infoCell和contentCell

  gitlabinfoCell和contentCell也和之前的一致,唯一变化的是giti在infoCell中多添加了一些展开收起content、添加、删除的操作,如下:

var onclickArrow: ((UserInfoCell) -> Void)?
  var deleteAction: ((UserInfoCell) -> Void)?
  var updateAction: ((UserInfoCell) -> Void)?
  var addAction: ((UserInfoCell) -> Void)?
  override func awakeFromNib() {
    super.awakeFromNib()
    self.avaterImageView.layer.cornerRadius = 20
    self.backgroundColor = UIColor.yellow
    showAndHiddenBtn.isSelected = false
    showAndHiddenBtn.setTitle("展开", for: .normal)
    showAndHiddenBtn.setTitle("收起", for: .selected)
  }
  @IBAction func click(_ sender: Any) {
    onclickArrow?(self)
    showAndHiddenBtn.isSelected = !showAndHiddenBtn.isSelected
  }
  @IBAction func deleteAction(_ sender: Any) {
    deleteAction?(self)
  }
  @IBAction func updateAction(_ sender: Any) {
    updateAction?(self)
  }
  @IBAction func addAction(_ sender: Any) {
    addAction?(self)
  }

  至此利用git教程ListBindingSectionController展示滚动列表数据的也完成了,需要注意的点是我们可以很方便的根据自己想要展示的不同模块数据,从初始的data中根据不同模块灵活划分不同的model,在ListBindingSectionControllerDataSource协议方法中通过不同的model去创建不同的cell,展示不同的cell,他们两者的区别也很明显

  • sectionConto数组ller中的section和cell是一对一的,bindingSectionCon数组排序trolleRTCr中的section和cell是一对多
  • sectionController中的section需要手动创建一个属性去接收数据模型,并且在协议方法cellForItem(at index: Int) -> UICollgiticomfort轮胎ectionVi数组和链表的区别ewCell中手动去将cell与model进行绑定,bindingSectionController在协议giti轮胎方法中是根据不同的model去创建展示不同的cell,会自动绑定,Git但是和se算法设计与分析ctionController一样,在cell中的ListBindable协议bindViewModel(_ viewModel: Any)需要对应好m数组指针odel
  • 代码的灵活度而言,bindingSectionController不需要反复创建多个section了,将原本多个sectionController的返回人体触电的极限电流是多少的不同size和cell逻辑全部集中到了一个section中协议方法中,很好地减少了代码的冗余程度

IglistKit刷新(更新、增加、删除)数组公式

iOS数据驱动IglistKit及StackedSectionController弃用替代

  我将cel算法的有穷性是指l显示内数组的定义容的变更、添加一行cell、删除一行cell统称为cell的刷新,我们先看git图的效果

展开收起(size变化)

  当content的内容超出一行时,点击「展开」数组排序,contentCe算法的有穷性是指ll会展开铺满,点击「收起」contentCell会收起为初始化状态,需要用一个bool值来记录点击情况,根据bool值来更新size的高度,同时在section级别的回调中执行方法 self.c算法的时间复杂度取决于ollectionContext?.invalidateLayout(for: self, completion: nil)self.update(animated: true, completion: nil)都可github中文官网网页,前者建议配合动画执行

//content展开和收起
    cell.onclickArrow = {[weak self] cell in
        guard let self = self else { return }
        self.expand.toggle()
        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.6, options: [], animations: {
            self.collectionContext?.invalidateLayout(for: self, completion: nil)
        }, completion: nil)
//   self.update(animated: true, completion: nil)
    }

更新(内容变化gitee

  点击「更新」按钮,期望将info信息中的nam数组e文字改为”修改后的”,数组c语言我们需要拿到点击的这一行的cell的model,当然这个model是infoModel,然后对model中的name进行调整即可,可以找到对应的section,仅更新对应的sectiongithub永久回家地址即可,这里update可以在section级别完成,也可以通过section回调给ViewController,使用adapter在ViewControll算法工程师er中github中文官网网页完成


//update 方式1 section级别
cell.updateAction = {[weak self] updateCell in
    guard let self = self else { return }
    self.collectionContext?.performBatch(animated: true, updates: {(batch) in
        let updateIndex: Int! = self.collectionContext?.index(for: updateCell, sectionController: self)
        guard let updateModel = self.viewModels[updateIndex] as? UserInfoViewModel else { return }
        updateModel.userName = "修改后的1"
        batch.reload(self)
    }, completion:nil)  
 }
***************************************************************************
//update 方式2 section回调给VC,由VC中的adapter完成
cell.updateAction = {[weak self] updateCell in
    guard let self = self else { return }
    self.updateHandler?(infoModel) 
}
//update userName
vc.updateHandler = { [weak self] infoModel in
    guard let self = self else { return }
    for item in self.showObjects {
        guard let feedItem = item as? Feed else { return }
        if feedItem.feedId == infoModel.feedId {
            feedItem.userName = "修改后的2"
            if let sectionController = self.adapter.sectionController(for: item) as? FirstSectionViewController {
               sectionController.update(animated: true, completion: nil)
            }
            break
        }
    }    
}

删除github汤姆

  点击「删除」期望删除掉info和content,对于删除的操作,在section层可以拿到viewModels,但是它是一个github直播平台永久回家只读属性,不能修改里面的数据源,所以只能回调到ViewController层,去处理数据源然后调用adapter的performUpdates(animated: true, completion: nil)

//delete
vc.deleteAvatarHandler = {[weak self] deleteItem in
    guard let self = self else { return }
    var deleteIndex = -1
    for (index, item) in self.showObjects.enumerated() {
        guard let feedItem = item as? Feed else { return }
        if feedItem.feedId == deleteItem.feedId {
            deleteIndex = index
            break
        }
    }
    if deleteIndex >= 0 {
        self.showObjects.remove(at: deleteIndex)
        self.adapter.performUpdates(animated: true, completion: nil)
    }
}

新增

  点击「新增」期望新增加一条数据包含了info和content,name为”新增的”github汤姆,content为”这个是新增加的cell”,和删除一样也是需要对数据源做操作,也是需要回调到ViewController中去处理数据然后调用adapter的performUpdates(animated: true, completion: nil)

//insert
vc.insertHandler = {[weak self] insertItem in
    guard let self = self else { return }
    var insertIndex = -1
    for (index, item) in self.showObjects.enumerated() {
        guard let feedItem = item as? Feed else { return }
        if feedItem.feedId == insertItem.feedId {
            insertIndex = index
            break
        }
    }
    do {
        if insertIndex >= 0 {
            let data = try JsonTool.decode([Feed].self, jsonfileName: "insertData")
            data[0].feedId += UInt(insertIndex)
            self.showObjects.insert(contentsOf: data, at: insertIndex)
            self.adapter.performUpdates(animated: true, completion: nil)
        }
    } catch {
        print("decode failure")
    }       
}

参考资料:

  • # 深入浅出 IGListKit
  • # 抛弃UITableView,让所github下载有列表页不再难构建