快速上手 Instagram/IGListKit 框架 (官方 Demo 教程文档翻译整理)

快速上手 Instagram/IGListKit 框架 (官方 Demo 教程文档翻译整理)_第1张图片
image

Instagram/IGListKit 快速上手 (官方文档翻译)

IGListKit是Instagram推出的新的UICollectionView框架,使用数据驱动,旨在创造一个更快更灵活的列表控件。

项目地址:https://github.com/Instagram/IGListKit
作者演讲: 大规模重构——重写 Instagram Feed 的经验之谈

官方文档地址: https://instagram.github.io/IGListKit/
官方原文地址: https://instagram.github.io/IGListKit/modeling-and-binding.html

相关优秀文章推荐:
但江的思考-IGListKit
比 UICollectionView更好用的IGListKit教程 -- 通过模拟实现NASA的简单需求来更好的了解IGListKit

翻译水平有限,在不影响原文意思的前提下适当做了修改, 有错误请在留言区指正
译者微信: MTMwMjAwOTkzNjY=
Blog: https://www.xuzhengke.cn


前言(译者注):

译者目前属于初级iOS开发者,难免有许多翻译或点评不恰当的地方,欢迎指正。

Q4项目重构阶段认识了这个框架,2017年被评为33个必须了解的iOS开源库之一。最开始阅读了文章头部推荐的作者演讲,他阐述重写Feed的原因是技术债务(Technical Debt),例如大家在刷Ins的时间线的时候,除了帖子之外,大家还会看到优秀创作者推荐等信息。因为这些信息不包含在FeedItem这个模型里面,所以做起来会比较难以实现。

通过Diff算法来对比Model是否有变化,来实现界面的局部更新。

本文只是片面的翻译官方提供的Demo文档,暂时没有深入研究各种原理。在文章头部提供了一个模拟实现NASA业务需求的Demo,可以帮助你更好的理解IGListKit的妙处。


Demo -- 实现Ins帖子内容

快速上手 Instagram/IGListKit 框架 (官方 Demo 教程文档翻译整理)_第2张图片
image

创建Model与绑定

本文将通过一个实际的例子来教大家如何使用IGListKit。

通过本文你将学会:

  • 一个顶级Model和多个ViewModel组合的设计规范

  • 使用 ListBindingSectionController 对Cell进行更新

  • Cell和Controller之间的响应事件和代理的处理

  • 使用本地变化的数据来更新UI

开始

我们可以跟着下面的例子一起做。首先下载官方提供的测试项目:rnystrom/IGListKit-Binding-Guide。 此项目已经通过CocoaPods集成了IGListKit,所以我们可以直接打开:ModelingAndBinding-Starter/ModelingAndBinding.xcworkspace

此项目的目的是实现类似Instagram帖子详情页面,所以我们可以大致思考一下它的数据模型:

  • 顶部Cell现实用户名和发布时间标签

  • 中部Cell包含一个通过URL加载的图片

  • 图片下方显示点赞数,同时我们也会加入交互功能,当用户点击此按钮时,点赞数将增加

  • 底部为评论列表,显示不确定数量的评论,主要包括用户名和评论内容

切记,IGListKit实现的是一个Model对应一个Section Controller。 上述所有的Cell都与一个顶级Model相关联。我们创建一个Post模型,其中包含所有cell需要的数据。

常见的错误是,对每一个单独的cell创建一个Model和一个Section Controller。在本例子中,我们将要创建一个非常复杂的顶级Model,它将包括用户,图片,评论,点赞等模型所需要的数据。


创建Model

在打开的项目中创建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声明,这样就不会被再次修改。上文代码会被提示 “找不到Comment模型”,这里可以忽略。

上文代码已经遵循了 ListDiffable 协议,接下来在Post中实现它。


// MARK: ListDiffable

func diffIdentifier() -> NSObjectProtocol {

  // 1

  return (username + timestamp) as NSObjectProtocol

}

// 2

func isEqual(toDiffableObject object: ListDiffable?) -> Bool {

  return true

}

