Components are immutable objects that specify how to configure views.
A component is a fixed description that can be used to paint a view but that is not a view itself.
What’s Special ?
Declarative: You directly declare the hierarchy of the component from up to down. It’s different from what we used to layout views. It turns to layout your children, not layout yourself. Functional: Data flows in one direction. Methods take data models and return totally immutable 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. Cause of one direction data flow, you can focus on how to display the model and simply write unit test for the component. Composable: A component is ofen composed of other components, building up a component hierarchy that describes a user interface. You can esaily reuse a component just in one line.
What’s Limitation ?
ListViews: ComponetKit is optimized to work well with a UICollectionView, and with more effort for UITableView. It’s not efficient in other situation. Gestures: ComponetKit can simply handle tap gesture but it’s hard to implement more complicated gestures like panning, pinching, swiping and so on. Animation: CKComponent forms a declarative mapping from model to view configuration. Animations should be handled fullyi n the view layer instead of inside Components. LearningCost: ComponetKit is developed in C++. It will be difficult when you dig into if you are not familiar with C++. You have to get used to Reactive Philosophy.
How to Define a Component ?
Avoid subclassing CKComponent directly. Instead, subclassing CKCompositeComponent.
A “composite component” simply wraps another component, hiding its implementation details from the outside world.
Steps to define a component:
1. Define the model;
2. Define a class inherit from CKCompositeComponent, and change the suffix from .m to .mm, because ComponetKit is developed in C++;
3. Add a constructor method use +newWith...;
4. Implement the +newWith... method by directly declare the hierarchy of the component.
5. Anywhere using component, the suffix of the implementation file should also be .mm.
An example to implement a custom VPADateComponent.
@implementationVPADateComponent// Data flows in one direction. Component is immutable.+(instancetype)newWithDateModel:(VPADateModel*)dateModel{if(!dateModel){returnnil;}// Directly declare the hierarchy of the component from up to down.// VPADateComponent is composed of CKInsetComponent, CKCenterLayoutComponent and CKLabelComponent and they are alse reuseable.return[supernewWithComponent:[CKInsetComponentnewWithInsets:{.left=10,.top=10,.right=10,.bottom=10}component:[CKCenterLayoutComponentnewWithCenteringOptions:CKCenterLayoutComponentCenteringXYsizingOptions:CKCenterLayoutComponentSizingOptionDefaultchild:[CKLabelComponentnewWithLabelAttributes:{.string=formatDate(dateModel.date),.font=[UIFontsystemFontOfSize:12],.color=[UIColorwhiteColor]}viewAttributes:{{@selector(setBackgroundColor:),[UIColorclearColor]},{@selector(setUserInteractionEnabled:),@NO}}size:{}]size:{}]]];}staticNSString*formatDate(NSDate*date){NSDateFormatter*formatter=[[NSDateFormatteralloc]init];formatter.dateFormat=@"yyyy-MM-dd hh:mm:ss";return[formatterstringFromDate:date];}@end
Common UI Components
CKComponent
The base class of all Components. You can never directly subclass CKComponent.
Constructor:
123456
/** @param view A struct describing the view for this component. Pass {} to specify that no view should be created. @param size A size constraint that should apply to this component. Pass {} to specify no size constraint. */+(instancetype)newWithView:(constCKComponentViewConfiguration&)viewsize:(constCKComponentSize&)size;
Exmaple:
12345678910111213141516171819
CKComponent*component=[CKComponentnewWithView:{[UIViewclass],{// set common properties of the view{@selector(setBackgroundColor:),[UIColorredColor]},// set user interaction{@selector(setUserInteractionEnabled:),@YES},// set tap gesture{CKComponentTapGestureAttribute(@selector(tapAction))},// set layer properties{CKComponentViewAttribute::LayerAttribute(@selector(setCornerRadius:)),@10.0}}}size:{50,50}];-(void)tapAction{// add tap action here}
CKCompositeComponent
CKCompositeComponent allows you to hide your implementation details and avoid subclassing layout components like CKStackLayoutComponent. In almost all cases, you should subclass CKCompositeComponent instead of subclassing any other class directly. This hides your layout implementation details from the outside world.
CKLabelComponent
CKLabelComponent is a simplified text component that just displays NSStrings.
Constructor:
12345678
/** @param attributes The content and styling information for the text component. @param viewAttributes These are passed directly to CKTextComponent and its backing view. @param size The component size or {} for the default which is for the layout to take the maximum space available. */+(instancetype)newWithLabelAttributes:(constCKLabelAttributes&)attributesviewAttributes:(constCKViewComponentAttributeValueMap&)viewAttributessize:(constCKComponentSize&)size;
/** This component chooses the smallest size within its SizeRange that will fit its content. If its max size is smaller than the size required to fit its content, it will be truncated. */+(instancetype)newWithTitles:(CKContainerWrapper<std::unordered_map<UIControlState,NSString*>>&&)titlestitleColors:(CKContainerWrapper<std::unordered_map<UIControlState,UIColor*>>&&)titleColorsimages:(CKContainerWrapper<std::unordered_map<UIControlState,UIImage*>>&&)imagesbackgroundImages:(CKContainerWrapper<std::unordered_map<UIControlState,UIImage*>>&&)backgroundImagestitleFont:(UIFont*)titleFontselected:(BOOL)selectedenabled:(BOOL)enabledaction:(constCKTypedComponentAction<UIEvent*>&)actionsize:(constCKComponentSize&)sizeaccessibilityConfiguration:(CKButtonComponentAccessibilityConfiguration)accessibilityConfiguration;
Example:
12345678910111213141516171819202122
CKButtonComponent*buttonComponent=[CKButtonComponentnewWithTitles:{{UIControlStateNormal,@"button"}}titleColors:{{UIControlStateNormal,[UIColorwhiteColor]}}images:{}backgroundImages:{}titleFont:[UIFontsystemFontOfSize:20]selected:NOenabled:YESaction:{scope,@selector(tapAction)}size:{}attributes:{{@selector(setBackgroundColor:),[UIColoryellowColor]}}accessibilityConfiguration:{}]-(void)tapAction{// add tap action here}
CKImageComponent
A component that displays an image using UIImageView.
Constructor:
123456
/** Uses a static layout with the given image size and applies additional attributes. */+(instancetype)newWithImage:(UIImage*)imageattributes:(constCKViewComponentAttributeValueMap&)attributessize:(constCKComponentSize&)size;
ComponentKit includes a library of components that can be composed to declaratively specify a layout.
CKStackLayoutComponent
A simple layout component that stacks a list of children vertically or horizontally.
Constructor:
12
/** @param view A view configuration, or {} for no view. @param size A size, or {} for the default size. @param style Specifies how children are laid out. @param children A vector of children components. */+(instancetype)newWithView:(constCKComponentViewConfiguration&)viewsize:(constCKComponentSize&)sizestyle:(constCKStackLayoutComponentStyle&)stylechildren:(CKContainerWrapper<std::vector<CKStackLayoutComponentChild>>&&)children;
A component that wraps another component, applying insets around it.
If the child component has a size specified as a percentage, the percentage is resolved against this component’s parent size after applying insets.
CKInsetComponent’s child behaves similarly to “box-sizing: border-box”.
Constructor:
12345678
/** @param view Passed to CKComponent +newWithView:size:. The view, if any, will extend outside the insets. @param insets The amount of space to inset on each side. @param component The wrapped child component to inset. If nil, this method returns nil. */+(instancetype)newWithView:(constCKComponentViewConfiguration&)viewinsets:(UIEdgeInsets)insetscomponent:(CKComponent*)component;
Lays out a single child component and position it so that it is centered into the layout bounds.
Constructor:
12345678910
/** @param centeringOptions, see CKCenterLayoutComponentCenteringOptions. @param sizingOptions, see CKCenterLayoutComponentSizingOptions. @param child The child to center. @param size The component size or {} for the default which is for the layout to take the maximum space available. */+(instancetype)newWithCenteringOptions:(CKCenterLayoutComponentCenteringOptions)centeringOptionssizingOptions:(CKCenterLayoutComponentSizingOptions)sizingOptionschild:(CKComponent*)childsize:(constCKComponentSize&)size;
Lays out a single child component, then lays out a background component behind it stretched to its size.
Constructor:
1234567
/** @param component A child that is laid out to determine the size of this component. If this is nil, then this method returns nil. @param background A child that is laid out behind it. May be nil, in which case the background is omitted. */+(instancetype)newWithComponent:(CKComponent*)componentbackground:(CKComponent*)background;
For when the content should respect a certain inherent ratio but can be scaled (think photos or videos).
The ratio passed is the ratio of height / width you expect.
Constructor:
The collection view in ComponentKit is really different from the collection view in UIKit.
The UIKit way to add content to a collection view is:
1. Tell the UICollectionView to add/insert/update items or sections.
2. Synchronously, the UICollectionView ask its data source for number of items, sections and layout info.
3. Depending on whether or not the updated index paths are visible the UICollectionView will Synchronously call cellForItemAtIndexPath:.
4. Finally, the data source returns a configured cell for this index path.
The ComponentKit uses an idiom that is more “reactive”:
1. Tell the CKCollectionViewTransactionalDataSource to add/insert/update items or sections.
2. Asynchronously, and in the background, computes the corresponding components.
3. When the computation is done, apply the changes to the UICollectionView.
ComponentKit really shines when used with a UICollectionView:
1. Automatic reuse. You just need to setup the components and ComponentKit will automaitc reuse and reconfiguration.
2. Flexible height. You don’t have to compute the height of each items yourself.
3. Scroll performance. ComponentKit addresses common scroll performance issues holistically.
4. Simple to use. You just need to care about two things, manipulating data and providing components.
NSMutableDictionary<NSIndexPath*,NewsModel*>*items=[NSMutableDictionarynew];for(NSIntegeri=0;i<50;i++){NewsModel*newsModel=[[NewsModelalloc]init];newsModel.title=[NSStringstringWithFormat:@"News Title: Title %ld",i];newsModel.category=@"科技";newsModel.updateTime=[NSDatedate];newsModel.source=@"网易新闻";[itemssetObject:newsModelforKey:[NSIndexPathindexPathForRow:iinSection:0]];}CKTransactionalComponentDataSourceChangeset*changeset=[[[CKTransactionalComponentDataSourceChangesetBuildertransactionalComponentDataSourceChangeset]withInsertedItems:items]build];[self.dataSourceapplyChangeset:changesetmode:CKUpdateModeAsynchronoususerInfo:nil];
/** If no children are flexible, how should this component justify its children in the available space? */typedefNS_ENUM(NSUInteger,CKStackLayoutJustifyContent){/** On overflow, children overflow out of this component's bounds on the right/bottom side. On underflow, children are left/top-aligned within this component's bounds. */// 左对齐CKStackLayoutJustifyContentStart,/** On overflow, children are centered and overflow on both sides. On underflow, children are centered within this component's bounds in the stacking direction. */// 居中CKStackLayoutJustifyContentCenter,/** On overflow, children overflow out of this component's bounds on the left/top side. On underflow, children are right/bottom-aligned within this component's bounds. */// 右对齐CKStackLayoutJustifyContentEnd,};
align-items:决定items在交叉轴上的对齐方式。
1234567891011121314
typedefNS_ENUM(NSUInteger,CKStackLayoutAlignItems){/** Align children to start of cross axis */// 交叉轴起点对齐CKStackLayoutAlignItemsStart,/** Align children with end of cross axis */CKStackLayoutAlignItemsEnd,// 交叉轴终点对齐/** Center children on cross axis */// 交叉轴居中对齐CKStackLayoutAlignItemsCenter,/** Expand children to fill cross axis */// 交叉轴方向拉伸CKStackLayoutAlignItemsStretch,};
/** Each child may override their parent stack's cross axis alignment. @see CKStackLayoutAlignItems */typedefNS_ENUM(NSUInteger,CKStackLayoutAlignSelf){/** Inherit alignment value from containing stack. */CKStackLayoutAlignSelfAuto,CKStackLayoutAlignSelfStart,CKStackLayoutAlignSelfEnd,CKStackLayoutAlignSelfCenter,CKStackLayoutAlignSelfStretch,};
FBSnapshotTestCase
FBSnapshotTestCase takes a configured UIView or CALayer and uses the renderInContext: method to get an image snapshot of its contents. It compares this snapshot to a “reference image” stored in your source code repository and fails the test if the two images don’t match.
It makes the comparison by drawing both the view or layer and the existing snapshot into two CGContextRefs and doing a memory comparison of them with the C function memcmp().
Features
It is esay to understand and visible and quickly.
Automatically names reference images on disk according to test class and selector.
Prints a descriptive error message to the console on failure.
Supply an optional “identifier” if you want to perform multiple snapshots in a single test method.
Support for UIView via FBSnapshotVerifyView.
Support for CALayer via FBSnapshotVerifyLayer.
Support for CKComponent via FBSnapshotVerifyComponent.
isDeviceAgnostic to allow appending the device model (iPhone, etc), OS version and screen size to the images (allowing to have multiple tests for the same «snapshot» for different OSs and devices).
Provide a diff image to show the difference when test failed.
It’s esay to test different state of the view.
Snapshot tests are run at the same time as the rest of your tests. They can mostly be run without pushing the view to the screen.
Snapshots give code reviews a narrative.
Snapshot tests are fast.
Disadvantages
Testing asynchronous code is hard.
Some components can be hard to test.
Apple’s OS patches can change the way their stock components are rendered.
Each snapshot is a PNG file stored in your repository, and together they average out at about 30-100kb per file.
Install
pod 'FBSnapshotTestCase'.
Define FB_REFERENCE_IMAGE_DIR and IMAGE_DIFF_DIR in scheme(Edit Scheme -> Run -> Arguments -> Environment Variables). FB_REFERENCE_IMAGE_DIR = $(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages IMAGE_DIFF_DIR = $(SOURCE_ROOT)/$(PROJECT_NAME)Tests/FailureDiffs
Create a snapshot test
Subclass FBSnapshotTestCase instead of XCTestCase.
From within your test, use FBSnapshotVerifyComponent.
Run the test once with self.recordMode = YES; in the test’s -setup method. (This creates the reference images on disk)
Run the test again with self.recordMode = NO;.
Notes
Your unit test must be an “application test”, not a “logic test.” (That is, it must be run within the Simulator so that it has access to UIKit.)