ComponentKit框架学习

一、特性

三大特性:

  1. 描述性:通过stack的特点纵向或横向堆砌,排版模具来告诉我们某一个元素A的子元素在A中如何排列。
  2. 函数式:保证数据流是单向的,也就是数据决定Component。比如方程“1 + X”,如果“X = 2”,则相对应的结果就是3是固定的。数据如果确定了,那么结果就是不变的。当数据发生改变时,对应的Component会进行重新渲染(底层实现会尽量少的重新渲染)。
  3. 可组合:有些部分写成component,其他地方可以重用。

使用心得:数据单向流的好处在于什么样的数据决定什么样的视图,在开发时可以无视很多各种交互产生的状态,只需要把精力放在数据层上,写好排版方程(functional)似乎好像可以做到一劳永逸。正因为如此,ComponentKit在写动画的时候注定会很麻烦,因为数据变化是连续的。

至于动画方面,FB回复:

Dynamic gesture-driven UIs are currently hard to implement in ComponentKit; consider using AsyncDisplayKit.

 

二、API

1⃣️CKComponent

Component是不可变的,且其可以在任何线程进行创建,避免了出现在主线程的竞争。

主要是两个API:

/** Returns a new component. */

+ (instancetype)newWithView:(const CKComponentViewConfiguration &)view

                       size:(const CKComponentSize &)size;



/** Returns a layout for the component and its children. */

- (CKComponentLayout)layoutThatFits:(CKSizeRange)constrainedSize

                         parentSize:(CGSize)parentSize;

一个用来创建Component,一个用来进行排版。

 

2⃣️Composite Components

重点:任何情况自定义Component下不要继承CKComponent,而是继承 Composite Components。不要因为一个简单的需求而直接进行继承并重写父类方法,而应该采用修饰的手段达成(装饰设计模式)。

 

这里给出坏代码以及推荐代码示例:

// 不推荐的坏代码:

@implementation HighlightedCardComponent : CardComponent

- (UIColor *)backgroundColor

{

  // This breaks silently if the superclass method is renamed.

  return [UIColor yellowColor];

}

@end

// 推荐代码:

@implementation HighlightedCardComponent : CKCompositeComponent

+ (instancetype)newWithArticle:(CKArticle *)article

{

  return [super newWithComponent:

          [CardComponent

           newWithArticle:article

           backgroundColor:[UIColor yellowColor]]];

}

@end

3⃣️Views

创建一个元素的类方法

+ (instancetype)newWithView:(const CKComponentViewConfiguration &)view

                       size:(const CKComponentSize &)size;

第一个参数告诉CK用什么图层类,第二个参数告诉CK如何配置这个图层类。

举个栗子:

[CKComponent 

 newWithView:{

   [UIImageView class],

   {

     {@selector(setImage:), image},

     {@selector(setContentMode:), @(UIViewContentModeCenter)} // Wrapping into an NSNumber

   }

 }

 size:{image.size.width, image.size.height}];

同样可以设置空值,举个栗子:

[CKComponent newWithView:{} size:{}];

// 更为直接

[CKComponent new];

4⃣️Layout && Layout Components

与UIView中的layoutSubViews对应的是CK中的layoutThatFits:

这里主要介绍几个常用的Layout Components

  • CKStackLayoutComponent 横向或者纵向堆砌子元素
  • CKInsetComponent内陷与大苹果内陷相似
  • CKBackgroundLayoutComponent 扩展底部的元素作为背景
  • CKOverlayLayoutComponent 扩展覆盖层的元素作为遮罩
  • CKCenterLayoutComponent 在空间内居中排列
  • CKRatioLayoutComponent 有比例关系的元素
  • CKStaticLayoutComponent 可指定子元素偏移量

 

三、响应者链 && Tap事件 && 手势支持

1⃣️响应者链

FB中的响应者链与苹果类似,但是两者是分离的。

FB中的响应者链大致为:

