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/SKNodeDescriptor.h>
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<NSString *, SKNodeDataChanged> *)sonar_getDataMutationsChanged;
- (NSArray<SKNamed<NSDictionary<NSString *, NSObject *> *> *> *)sonar_getData;
- (NSDictionary<NSString *, SKNodeUpdateData> *)sonar_getDataMutations;
- (NSString *)sonar_getName;

View File

@@ -14,12 +14,17 @@
#import <ComponentKit/CKComponentInternal.h>
#import <ComponentKit/CKComponentSubclass.h>
#import <ComponentKit/CKComponentViewConfiguration.h>
#import <ComponentKit/CKComponentDebugController.h>
#import <ComponentKit/CKMutex.h>
#import <FlipperKitLayoutPlugin/SKNamed.h>
#import <FlipperKitLayoutPlugin/SKObject.h>
#import <objc/runtime.h>
#import <objc/message.h>
#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<NSString *, NSObject *> *AccessibilityContextDict(CKComponen
FB_LINKABLE(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
{
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<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 {
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<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 = ' ';

View File

@@ -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];
}

View File

@@ -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);