UI

浅谈iOSFeed流布局之IGListKit.

2020-06-24  本文已影响0人  苹果上的小豌豆

前言

翻墙的小伙伴们都有接触到过Instagram这个APP吧,我们国内的小红书最开始就是仿照他的布局和设计风格。可惜不久前小红书被苹果下架了。紧接着,微博孵化了一个绿洲的APP,整个风格完全就是Instagram的翻版,连图片放大缩小,评论等一模一样的交互。今天,我就带来给大家揭秘Instagram的布局架构————IGListKit。

初见社交feed流

在初次接触社交类型的需求时候,看到YYKit作者写的微博demo里面,列表滑动还能保持50到60的FPS,但是也有稍许卡顿。而造成卡顿的原因就和CPU资源消耗相关了。

初见IGListKit

在我们构建传统的feed流的时候,布局可能存在各种组合情况,纯文字,纯图片,纯视频,文字+图片,文字+视频+评论,一张图片,3张图片,等等。或者过段时间再迭代加上一个点赞分享,或者关注,大V动图。。。这个时候我们用传统的tableview就会有些吃力了,而且是牵一发而动全身了。

IGListKit是Instagram出的一个基于UICollectionView的数据驱动UI框架,目前在github上有10k+ star,被充分利用在Instagram App上。
它不是由 Swift 开发的一个库,依然使用了 Objective-C 为主要语言开发。我想是因为 Instagram 作为已经七年以上的 App,也就是一定是一个 Objective-C 项目。它运作良好,没有特殊原因没必要用 Swift 重写。所以它的核心 UI 组件库 IGListKit 依然是 Objective-C 也是正常的。
虽然 IGListKit 不是 Swift 写的,但是你不得不忽视 Swift 在这个库中的存在感,列举一二:

综上,IGListKit 是一个很典型的使用 Objective-C 开发的,但却是个偏向使用 Swift 语言开发者的一个 UI 组件库。

IGListKit 初体验

首先要介绍几个概念:

简单来说这个算法就是计算tableView或者collectionView前后数据变化增删改移关系的一个算法,时间复杂度是O(n),算是IGListKit的特色特点之一。其实这个算法单独拿出来不只可以计算collectionView模型,稍加改造,也适用于其他模型或者文件的变化,使用的是Paul Heckel 的A technique for isolating differences between files 的算法,这份paper是收费。不过这并不妨碍我们直接看源码,我们可以看一下IGListDiff.mm文件,该算法使用C++来编写。主要是通过hashtable和新旧的两个数组结构:

运用IGListKit 仿绿洲(因为小红书下架了)简单demo

下面是写好的简单首页布局

1. ViewCell的分解

下面是UserInfoCell对应的示例代码:

class UserInfoCell: UICollectionViewCell {
     lazy var avatar:UIImageView = {
           let avatar = UIImageView.init()
           avatar.layer.cornerRadius = 20.0
           avatar.layer.masksToBounds = true
           avatar.isUserInteractionEnabled = true
           avatar.image = UIImage.init(named: "avaterplaceholder")
           return avatar
       }()
    lazy var nameLab:UILabel = {
           let Lab = UILabel.init()
           Lab.textAlignment = NSTextAlignment.left
           Lab.font = UIFont.boldSystemFont(ofSize: 15)
           Lab.textColor = ColorTitle
           return Lab
       }()
    lazy var TimeLab:UILabel = {
           let Lab = UILabel.init()
           Lab.textAlignment = NSTextAlignment.left
           Lab.font = UIFont.pingFangTextFont(size: 10)
           Lab.textColor = ColorDarkGrayTextColor
           return Lab
       }()
    var viewModel: UserInfoCellModel?
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = ColorBackGround
        setupUI()
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    // MARK:- UI
    private func setupUI(){
        self.contentView.addSubviews([avatar, nameLab, TimeLab])
       avatar.snp.makeConstraints { (make) in
           make.left.equalTo(15)
           make.top.equalTo(15)
           make.width.height.equalTo(UserInfoCellModel.avatarSize.width)
       }
       nameLab.snp.makeConstraints { (make) in
           make.top.equalTo(15)
           make.left.equalTo(avatar.snp.right).offset(10)
           make.right.equalTo(-100)
           make.height.equalTo(20)
       }
       TimeLab.snp.makeConstraints { (make) in
           make.top.equalTo(nameLab.snp.bottom)
           make.left.equalTo(avatar.snp.right).offset(10)
           make.right.equalTo(-100)
           make.height.equalTo(20)
       }
    }
}
extension UserInfoCell: ListBindable {
    func bindViewModel(_ viewModel: Any) {
        guard let viewModel = viewModel as? UserInfoCellModel else { return }
        self.viewModel = viewModel
         avatar.kf.setImage(urlString: viewModel.avatar)
        nameLab.text = viewModel.userName
        TimeLab.text = viewModel.times
    }
}

2. ViewModel的实现

要使用IGListKit,我们的ViewModel必须遵守ListDiffable协定:
func diffIdentifier() -> NSObjectProtocol { return identifier as NSObjectProtocol }
用于区别定义项目
func isEqual(toDiffableObject object: ListDiffable?) -> Bool { if self === object { return true } guard let obj = object as? CommentCellModel else { return false } return self.identifier == obj.identifier }
用于区分两者是否为同一个Model

