Generic solution for live editing

Summary: Preparation for making components live editable

Reviewed By: kevin0571

Differential Revision: D16379961

fbshipit-source-id: d0ea3d753eb588fe7b55f2345124427b4a5a58b5
This commit is contained in:
Roman Gorbunov
2019-07-25 03:59:05 -07:00
committed by Facebook Github Bot
parent cb374ffccd
commit f2bc5d3fb2
7 changed files with 139 additions and 14 deletions

View File

@@ -9,10 +9,14 @@
#import <FlipperKitLayoutPlugin/SKNamed.h> #import <FlipperKitLayoutPlugin/SKNamed.h>
#import <FlipperKitLayoutPlugin/SKNodeDescriptor.h> #import <FlipperKitLayoutPlugin/SKNodeDescriptor.h>
typedef id (^SKNodeDataChanged)(id value);
FB_LINK_REQUIRE_CATEGORY(CKComponent_Sonar) FB_LINK_REQUIRE_CATEGORY(CKComponent_Sonar)
@interface CKComponent (Sonar) @interface CKComponent (Sonar)
@property (assign, nonatomic) NSUInteger flipper_canBeReusedCounter; @property (assign, nonatomic) NSUInteger flipper_canBeReusedCounter;
- (void)setMutableData:(id)data;
- (NSDictionary<NSString *, SKNodeDataChanged> *)sonar_getDataMutationsChanged;
- (NSArray<SKNamed<NSDictionary<NSString *, NSObject *> *> *> *)sonar_getData; - (NSArray<SKNamed<NSDictionary<NSString *, NSObject *> *> *> *)sonar_getData;
- (NSDictionary<NSString *, SKNodeUpdateData> *)sonar_getDataMutations; - (NSDictionary<NSString *, SKNodeUpdateData> *)sonar_getDataMutations;
- (NSString *)sonar_getName; - (NSString *)sonar_getName;

View File

