[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)
,指在木片、紙板、金屬或塑料等片材上刻劃出圖文,並挖空製成鏤空版,通過刷塗或噴塗方法使油墨透過通孔附著於承印物上。
一個元件通常由其他元件組合而成,元件的層級結構可以用來描述使用者介面。
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
複製程式碼