下面是UserInfoCellModel对应的示例代码:

class UserInfoCellModel: ListDiffable{
    var avatar: String?
    var userName: String?
    var times: String?
    let identifier: String
    var size: CGSize = CGSize.init(width: ScreenW, height: 61)
    static var avatarSize: CGSize = CGSize(width: 36, height: 36)
    init(model: homeModel) {
        identifier = model.user?.nickname ?? "---"
        self.avatar = model.user?.image
        self.userName = model.user?.nickname
        self.times = Date.timeStampChangeDate(model.note_list.first?.time ?? 0)
    }
    func diffIdentifier() -> NSObjectProtocol {
        return identifier as NSObjectProtocol
    }
    func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
        if self === object {
            return true
        }
        guard let obj = object as? UserInfoCellModel else { return false }
        return self.identifier == obj.identifier
    }
}


3. Controller的实现

我们需要生成UICollectionView,ListAdapter和ListAdapterUpdater。
ListAdapterUpdater 负责 row 和 section 的更新,而 ListAdapter 负责 CollectionView。

 let collectionView: UICollectionView = {
         let flow = UICollectionViewFlowLayout()
         let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: flow)
         collectionView.backgroundColor = ColorBackGround
         return collectionView
     }()
 lazy var adapter: ListAdapter = {
         let adapter = ListAdapter(updater: ListAdapterUpdater(), viewController: self)
        adapter.collectionView = collectionView
        adapter.dataSource = self
     return adapter
     }()

4. 使用 ListBindingSectionController

接下来我们通过使用 ListBindingSectionController 实现 Model 和 Cell 的绑定。

此 Controller 获取一个FeedViewModel, 向数据源请求 ViewModel 数组,获取到 ViewModels 之后将它们绑定到 Cell 上。
以下是FeedViewModel的示例代码:

class FeedViewModel: ListDiffable {
   var viewModels: [Any]
   let identifier: String
   init(model: homeModel) {
       identifier = model.track_id ?? "---"
        var sections: [Any] = []
        sections.append(UserInfoCellModel.init(model: model))
        sections.append(ContentCellModel.init(model: model))
        sections.append(PhotosCellModel.init(model: model))
        model.comment_list.forEach { (model) in
              sections.append(CommentCellModel.init(model: model))
        }
        viewModels = sections
   }
   func diffIdentifier() -> NSObjectProtocol {
       return identifier as NSObjectProtocol
   }
   func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
       if self === object {
           return true
       }
       guard let obj = object as? FeedViewModel else { return false }
       return self.identifier == obj.identifier
   }
}

在FeedListSectionController实现代理和数据
以下是FeedListSectionController的示例代码:

 class FeedListSectionController: ListBindingSectionController<ListDiffable>,ListBindingSectionControllerDataSource,ListBindingSectionControllerSelectionDelegate {
   override init() {
           super.init()
           dataSource = self
           selectionDelegate = self
       }

    override func didUpdate(to object: Any) {
           super.didUpdate(to: object)
    }
    func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, didSelectItemAt index: Int, viewModel: Any) {
    }
    func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, viewModelsFor object: Any) -> [ListDiffable] {
        guard let data = object as? FeedViewModel else { return [] }
        return data.viewModels as! [ListDiffable]
    }

   func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, cellForViewModel viewModel: Any, at index: Int) -> UICollectionViewCell & ListBindable {
          switch viewModel {
          case is UserInfoCellModel:
            guard let cell = collectionContext?.dequeueReusableCell(of: UserInfoCell.self, withReuseIdentifier: nil, for: self, at: index) as? UserInfoCell else { fatalError() }
               return cell
          case is ContentCellModel:
            guard let cell = collectionContext?.dequeueReusableCell(of: ContentCell.self, withReuseIdentifier: nil, for: self, at: index) as? ContentCell else { fatalError() }
              return cell
          case is PhotosCellModel:
            guard let cell = collectionContext?.dequeueReusableCell(of: PhotosCell.self, withReuseIdentifier: nil, for: self, at: index) as? PhotosCell else { fatalError() }
              return cell
          case is CommentCellModel:
            guard let cell = collectionContext?.dequeueReusableCell(of: CommentCell.self, withReuseIdentifier: nil, for: self, at: index) as? CommentCell else { fatalError() }
                return cell
          default:
              fatalError()
          }
      }
      func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, sizeForViewModel viewModel: Any, at index: Int) -> CGSize
      {
         switch viewModel {
          case is UserInfoCellModel:
                return (viewModel as! UserInfoCellModel).size
          case is ContentCellModel:
                return (viewModel as! ContentCellModel).size
          case is PhotosCellModel:
                return (viewModel as! PhotosCellModel).size
          case is CommentCellModel:
                return (viewModel as! CommentCellModel).size
          default:
                return CGSize.zero
          }
      }
}

好吧,看到这里,我们也或多或少感受到了IGListKit强大的能力,它强调的页面粒度的细化恰恰是我们所推崇的,一旦样式积木多了之后,构造页面的方式就具备了无限的可能性,它充分展现了高内聚低耦合的思想,拥有高易用性、可扩展性、可维护性,体现了化整为零、化繁为简的哲学。

上一篇 下一篇

猜你喜欢

热点阅读