1.为每一篇文章派生一个唯一的标识符,因为一篇帖子不可能发帖人用户名和发帖时间都相同,所以我们通过这两个属性来生成标识符;

  1. 使用 ListBindingSectionController 的一个核心要求是,如果两个Model具有相同的diffIdentifier,那么他们必定相同,以便于Section Controller可以比较这些View Model

译者注: 关于此处算法的由来,可以阅读文章头部 ‘作者演讲’,或参考文章:[Instagram/IGListKit实践谈

](https://www.jianshu.com/p/44bda1421757)

View Models

创建Comment.swift文件,参考下列需求完成代码:

  • String类型的username

  • String类型的text

  • 实现ListDiffable

参考代码:


import IGListKit

final class Comment: ListDiffable {

    let username: String

    let text: String



    init(username: String, text: String) {

        self.username = username

        self.text = text

    }



    // MARK: ListDiffable

    func diffIdentifier() -> NSObjectProtocol {

        return (username + text) as NSObjectProtocol

    }



    func isEqual(toDiffableObject object: ListDiffable?) -> Bool {

        return true

    }

}

关于 "diffIdentifier"方法的解释: 根据定义,对象实际上和标识符已经一一对应。在检测两个对象是否相等时,我们可以直接检测diffIdentifier

在上述Post模型中用到了Comment,每一条帖子的评论数是不一样的,对于每一条评论,我们都用一个Cell来展示。

不过,可能有一点新的概念。就是即使使用了ListBindingSectionController,我们仍然需要为ImageCell,ActionCell,UserCell创建Model。

每一个绑定的Section Controller 其实就想一个小型的IGListKit。Section Controller包含一个数组,里面是所有的View Model,然后把他们装配到指定的Cell里面。现在为每一个Cell创建Model

创建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,所以我们可以硬编码一个标识符,这强调只使用一个此类Model和Cell

  • 为ViewModel实现编写一个好的等式方法是很重要的。任何时候发生变化,迫使模型不相等时,Cell就会被刷新。

参考UserViewModel,实现其他两个ViewModel


import IGListKit

final class ImageViewModel: ListDiffable {

    let url: URL

    init(url: URL) {

        self.url = url

    }



    // MARK: ListDiffable

    func diffIdentifier() -> NSObjectProtocol {

        return "image" as NSObjectProtocol

    }



    func isEqual(toDiffableObject object: ListDiffable?) -> Bool {

        guard let object = object as? ImageViewModel else { return false }

        return url == object.url

    }

}

final class ActionViewModel: ListDiffable {

    let likes: Int



    init(likes: Int) {

        self.likes = likes

    }



    // MARK: ListDiffable



    func diffIdentifier() -> NSObjectProtocol {

        return "action" as NSObjectProtocol

    }



    func isEqual(toDiffableObject object: ListDiffable?) -> Bool {

        guard let object = object as? ActionViewModel else { return false }

        return likes == object.likes

    }

}


使用 ListBindingSectionController

现在我们有了如下ViewModel,这些都能从一篇帖子中找到:

  • UserViewModel

  • ImageViewModel

  • ActionViewModel

  • Comment

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

此Controller获取一个顶级Model (Post), 向数据源请求ViewModel数组,获取到ViewModels之后将它们绑定到Cell上。

创建 PostSectionController.swift


final class PostSectionController: ListBindingSectionController,

ListBindingSectionControllerDataSource {

  override init() {

    super.init()

    dataSource = self

  }

}

上文代码可以看到我们继承 ListBindingSectionController , 这表明PostSectionController接收Post模型。

接下来的三个方法将实现data source 协议:

  • 返回一个数组,数组中包括Post模型用到的所有ViewModel

  • 返回ViewModel的size

  • 给定ViewModel返回Cell

首先关注Post和ViewModels的转换


// MARK: ListBindingSectionControllerDataSource

func sectionController(

  _ sectionController: ListBindingSectionController,

  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

}

通过将Post模型分解为较小的模型来创建ViewModel数组;

添加必须的API返回每一个ViewModel的Size


func sectionController(

  _ sectionController: ListBindingSectionController,

  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)

}