儿子component -> 儿子componentController(如果有) -> 父亲component -> 父亲componentController(如果有) -> (...递归 blabla) -> 【通过CKComponentActionSend桥接】-> (过程:找到被附着的那个View,通过这个View找到最底层的叶子节点ViewA -> (往上遍历ViewA的父亲ViewB -> (...递归 blabla)

这里一个要点是component不是UIResponder子类,自然无法成为第一响应者~

 

2⃣️点击事件

解决发生在UIControl视图上的点击事件很简单,只要将某个SEL绑定到CKComponentActionAttribute即可,在接收外界UIControlEvent时候触发:

@implementation SomeComponent



+ (instancetype)new

{

  return [self newWithView:{

    [UIButton class],

    {CKComponentActionAttribute(@selector(didTapButton))}

  }];

}



- (void)didTapButton

{

  // Aha! The button has been tapped.

}



@end

3⃣️手势

以上对UIControl适用,对于一般View则要绑定更直接的属性,比如tap手势绑定SEL到CKComponentTapGestureAttribute,代码如下:

@implementation SomeComponent



+ (instancetype)new

{

  return [self newWithView:{

    [UIView class],

    {CKComponentTapGestureAttribute(@selector(didTapView))}

  }];

}



- (void)didTapView

{

  // The view has been tapped.

}



@end

4⃣️Component Actions

总之,元素Action机制 就是通过无脑绑定SEL,顺着响应链找到可以响应该SEL的元素。

 

四、对iOS中容器类视图的支持(UITableView UICollectionView

1⃣️概述:

FB广告:ComponentKit really shines when used with a UICollectionView.

之所以特地强调,因为任何一款APP都特么离不开UITableView或者UICollectionView。只要会UITableView或者UICollectionView那就具备了独立开发的能力。

 

FB鼓吹的优点:

  1. 自动重用
  2. 流畅的滑动体验 -> CK自身保证非UI相关的计算全在次线程
  3. 数据源  这个模块由CKComponentDataSource负责。

 

PS:CKComponentDataSource模块的主要功能:

  1. 提供输入数据源的操作指令以及数据
  2. 变化后的数据源布局后台生成
  3. 提供UITableView或者UICollectionView可用的输出数据源

 

CKComponentCollectionViewDataSource

CKComponentCollectionViewDataSource是CKComponentDataSource的简单封装。

存在价值:

  1. 负责让UICollectionView适时进行添加/插入/更新“行”,“段”。
  2. 负责提供给UICollectionView“行”和“段“排版信息。
  3. UICollectionView可见行讲同步调用cellForItemAtIndexPath:
  4. 保证返回配置好的cell

这里UICollectionView与CKCollectionViewDataSource数据表现来说仍是单向的。

 

2⃣️基础

Component Provider

CKCollectionViewDataSource负责将每一个数据丢给元素(component)进行自我Config。也就是在某一个元素(component)需要进行数据配置时,将会把CKCollectionViewDataSource提供的数据源通过CKComponentProvider提供的类方法传入:

@interface MyController 

    ...

    @end



    @implementation MyController

    ...

    + (CKComponent *)componentForModel:(MyModel*)model context:(MyContext*)context {

        return [MyComponent newWithModel:model context:context];

    }

    ...
  • 用类方法不用block 为了保证数据是不可变的
  • 上下文  这里可以是任意不可变对象,其被CKCollectionViewDataSource带入。它一般是:1)设备类型 2)外部依赖 比如图片下载器

3⃣️创建CKCollectionViewDataSource:

- (void)viewDidLoad {

    ...

    self.dataSource = _dataSource = [[CKCollectionViewDataSource alloc] initWithCollectionView:self.collectionView supplementaryViewDataSource:nil componentProvider:[self class] context:context cellConfigurationFunction:nil];

}

4⃣️添加/修改

需要做的就是将Model与indexPath进行绑定:

- (void)viewDidAppear {

        ...

        CKArrayControllerSections sections;

        CKArrayControllerInputItems items;

        // Don't forget the insertion of section 0

        sections.insert(0);

        items.insert({0,0}, firstModel);

        // You can also use NSIndexPath

        NSIndexPath indexPath = [NSIndexPath indexPathForItem:1 inSection:0];

        items.insert(indexPath, secondModel);

        [self.dataSource enqueueChangeset:{sections, items} constrainedSize:{{0,0}, {50, 50}}];

    }

比如indexPath(0, 0),model是一个字符串”我是0段0行”,告诉CKCollectionViewDataSource将他们绑定到一起(即绑定0段0行和Component元素)。

5⃣️排版

贴代码:

- (CGSize)collectionView:(UICollectionView *)collectionView

                 layout:(UICollectionViewLayout *)collectionViewLayout

                 sizeForItemAtIndexPath:(NSIndexPath *)indexPath {

        return [self.dataSource sizeForItemAtIndexPath:indexPath];

    }

6⃣️事件处理

- (void)dataSource:(CKCollectionViewDataSource *)dataSource didSelectItemAtIndexPath:(NSIndexPath *)indexPath{

        MyModel *model = (MyModel *)[self.dataSource modelForItemAtIndexPath:indexPath];

        NSURL *navURL = model.url;

        if (navURL) {

            [[UIApplication sharedApplication] openURL:navURL];

        }
}

五、(数据源)改变集API

这里主要是指与数据源交互部分的API,主要分为三类:

  1. 动作(针对行的插入/删除/更新,针对段的插入/删除)
  2. 位置指定(行/段位置指定)
  3. 分配数据(丢给Component用的)

贴代码:

CKArrayControllerInputItems items;

// Insert an item at index 0 in section 0 and compute the component for the model @"Hello"

items.insert({0, 0}, @"Hello");

// Update the item at index 1 in section 0 and update it with the component computed for the model @"World"

items.update({0, 1}, @"World");

// Delete the item at index 2 in section 0, no need for a model here :)

Items.delete({0, 2});


Sections sections;

sections.insert(0);

sections.insert(2);

sections.insert(3);


[datasource enqueueChangeset:{sections, items}];

这里需要注意的是:

  1. The order in which commands are added to the changeset doesn't define the order in which those changes will eventually be applied to the UICollectionView (same for UITableViews).

         即是说:加入changeset的顺序并不代表最终UICollectionView最终应用上的改变顺序。

     2. 记得初始化的时候要执行sections.insert(0);

     3. 因为所有的改变集都是异步计算的,所以需要注意可能出现数据与UI不同步的问题

        3.1 始终以datasource为唯一标准,不要试图从曾经的数据源like下面的例子中的_listOfModels获取model:

@implementation MyAwesomeController {

    CKComponentCollectionViewDataSource *_datasource;

    NSMutableArray *_listOfModels;

}

例子中的_datasource才是正房,_listOfModels是小三。

坚持使用:

[datasource objectAtindexPath:indexPath];

        3.2 不要执行像:items.insert({0, _datasource.collectionView numberOfItemsInSection});的语句,因为你所希望插入的位置未必是你想要插入的位置。

你可能感兴趣的:(iOS,objective-C)