@@ -14,12 +14,17 @@
#import <ComponentKit/CKComponentInternal.h> #import <ComponentKit/CKComponentInternal.h>
#import <ComponentKit/CKComponentSubclass.h> #import <ComponentKit/CKComponentSubclass.h>
#import <ComponentKit/CKComponentViewConfiguration.h> #import <ComponentKit/CKComponentViewConfiguration.h>
#import <ComponentKit/CKComponentDebugController.h>
#import <ComponentKit/CKMutex.h>
#import <FlipperKitLayoutPlugin/SKNamed.h> #import <FlipperKitLayoutPlugin/SKNamed.h>
#import <FlipperKitLayoutPlugin/SKObject.h> #import <FlipperKitLayoutPlugin/SKObject.h>
#import <objc/runtime.h>
#import <objc/message.h>
#import "CKFlexboxComponent+Sonar.h" #import "CKFlexboxComponent+Sonar.h"
#import "CKInsetComponent+Sonar.h" #import "CKInsetComponent+Sonar.h"
#import "CKStatelessComponent+Sonar.h" #import "CKStatelessComponent+Sonar.h"
#import "FKDataStorageForLiveEditing.h"
/** This protocol isn't actually adopted anywhere, it just lets us use the SEL below */ /** This protocol isn't actually adopted anywhere, it just lets us use the SEL below */
@protocol SonarKitLayoutComponentKitOverrideInformalProtocol @protocol SonarKitLayoutComponentKitOverrideInformalProtocol
@@ -58,6 +63,30 @@ static NSDictionary<NSString *, NSObject *> *AccessibilityContextDict(CKComponen
FB_LINKABLE(CKComponent_Sonar) FB_LINKABLE(CKComponent_Sonar)
@implementation CKComponent (Sonar) @implementation CKComponent (Sonar)
static FKDataStorageForLiveEditing *_dataStorage;
static NSMutableSet<NSString *> *_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 - (NSString *)sonar_getName
{ {
if ([self respondsToSelector:@selector(sonar_componentNameOverride)]) { if ([self respondsToSelector:@selector(sonar_componentNameOverride)]) {
@@ -171,7 +200,6 @@ FB_LINKABLE(CKComponent_Sonar)
@"accessibilityEnabled": SKMutableObject(@(CK::Component::Accessibility::IsAccessibilityEnabled())), @"accessibilityEnabled": SKMutableObject(@(CK::Component::Accessibility::IsAccessibilityEnabled())),
}]]; }]];
} }
if ([self respondsToSelector:@selector(sonar_additionalDataOverride)]) { if ([self respondsToSelector:@selector(sonar_additionalDataOverride)]) {
[data addObjectsFromArray:[(id)self sonar_additionalDataOverride]]; [data addObjectsFromArray:[(id)self sonar_additionalDataOverride]];
} }
@@ -179,12 +207,89 @@ FB_LINKABLE(CKComponent_Sonar)
return data; 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<CKComponent *>)sonar_renderChildren:(id)state {
[self setMutableDataFromStorage];
SEL resultSelector = NSSelectorFromString([[self class] swizzledMethodNameForRender]);
return ((std::vector<CKComponent *>(*)(CKComponent *, SEL, id))objc_msgSend_stret)(self, resultSelector, state);
}
- (NSDictionary<NSString *, SKNodeDataChanged> *)sonar_getDataMutationsChanged {
return @{};
}
- (NSDictionary<NSString *, SKNodeUpdateData> *)sonar_getDataMutations { - (NSDictionary<NSString *, SKNodeUpdateData> *)sonar_getDataMutations {
return @{ static dispatch_once_t onceToken;
@"Accessibility.accessibilityEnabled": ^(NSNumber *value) { dispatch_once(&onceToken, ^{
CK::Component::Accessibility::SetForceAccessibilityEnabled([value boolValue]); _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<NSString *, SKNodeDataChanged> *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 = ' '; static char const kCanBeReusedKey = ' ';

View File

@@ -69,6 +69,11 @@
return node.identifier; return node.identifier;
} }
- (NSString *)identifierForInvalidation:(SKComponentLayoutWrapper *)node
{
return [NSString stringWithFormat:@"%p", node.rootNode];
}
- (NSString *)nameForNode:(SKComponentLayoutWrapper *)node { - (NSString *)nameForNode:(SKComponentLayoutWrapper *)node {
return [node.component sonar_getName]; return [node.component sonar_getName];
} }

View File

@@ -57,7 +57,7 @@ static CKFlexboxComponentChild findFlexboxLayoutParams(CKComponent *parent, CKCo
SKComponentLayoutWrapper *const wrapper = SKComponentLayoutWrapper *const wrapper =
[[SKComponentLayoutWrapper alloc] initWithLayout:layout [[SKComponentLayoutWrapper alloc] initWithLayout:layout
position:CGPointMake(0, 0) position:CGPointMake(0, 0)
parentKey:[NSString stringWithFormat: @"%p.", layout.component] parentKey:[NSString stringWithFormat: @"%d.", layout.component.treeNode.nodeIdentifier]
reuseWrapper:reuseWrapper reuseWrapper:reuseWrapper
rootNode: root]; rootNode: root];
// Cache the result. // Cache the result.
@@ -97,7 +97,7 @@ static CKFlexboxComponentChild findFlexboxLayoutParams(CKComponent *parent, CKCo
position:child.position position:child.position
parentKey:[_identifier stringByAppendingFormat:@"[%d].", index++] parentKey:[_identifier stringByAppendingFormat:@"[%d].", index++]
reuseWrapper:reuseWrapper reuseWrapper:reuseWrapper
rootNode: nil rootNode:node
]; ];
childWrapper->_isFlexboxChild = [_component isKindOfClass:[CKFlexboxComponent class]]; childWrapper->_isFlexboxChild = [_component isKindOfClass:[CKFlexboxComponent class]];
childWrapper->_flexboxChild = findFlexboxLayoutParams(_component, child.layout.component); childWrapper->_flexboxChild = findFlexboxLayoutParams(_component, child.layout.component);

View File

@@ -213,11 +213,13 @@
NSString *dotJoinedPath = [path componentsJoinedByString: @"."]; NSString *dotJoinedPath = [path componentsJoinedByString: @"."];
SKNodeUpdateData updateDataForPath = [[descriptor dataMutationsForNode: node] objectForKey: dotJoinedPath]; SKNodeUpdateData updateDataForPath = [[descriptor dataMutationsForNode: node] objectForKey: dotJoinedPath];
if (updateDataForPath != nil) { 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); updateDataForPath(value);
NSMutableArray *children = [self getChildrenForNode:node withDescriptor:descriptor];
[connection send: @"invalidate" withParams: @{ [connection send: @"invalidate" withParams: @{
@"nodes": @[@{@"id": [descriptor identifierForNode: node], @"children": children}] @"nodes": @[@{@"id": [descriptorForInvalidation identifierForNode: nodeForInvalidation], @"children": children}]
}]; }];
} }
} }
@@ -446,9 +448,7 @@
return nil; return nil;
} }
if (! [_trackedObjects objectForKey: objectIdentifier]) { [_trackedObjects setObject:object forKey:objectIdentifier];
[_trackedObjects setObject:object forKey:objectIdentifier];
}
return objectIdentifier; return objectIdentifier;
} }

View File

@@ -43,6 +43,12 @@ typedef void (^SKNodeUpdateData)(id value);
*/ */
- (NSString *)identifierForNode:(T)node; - (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 The name used to identify this node in the Sonar desktop application. This is what
will be visible in the hierarchy. will be visible in the hierarchy.

View File

@@ -33,6 +33,11 @@
@throw [NSString stringWithFormat:@"need to implement %@", NSStringFromSelector(_cmd)]; @throw [NSString stringWithFormat:@"need to implement %@", NSStringFromSelector(_cmd)];
} }
- (NSString *)identifierForInvalidation:(id)node
{
return [self identifierForNode:node];
}
- (NSString *)nameForNode:(id)node { - (NSString *)nameForNode:(id)node {
return NSStringFromClass([node class]); return NSStringFromClass([node class]);
} }