最后,实现API返回每个ViewModel对应的Cell。 官方已经提供了各种Cell,请在 Main.storyboard查看或者参考下文代码


func sectionController(_ sectionController: ListBindingSectionController, 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()

        }



        if let cell = cell as? ActionCell {

            cell.delegate = self

        }



        return cell

    }


绑定Model - Cell

现在已经实现了 PostSectionController 创建不同的ViewModel,Size和Cell。Cell通过 ListBindingSectionController 接收ViewModel。

实现 ListBindable,Cell便可以接收ViewModel。

接下来完善每个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)

  }

}

剩余Cell请自行绑定


在Controller调用

回到ViewController.swift ,在ViewDidload()之后,设置dataSource和collectionView之前,增加以下测试代码。


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"),

  ]

))

最后,修改以下方法,替换Return值


func listAdapter(

  _ listAdapter: ListAdapter,

  sectionControllerFor object: Any

  ) -> ListSectionController {

  return PostSectionController()

}


绑定点击事件

接下来将为ActionCell的❤️(点赞按钮)绑定事件。为此,我们需要处理UIButton的点击,然后将事件转发到PostSectionController

打开ActionCell.swift加入以下代码


protocol ActionCellDelegate: class {

  func didTapHeart(cell: ActionCell)

}

在ActionCell中添加delegate


weak var delegate: ActionCellDelegate? = nil

重写awakeFromNib()添加action


override func awakeFromNib() {

  super.awakeFromNib()

  likeButton.addTarget(self, action: #selector(ActionCell.onHeart), for: .touchUpInside)

}

最后添加action的实现


func onHeart() {

  delegate?.didTapHeart(cell: self)

}

打开PostSectionController.swift,更新cellForViewModel:方法,在guard和return cell 之间添加代码:


if let cell = cell as? ActionCell {

  cell.delegate = self

}

此时编译器会报错,这时候我们在PostSectionController暂时实现协议中的方法。


final class PostSectionController: ListBindingSectionController,

ListBindingSectionControllerDataSource,

ActionCellDelegate {

//...

// MARK: ActionCellDelegate

func didTapHeart(cell: ActionCell) {

  print("like")

}

运行代码,现在可以实现点击事件了。


局部变化

每当用户点击点赞按钮时,我们需要更新帖子页面的点赞数。然而我们Model的属性都是由let定义的,因为这样更安全。如果一切都是不可变的,那我们如何改变详情页的点赞数呢。

PostSectionController是处理和存储变化的最佳场所,打开PostSectionController.swift 添加以下变量


var localLikes: Int? = nil

回到didTapHeart(cell:),我们把print()修改成具体内容。


func didTapHeart(cell: ActionCell) {

  // 1

  localLikes = (localLikes ?? object?.likes ?? 0) + 1

  // 2

  update(animated: true)

}

  1. localLIkes变量为在上一个值的基础上+1,如果localLike本身是nil则使用Model里的likes值,如果此值也不存在时,使用0作为初始值;

  2. 调用update(animated:,completion:)API, 刷新cell

为了将变化实际发送到Model,我们需要在开始的时候使用localLikes代替原先从服务器获取的likes点赞数,赋值给ActionViewModel

还是在PostSectionController.swift ,回到cellForViewModel: 方法,把ActionViewModel的初始化修改为下面的样子


// 之前的写法,直接把后端api传过来的likes赋值给ViewModel

// ActionViewModel(likes: object.likes)

ActionViewModel(likes: localLikes ?? object.likes)

编译代码,OK,功能实现。


总结

ListBindingSectionController是IGListKit最强大的功能之一,因为它进一步鼓励你设计小型、可组合的Models、Views和Controllers。

我们可以使用Section Controller 来处理任何交互,以及各种变化(例如点赞数改变),就像普通控制器一样。

你可能感兴趣的:(快速上手 Instagram/IGListKit 框架 (官方 Demo 教程文档翻译整理))