ComponentKit/Core/CKComponentAttachController.mm (200 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 <objc/runtime.h> #import "CKComponentInternal.h" #import <ComponentKit/CKAnalyticsListener.h> #import <ComponentKit/RCAssociatedObject.h> #import <ComponentKit/CKDelayedNonNull.h> #import <ComponentKit/CKOptional.h> #import "CKComponentAnimations.h" #import "CKComponentAttachController.h" #import "CKComponentAttachControllerInternal.h" #import "CKDataSourceItem.h" @implementation CKComponentAttachController { /** We keep a strong reference to the mounted view to enforce that every view being attached will have to be detached before the view is deallocated. */ NSMutableDictionary *_scopeIdentifierToAttachedViewMap; NSMapTable<NSNumber *, id<CKComponentRootLayoutProvider>> *_scopeIdentifierToLayoutProvider; } #pragma mark - Initialization/Teardown - (instancetype)init { self = [super init]; if (self) { _scopeIdentifierToAttachedViewMap = [NSMutableDictionary dictionary]; _scopeIdentifierToLayoutProvider = [NSMapTable strongToWeakObjectsMapTable]; } return self; } - (void)dealloc { NSDictionary *scopeIdentifierToAttachedViewMap = _scopeIdentifierToAttachedViewMap; dispatch_block_t viewTearDownBlock = ^{ NSArray *views = [scopeIdentifierToAttachedViewMap allValues]; tearDownAttachStateFromViews(views); }; if ([[NSThread currentThread] isMainThread]) { viewTearDownBlock(); } else { dispatch_async(dispatch_get_main_queue(), viewTearDownBlock); } } #pragma mark - Public API void CKComponentAttachControllerAttachComponentRootLayout( CKComponentAttachController *const self, const CKComponentAttachControllerAttachComponentRootLayoutParams &params) { RCCAssertMainThread(); if (self == nil) { RCCAssert(self, @"Impossible to attach a component layout to a nil attachController"); return; } const auto view = params.view; UIView *currentlyAttachedView = self->_scopeIdentifierToAttachedViewMap[@(params.scopeIdentifier)]; // If the component tree currently attached to the view is different from the one we want to attach if (currentlyAttachedView != view) { // 1 - If the component layout want to attach is currently attached somewhere else then detach it [self _detachComponentLayoutFromView:currentlyAttachedView]; // 2 - Unmount the component tree currently in the view we want to attach our component layout to [self _detachComponentLayoutFromView:view]; } const auto &prevLayout = [&]() { if (const auto layoutProvider = [self->_scopeIdentifierToLayoutProvider objectForKey:@(params.scopeIdentifier)]) { return layoutProvider.rootLayout; } else { return CKComponentRootLayout {}; } }(); // Mount the component tree on the view const auto &layout = params.layoutProvider ? params.layoutProvider.rootLayout : CKComponentRootLayout {}; const auto attachState = mountComponentLayoutInView(layout, prevLayout, view, params.scopeIdentifier, params.boundsAnimation, params.analyticsListener); // Mark the view as attached and associates it to the right attach state self->_scopeIdentifierToAttachedViewMap[@(params.scopeIdentifier)] = view; // Save layout provider in map, it will be used for figuring out animations between two layouts. [self->_scopeIdentifierToLayoutProvider setObject:params.layoutProvider forKey:@(params.scopeIdentifier)]; CKSetAttachStateForView(view, attachState); } - (void)detachComponentLayoutWithScopeIdentifier:(CKComponentScopeRootIdentifier)scopeIdentifier { RCAssertMainThread(); [self _detachComponentLayoutWithScopeIdentifier:@(scopeIdentifier)]; } - (void)detachAll { RCAssertMainThread(); for (NSNumber *const scopeIdentifier in _scopeIdentifierToAttachedViewMap.allKeys) { [self _detachComponentLayoutWithScopeIdentifier:scopeIdentifier]; } } #pragma mark - Internal API - (CKComponentAttachState *)attachStateForScopeIdentifier:(CKComponentScopeRootIdentifier)scopeIdentifier { return CKGetAttachStateForView(((UIView *)_scopeIdentifierToAttachedViewMap[@(scopeIdentifier)])); } - (id<CKComponentRootLayoutProvider>)layoutProviderForScopeIdentifier:(CKComponentScopeRootIdentifier)scopeIdentifier { return [_scopeIdentifierToLayoutProvider objectForKey:@(scopeIdentifier)]; } #pragma mark - Attach helpers - (void)_detachComponentLayoutWithScopeIdentifier:(NSNumber *)scopeIdentifier { [self _detachComponentLayoutFromView:[_scopeIdentifierToAttachedViewMap objectForKey:scopeIdentifier]]; [_scopeIdentifierToLayoutProvider removeObjectForKey:scopeIdentifier]; } - (void)_detachComponentLayoutFromView:(UIView *)view { CKComponentAttachState *attachState = CKGetAttachStateForView(view); if (attachState) { CKUnmountComponents(attachState.mountedComponents); // Mark the view as detached [_scopeIdentifierToAttachedViewMap removeObjectForKey:@(attachState.scopeIdentifier)]; CKSetAttachStateForView(view, nil); } } static CKComponentAttachState *mountComponentLayoutInView(const CKComponentRootLayout &rootLayout, const CKComponentRootLayout &prevLayout, UIView *view, CKComponentScopeRootIdentifier scopeIdentifier, const CKComponentBoundsAnimation &boundsAnimation, id<CKAnalyticsListener> analyticsListener) { RCCAssertNotNil(view, @"Impossible to mount a component layout on a nil view"); [analyticsListener willCollectAnimationsFromComponentTreeWithRootComponent:rootLayout.component()]; const auto animatedComponents = CK::animatedComponentsBetweenLayouts(rootLayout, prevLayout); const auto animations = CK::animationsForComponents(animatedComponents, view); [analyticsListener didCollectAnimations:animations fromComponents:animatedComponents inComponentTreeWithRootComponent:rootLayout.component() scopeRootIdentifier:scopeIdentifier]; auto const oldAttachState = CKGetAttachStateForView(view); NSSet *currentlyMountedComponents = oldAttachState.mountedComponents; __block NSSet *newMountedComponents = nil; const auto mountPerformer = ^{ __block NSMutableSet<CKComponent *> *unmountedComponents; CKComponentBoundsAnimationApply(boundsAnimation, ^{ newMountedComponents = CKMountComponentLayout(rootLayout.layout(), view, currentlyMountedComponents, nil, analyticsListener); // This could probably be done more efficiently by making mountPerformer // return a pair: currentlyMountedComponents & newMountedComponents. // Then we can skip the allocation and simply enumerate over the two sets. unmountedComponents = [currentlyMountedComponents mutableCopy]; [unmountedComponents minusSet:newMountedComponents]; }, nil); return unmountedComponents; }; std::shared_ptr<CK::AnimationApplicator<>> animationApplicator; animationApplicator = oldAttachState != nil ? oldAttachState.animationApplicator : CK::AnimationApplicatorFactory::make(); animationApplicator->runAnimationsWhenMounting(animations, mountPerformer); const auto attachState = [[CKComponentAttachState alloc] initWithScopeIdentifier:scopeIdentifier mountedComponents:CK::makeNonNull(newMountedComponents) animationApplicator:animationApplicator]; CKComponentAttachStateSetRootLayout(attachState, rootLayout); return attachState; } static void tearDownAttachStateFromViews(NSArray<UIView *> *views) { for (UIView *view in views) { CKComponentAttachState *attachState = CKGetAttachStateForView(view); if (attachState) { CKUnmountComponents(attachState.mountedComponents); CKSetAttachStateForView(view, nil); } } } @end @implementation CKComponentAttachState { CKComponentRootLayout _rootLayout; // The ownership isn't really shared with anyone, this is just to get copying the pointer in and out of the attach state easier std::shared_ptr<CK::AnimationApplicator<>> _animationApplicator; CK::DelayedNonNull<NSSet *> _mountedComponents; } - (instancetype)initWithScopeIdentifier:(CKComponentScopeRootIdentifier)scopeIdentifier mountedComponents:(CK::NonNull<NSSet *>)mountedComponents animationApplicator:(const std::shared_ptr<CK::AnimationApplicator<>> &)animationApplicator { self = [super init]; if (self) { _scopeIdentifier = scopeIdentifier; _mountedComponents = CK::makeNonNull([mountedComponents copy]); _animationApplicator = animationApplicator; } return self; } const CKComponentRootLayout &CKComponentAttachStateRootLayout(const CKComponentAttachState *const self) { return self->_rootLayout; } void CKComponentAttachStateSetRootLayout(CKComponentAttachState *const self, const CKComponentRootLayout &rootLayout) { self->_rootLayout = rootLayout; } - (const std::shared_ptr<CK::AnimationApplicator<>> &)animationApplicator { return _animationApplicator; } - (CK::NonNull<NSSet *>)mountedComponents { return _mountedComponents; } @end static char const kViewAttachStateKey = ' '; auto CKGetAttachStateForView(UIView *view) -> CKComponentAttachState * { return objc_getAssociatedObject(view, &kViewAttachStateKey); } auto CKSetAttachStateForView(UIView *view, CKComponentAttachState *attachState) -> void { objc_setAssociatedObject(view, &kViewAttachStateKey, attachState, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }