ComponentKit/Core/CKComponent.mm (449 lines of code) (raw):
/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
#import "CKComponent.h"
#import "CKComponentControllerInternal.h"
#import "CKComponentInternal.h"
#import "CKComponentSubclass.h"
#import <ComponentKit/CKAnalyticsListener.h>
#import <RenderCore/RCAssert.h>
#import <ComponentKit/RCArgumentPrecondition.h>
#import <ComponentKit/CKBuildComponent.h>
#import <ComponentKit/CKComponentScopeEnumeratorProvider.h>
#import <ComponentKit/CKComponentContextHelper.h>
#import <ComponentKit/CKFatal.h>
#import <ComponentKit/CKInternalHelpers.h>
#import <ComponentKit/CKMacros.h>
#import <ComponentKit/CKTreeNode.h>
#import <ComponentKit/CKInternalHelpers.h>
#import <ComponentKit/CKWeakObjectContainer.h>
#import <ComponentKit/RCComponentDescriptionHelper.h>
#import <ComponentKit/CKMountableHelpers.h>
#import <ComponentKit/RCComponentSize_SwiftBridge+Internal.h>
#import <ComponentKit/CKComponentViewConfiguration_SwiftBridge+Internal.h>
#import "CKComponent+LayoutLifecycle.h"
#import "CKComponent+UIView.h"
#import "CKComponentAccessibility.h"
#import "CKAccessibilityAggregation.h"
#import "CKComponentAnimation.h"
#import "CKComponentController.h"
#import "CKComponentDebugController.h"
#import "CKComponentLayout.h"
#import "CKComponentScopeHandle.h"
#import "CKComponentViewConfiguration.h"
#import "CKMountAnimationGuard.h"
#import "ComponentLayoutContext.h"
#import "CKThreadLocalComponentScope.h"
#import "CKComponentScopeRoot.h"
#import "CKRenderHelpers.h"
#import "CKComponentCreationValidation.h"
#import "CKSizeAssert.h"
CGFloat const kCKComponentParentDimensionUndefined = NAN;
CGSize const kCKComponentParentSizeUndefined = {kCKComponentParentDimensionUndefined, kCKComponentParentDimensionUndefined};
@implementation CKComponent
{
CKTreeNode *_treeNode;
CKComponentViewConfiguration _viewConfiguration;
/** Only non-null while mounted. */
std::unique_ptr<CKMountInfo> _mountInfo;
}
#if DEBUG
+ (void)initialize
{
if (self != [CKComponent class]) {
RCAssert(!CKSubclassOverridesInstanceMethod([CKComponent class], self, @selector(layoutThatFits:parentSize:)),
@"%@ overrides -layoutThatFits:parentSize: which is not allowed. Override -computeLayoutThatFits: "
"or -computeLayoutThatFits:restrictedToSize:relativeToParentSize: instead.",
self);
}
}
#endif
+ (instancetype)newWithView:(const CKComponentViewConfiguration &)view size:(const RCComponentSize &)size
{
return [[self alloc] initWithView:view size:size];
}
- (instancetype)init
{
return [self initWithView:{} size:{}];
}
- (instancetype)initWithSwiftView:(CKComponentViewConfiguration_SwiftBridge *)swiftView
swiftSize:(RCComponentSize_SwiftBridge *)swiftSize
{
const auto view = swiftView != nil ? swiftView.viewConfig : CKComponentViewConfiguration{};
const auto size = swiftSize != nil ? swiftSize.componentSize : RCComponentSize{};
return [self initWithView:view size:size];
}
- (instancetype)initWithView:(const CKComponentViewConfiguration &)view
size:(const RCComponentSize &)size
{
if (self = [super init]) {
_viewConfiguration = view;
_size = size;
[self didFinishComponentInitialization];
}
return self;
}
- (void)dealloc
{
// Since the component and its view hold strong references to each other, this should never happen!
RCAssert(_mountInfo == nullptr, @"%@ must be unmounted before dealloc", self.className);
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<%s: %p>", self.typeName, self];
}
- (void)didFinishComponentInitialization
{
CKValidateComponentCreation();
_treeNode = CK::TreeNode::nodeForComponent(self);
}
- (BOOL)hasAnimations
{
// NOTE: The default implementation is expected to be class-static. Check -[CKRenderComponent requiresScopeHandle] for more context.
return CKSubclassOverridesInstanceMethod([CKComponent class], [self class], @selector(animationsFromPreviousComponent:));
}
- (BOOL)hasBoundsAnimations
{
// NOTE: The default implementation is expected to be class-static. Check -[CKRenderComponent requiresScopeHandle] for more context.
return CKSubclassOverridesInstanceMethod([CKComponent class], [self class], @selector(boundsAnimationFromPreviousComponent:));
}
- (BOOL)hasInitialMountAnimations
{
// NOTE: The default implementation is expected to be class-static. Check -[CKRenderComponent requiresScopeHandle] for more context.
return CKSubclassOverridesInstanceMethod([CKComponent class], [self class], @selector(animationsOnInitialMount));
}
- (BOOL)hasFinalUnmountAnimations
{
// NOTE: The default implementation is expected to be class-static. Check -[CKRenderComponent requiresScopeHandle] for more context.
return CKSubclassOverridesInstanceMethod([CKComponent class], [self class], @selector(animationsOnFinalUnmount));
}
- (BOOL)controllerOverridesDidPrepareLayout
{
const Class<CKComponentControllerProtocol> controllerClass = [[self class] controllerClass];
return CKSubclassOverridesInstanceMethod([CKComponentController class],
controllerClass,
@selector(didPrepareLayout:forComponent:));
}
- (id<CKComponentControllerProtocol>)buildController
{
return [[(Class)[self.class controllerClass] alloc] initWithComponent:self];
}
- (const CKComponentViewConfiguration &)viewConfiguration
{
return _viewConfiguration;
}
- (void)setViewConfiguration:(const CKComponentViewConfiguration &)viewConfiguration
{
RCAssert(_viewConfiguration.isDefaultConfiguration(), @"Component(%@) already has '_viewConfiguration'.", self);
_viewConfiguration = viewConfiguration;
}
- (CKComponentViewContext)viewContext
{
RCAssertMainThread();
return _mountInfo ? _mountInfo->viewContext : CKComponentViewContext();
}
- (void)acquireTreeNode:(CKTreeNode *)treeNode
{
_treeNode = treeNode;
}
- (CKTreeNode *)treeNode
{
return _treeNode;
}
#pragma mark - ComponentTree
- (void)buildComponentTree:(CKTreeNode *)parent
previousParent:(CKTreeNode *_Nullable)previousParent
params:(const CKBuildComponentTreeParams &)params
parentHasStateUpdate:(BOOL)parentHasStateUpdate
{
CKRender::ComponentTree::Iterable::build(self, parent, previousParent, params, parentHasStateUpdate);
}
#pragma mark - Mounting and Unmounting
- (CK::Component::MountResult)mountInContext:(const CK::Component::MountContext &)context
layout:(const RCLayout &)layout
supercomponent:(CKComponent *)supercomponent
{
RCCAssertWithCategory([NSThread isMainThread], self.className, @"This method must be called on the main thread");
// Taking a const ref to a temporary extends the lifetime of the temporary to the lifetime of the const ref
const CKComponentViewConfiguration &viewConfiguration =
(CK::Component::Accessibility::IsAccessibilityEnabled() || CKReadGlobalConfig().alwaysMountViewForAccessibityContextComponent)
? CK::Component::Accessibility::AccessibleViewConfiguration(_viewConfiguration)
: _viewConfiguration;
CKComponentController *controller = _treeNode.scopeHandle.controller;
[controller componentWillMount:self];
const CK::Component::MountContext &effectiveContext = [CKComponentDebugController debugMode]
? CKDebugMountContext([self class], context, _viewConfiguration, layout.size) : context;
return CKPerformMount(_mountInfo, layout, viewConfiguration, effectiveContext, supercomponent, &didAcquireView, &willRelinquishView, &blockAnimationIfNeeded, &unblockAnimation);
}
__attribute__((objc_externally_retained)) // parameters are retained by the caller
static void didAcquireView(id<CKMountable> mountable, UIView *view)
{
CKComponent *component = (CKComponent *)mountable;
CKComponentController *controller = component.treeNode.scopeHandle.controller;
[controller component:component didAcquireView:view];
}
__attribute__((objc_externally_retained)) // parameters are retained by the caller
static void willRelinquishView(id<CKMountable> mountable, UIView *view)
{
CKComponent *component = (CKComponent *)mountable;
[(CKComponentController *)component.treeNode.scopeHandle.controller component:component willRelinquishView:view];
}
- (NSString *)backtraceStackDescription
{
return RCComponentBacktraceStackDescription(RCComponentGenerateBacktrace(self));
}
- (void)unmount
{
RCAssertMainThread();
if (_mountInfo != nullptr) {
CKComponentController *const controller = _treeNode.scopeHandle.controller;
[controller componentWillUnmount:self];
CKPerformUnmount(_mountInfo, self, &willRelinquishView);
[controller componentDidUnmount:self];
}
}
- (void)childrenDidMount
{
[(CKComponentController *)_treeNode.scopeHandle.controller componentDidMount:self];
}
#pragma mark - Animation
- (std::vector<CKComponentAnimation>)animationsOnInitialMount
{
return {};
}
- (std::vector<CKComponentAnimation>)animationsFromPreviousComponent:(CKComponent *)previousComponent
{
return {};
}
- (CKComponentBoundsAnimation)boundsAnimationFromPreviousComponent:(CKComponent *)previousComponent
{
return {};
}
- (std::vector<CKComponentFinalUnmountAnimation>)animationsOnFinalUnmount
{
return {};
}
- (UIView *)viewForAnimation
{
RCAssertMainThread();
return _mountInfo ? _mountInfo->view : nil;
}
__attribute__((objc_externally_retained)) // parameters are retained by the caller
static BOOL blockAnimationIfNeeded(id<CKMountable> oldComponent, id<CKMountable> newComponent, const CK::Component::MountContext &ctx, const CKViewConfiguration &viewConfig)
{
return CKMountAnimationGuard::blockAnimationsIfNeeded(oldComponent, newComponent, ctx, viewConfig);
}
__attribute__((objc_externally_retained)) // parameters are retained by the caller
static void unblockAnimation()
{
CKMountAnimationGuard::unblockAnimation();
}
#pragma mark - Layout
#if CK_ASSERTIONS_ENABLED
- (void)_validate_layoutThatFits:(const CKSizeRange &)constrainedSize layout:(const RCLayout &)layout parentSize:(const CGSize &)parentSize
{
// If this component has children in its layout, this means that it's not a real leaf component.
// As a result, the infrastructure won't call `buildComponentTree:` on the component's children and can affect the render process.
if (self.superclass == [CKComponent class] && layout.children != nullptr && layout.children->size() > 0) {
const auto overridesIterableMethods =
CKSubclassOverridesInstanceMethod([CKComponent class], self.class, @selector(childAtIndex:)) &&
CKSubclassOverridesInstanceMethod([CKComponent class], self.class, @selector(numberOfChildren));
RCAssertWithCategory(overridesIterableMethods,
self.className,
@"%@ is subclassing CKComponent directly, you need to subclass CKLayoutComponent instead. "
"Context: we’re phasing out CKComponent subclasses for in favor of CKLayoutComponent subclasses. "
"While this is still kinda OK for leaf components, things start to break when you introduce a CKComponent subclass with children.",
self.className);
}
RCAssert(layout.component == self, @"Layout computed by %@ should return self as component, but returned %@",
self.className, layout.component.className);
CKAssertResolvedSize(_size, parentSize);
CKSizeRange resolvedRange __attribute__((unused)) = constrainedSize.intersect(_size.resolve(parentSize));
CKAssertSizeRange(resolvedRange);
RCAssertWithCategory(CKIsGreaterThanOrEqualWithTolerance(resolvedRange.max.width, layout.size.width)
&& CKIsGreaterThanOrEqualWithTolerance(layout.size.width, resolvedRange.min.width)
&& CKIsGreaterThanOrEqualWithTolerance(resolvedRange.max.height,layout.size.height)
&& CKIsGreaterThanOrEqualWithTolerance(layout.size.height,resolvedRange.min.height),
self.className,
@"Computed size %@ for %@ does not fall within constrained size %@\n%@",
NSStringFromCGSize(layout.size), self.className, resolvedRange.description(),
CK::Component::LayoutContext::currentStackDescription());
}
#endif
__attribute__((objc_externally_retained)) // parameters are retained by the caller
void CKComponentWillLayout(CKComponent *component, CKSizeRange constrainedSize, CGSize parentSize, id<CKSystraceListener> systraceListener)
{
CKAssertSizeRange(constrainedSize);
[systraceListener willLayoutComponent:component];
}
- (RCLayout)layoutThatFits:(CKSizeRange)constrainedSize parentSize:(CGSize)parentSize
{
#if CK_ASSERTIONS_ENABLED
const CKComponentContext<CKComponentCreationValidationContext> validationContext([[CKComponentCreationValidationContext alloc] initWithSource:CKComponentCreationValidationSourceLayout]);
#endif
CK::Component::LayoutContext context(self, constrainedSize);
auto const systraceListener = context.systraceListener;
CKComponentWillLayout(self, constrainedSize, parentSize, systraceListener);
RCLayout layout = [self computeLayoutThatFits:constrainedSize
restrictedToSize:_size
relativeToParentSize:parentSize];
CKComponentDidLayout(self, layout, constrainedSize, parentSize, systraceListener);
return layout;
}
__attribute__((objc_externally_retained)) // parameters are retained by the caller
void CKComponentDidLayout(CKComponent *component, const RCLayout &layout, CKSizeRange constrainedSize, CGSize parentSize, id<CKSystraceListener> systraceListener)
{
#if CK_ASSERTIONS_ENABLED
[component _validate_layoutThatFits:constrainedSize layout:layout parentSize:parentSize];
#endif
[systraceListener didLayoutComponent:component];
}
- (RCLayout)computeLayoutThatFits:(CKSizeRange)constrainedSize
restrictedToSize:(const RCComponentSize &)size
relativeToParentSize:(CGSize)parentSize
{
CKAssertResolvedSize(_size, parentSize);
CKSizeRange resolvedRange = constrainedSize.intersect(_size.resolve(parentSize));
return [self computeLayoutThatFits:resolvedRange];
}
- (RCLayout)computeLayoutThatFits:(CKSizeRange)constrainedSize
{
return {self, constrainedSize.min};
}
#pragma mark - Responder
- (id)nextResponder
{
return _treeNode.scopeHandle.controller ?: [self nextResponderAfterController];
}
- (id)nextResponderAfterController
{
RCAssertMainThread();
if (_mountInfo && _mountInfo->supercomponent) {
return _mountInfo->supercomponent;
}
return [self rootComponentMountedView];
}
- (id)targetForAction:(SEL)action withSender:(id)sender
{
return [self canPerformAction:action withSender:sender] ? self : [[self nextResponder] targetForAction:action withSender:sender];
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
return [self respondsToSelector:action];
}
// Because only the root component in each mounted tree will have a non-nil rootComponentMountedView, we use Obj-C
// associated objects to save the memory overhead of storing such a pointer on every single CKComponent instance in
// the app. With tens of thousands of component instances, this adds up to several KB.
static void *kRootComponentMountedViewKey = &kRootComponentMountedViewKey;
- (void)setRootComponentMountedView:(UIView *)rootComponentMountedView
{
ck_objc_setNonatomicAssociatedWeakObject(self, kRootComponentMountedViewKey, rootComponentMountedView);
}
- (UIView *)rootComponentMountedView
{
return ck_objc_getAssociatedWeakObject(self, kRootComponentMountedViewKey);
}
#pragma mark - CKMountable
- (unsigned int)numberOfChildren
{
return 0;
}
- (id<CKMountable>)childAtIndex:(unsigned int)index
{
return nil;
}
#pragma mark - CKComponentProtocol
+ (RCComponentCoalescingMode)coalescingMode {
return RCComponentCoalescingModeNone;
}
+ (Class<CKComponentControllerProtocol>)controllerClass
{
const Class componentClass = self;
if (componentClass == [CKComponent class]) {
return Nil; // Don't create root CKComponentControllers as it does nothing interesting.
}
RCAssertWithCategory(!NSClassFromString([NSStringFromClass(componentClass) stringByAppendingString:@"Controller"]),
[self class], @"Should override + (Class<CKComponentControllerProtocol>)controllerClass to return its controllerClass");
return Nil;
}
+ (id)initialState
{
return nil;
}
#pragma mark - State
- (void)updateState:(id (^)(id))updateBlock mode:(CKUpdateMode)mode
{
RCAssertWithCategory(_treeNode.scopeHandle != nil, self.className, @"A component without state cannot update its state.");
RCAssertWithCategory(updateBlock != nil, self.className, @"Cannot enqueue component state modification with a nil update block.");
[_treeNode.scopeHandle updateState:updateBlock metadata:{} mode:mode];
}
- (CKComponentController *)controller
{
return _treeNode.scopeHandle.controller;
}
- (id<NSObject>)uniqueIdentifier
{
return _treeNode ? @(_treeNode.scopeHandle.globalIdentifier) : nil;
}
-(id<CKComponentScopeEnumeratorProvider>)scopeEnumeratorProvider
{
CKThreadLocalComponentScope *currentScope = CKThreadLocalComponentScope::currentScope();
if (currentScope == nullptr) {
return nil;
}
return currentScope->newScopeRoot;
}
- (UIView *)mountedView
{
return _mountInfo ? _mountInfo->view : nil;
}
- (CKMountInfo)mountInfo
{
if (_mountInfo) {
return *_mountInfo.get();
}
return {};
}
- (id)state
{
return _treeNode.scopeHandle.state;
}
- (NSString *)className
{
return [NSString stringWithUTF8String:self.typeName];
}
- (const char *)typeName
{
// Coalesced component require their type names to differ from their class names.
// https://fburl.com/codesearch/tjepeywh
return class_getName(self.class);
}
- (NSDictionary<NSString *, id> *)metadata
{
return nil;
}
// This method can be used to override what accessible elements are
// provided by the component. Very similar to UIKit accessibilityElements.
#pragma mark - Accessibility
- (NSArray<NSObject *> *)accessibilityChildren
{
const auto numChildren = [self numberOfChildren];
if (numChildren == 0) {
return nil;
}
NSMutableArray *const contents = [NSMutableArray arrayWithCapacity:numChildren];
for(unsigned int i = 0; i < numChildren; i++) {
const auto child = [self childAtIndex:i];
if (child != nil) {
[contents addObject:child];
}
}
return contents;
}
- (CGRect)accessibilityFrame {
if (_mountInfo == nullptr) {
return CGRectNull;
}
return UIAccessibilityConvertFrameToScreenCoordinates(_mountInfo->viewContext.frame, _mountInfo->viewContext.view);
}
- (void)setAccessibilityElements:(NSArray *)accessibilityElements {
RCFailAssert(@"Attempt to setAccessibilityElements in %@", NSStringFromClass([self class]));
}
// In base Component we rely on the view to provide the accessible elements:
// If the component itself has isAccessibilityElement == NO and
// 1) It has a mounted view that has accessibilityElements
// 2) It has a mounted view that is an accessibile element
- (NSArray<NSObject *> *)accessibilityElements
{
const auto mountedView = self.mountedView;
if ([[mountedView accessibilityElements] count] > 0
|| [mountedView accessibilityElementCount] > 0
|| [mountedView isAccessibilityElement]) {
if ([mountedView isAccessibilityElement]) {
return @[self.mountedView];
} else if (![mountedView isAccessibilityElement] && CKAccessibilityAggregationIsActive()) {
return [self accessibilityChildren];
} else {
return @[self.mountedView];
}
}
return [self accessibilityChildren];
}
@end