标签:
传统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可用的输出数据源
CKComponentCollectionViewDataSourceCKComponentCollectionViewDataSource是CKComponentDataSource的简单封装。
存在价值:
负责让UICollectionView适时进行添加/插入/更新“行”,“段”。
负责提供给UICollectionView“行”和“段“排版信息。
UICollectionView可见行讲同步调用cellForItemAtIndexPath:
保证返回配置好的cell
这里UICollectionView与CKCollectionViewDataSource数据表现来说仍是单向的。
Component ProviderCKCollectionViewDataSource负责将每一个数据丢给元素(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排版
标签:
原文地址:http://my.oschina.net/hejunbinlan/blog/424618