三大特性:
使用心得:数据单向流的好处在于什么样的数据决定什么样的视图,在开发时可以无视很多各种交互产生的状态,只需要把精力放在数据层上,写好排版方程(functional)似乎好像可以做到一劳永逸。正因为如此,ComponentKit在写动画的时候注定会很麻烦,因为数据变化是连续的。
至于动画方面,FB回复:
Dynamic gesture-driven UIs are currently hard to implement in ComponentKit; consider using AsyncDisplayKit.
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
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的元素。
1⃣️概述:
FB广告:ComponentKit really shines when used with a UICollectionView.
之所以特地强调,因为任何一款APP都特么离不开UITableView或者UICollectionView。只要会UITableView或者UICollectionView那就具备了独立开发的能力。
FB鼓吹的优点:
PS:CKComponentDataSource模块的主要功能:
CKComponentCollectionViewDataSource
CKComponentCollectionViewDataSource是CKComponentDataSource的简单封装。
存在价值:
这里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];
}
...
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,主要分为三类:
贴代码:
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}];
这里需要注意的是:
即是说:加入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});的语句,因为你所希望插入的位置未必是你想要插入的位置。