ComponentKit/HostingView/CKComponentHostingView.mm (309 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 "CKComponentHostingView.h" #import "CKComponentHostingViewInternal.h" #import <RenderCore/RCAssert.h> #import <ComponentKit/CKBlockSizeRangeProvider.h> #import <ComponentKit/CKGlobalConfig.h> #import <ComponentKit/CKMacros.h> #import <ComponentKit/CKOptional.h> #import <ComponentKit/CKRootTreeNode.h> #import <ComponentKit/CKComponentAccessibility.h> #import <algorithm> #import <vector> #import "CKAnimationApplicator.h" #import "CKBuildComponent.h" #import "CKComponentAnimation.h" #import "CKComponentController.h" #import "CKComponentDebugController.h" #import "CKComponentGenerator.h" #import "CKComponentHostingViewDelegate.h" #import "CKComponentLayout.h" #import "CKComponentRootViewInternal.h" #import "CKComponentScopeRoot.h" #import "CKComponentScopeRootFactory.h" #import "CKComponentSizeRangeProviding.h" #import "CKComponentSubclass.h" #import "CKComponentControllerEvents.h" #import "CKComponentControllerHelper.h" #import "CKComponentEvents.h" #import "CKComponentHostingContainerViewProvider.h" static auto nilProvider(id<NSObject>, id<NSObject>) -> CKComponent * { return nil; } @interface CKComponentHostingView () <CKComponentDebugReflowListener, CKComponentGeneratorDelegate> { CKComponentGenerator *_componentGenerator; CKComponentHostingContainerViewProvider *_containerViewProvider; CKComponent *_component; BOOL _componentNeedsUpdate; CK::Optional<CKComponentRootLayout> _mountedRootLayout; BOOL _scheduledAsynchronousComponentUpdate; BOOL _isSynchronouslyUpdatingComponent; BOOL _isMountingComponent; BOOL _allowTapPassthrough; CK::Optional<CGSize> _initialSize; } @end @implementation CKComponentHostingView #pragma mark - Lifecycle - (instancetype)initWithComponentProviderFunc:(CKComponentProviderFunc)componentProvider sizeRangeProvider:(id<CKComponentSizeRangeProviding>)sizeRangeProvider { return [self initWithComponentProviderFunc:componentProvider sizeRangeProvider:sizeRangeProvider componentPredicates:{} componentControllerPredicates:{} analyticsListener:nil options:{}]; } - (instancetype)initWithComponentProvider:(CKComponentProviderFunc)componentProvider sizeRangeProviderBlock:(CKComponentSizeRangeProviderBlock)sizeRangeProvider { return [self initWithComponentProviderFunc:componentProvider sizeRangeProvider:[[CKBlockSizeRangeProvider alloc] initWithBlock:sizeRangeProvider] componentPredicates:{} componentControllerPredicates:{} analyticsListener:nil options:{}]; } - (instancetype)initWithComponentProviderFunc:(CKComponentProviderFunc)componentProvider sizeRangeProvider:(id<CKComponentSizeRangeProviding>)sizeRangeProvider componentPredicates:(const std::unordered_set<CKComponentPredicate> &)componentPredicates componentControllerPredicates:(const std::unordered_set<CKComponentControllerPredicate> &)componentControllerPredicates analyticsListener:(id<CKAnalyticsListener>)analyticsListener options:(const CKComponentHostingViewOptions &)options { if (self = [super initWithFrame:CGRectZero]) { _componentGenerator = [[CKComponentGenerator alloc] initWithOptions:{ .delegate = CK::makeNonNull(self), .componentProvider = CK::makeNonNull(componentProvider ?: nilProvider), .componentPredicates = componentPredicates, .componentControllerPredicates = componentControllerPredicates, .analyticsListener = analyticsListener, }]; _allowTapPassthrough = options.allowTapPassthrough; _containerViewProvider = [[CKComponentHostingContainerViewProvider alloc] initWithFrame:CGRectZero scopeIdentifier:_componentGenerator.scopeRoot.globalIdentifier analyticsListener:_componentGenerator.scopeRoot.analyticsListener sizeRangeProvider:sizeRangeProvider allowTapPassthrough:_allowTapPassthrough]; [self addSubview:self.containerView]; _initialSize = options.initialSize; _initialSize.apply([&](const auto initialSize) { self.frame = {CGPointZero, initialSize}; }); _componentNeedsUpdate = !_initialSize.hasValue(); [CKComponentDebugController registerReflowListener:self]; } return self; } - (UIView *)containerView { return _containerViewProvider.containerView; } #pragma mark - Layout - (void)layoutSubviews { RCAssertMainThread(); [super layoutSubviews]; // It is possible for a view change due to mounting to trigger a re-layout of the entire screen. This can // synchronously call layoutIfNeeded on this view, which could cause a re-entrant component mount, which we want // to avoid. if (!_isMountingComponent) { _isMountingComponent = YES; self.containerView.frame = self.bounds; const CGSize size = self.bounds.size; auto const buildTrigger = [self _synchronouslyUpdateComponentIfNeeded]; const auto mountedComponent = _mountedRootLayout.mapToPtr([](const auto &rootLayout){ return rootLayout.component(); }); // We shouldn't layout component if there is no `_mountedRootLayout` even though sizes are different. const auto shouldLayoutComponent = _mountedRootLayout.map([&](const auto &rootLayout) { return !CGSizeEqualToSize(rootLayout.size(), size); }).valueOr(NO); if (mountedComponent != _component || shouldLayoutComponent) { auto const rootLayout = CKComputeRootComponentLayout(_component, {size, size}, _componentGenerator.scopeRoot.analyticsListener, buildTrigger, _componentGenerator.scopeRoot); [self _applyRootLayout:rootLayout]; } [_containerViewProvider mount]; _isMountingComponent = NO; } } - (CGSize)sizeThatFits:(CGSize)size { RCAssertMainThread(); [self _synchronouslyUpdateComponentIfNeeded]; if (!_component) { // This could only happen when `initialSize` is specified. return _initialSize.valueOr(CGSizeZero); } return [self.containerView sizeThatFits:size]; } - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { [_componentGenerator updateTraitCollection:self.traitCollection]; } #pragma mark - Hit Testing - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *const hitView = [super hitTest:point withEvent:event]; if (_allowTapPassthrough && hitView == self) { return nil; } else { return hitView; } } #pragma mark - Accessors - (void)updateModel:(id<NSObject>)model mode:(CKUpdateMode)mode { RCAssertMainThread(); [_componentGenerator updateModel:model]; [self _setNeedsUpdateWithMode:mode]; } - (void)updateContext:(id<NSObject>)context mode:(CKUpdateMode)mode { RCAssertMainThread(); [_componentGenerator updateContext:context]; [self _setNeedsUpdateWithMode:mode]; } - (void)updateAccessibilityStatus:(BOOL)accessibilityStatus mode:(CKUpdateMode)mode { RCAssertMainThread(); [_componentGenerator updateAccessibilityStatus:accessibilityStatus]; [self _setNeedsUpdateWithMode:mode]; } - (void)applyResult:(const CKBuildComponentResult &)result { RCAssertMainThread(); _componentGenerator.scopeRoot = result.scopeRoot; [self _applyResult:result]; [self setNeedsLayout]; [_delegate componentHostingViewDidInvalidateSize:self]; } - (void)reloadWithMode:(CKUpdateMode)mode { RCAssertMainThread(); [_componentGenerator forceReloadInNextGeneration]; [self _setNeedsUpdateWithMode:mode]; } - (RCLayout)mountedLayout { return _mountedRootLayout.map([](const auto &rootLayout) { return rootLayout.layout(); }).valueOr({}); } - (id<CKComponentScopeEnumeratorProvider>)scopeEnumeratorProvider { return _componentGenerator.scopeRoot; } #pragma mark - Appearance - (void)hostingViewWillAppear { CKComponentScopeRootAnnounceControllerAppearance(_componentGenerator.scopeRoot); } - (void)hostingViewDidDisappear { CKComponentScopeRootAnnounceControllerDisappearance(_componentGenerator.scopeRoot); } #pragma mark - CKComponentDebugController - (void)didReceiveReflowComponentsRequest { [self _setNeedsUpdateWithMode:CKUpdateModeAsynchronous]; } - (void)didReceiveReflowComponentsRequestWithTreeNodeIdentifier:(CKTreeNodeIdentifier)treeNodeIdentifier { if (_componentGenerator.scopeRoot.rootNode.parentForNodeIdentifier(treeNodeIdentifier) != nil) { [self _setNeedsUpdateWithMode:CKUpdateModeSynchronous]; } } #pragma mark - Private - (BOOL)_hasScheduledSyncUpdate { return _componentNeedsUpdate && !_scheduledAsynchronousComponentUpdate; } - (void)_setNeedsUpdateWithMode:(CKUpdateMode)mode { if ([self _hasScheduledSyncUpdate]) { return; // Already scheduled a synchronous update; nothing more to do. } _componentNeedsUpdate = YES; switch (mode) { case CKUpdateModeAsynchronous: [self _asynchronouslyUpdateComponentIfNeeded]; break; case CKUpdateModeSynchronous: _scheduledAsynchronousComponentUpdate = NO; [self setNeedsLayout]; [_delegate componentHostingViewDidInvalidateSize:self]; break; } } - (void)_asynchronouslyUpdateComponentIfNeeded { if (_scheduledAsynchronousComponentUpdate) { return; } _scheduledAsynchronousComponentUpdate = YES; // Wait until the end of the run loop so that if multiple async updates are triggered we don't thrash. dispatch_async(dispatch_get_main_queue(), ^{ if (!_scheduledAsynchronousComponentUpdate) { // A synchronous update was either scheduled or completed, so we can skip the async update. return; } // Sync trait collection in `componentGenerator` before building the next generation. [_componentGenerator updateTraitCollection:self.traitCollection]; [_componentGenerator updateAccessibilityStatus:CK::Component::Accessibility::IsAccessibilityEnabled()]; [_componentGenerator generateComponentAsynchronously]; }); } - (void)_applyResult:(const CKBuildComponentResult &)result { _component = result.component; [_containerViewProvider setBoundsAnimation:result.boundsAnimation]; [_containerViewProvider setComponent:result.component]; _componentNeedsUpdate = NO; } - (void)_applyRootLayout:(const CKComponentRootLayout &)rootLayout { _mountedRootLayout = rootLayout; [self _sendDidPrepareLayoutIfNeeded]; [_containerViewProvider setRootLayout:rootLayout]; } - (CK::Optional<CKBuildTrigger>)_synchronouslyUpdateComponentIfNeeded { if (!_componentNeedsUpdate || _scheduledAsynchronousComponentUpdate) { return CK::none; } if (_isSynchronouslyUpdatingComponent) { RCFailAssert(@"CKComponentHostingView is not re-entrant. This is called by -layoutSubviews, so ensure " "that there is nothing that is triggering a nested call to -layoutSubviews."); return CK::none; } _isSynchronouslyUpdatingComponent = YES; // Sync trait collection in `componentGenerator` before building the next generation. [_componentGenerator updateTraitCollection:self.traitCollection]; [_componentGenerator updateAccessibilityStatus:CK::Component::Accessibility::IsAccessibilityEnabled()]; const auto result = [_componentGenerator generateComponentSynchronously]; [self _applyResult:result]; _isSynchronouslyUpdatingComponent = NO; return result.buildTrigger; } - (void)_sendDidPrepareLayoutIfNeeded { _mountedRootLayout.apply([&](const auto &rootLayout) { CKComponentSendDidPrepareLayoutForComponent(_componentGenerator.scopeRoot, rootLayout); }); } #pragma mark - CKComponentGeneratorDelegate - (BOOL)componentGeneratorShouldApplyAsynchronousGenerationResult:(CKComponentGenerator *)componentGenerator { return _componentNeedsUpdate; } - (void)componentGenerator:(CKComponentGenerator *)componentGenerator didAsynchronouslyGenerateComponentResult:(CKBuildComponentResult)result { _scheduledAsynchronousComponentUpdate = NO; [self _applyResult:result]; [self setNeedsLayout]; [_delegate componentHostingViewDidInvalidateSize:self]; } - (void)componentGenerator:(CKComponentGenerator *)componentGenerator didReceiveComponentStateUpdateWithMode:(CKUpdateMode)mode { [self _setNeedsUpdateWithMode:mode]; } @end