传统MVC模式,数据(s)-控制器(s)-视图(s)之间的双向流所产生的大量状态将导致:
1)代码激增
2)BUG出现得概率增大
3)视图渲染效率低
4)APP流畅度不高(特指ScrollView不能达到60FPS)
所以我们需要一个更为简单粗暴的框架来写我们的APP。。(真正的原因,绝不会告诉你其实只是被要求...)
既然名字叫做ComponentKit
,自然先说说Component(元素)
。对于开发者来说,所有的图层(可见/不可见)其实都是由一个元素排版而来。不同的元素根据不同的排版展示出不同的视图。我做个类比:正如中国四大发明之一-活字印刷一样通过改变排版可以展示不同的文字内容。
这里引用文档的一句话:
A simple analogy is to think of a component as a stencil: a fixed description that can be used to paint a view but that is not a view itself.
意思也大概如此Component不并不直接当做视图(印刷出来的东西)展示,而是告诉你视图长什么样(印刷模具的存在)~
三大特性:(不明觉厉的地方)
描述性:通过stack
为主(这里我翻译成“(纵向或者横向)堆砌”)排版模具来告诉我们某一个元素A的子元素在A中如何排列。
函数式:保证数据流是单向的,也就是数据决定Component。比如方程“1 + X”,如果X=2或者X=3相对应结果“1 + 2”与“1 + 3”是固定的一样。数据如果确定了,那么结果就是不变的。当数据发生改变的时候,对应的component会进行重新渲染。(这里FB宣称该框架会尽量少的重新渲染,没有读过代码,没有发言权)
可组合:这里可以想下积木,有些部分写成component,其他地方可以重用。
个人使用的心得?:数据单向流,好处无非在于什么样的数据决定什么样的视图,我们可以无视很多各种交互产生的状态,而仅仅只需要把精力放在数据层上,写好排版方程(functional)似乎好像可以做到一劳永逸。但是正因为如此,ComponentKit在写动画的时候注定较麻烦,因为数据变化是连续的~~也就是model是不断变化的。使用上可以做一些取舍。用ComponentKit的好处就在于写代码可以处于无脑状态,抓着绳子(数据)的一端就好,不容易打死结~
至于动画方面的解释,FB如是说:
Dynamic gesture-driven UIs are currently hard to implement in ComponentKit; consider using AsyncDisplayKit.
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,一个用来进行排版。
Composite Components
这里只有一句话重点:任何情况自定义component下不要继承CKComponent
,而是继承Composite Components
。
大概原因就是不要污染清纯的父类Component,不要因为一个简单得需求而直接进行继承并重写父类方法(很多弊端,FB blabla),而应该采用修饰的手段来达成(装饰设计模式?)。
这里给出坏代码以及推荐代码示例:
// 不推荐的坏代码:@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
创建一个元素的类方法
+ (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]
与UIView中的layoutSubViews
对应的是CK中的layoutThatFits:
。
这里主要介绍几个常用的Layout Components
CKStackLayoutComponent
横向或者纵向堆砌子元素
CKInsetComponent
内陷与大苹果内陷相似
CKBackgroundLayoutComponent
扩展底部的元素作为背景
CKOverlayLayoutComponent
扩展覆盖层的元素作为遮罩
CKCenterLayoutComponent
在空间内居中排列
CKRatioLayoutComponent
有比例关系的元素
CKStaticLayoutComponent
可指定子元素偏移量
FB中的响应者链与苹果类似,但是两者是分离的。
FB中的链大概长相:
儿子component-> 儿子componentController(如果有) -> 父亲component-> 父亲componentController(如果有) -> (...递归 blabla) -> 【通过CKComponentActionSend
桥接】-> (过程:找到被附着的那个View,通过这个View找到最底层的叶子节点ViewA -> (往上遍历ViewA的父亲ViewB -> (...递归 blabla)。
这里一个要点是component不是UIResponder子类,自然无法成为第一响应者~
解决发生在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
以上对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
一句话?元素Action机制 就是通过无脑绑定SEL,顺着响应链找到可以响应该SEL的元素。
FB也很淫荡的做了广告:
ComponentKit really shines when used with a UICollectionView.
吐槽下,之所以特地强调,那是必须啊,任何一款APP都特么离不开UITableView或者UICollectionView。只要会UITableView或者UICollectionView那就具备了独立开发的能力,精通这两者的就算具备了犀利哥的潜质。
FB鼓吹的优点:
自动重用
流畅的滑动体验 -> CK自身保证非UI相关的计算全在次线程
数据源 这个模块由CKComponentDataSource
负责。
PS:CKComponentDataSource
模块的主要功能:
1)提供输入数据源的操作指令以及数据
2)变化后的数据源布局后台生成
3)提供UITableView
或者UICollectionView
可用的输出数据源
CKComponentCollectionViewDataSource
CKComponentCollectionViewDataSource
是CKComponentDataSource
的简单封装。
存在价值:
负责让UICollectionView
适时进行添加/插入/更新“行”,“段”。
负责提供给UICollectionView
“行”和“段“排版信息。
UICollectionView
可见行讲同步调用cellForItemAtIndexPath:
保证返回配置好的cell
这里UICollectionView
与CKCollectionViewDataSource
数据表现来说仍是单向的。
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)外部依赖 比如图片下载器
CKCollectionViewDataSource
:- (void)viewDidLoad { ... self.dataSource = _dataSource = [[CKCollectionViewDataSource alloc] initWithCollectionView:self.collectionView supplementaryViewDataSource:nil componentProvider:[self class] context:context cellConfigurationFunction:nil];
需要做的就是将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
将他们绑在一起。
因为无脑,只贴代码:
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { return [self.dataSource sizeForItemAtIndexPath:indexPath]; }
因为无脑,只贴代码:
- (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,主要分为三类:
动作(针对行的插入/删除/更新,针对段的插入/删除)
位置指定(行/段位置指定)
分配数据(丢给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 forUITableViews
).
加入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});
的语句,因为你所希望插入的位置未必是你想要插入的位置。
Facebook's iOS Infrastructure - @Scale 2014 - Mobile
OJBC.IO ComponentKit介绍
官方文档
Making News Feed nearly 50% faster on iOS
Flexbox排版