本文首先会讲解如何使用 Texture,以前叫 AsyncDisplayKit,讲解其核心概念、布局、便捷工具和优化方式,然后会讲解如何同 IGListKit 结合,最后会简短说一下 Yoga 和 Flexbox。

Texture Logo

核心概念

Node Container

要使用 Texture 的 Node Container 才能管理到 Node 的生命周期,这样才有预加载和异步加载的功能,反正肯定要用的:

  • ASCollectionNode 对应 UIKit 的 UICollectionView
  • ASPagerNode 对应 UIKit 的 UIPageViewController
  • ASTableNode 对应 UIKit 的 UITableView
  • ASViewController 对应 UIKit 的 UIViewController

ASViewController 的子类

以下是要实现 ASViewController 子类的注意事项,首先所有方法都在主线程调用:

init()

不要在此方法中调用 view 或 node.view,会强迫 view 被提前创建,

func loadView()

不要使用

func viewDidLoad()

可以访问 view 和 layer,设置只需要运行一次的代码

func viewWillLayoutSubviews()

bounds 变化的时候被调用,包括旋转、分屏、弹出键盘,以及视图层级的变化,在这里可以做一些布局的工作

Node

Node 是对 UIView 或 CALayer 的封装,最基本的 UI 单元,支持异步布局和加载:

  • ASDisplayNode 对应 UIView
  • ASCellNode 对应 ASCollectionNode 或 ASTableNode 的 Cell,没有对应 Header、Footer、SectionHeader、SupplementaryView 的 Node
  • ASTextNode 对应 UILabel
  • ASImageNode 对应 UIImage 本地图片
  • ASNetworkImageNode 对应 UIImage 网络图片
  • ASButtonNode 对应 UIButton

Node 的子类

init()

使用 nodeBlock 创建时,此方法会从后台线程调用,不用在此方法初始化任何 UIKit 的对象,以及使用 node 的 view 或 layer

func didLoad()

此方法在主线程调用,保证 view 已经加载好,且只调用一次,所以可以做一些针对 UIKit 对象的工作

func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec

此方法在后台线程调用,主要做布局的运算,布局的结果不会被缓存,每次重新计算,不要在此方法中创建新的 Node

func layout()

此方法在主线程调用,布局已经计算完成,可以做一些显隐的控制,背景色之类的设置,如果通过 -initWithViewBlock: 使用了 UIView,这里可以修改其 frame

布局

Layout Specs

layout spec 只是用来做布局运算,并不会产生实际的 UI 元素,可以做为布局元素的容器

Layout Elements

ASDisplayNode 和 ASLayoutSpec 都遵循 ASLayoutElement 协议,所以布局时候可以把 layout spec 和 node 组合起来。

不同的 layout spec 都根据其自身的情况有特定的属性,常见的 layout spec 如下:

  • ASWrapperLayoutSpec 简单包装一个 node
  • ASStackLayoutSpec Flexbox 布局容器
  • ASInsetLayoutSpec 四个方向的 Margin 布局容器
  • ASOverlayLayoutSpec 覆盖在上面的布局容器,大小由下层决定
  • ASBackgroundLayoutSpec 做为背景的布局容器,大小由上层决定
  • ASCenterLayoutSpec 在容器中居中的布局容器,还可以设置四个方向的偏移
  • ASRatioLayoutSpec 比例容器
  • ASRelativeLayoutSpec 九宫格的容器,放置在九宫格的任意位置
  • ASAbsoluteLayoutSpec 绝对布局的容器

具体的布局示例和上面布局容器示例代码:

其他

便捷工具

优化

为什么界面会闪烁

node 都是异步加载,在加载的过程中,Texture 会在 node 显示的位置放置一个图片的 placeholder,加载完毕后,替换为真实内容,所以从 placeholder 变为真实内容就会看到闪烁,再次 reload 这个 node 也会重复上面的过程,同样会闪烁。

node 中实现如下方法可以定制 placeholder

class MyNode: ASDisplayNode {
  
  override func placeholderImage() -> UIImage? {
    return nil
  }
  
}

针对局部的变化,为了避免闪烁,可以采用 Synchronous Concurrency

IGListKit

之前介绍过的 IGListKit 可以和 Texture 结合起来,IGListKit 解决数据的 Diffing,Texture 解决 UI 加载和布局:

extension CartViewController: ListAdapterDataSource {
  
  func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
    return list
  }
  
  func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
    if object as? CartItem != nil {
      return CartItemSectionController()
    }
    return ListSectionController()
  }
  
  func emptyView(for listAdapter: ListAdapter) -> UIView? {
    return nil
  }
  
}

ListAdapter 的实现并没有什么特别的

class CartItemSectionController: ListSectionController {
    
  private var item: CartItem!
  
  override func didUpdate(to object: Any) {
    item = object as? CartItem
  }
  
  override func cellForItem(at index: Int) -> UICollectionViewCell {
    return ASIGListSectionControllerMethods.cellForItem(at: index,
                                                        sectionController: self)
  }
  
}

extension CartItemSectionController: ASSectionController {
  
  func nodeBlockForItem(at index: Int) -> ASCellNodeBlock {
    return { [unowned self] in
      return CartItemCell(item: self.item)
    }
  }
  
}

主要利用 ASIGListSectionControllerMethods.cellForItem(at: index, sectionController: self) 将 IGListKit 和 Texture 结合起来, nodeBlockForItem 中返回的 CartItemCell 就是 Texture 的 ASCellNode,按照 Node 子类化的方式实现就可以了

更完整的示例:IGListKit-AsyncDisplayKit-Example

Yoga

使用了 Texture 对 Flexbox 布局的支持,是不是感觉很方便,如果只是想在 iOS 中使用 Flexbox 布局,可以使用 Yoga: