[TOC]
一、概况
Components are immutable objects that specify how to configure views.
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.
A component is often composed of other components,
building up a component hierarchy that describes a user interface.
理解:
Component(组件)
是不可变的对象,用来告诉我们如何初始化view。
Component(组件)
是一个固定的描述,这个描述可以用来打印view。
简单恰当的比喻:
Component(组件)
相当于stencil(镂空板)
,是一个固定的图案,用来打印view,但不是view。
镂空版印刷(stencil printing)
,指在木片、纸板、金属或塑料等片材上刻划出图文,并挖空制成镂空版,通过刷涂或喷涂方法使油墨透过通孔附着于承印物上。
Component(组件)相当于印章
view相当于印章盖出来的图案
一个组件通常由其他组件组合而成,组件的层级结构可以用来描述用户界面。
1.1Components 三大特性:
demo代码
@implementation ArticleComponent
+ (instancetype)newWithArticle:(ArticleModel *)article
{
return [super newWithComponent:
[CKFlexboxComponent
newWithView:{}
size:{}
style:{
.direction = CKFlexboxDirectionVertical,
}
children:{
{[HeaderComponent newWithArticle:article]},
{[MessageComponent newWithMessage:article.message]},
{[FooterComponent newWithFooter:article.footer]},
}];
}
@end
声明式 Declarative:
Instead of implementing -sizeThatFits: and -layoutSubviews and positioning subviews manually, you declare the subcomponents of your component (here, we say “stack them vertically”).
相比原生设置UI需要手动设置位置,声明式的特性只需要我们做一个声明描述即可,比如垂直排列元素。
非常方便。
函数式Functional:
Data flows in one direction.
Methods take data models and return totally immutable components.
数据单向流动,数据流向UI
根据Data获取对应的不可变的Components组件
When state changes, ComponentKit re-renders from the root and reconciles the two component trees from the top with as few changes to the view hierarchy as possible.
状态改变时,ComponentKit会重新绘制。这时候有oldState和newState,会仔细核对两个组件树(Component tree),从root node到top node 尽量使用最少的重绘来更新view层级结构。
组合式Composable:
Here FooterComponent is used in an article, but it could be reused for other UI with a similar footer.
Reusing it is a one-liner.
CKFlexboxComponent is inspired by the flexbox model of the web and can easily be used to implement many layouts.
比如上文demo中的FooterComponent可以复用在别的组件里。
CKFlexboxComponent是类似于flexbox的Component。
1.2、Components 优缺点
Strengths 优点
Simple and Declarative 简单和声明式: 比较类似 React.
Scroll Performance 滚动流畅: 布局都在后台线程,保证了主线程的流畅度。
View Recycling 视图复用
Composability 组合式使用:Component可以组合起来使用
Considerations 可改进的地方
- 列表式的界面支持比较好,不是列表式的界面支持不够理想。
- ComponentKit is fully native and compiled. 全Native,不支持跨端。
- ComponentKit 基于 Objective-C++. 不支持Swift,有学习成本。
二、相关API
2.1 Component类(避免直接继承CKComponent类)
@interface CKComponent : NSObject
/** Returns a new component. */
+ (instancetype)newWithView:(const CKComponentViewConfiguration &)view
size:(const CKComponentSize &)size;
@end
component 是不可变的,没有addSubcomponent方法 。
component可以在任意线程创建,因此所有的布局计算可以避免阻塞主线程。The Objective-C 使用 +newWith... 便利构造器保持代码可读
2.2 Composite Components(复合组件,可以直接继承此类)
避免直接继承 CKComponent 类。
可以直接继承CKCompositeComponent.
composite component包含了另一个component,对外隐藏了它的实现。
比如需要做一个分享按钮,可以制作一个ShareButtonComponent 复合组件,里面包含CKButtonComponent组件
@implementation ShareButtonComponent
+ (instancetype)newWithArticle:(ArticleModel *)article
{
return [super newWithComponent:
[CKButtonComponent
newWithAction:@selector(shareTapped)
options:{...}]];
}
- (void)shareTapped
{
// Share the article
}
@end
2.3 Views(视图)
使用 newWithView:size: class method:来创建component组件
+ (instancetype)newWithView:(const CKComponentViewConfiguration &)view
size:(const CKComponentSize &)size;
关于CKComponentViewConfiguration
struct CKComponentViewConfiguration {
CKComponentViewClass viewClass;//使用[UIImageView class] 或者 [UIButton class]
std::unordered_map<CKComponentViewAttribute, id> attributes;//属性map
};
使用newWithView
[CKComponent
newWithView:{
[UIImageView class],
{
{@selector(setImage:), image},
{@selector(setContentMode:), @(UIViewContentModeCenter)} // Wrapping into an NSNumber
}
}
size:{image.size.width, image.size.height}];
ComponentKit 会做如下的事情:
1.当component被挂载时候自动创建或复用 UIImageView
2.自动调用 setImage: and setContentMode: 使用给定的值
3.如果更新view tree时候value没有变化就跳过不调用setImage: or setContentMode:方法
2.4 Layout(布局)
UIView 实例在属性里面存储 position 和 size 信息。
当约束条件变化的时候Core Animation 通过调用layoutSubviews来更新这些属性。
CKComponent 实例并不存储position 和 size 信息。
ComponentKit 使用给定的约束,调用 layoutThatFits: 方法 ,
component 会返回 CKComponentLayout,包含自身的size和children component的size和position信息
struct CKComponentLayout {
CKComponent *component;
CGSize size;
std::vector<CKComponentLayoutChild> children;
};
struct CKComponentLayoutChild {
CGPoint position;
CKComponentLayout layout;
};
Layout Components
CKFlexboxComponent
基于简化版的 CSS flexbox.
允许垂直或者水平排列元素,各种对齐等。CKInsetComponent
应用内边距的布局CKBackgroundLayoutComponent
背景布局CKOverlayLayoutComponent
覆盖布局CKCenterLayoutComponent
中心布局CKRatioLayoutComponent
比例布局CKStaticLayoutComponent
固定布局
2.5 Responder Chain(响应者链)
- component的下一级响应者是它自己的 controller(如果有的话)
- component的 controller 的下一级响应者是component的父级component.
- 如果 component 没有controller, 它的下一级响应者是自己的父级component
- 根 component的下一级响应者是它所被添加的view
- 一般来说 view的下一级响应者是它的superview.
- 最终,view会找到和component层级结构一样的根view
- 如果要使用CKComponentActionSend,可以手动桥接view responder chain 和 the component responder chain。
注意component 并不是UIResponder的子类,不能成为 first responder.
但是component也是实现了nextResponder 和 targetForAction:withSender:方法.
Tap点击的实现
使用CKComponentActionAttribute
在 UIControl
上实现Tap点击
@implementation SomeComponent
+ (instancetype)new
{
return [self newWithView:{
[UIButton class],
{CKComponentActionAttribute(@selector(didTapButton))}
}];
}
- (void)didTapButton
{
// Aha! The button has been tapped.
}
@end
手势的实现
使用CKComponentTapGestureAttribute
可以在任何UIView
上实现
@implementation SomeComponent
+ (instancetype)new
{
return [self newWithView:{
[UIView class],
{CKComponentTapGestureAttribute(@selector(didTapView))}
}];
}
- (void)didTapView
{
// The view has been tapped.
}
@end
2.6 Actions(子component和父component通信)
一般来说子components 需要和.父component进行通信。
比如一个按钮component需要告诉父component它被点击了。
Component actions 可以实现这个目的。
Component Actions是什么?
CKAction<T...>
是 Objective-C++ 类,包含 一个 SEL
(Objective-C的方法名), 和一个 target.
CKAction<T...>
允许你指定参数传给指定方法。
CKAction
的 send
方法可以带着发送者component和参数传递给receiver
由于历史原因,CKComponentActionSend
可带action,sender,和一个可选对象。
循着响应者链,找到一个响应者响应对应的方法,把参数传给它。
CKComponentActionSend
只能在主线程被调用!
@implementation SampleComponent
+ (instancetype)new
{
CKComponentScope scope(self);
return [super newWithComponent:
[CKButtonComponent
newWithAction:{scope, @selector(someAction:event:)}
options:{}]];
}
- (void)someAction:(CKButtonComponent *)sender event:(UIEvent *)event
{
// Do something
}
@end
@implementation SampleOtherComponentThatDoesntCareAboutEvents
+ (instancetype)new
{
CKComponentScope scope(self);
return [super newWithComponent:
[CKButtonComponent
newWithAction:{scope, @selector(someAction:)}
options:{}]];
}
- (void)someAction:(CKButtonComponent *)sender
{
// Do something
}
@end
@implementation SampleOtherComponentThatDoesntCareAboutAnyParameters
+ (instancetype)new
{
CKComponentScope scope(self);
return [super newWithComponent:
[CKButtonComponent
newWithAction:{scope, @selector(someAction)}
options:{}]];
}
- (void)someAction
{
// We don't take any arguments in this example.
}
@end
@interface SampleControllerDelegatingComponentController : CKComponentController
/** Component actions may be implemented either on the component, or the controller for that component. */
- (void)someAction;
@end
@implementation SampleControllerDelegatingComponent
+ (instancetype)new
{
CKComponentScope scope(self);
return [super newWithComponent:
[CKButtonComponent
newWithAction:{scope, @selector(someAction)}
options:{}]];
}
@end
@implementation SampleControllerDelegatingComponentController
- (void)someAction
{
// Do something
}
@end
如何传递Action
简单规则: 方法应该在它们被引用的地方在一个文件中
下面的例子父component和子component耦合比较厉害,如果别的component想要使用子component,或者父类改了方法名,运行的时候就会崩溃。
//有隐患
@implementation ParentComponent
+ (instancetype)new
{
return [super newWithComponent:[ChildComponent new]];
}
- (void)someAction:(CKComponent *)sender
{
// Do something
}
@end
@implementation ChildComponent
+ (instancetype)new
{
return [super newWithComponent:
[CKButtonComponent
newWithAction:@selector(someAction:)]];
}
@end
从父component传递方法到子component,子component只知道需要一个action,耦合比较小。
//正确的例子
@implementation ParentComponent
+ (instancetype)new
{
CKComponentScope scope(self);
return [super newWithComponent:
[ChildComponent
newWithAction:{scope, @selector(someAction:)}]];
}
- (void)someAction:(CKComponent *)sender
{
// Do something
}
@end
@implementation ChildComponent
+ (instancetype)newWithAction:(CKTypedComponentAction<>)action
{
return [super newWithComponent:
[CKButtonComponent
newWithAction:action]];
}
@end
2.7 State(对应React的State)
ComponentKit是受 React启发的.
React components 拥有下面两个元素
props: 从parent传过来。在ComponentKit中类似
+new
方法传给子component的参数state: 父component不用关心,这是子component内部实现。
在 Thinking in React 有相关的讨论。
CKComponent 的 state.
@interface CKComponent
- (void)updateState:(id (^)(id))updateBlock mode:(CKUpdateMode)mode;
@end
继续阅读... 展开的demo
#import "CKComponentSubclass.h" // import to expose updateState:
@implementation MessageComponent
+ (id)initialState
{
return @NO;
}
+ (instancetype)newWithMessage:(NSAttributedString *)message
{
CKComponentScope scope(self);
NSNumber *state = scope.state();
return [super newWithComponent:
[CKTextComponent
newWithAttributes:{
.attributedString = message,
.maximumNumberOfLines = [state boolValue] ? 0 : 5,
}
viewAttributes:{}
accessibilityContext:{}]];
}
- (void)didTapContinueReading
{
[self updateState:^(id oldState){ return @YES; } mode:CKUpdateModeAsynchronous];
}
@end
2.8 Scopes(用来作为component的id)
如下图,如果item没有id 就没法进行区分
如下图,每个item都有自己的id,这样可以具体区分
Scopes给component提供了一个永久的身份标识符。不管component被创建过多少次,scope都是一样的。
- component 有 state,那么必须要定义一个 scope
- component 有component controller,那么必须要定义一个 scope
- component的子component有 state 或者 component controllers 那么必须要定义一个 scope
定义 Scope
使用 CKComponentScope
在 +new
方法中
@implementation ListItemComponent
+ (instancetype)newWithListItem:(ListItem *)listItem
{
// Defines a scope that is uniquely identified by the component's class (i.e. ListItemComponent) and the provided identifier.
CKComponentScope scope(self, listItem.uniqueID);
const auto c = /* ... */;
return [super newWithComponent:c];
}
@end
component 没有 model object
@implementation ListComponent
+ (instancetype)newWithList:(List *)list
{
// Defines a scope that is uniquely identified by the component's class (i.e. ListComponent).
CKComponentScope scope(self);
const auto c = /* ... */;
return [super newWithComponent:c];
}
@end