From f2bc5d3fb2d1af752c9fada4289db65d33a946c0 Mon Sep 17 00:00:00 2001 From: Roman Gorbunov Date: Thu, 25 Jul 2019 03:59:05 -0700 Subject: [PATCH] Generic solution for live editing Summary: Preparation for making components live editable Reviewed By: kevin0571 Differential Revision: D16379961 fbshipit-source-id: d0ea3d753eb588fe7b55f2345124427b4a5a58b5 --- .../CKComponent+Sonar.h | 4 + .../CKComponent+Sonar.mm | 117 +++++++++++++++++- .../SKComponentLayoutDescriptor.mm | 5 + .../SKComponentLayoutWrapper.mm | 4 +- .../FlipperKitLayoutPlugin.mm | 12 +- .../FlipperKitLayoutPlugin/SKNodeDescriptor.h | 6 + .../SKNodeDescriptor.mm | 5 + 7 files changed, 139 insertions(+), 14 deletions(-) diff --git a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/CKComponent+Sonar.h b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/CKComponent+Sonar.h index 56a9292e5..f9b44a1fa 100644 --- a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/CKComponent+Sonar.h +++ b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/CKComponent+Sonar.h @@ -9,10 +9,14 @@ #import #import +typedef id (^SKNodeDataChanged)(id value); + FB_LINK_REQUIRE_CATEGORY(CKComponent_Sonar) @interface CKComponent (Sonar) @property (assign, nonatomic) NSUInteger flipper_canBeReusedCounter; +- (void)setMutableData:(id)data; +- (NSDictionary *)sonar_getDataMutationsChanged; - (NSArray *> *> *)sonar_getData; - (NSDictionary *)sonar_getDataMutations; - (NSString *)sonar_getName; diff --git a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/CKComponent+Sonar.mm b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/CKComponent+Sonar.mm index 10d90c5c4..e84b47081 100644 --- a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/CKComponent+Sonar.mm +++ b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/CKComponent+Sonar.mm @@ -14,12 +14,17 @@ #import #import #import +#import +#import #import #import +#import +#import #import "CKFlexboxComponent+Sonar.h" #import "CKInsetComponent+Sonar.h" #import "CKStatelessComponent+Sonar.h" +#import "FKDataStorageForLiveEditing.h" /** This protocol isn't actually adopted anywhere, it just lets us use the SEL below */ @protocol SonarKitLayoutComponentKitOverrideInformalProtocol @@ -58,6 +63,30 @@ static NSDictionary *AccessibilityContextDict(CKComponen FB_LINKABLE(CKComponent_Sonar) @implementation CKComponent (Sonar) +static FKDataStorageForLiveEditing *_dataStorage; +static NSMutableSet *_swizzledClasses; +static CK::StaticMutex _mutex = CK_MUTEX_INITIALIZER; + ++ (void)swizzleOriginalSEL:(SEL)originalSEL to:(SEL)replacementSEL +{ + Class targetClass = self; + Method original = class_getInstanceMethod(targetClass, originalSEL); + Method replacement = class_getInstanceMethod(targetClass, replacementSEL); + BOOL didAddMethod = + class_addMethod(targetClass, + originalSEL, + method_getImplementation(replacement), + method_getTypeEncoding(replacement)); + if (didAddMethod) { + class_replaceMethod(targetClass, + replacementSEL, + method_getImplementation(original), + method_getTypeEncoding(original)); + } else { + method_exchangeImplementations(original, replacement); + } +} + - (NSString *)sonar_getName { if ([self respondsToSelector:@selector(sonar_componentNameOverride)]) { @@ -171,7 +200,6 @@ FB_LINKABLE(CKComponent_Sonar) @"accessibilityEnabled": SKMutableObject(@(CK::Component::Accessibility::IsAccessibilityEnabled())), }]]; } - if ([self respondsToSelector:@selector(sonar_additionalDataOverride)]) { [data addObjectsFromArray:[(id)self sonar_additionalDataOverride]]; } @@ -179,12 +207,89 @@ FB_LINKABLE(CKComponent_Sonar) return data; } +- (void)setMutableData:(id)value { + +} + +- (void) setMutableDataFromStorage { + const auto globalID = self.treeNode.nodeIdentifier; + id data = [_dataStorage dataForTreeNodeIdentifier:globalID]; + if (data) { + [self setMutableData:data]; + } +} + ++ (NSString *)swizzledMethodNameForRender { + return [NSString stringWithFormat:@"sonar_render_%@", NSStringFromClass(self)]; +} + ++ (SEL)registerNewImplementation:(SEL)selector { + SEL resultSelector = sel_registerName([[self swizzledMethodNameForRender] UTF8String]); + Method method = class_getInstanceMethod(self, selector); + class_addMethod(self, + resultSelector, + method_getImplementation(method), + method_getTypeEncoding(method) + ); + return resultSelector; +} + +- (CKComponent *)sonar_render:(id)state { + [self setMutableDataFromStorage]; + SEL resultSelector = NSSelectorFromString([[self class] swizzledMethodNameForRender]); + return ((CKComponent *(*)(CKComponent *, SEL, id))objc_msgSend)(self, resultSelector, state); +} + +- (std::vector)sonar_renderChildren:(id)state { + [self setMutableDataFromStorage]; + SEL resultSelector = NSSelectorFromString([[self class] swizzledMethodNameForRender]); + return ((std::vector(*)(CKComponent *, SEL, id))objc_msgSend_stret)(self, resultSelector, state); +} + +- (NSDictionary *)sonar_getDataMutationsChanged { + return @{}; +} + - (NSDictionary *)sonar_getDataMutations { - return @{ - @"Accessibility.accessibilityEnabled": ^(NSNumber *value) { - CK::Component::Accessibility::SetForceAccessibilityEnabled([value boolValue]); - } - }; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _dataStorage = [[FKDataStorageForLiveEditing alloc] init]; + _swizzledClasses = [[NSMutableSet alloc] init]; + }); + { + CK::StaticMutexLocker l(_mutex); + if (![_swizzledClasses containsObject:NSStringFromClass([self class])]) { + [_swizzledClasses addObject:NSStringFromClass([self class])]; + if ([self respondsToSelector:@selector(render:)]) { + SEL replacement = [[self class] registerNewImplementation:@selector(sonar_render:)]; + [[self class] swizzleOriginalSEL:@selector(render:) to:replacement]; + } else if ([self respondsToSelector:@selector(renderChildren:)]) { + SEL replacement = [[self class] registerNewImplementation:@selector(sonar_renderChildren:)]; + [[self class] swizzleOriginalSEL:@selector(renderChildren:) to:replacement]; + } else { + CKAssert(NO, @"Only CKRenderLayoutComponent and CKRenderLayoutWithChildrenComponent children are now able to be live editable"); + } + } + } + NSDictionary *dataChanged = [self sonar_getDataMutationsChanged]; + NSMutableDictionary *dataMutation = [[NSMutableDictionary alloc] init]; + [dataMutation addEntriesFromDictionary:@{ + @"Accessibility.accessibilityEnabled": ^(NSNumber *value) { + CK::Component::Accessibility::SetForceAccessibilityEnabled([value boolValue]); + } + } + ]; + const auto globalID = self.treeNode.nodeIdentifier; + for (NSString *key in dataChanged) { + const auto block = dataChanged[key]; + [dataMutation setObject:^(id value) { + id data = block(value); + [_dataStorage setData:data forTreeNodeIdentifier:globalID]; + [CKComponentDebugController reflowComponentsWithTreeNodeIdentifier:globalID]; + } + forKey:key]; + } + return dataMutation; } static char const kCanBeReusedKey = ' '; diff --git a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutDescriptor.mm b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutDescriptor.mm index b6744f39a..0069bab0c 100644 --- a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutDescriptor.mm +++ b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutDescriptor.mm @@ -69,6 +69,11 @@ return node.identifier; } +- (NSString *)identifierForInvalidation:(SKComponentLayoutWrapper *)node +{ + return [NSString stringWithFormat:@"%p", node.rootNode]; +} + - (NSString *)nameForNode:(SKComponentLayoutWrapper *)node { return [node.component sonar_getName]; } diff --git a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutWrapper.mm b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutWrapper.mm index 4fded345c..2a81155b7 100644 --- a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutWrapper.mm +++ b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutComponentKitSupport/SKComponentLayoutWrapper.mm @@ -57,7 +57,7 @@ static CKFlexboxComponentChild findFlexboxLayoutParams(CKComponent *parent, CKCo SKComponentLayoutWrapper *const wrapper = [[SKComponentLayoutWrapper alloc] initWithLayout:layout position:CGPointMake(0, 0) - parentKey:[NSString stringWithFormat: @"%p.", layout.component] + parentKey:[NSString stringWithFormat: @"%d.", layout.component.treeNode.nodeIdentifier] reuseWrapper:reuseWrapper rootNode: root]; // Cache the result. @@ -97,7 +97,7 @@ static CKFlexboxComponentChild findFlexboxLayoutParams(CKComponent *parent, CKCo position:child.position parentKey:[_identifier stringByAppendingFormat:@"[%d].", index++] reuseWrapper:reuseWrapper - rootNode: nil + rootNode:node ]; childWrapper->_isFlexboxChild = [_component isKindOfClass:[CKFlexboxComponent class]]; childWrapper->_flexboxChild = findFlexboxLayoutParams(_component, child.layout.component); diff --git a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm index d1a4c9774..ba022e2b9 100644 --- a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm +++ b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.mm @@ -213,11 +213,13 @@ NSString *dotJoinedPath = [path componentsJoinedByString: @"."]; SKNodeUpdateData updateDataForPath = [[descriptor dataMutationsForNode: node] objectForKey: dotJoinedPath]; if (updateDataForPath != nil) { + const auto identifierForInvalidation = [descriptor identifierForInvalidation:node]; + id nodeForInvalidation = [_trackedObjects objectForKey:identifierForInvalidation]; + SKNodeDescriptor *descriptorForInvalidation = [_descriptorMapper descriptorForClass:[nodeForInvalidation class]]; + NSMutableArray *children = [self getChildrenForNode:nodeForInvalidation withDescriptor:descriptorForInvalidation]; updateDataForPath(value); - - NSMutableArray *children = [self getChildrenForNode:node withDescriptor:descriptor]; [connection send: @"invalidate" withParams: @{ - @"nodes": @[@{@"id": [descriptor identifierForNode: node], @"children": children}] + @"nodes": @[@{@"id": [descriptorForInvalidation identifierForNode: nodeForInvalidation], @"children": children}] }]; } } @@ -446,9 +448,7 @@ return nil; } - if (! [_trackedObjects objectForKey: objectIdentifier]) { - [_trackedObjects setObject:object forKey:objectIdentifier]; - } + [_trackedObjects setObject:object forKey:objectIdentifier]; return objectIdentifier; } diff --git a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/SKNodeDescriptor.h b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/SKNodeDescriptor.h index 2f7458b8d..97717e6e6 100644 --- a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/SKNodeDescriptor.h +++ b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/SKNodeDescriptor.h @@ -43,6 +43,12 @@ typedef void (^SKNodeUpdateData)(id value); */ - (NSString *)identifierForNode:(T)node; +/** + An ID which is equal between reflowing components is needed to get the identifier of root + node of a tree which need to be invalidated on FlipperKitLayoutPlugin side. +*/ +- (NSString *)identifierForInvalidation:(T)node; + /** The name used to identify this node in the Sonar desktop application. This is what will be visible in the hierarchy. diff --git a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/SKNodeDescriptor.mm b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/SKNodeDescriptor.mm index 5d77ff462..1d73bcbd4 100644 --- a/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/SKNodeDescriptor.mm +++ b/iOS/Plugins/FlipperKitLayoutPlugin/FlipperKitLayoutPlugin/SKNodeDescriptor.mm @@ -33,6 +33,11 @@ @throw [NSString stringWithFormat:@"need to implement %@", NSStringFromSelector(_cmd)]; } +- (NSString *)identifierForInvalidation:(id)node +{ + return [self identifierForNode:node]; +} + - (NSString *)nameForNode:(id)node { return NSStringFromClass([node class]); }