一、特性
三大特性:
- 描述性:通过stack的特点纵向或横向堆砌,排版模具来告诉我们某一个元素A的子元素在A中如何排列。
- 函数式:保证数据流是单向的,也就是数据决定Component。比如方程“1 + X”,如果“X = 2”,则相对应的结果就是3是固定的。数据如果确定了,那么结果就是不变的。当数据发生改变时,对应的Component会进行重新渲染(底层实现会尽量少的重新渲染)。
- 可组合:有些部分写成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鼓吹的优点:
- 自动重用
- 流畅的滑动体验 -> CK自身保证非UI相关的计算全在次线程
- 数据源 这个模块由CKComponentDataSource负责。
PS:CKComponentDataSource模块的主要功能:
- 提供输入数据源的操作指令以及数据
- 变化后的数据源布局后台生成
- 提供UITableView或者UICollectionView可用的输出数据源
CKComponentCollectionViewDataSource
CKComponentCollectionViewDataSource是CKComponentDataSource的简单封装。
存在价值:
- 负责让UICollectionView适时进行添加/插入/更新“行”,“段”。
- 负责提供给UICollectionView“行”和“段“排版信息。
- UICollectionView可见行讲同步调用cellForItemAtIndexPath:
- 保证返回配置好的cell
这里UICollectionView与CKCollectionViewDataSource数据表现来说仍是单向的。
2⃣️基础
Component Provider
CKCollectionViewDataSource负责将每一个数据丢给元素(component)进行自我Config。也就是在某一个元素(component)需要进行数据配置时,将会把CKCollectionViewDataSource提供的数据源通过CKComponentProvider提供的类方法传入:
@interface MyController <CKComponentProvider>
...
@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,主要分为三类:
- 动作(针对行的插入/删除/更新,针对段的插入/删除)
- 位置指定(行/段位置指定)
- 分配数据(丢给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}];
这里需要注意的是:
- 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});的语句,因为你所希望插入的位置未必是你想要插入的位置。