Initial commit 🎉
fbshipit-source-id: b6fc29740c6875d2e78953b8a7123890a67930f2 Co-authored-by: Sebastian McKenzie <sebmck@fb.com> Co-authored-by: John Knox <jknox@fb.com> Co-authored-by: Emil Sjölander <emilsj@fb.com> Co-authored-by: Pritesh Nandgaonkar <prit91@fb.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <ComponentKit/CKComponent.h>
|
||||
#import <SonarKit/SKMacros.h>
|
||||
#import <SonarKitLayoutPlugin/SKNamed.h>
|
||||
#import <SonarKitLayoutPlugin/SKNodeDescriptor.h>
|
||||
|
||||
FB_LINK_REQUIRE(CKComponent_Sonar)
|
||||
@interface CKComponent (Sonar)
|
||||
|
||||
- (NSArray<SKNamed<NSDictionary<NSString *, NSObject *> *> *> *)sonar_getData;
|
||||
- (NSDictionary<NSString *, SKNodeUpdateData> *)sonar_getDataMutations;
|
||||
- (NSString *)sonar_getName;
|
||||
- (NSString *)sonar_getDecoration;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (c) 2004-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "CKComponent+Sonar.h"
|
||||
|
||||
#import <ComponentKit/CKComponentAccessibility.h>
|
||||
#import <ComponentKit/CKComponentController.h>
|
||||
#import <ComponentKit/CKComponentInternal.h>
|
||||
#import <ComponentKit/CKComponentSubclass.h>
|
||||
#import <ComponentKit/CKComponentViewConfiguration.h>
|
||||
#import <SonarKitLayoutPlugin/SKNamed.h>
|
||||
#import <SonarKitLayoutPlugin/SKObject.h>
|
||||
|
||||
/** This protocol isn't actually adopted anywhere, it just lets us use the SEL below */
|
||||
@protocol SonarKitLayoutComponentKitOverrideInformalProtocol
|
||||
- (NSString *)sonar_componentNameOverride;
|
||||
- (NSString *)sonar_componentDecorationOverride;
|
||||
- (NSArray<SKNamed<NSDictionary<NSString *, NSObject *> *> *> *)sonar_additionalDataOverride;
|
||||
@end
|
||||
|
||||
static BOOL AccessibilityContextIsDefault(CKComponentAccessibilityContext accessibilityContext) {
|
||||
return accessibilityContext == CKComponentAccessibilityContext();
|
||||
}
|
||||
|
||||
static NSDictionary<NSString *, NSObject *> *AccessibilityContextDict(CKComponentAccessibilityContext accessibilityContext) {
|
||||
NSMutableDictionary<NSString *, NSObject *> *accessibilityDict = [NSMutableDictionary new];
|
||||
if (accessibilityContext.isAccessibilityElement != nil) {
|
||||
accessibilityDict[@"isAccessibilityElement"] = SKObject(@([accessibilityContext.isAccessibilityElement boolValue]));
|
||||
}
|
||||
if (accessibilityContext.accessibilityLabel.hasText()) {
|
||||
accessibilityDict[@"accessibilityLabel"] = SKObject(accessibilityContext.accessibilityLabel.value());
|
||||
}
|
||||
if (accessibilityContext.accessibilityHint.hasText()) {
|
||||
accessibilityDict[@"accessibilityHint"] = SKObject(accessibilityContext.accessibilityHint.value());
|
||||
}
|
||||
if (accessibilityContext.accessibilityValue.hasText()) {
|
||||
accessibilityDict[@"accessibilityValue"] = SKObject(accessibilityContext.accessibilityValue.value());
|
||||
}
|
||||
if (accessibilityContext.accessibilityTraits != nil) {
|
||||
accessibilityDict[@"accessibilityTraits"] = SKObject(@([accessibilityContext.accessibilityTraits integerValue]));
|
||||
}
|
||||
if (accessibilityContext.accessibilityComponentAction) {
|
||||
accessibilityDict[@"accessibilityComponentAction.identifier"] = SKObject(@(accessibilityContext.accessibilityComponentAction.identifier().c_str()));
|
||||
}
|
||||
return accessibilityDict;
|
||||
}
|
||||
|
||||
FB_LINKABLE(CKComponent_Sonar)
|
||||
@implementation CKComponent (Sonar)
|
||||
|
||||
- (NSString *)sonar_getName
|
||||
{
|
||||
if ([self respondsToSelector:@selector(sonar_componentNameOverride)]) {
|
||||
return [(id)self sonar_componentNameOverride];
|
||||
}
|
||||
return NSStringFromClass([self class]);
|
||||
}
|
||||
|
||||
- (NSString *)sonar_getDecoration
|
||||
{
|
||||
if ([self respondsToSelector:@selector(sonar_componentDecorationOverride)]) {
|
||||
return [(id)self sonar_componentDecorationOverride];
|
||||
}
|
||||
return @"componentkit";
|
||||
}
|
||||
|
||||
- (NSArray<SKNamed<NSDictionary<NSString *, NSObject *> *> *> *)sonar_getData
|
||||
{
|
||||
static NSDictionary<NSNumber *, NSString *> *UIControlEventsEnumMap = @{
|
||||
@(UIControlEventTouchDown): @"UIControlEventTouchDown",
|
||||
@(UIControlEventTouchDownRepeat): @"UIControlEventTouchDownRepeat",
|
||||
@(UIControlEventTouchDragInside): @"UIControlEventTouchDragInside",
|
||||
@(UIControlEventTouchDragOutside): @"UIControlEventTouchDragOutside",
|
||||
@(UIControlEventTouchDragEnter): @"UIControlEventTouchDragEnter",
|
||||
@(UIControlEventTouchDragExit): @"UIControlEventTouchDragExit",
|
||||
@(UIControlEventTouchUpInside): @"UIControlEventTouchUpInside",
|
||||
@(UIControlEventTouchUpOutside): @"UIControlEventTouchUpOutside",
|
||||
@(UIControlEventTouchCancel): @"UIControlEventTouchTouchCancel",
|
||||
|
||||
@(UIControlEventValueChanged): @"UIControlEventValueChanged",
|
||||
@(UIControlEventPrimaryActionTriggered): @"UIControlEventPrimaryActionTriggered",
|
||||
|
||||
@(UIControlEventEditingDidBegin): @"UIControlEventEditingDidBegin",
|
||||
@(UIControlEventEditingChanged): @"UIControlEventEditingChanged",
|
||||
@(UIControlEventEditingDidEnd): @"UIControlEventEditingDidEnd",
|
||||
@(UIControlEventEditingDidEndOnExit): @"UIControlEventEditingDidEndOnExit",
|
||||
};
|
||||
|
||||
|
||||
NSMutableArray<SKNamed<NSDictionary<NSString *, NSObject *> *> *> *data = [NSMutableArray new];
|
||||
|
||||
[data addObject: [SKNamed newWithName: @"CKComponent"
|
||||
withValue: @{
|
||||
@"frame": SKObject(self.viewContext.frame),
|
||||
@"controller": SKObject(NSStringFromClass([self.controller class])),
|
||||
}]];
|
||||
|
||||
if (self.viewContext.view) {
|
||||
auto _actions = _CKComponentDebugControlActionsForComponent(self);
|
||||
if (_actions.size() > 0) {
|
||||
NSMutableDictionary<NSString *, NSObject *> *actions = [NSMutableDictionary new];
|
||||
|
||||
for (NSNumber *controlEvent : [UIControlEventsEnumMap allKeys]) {
|
||||
NSMutableArray<NSDictionary<NSString *, NSObject *> *> *responders = [NSMutableArray new];
|
||||
|
||||
for (const auto action : _actions) {
|
||||
if ((action.first & [controlEvent integerValue]) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto responder : action.second) {
|
||||
id initialTarget = _CKTypedComponentDebugInitialTarget(responder).get(self);
|
||||
const CKActionInfo actionInfo = CKActionFind(responder.selector(), initialTarget);
|
||||
[responders addObject: @{
|
||||
@"initialTarget": SKObject(NSStringFromClass([initialTarget class])),
|
||||
@"identifier": SKObject(@(responder.identifier().c_str())),
|
||||
@"handler": SKObject(NSStringFromClass([actionInfo.responder class])),
|
||||
@"selector": SKObject(NSStringFromSelector(responder.selector())),
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
if (responders.count > 0) {
|
||||
actions[UIControlEventsEnumMap[controlEvent]] = responders;
|
||||
}
|
||||
}
|
||||
|
||||
[data addObject: [SKNamed newWithName: @"Actions" withValue: actions]];
|
||||
}
|
||||
}
|
||||
|
||||
// Only add accessibility panel if accessibilityContext is not default
|
||||
CKComponentAccessibilityContext accessibilityContext = [self viewConfiguration].accessibilityContext();
|
||||
if (!AccessibilityContextIsDefault(accessibilityContext)) {
|
||||
[data addObject:
|
||||
[SKNamed newWithName: @"Accessibility"
|
||||
withValue: @{
|
||||
@"accessibilityContext": AccessibilityContextDict(accessibilityContext),
|
||||
@"accessibilityEnabled": SKMutableObject(@(CK::Component::Accessibility::IsAccessibilityEnabled())),
|
||||
}]];
|
||||
}
|
||||
|
||||
if ([self respondsToSelector:@selector(sonar_additionalDataOverride)]) {
|
||||
[data addObjectsFromArray:[(id)self sonar_additionalDataOverride]];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, SKNodeUpdateData> *)sonar_getDataMutations {
|
||||
return @{
|
||||
@"CKComponentAccessibility.accessibilityEnabled": ^(NSNumber *value) {
|
||||
CK::Component::Accessibility::SetForceAccessibilityEnabled([value boolValue]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <ComponentKit/CKFlexboxComponent.h>
|
||||
#import <SonarKit/SKMacros.h>
|
||||
|
||||
FB_LINK_REQUIRE(CKFlexboxComponent_Sonar)
|
||||
@interface CKFlexboxComponent (Sonar)
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "CKFlexboxComponent+Sonar.h"
|
||||
|
||||
#import <SonarKitLayoutPlugin/SKNamed.h>
|
||||
#import <SonarKitLayoutPlugin/SKObject.h>
|
||||
|
||||
#import "CKComponent+Sonar.h"
|
||||
#import "Utils.h"
|
||||
|
||||
FB_LINKABLE(CKFlexboxComponent_Sonar)
|
||||
@implementation CKFlexboxComponent (Sonar)
|
||||
|
||||
- (NSArray<SKNamed<NSDictionary<NSString *, NSObject *> *> *> *)sonar_additionalDataOverride
|
||||
{
|
||||
static NSDictionary<NSNumber *, NSString *> *CKFlexboxDirectionEnumMap = @{
|
||||
@(CKFlexboxDirectionColumn): @"vertical",
|
||||
@(CKFlexboxDirectionRow): @"horizontal",
|
||||
@(CKFlexboxDirectionColumnReverse): @"vertical-reverse",
|
||||
@(CKFlexboxDirectionRowReverse): @"horizontal-reverse",
|
||||
};
|
||||
|
||||
static NSDictionary<NSNumber *, NSString *> *CKFlexboxJustifyContentEnumMap = @{
|
||||
@(CKFlexboxJustifyContentStart): @"start",
|
||||
@(CKFlexboxJustifyContentCenter): @"center",
|
||||
@(CKFlexboxJustifyContentEnd): @"end",
|
||||
@(CKFlexboxJustifyContentSpaceBetween): @"space-between",
|
||||
@(CKFlexboxJustifyContentSpaceAround): @"space-around",
|
||||
};
|
||||
|
||||
static NSDictionary<NSNumber *, NSString *> *CKFlexboxAlignItemsEnumMap = @{
|
||||
@(CKFlexboxAlignItemsStart): @"start",
|
||||
@(CKFlexboxAlignItemsEnd): @"end",
|
||||
@(CKFlexboxAlignItemsCenter): @"center",
|
||||
@(CKFlexboxAlignItemsBaseline): @"baseline",
|
||||
@(CKFlexboxAlignItemsStretch): @"stretch",
|
||||
};
|
||||
|
||||
static NSDictionary<NSNumber *, NSString *> *CKFlexboxAlignContentEnumMap = @{
|
||||
@(CKFlexboxAlignContentStart): @"start",
|
||||
@(CKFlexboxAlignContentEnd): @"end",
|
||||
@(CKFlexboxAlignContentCenter): @"center",
|
||||
@(CKFlexboxAlignContentSpaceBetween): @"space-between",
|
||||
@(CKFlexboxAlignContentSpaceAround): @"space-around",
|
||||
@(CKFlexboxAlignContentStretch): @"stretch",
|
||||
};
|
||||
|
||||
static NSDictionary<NSNumber *, NSString *> *CKFlexboxWrapEnumMap = @{
|
||||
@(CKFlexboxWrapWrap): @"wrap",
|
||||
@(CKFlexboxWrapNoWrap): @"no-wrap",
|
||||
@(CKFlexboxWrapWrapReverse): @"wrap-reverse",
|
||||
};
|
||||
|
||||
CKFlexboxComponentStyle style;
|
||||
[[self valueForKey: @"_style"] getValue: &style];
|
||||
|
||||
return @[[SKNamed
|
||||
newWithName:@"CKFlexboxComponent"
|
||||
withValue:@{
|
||||
@"spacing": SKObject(@(style.spacing)),
|
||||
@"direction": SKObject(CKFlexboxDirectionEnumMap[@(style.direction)]),
|
||||
@"justifyContent": SKObject(CKFlexboxJustifyContentEnumMap[@(style.justifyContent)]),
|
||||
@"alignItems": SKObject(CKFlexboxAlignItemsEnumMap[@(style.alignItems)]),
|
||||
@"alignContent": SKObject(CKFlexboxAlignContentEnumMap[@(style.alignContent)]),
|
||||
@"wrap": SKObject(CKFlexboxWrapEnumMap[@(style.wrap)]),
|
||||
@"margin": SKObject(flexboxRect(style.margin)),
|
||||
@"padding": SKObject(flexboxRect(style.padding)),
|
||||
}]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <ComponentKit/CKInsetComponent.h>
|
||||
#import <SonarKit/SKMacros.h>
|
||||
|
||||
FB_LINK_REQUIRE(CKInsetComponent_Sonar)
|
||||
@interface CKInsetComponent (Sonar)
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "CKInsetComponent+Sonar.h"
|
||||
|
||||
#import <SonarKitLayoutPlugin/SKNamed.h>
|
||||
#import <SonarKitLayoutPlugin/SKObject.h>
|
||||
|
||||
#import "CKComponent+Sonar.h"
|
||||
|
||||
FB_LINKABLE(CKInsetComponent_Sonar)
|
||||
@implementation CKInsetComponent (Sonar)
|
||||
|
||||
- (NSArray<SKNamed<NSDictionary<NSString *, NSObject *> *> *> *)sonar_additionalDataOverride
|
||||
{
|
||||
return @[[SKNamed newWithName:@"CKInsetComponent" withValue:@{@"insets": SKObject([[self valueForKey: @"_insets"] UIEdgeInsetsValue])}]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <SonarKitLayoutPlugin/SKNodeDescriptor.h>
|
||||
|
||||
@class CKComponentHostingView;
|
||||
|
||||
@interface SKComponentHostingViewDescriptor : SKNodeDescriptor<CKComponentHostingView *>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKComponentHostingViewDescriptor.h"
|
||||
|
||||
#import <ComponentKit/CKComponentDataSourceAttachController.h>
|
||||
#import <ComponentKit/CKComponentDataSourceAttachControllerInternal.h>
|
||||
#import <ComponentKit/CKComponentHostingView.h>
|
||||
#import <ComponentKit/CKComponentHostingViewInternal.h>
|
||||
#import <ComponentKit/CKComponentLayout.h>
|
||||
#import <ComponentKit/CKComponentHostingViewInternal.h>
|
||||
|
||||
#import <SonarKitLayoutPlugin/SKDescriptorMapper.h>
|
||||
|
||||
#import "SKComponentLayoutWrapper.h"
|
||||
|
||||
@implementation SKComponentHostingViewDescriptor
|
||||
|
||||
- (NSString *)identifierForNode:(CKComponentHostingView *)node {
|
||||
return [NSString stringWithFormat: @"%p", node];
|
||||
}
|
||||
|
||||
- (NSUInteger)childCountForNode:(CKComponentHostingView *)node {
|
||||
return node.mountedLayout.component ? 1 : 0;
|
||||
}
|
||||
|
||||
- (id)childForNode:(CKComponentHostingView *)node atIndex:(NSUInteger)index {
|
||||
return [SKComponentLayoutWrapper newFromRoot:node];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted forNode:(CKComponentHostingView *)node {
|
||||
SKNodeDescriptor *viewDescriptor = [self descriptorForClass: [UIView class]];
|
||||
[viewDescriptor setHighlighted: highlighted forNode: node];
|
||||
}
|
||||
|
||||
- (void)hitTest:(SKTouch *)touch forNode:(CKComponentHostingView *)node {
|
||||
[touch continueWithChildIndex: 0 withOffset: (CGPoint){ 0, 0 }];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <SonarKitLayoutPlugin/SKNodeDescriptor.h>
|
||||
|
||||
@class SKComponentLayoutWrapper;
|
||||
|
||||
@interface SKComponentLayoutDescriptor: SKNodeDescriptor<SKComponentLayoutWrapper *>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKComponentLayoutDescriptor.h"
|
||||
|
||||
#import <ComponentKit/CKComponent.h>
|
||||
#import <ComponentKit/CKComponentInternal.h>
|
||||
#import <ComponentKit/CKComponentActionInternal.h>
|
||||
#import <ComponentKit/CKComponentLayout.h>
|
||||
#import <ComponentKit/CKComponentRootView.h>
|
||||
#import <ComponentKit/CKComponentViewConfiguration.h>
|
||||
#import <ComponentKit/CKComponentAccessibility.h>
|
||||
#import <ComponentKit/CKComponentDebugController.h>
|
||||
#import <ComponentKit/CKInsetComponent.h>
|
||||
#import <ComponentKit/CKFlexboxComponent.h>
|
||||
|
||||
#import <SonarKitLayoutPlugin/SKHighlightOverlay.h>
|
||||
#import <SonarKitLayoutPlugin/SKObject.h>
|
||||
|
||||
#import "SKComponentLayoutWrapper.h"
|
||||
#import "CKComponent+Sonar.h"
|
||||
#import "Utils.h"
|
||||
|
||||
@implementation SKComponentLayoutDescriptor
|
||||
{
|
||||
NSDictionary<NSNumber *, NSString *> *CKFlexboxAlignSelfEnumMap;
|
||||
NSDictionary<NSNumber *, NSString *> *CKFlexboxPositionTypeEnumMap;
|
||||
}
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
[self initEnumMaps];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)initEnumMaps {
|
||||
CKFlexboxAlignSelfEnumMap = @{
|
||||
@(CKFlexboxAlignSelfAuto): @"auto",
|
||||
@(CKFlexboxAlignSelfStart): @"start",
|
||||
@(CKFlexboxAlignSelfEnd): @"end",
|
||||
@(CKFlexboxAlignSelfCenter): @"center",
|
||||
@(CKFlexboxAlignSelfBaseline): @"baseline",
|
||||
@(CKFlexboxAlignSelfStretch): @"stretch",
|
||||
};
|
||||
|
||||
CKFlexboxPositionTypeEnumMap = @{
|
||||
@(CKFlexboxPositionTypeRelative): @"relative",
|
||||
@(CKFlexboxPositionTypeAbsolute): @"absolute",
|
||||
};
|
||||
}
|
||||
|
||||
- (NSString *)identifierForNode:(SKComponentLayoutWrapper *)node {
|
||||
return node.identifier;
|
||||
}
|
||||
|
||||
- (NSString *)nameForNode:(SKComponentLayoutWrapper *)node {
|
||||
return [node.component sonar_getName];
|
||||
}
|
||||
|
||||
- (NSString *)decorationForNode:(SKComponentLayoutWrapper *)node {
|
||||
return [node.component sonar_getDecoration];
|
||||
}
|
||||
|
||||
- (NSUInteger)childCountForNode:(SKComponentLayoutWrapper *)node {
|
||||
NSUInteger count = node.children.size();
|
||||
if (count == 0) {
|
||||
count = node.component.viewContext.view ? 1 : 0;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
- (id)childForNode:(SKComponentLayoutWrapper *)node atIndex:(NSUInteger)index {
|
||||
if (node.children.size() == 0) {
|
||||
return node.component.viewContext.view;
|
||||
}
|
||||
return node.children[index];
|
||||
}
|
||||
|
||||
- (NSArray<SKNamed<NSDictionary<NSString *, NSObject *> *> *> *)dataForNode:(SKComponentLayoutWrapper *)node {
|
||||
NSMutableArray<SKNamed<NSDictionary<NSString *, NSObject *> *> *> *data = [NSMutableArray new];
|
||||
|
||||
if (node.isFlexboxChild) {
|
||||
[data addObject: [SKNamed newWithName:@"Layout" withValue:[self propsForFlexboxChild:node.flexboxChild]]];
|
||||
}
|
||||
|
||||
[data addObjectsFromArray:[node.component sonar_getData]];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, NSObject *> *)propsForFlexboxChild:(CKFlexboxComponentChild)child {
|
||||
return @{
|
||||
@"spacingBefore": SKObject(@(child.spacingBefore)),
|
||||
@"spacingAfter": SKObject(@(child.spacingAfter)),
|
||||
@"flexGrow": SKObject(@(child.flexGrow)),
|
||||
@"flexShrink": SKObject(@(child.flexShrink)),
|
||||
@"zIndex": SKObject(@(child.zIndex)),
|
||||
@"useTextRounding": SKObject(@(child.useTextRounding)),
|
||||
@"margin": flexboxRect(child.margin),
|
||||
@"flexBasis": relativeDimension(child.flexBasis),
|
||||
@"padding": flexboxRect(child.padding),
|
||||
@"alignSelf": CKFlexboxAlignSelfEnumMap[@(child.alignSelf)],
|
||||
@"position": @{
|
||||
@"type": CKFlexboxPositionTypeEnumMap[@(child.position.type)],
|
||||
@"start": relativeDimension(child.position.start),
|
||||
@"top": relativeDimension(child.position.top),
|
||||
@"end": relativeDimension(child.position.end),
|
||||
@"bottom": relativeDimension(child.position.bottom),
|
||||
@"left": relativeDimension(child.position.left),
|
||||
@"right": relativeDimension(child.position.right),
|
||||
},
|
||||
@"aspectRatio": @(child.aspectRatio.aspectRatio()),
|
||||
};
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, SKNodeUpdateData> *)dataMutationsForNode:(SKComponentLayoutWrapper *)node {
|
||||
return [node.component sonar_getDataMutations];
|
||||
}
|
||||
|
||||
- (NSArray<SKNamed<NSString *> *> *)attributesForNode:(SKComponentLayoutWrapper *)node {
|
||||
return @[
|
||||
[SKNamed newWithName: @"responder"
|
||||
withValue: SKObject(NSStringFromClass([node.component.nextResponder class]))]
|
||||
];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted forNode:(SKComponentLayoutWrapper *)node {
|
||||
SKHighlightOverlay *overlay = [SKHighlightOverlay sharedInstance];
|
||||
if (highlighted) {
|
||||
CKComponentViewContext viewContext = node.component.viewContext;
|
||||
[overlay mountInView: viewContext.view
|
||||
withFrame: viewContext.frame];
|
||||
} else {
|
||||
[overlay unmount];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)hitTest:(SKTouch *)touch forNode:(SKComponentLayoutWrapper *)node {
|
||||
if (node.children.size() == 0) {
|
||||
UIView *componentView = node.component.viewContext.view;
|
||||
if (componentView != nil) {
|
||||
if ([touch containedIn: componentView.bounds]) {
|
||||
[touch continueWithChildIndex: 0 withOffset: componentView.bounds.origin];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSInteger index = 0;
|
||||
for (index = node.children.size() - 1; index >= 0; index--) {
|
||||
const auto child = node.children[index];
|
||||
|
||||
CGRect frame = {
|
||||
.origin = child.position,
|
||||
.size = child.size
|
||||
};
|
||||
|
||||
if ([touch containedIn: frame]) {
|
||||
[touch continueWithChildIndex: index withOffset: child.position];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[touch finish];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <ComponentKit/CKComponentLayout.h>
|
||||
#import <ComponentKit/CKFlexboxComponent.h>
|
||||
|
||||
@protocol CKInspectableView;
|
||||
|
||||
@interface SKComponentLayoutWrapper : NSObject
|
||||
|
||||
@property (nonatomic, weak, readonly) CKComponent *component;
|
||||
@property (nonatomic, readonly) NSString *identifier;
|
||||
@property (nonatomic, readonly) CGSize size;
|
||||
@property (nonatomic, readonly) CGPoint position;
|
||||
@property (nonatomic, readonly) std::vector<SKComponentLayoutWrapper *> children;
|
||||
|
||||
// Null for layouts which are not direct children of a CKFlexboxComponent
|
||||
@property (nonatomic, readonly) BOOL isFlexboxChild;
|
||||
@property (nonatomic, readonly) CKFlexboxComponentChild flexboxChild;
|
||||
|
||||
+ (instancetype)newFromRoot:(id<CKInspectableView>)root;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKComponentLayoutWrapper.h"
|
||||
|
||||
#import <ComponentKit/CKComponent.h>
|
||||
#import <ComponentKit/CKComponentRootView.h>
|
||||
#import <ComponentKit/CKComponentDataSourceAttachController.h>
|
||||
#import <ComponentKit/CKComponentDataSourceAttachControllerInternal.h>
|
||||
#import <ComponentKit/CKInspectableView.h>
|
||||
|
||||
static char const kLayoutWrapperKey = ' ';
|
||||
|
||||
static CKFlexboxComponentChild findFlexboxLayoutParams(CKComponent *parent, CKComponent *child) {
|
||||
if ([parent isKindOfClass:[CKFlexboxComponent class]]) {
|
||||
static Ivar ivar = class_getInstanceVariable([CKFlexboxComponent class], "_children");
|
||||
static ptrdiff_t offset = ivar_getOffset(ivar);
|
||||
|
||||
unsigned char *pComponent = (unsigned char*)(__bridge void*)parent;
|
||||
auto children = (std::vector<CKFlexboxComponentChild> *)(pComponent + offset);
|
||||
|
||||
if (children) {
|
||||
for (auto it = children->begin(); it != children->end(); it++) {
|
||||
if (it->component == child) {
|
||||
return *it;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@implementation SKComponentLayoutWrapper
|
||||
|
||||
+ (instancetype)newFromRoot:(id<CKInspectableView>)root {
|
||||
const CKComponentLayout layout = [root mountedLayout];
|
||||
SKComponentLayoutWrapper *const wrapper =
|
||||
[[SKComponentLayoutWrapper alloc] initWithLayout:layout
|
||||
position:CGPointMake(0, 0)
|
||||
parentKey:[NSString stringWithFormat: @"%p.", layout.component]];
|
||||
if (layout.component)
|
||||
objc_setAssociatedObject(layout.component, &kLayoutWrapperKey, wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
- (instancetype)initWithLayout:(const CKComponentLayout &)layout position:(CGPoint)position parentKey:(NSString *)parentKey {
|
||||
if (self = [super init]) {
|
||||
_component = layout.component;
|
||||
_size = layout.size;
|
||||
_position = position;
|
||||
_identifier = [parentKey stringByAppendingString:layout.component ? NSStringFromClass([layout.component class]) : @"(null)"];
|
||||
|
||||
if (layout.children != nullptr) {
|
||||
int index = 0;
|
||||
for (const auto &child : *layout.children) {
|
||||
if (child.layout.component == nil) {
|
||||
continue; // nil children are allowed, ignore them
|
||||
}
|
||||
SKComponentLayoutWrapper *childWrapper = [[SKComponentLayoutWrapper alloc] initWithLayout:child.layout
|
||||
position:child.position
|
||||
parentKey:[_identifier stringByAppendingFormat:@"[%d].", index++]];
|
||||
childWrapper->_isFlexboxChild = [_component isKindOfClass:[CKFlexboxComponent class]];
|
||||
childWrapper->_flexboxChild = findFlexboxLayoutParams(_component, child.layout.component);
|
||||
_children.push_back(childWrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <SonarKitLayoutPlugin/SKNodeDescriptor.h>
|
||||
|
||||
@class CKComponentRootView;
|
||||
|
||||
@interface SKComponentRootViewDescriptor : SKNodeDescriptor<CKComponentRootView *>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKComponentRootViewDescriptor.h"
|
||||
|
||||
#import <ComponentKit/CKComponentDataSourceAttachController.h>
|
||||
#import <ComponentKit/CKComponentDataSourceAttachControllerInternal.h>
|
||||
#import <ComponentKit/CKComponentHostingView.h>
|
||||
#import <ComponentKit/CKComponentHostingViewInternal.h>
|
||||
#import <ComponentKit/CKComponentLayout.h>
|
||||
#import <ComponentKit/CKComponentRootViewInternal.h>
|
||||
|
||||
#import <SonarKitLayoutPlugin/SKDescriptorMapper.h>
|
||||
|
||||
#import "SKComponentLayoutWrapper.h"
|
||||
|
||||
@implementation SKComponentRootViewDescriptor
|
||||
|
||||
- (NSString *)identifierForNode:(CKComponentRootView *)node {
|
||||
return [NSString stringWithFormat: @"%p", node];
|
||||
}
|
||||
|
||||
- (NSUInteger)childCountForNode:(CKComponentRootView *)node {
|
||||
if ([node respondsToSelector:@selector(ck_attachState)]) {
|
||||
CKComponentDataSourceAttachState *state = [node ck_attachState];
|
||||
return state == nil ? 0 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (id)childForNode:(CKComponentRootView *)node atIndex:(NSUInteger)index {
|
||||
return [SKComponentLayoutWrapper newFromRoot:node];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted forNode:(CKComponentRootView *)node {
|
||||
SKNodeDescriptor *viewDescriptor = [self descriptorForClass: [UIView class]];
|
||||
[viewDescriptor setHighlighted: highlighted forNode: node];
|
||||
}
|
||||
|
||||
- (void)hitTest:(SKTouch *)touch forNode:(CKComponentRootView *)node {
|
||||
[touch continueWithChildIndex: 0 withOffset: (CGPoint){ 0, 0 }];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <SonarKitLayoutPlugin/SKDescriptorMapper.h>
|
||||
|
||||
@interface SonarKitLayoutComponentKitSupport : NSObject
|
||||
|
||||
+ (void)setUpWithDescriptorMapper:(SKDescriptorMapper *)mapper;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SonarKitLayoutComponentKitSupport.h"
|
||||
|
||||
#import <ComponentKit/CKComponentRootView.h>
|
||||
#import <ComponentKit/CKComponentHostingView.h>
|
||||
|
||||
#import <SonarKitLayoutPlugin/SKDescriptorMapper.h>
|
||||
|
||||
#import "SKComponentHostingViewDescriptor.h"
|
||||
#import "SKComponentRootViewDescriptor.h"
|
||||
#import "SKComponentLayoutDescriptor.h"
|
||||
#import "SKComponentLayoutWrapper.h"
|
||||
|
||||
@implementation SonarKitLayoutComponentKitSupport
|
||||
|
||||
+ (void)setUpWithDescriptorMapper:(SKDescriptorMapper *)mapper {
|
||||
// What we really want here is "forProtocol:@protocol(CKInspectableView)" but no such luck.
|
||||
[mapper registerDescriptor: [[SKComponentHostingViewDescriptor alloc] initWithDescriptorMapper: mapper]
|
||||
forClass: [CKComponentHostingView class]];
|
||||
[mapper registerDescriptor: [[SKComponentRootViewDescriptor alloc] initWithDescriptorMapper: mapper]
|
||||
forClass: [CKComponentRootView class]];
|
||||
[mapper registerDescriptor: [[SKComponentLayoutDescriptor alloc] initWithDescriptorMapper: mapper]
|
||||
forClass: [SKComponentLayoutWrapper class]];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <ComponentKit/CKComponent.h>
|
||||
#import <ComponentKit/CKFlexboxComponent.h>
|
||||
|
||||
NSString *relativeDimension(CKRelativeDimension dimension);
|
||||
NSDictionary<NSString *, NSString *> *flexboxRect(CKFlexboxSpacing spacing);
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#include "Utils.h"
|
||||
|
||||
NSString *relativeDimension(CKRelativeDimension dimension) {
|
||||
switch(dimension.type()) {
|
||||
case CKRelativeDimension::Type::PERCENT:
|
||||
return [NSString stringWithFormat: @"%@%%", @(dimension.value())];
|
||||
case CKRelativeDimension::Type::POINTS:
|
||||
return [NSString stringWithFormat: @"%@pt", @(dimension.value())];
|
||||
default:
|
||||
return @"auto";
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, NSString *> *flexboxRect(CKFlexboxSpacing spacing) {
|
||||
return @{
|
||||
@"top": relativeDimension(spacing.top.dimension()),
|
||||
@"bottom": relativeDimension(spacing.bottom.dimension()),
|
||||
@"start": relativeDimension(spacing.start.dimension()),
|
||||
@"end": relativeDimension(spacing.end.dimension())
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class SKNodeDescriptor;
|
||||
|
||||
@interface SKDescriptorMapper : NSObject
|
||||
|
||||
- (instancetype)initWithDefaults;
|
||||
|
||||
- (SKNodeDescriptor *)descriptorForClass:(Class)cls;
|
||||
|
||||
- (void)registerDescriptor:(SKNodeDescriptor *)descriptor forClass:(Class)cls;
|
||||
|
||||
- (NSArray<SKNodeDescriptor *> *)allDescriptors;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKDescriptorMapper.h"
|
||||
|
||||
#import "SKApplicationDescriptor.h"
|
||||
#import "SKButtonDescriptor.h"
|
||||
#import "SKScrollViewDescriptor.h"
|
||||
#import "SKViewControllerDescriptor.h"
|
||||
#import "SKViewDescriptor.h"
|
||||
|
||||
@implementation SKDescriptorMapper
|
||||
{
|
||||
NSMutableDictionary<NSString *, SKNodeDescriptor *> *_descriptors;
|
||||
}
|
||||
|
||||
- (instancetype)initWithDefaults {
|
||||
if (self = [super init]) {
|
||||
_descriptors = [NSMutableDictionary new];
|
||||
|
||||
[self registerDescriptor: [[SKApplicationDescriptor alloc] initWithDescriptorMapper: self]
|
||||
forClass: [UIApplication class]];
|
||||
[self registerDescriptor: [[SKViewControllerDescriptor alloc] initWithDescriptorMapper: self]
|
||||
forClass: [UIViewController class]];
|
||||
[self registerDescriptor: [[SKScrollViewDescriptor alloc] initWithDescriptorMapper: self]
|
||||
forClass: [UIScrollView class]];
|
||||
[self registerDescriptor: [[SKButtonDescriptor alloc] initWithDescriptorMapper: self]
|
||||
forClass: [UIButton class]];
|
||||
[self registerDescriptor: [[SKViewDescriptor alloc] initWithDescriptorMapper: self]
|
||||
forClass: [UIView class]];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (SKNodeDescriptor *)descriptorForClass:(Class)cls {
|
||||
SKNodeDescriptor *classDescriptor = nil;
|
||||
|
||||
while (classDescriptor == nil && cls != nil) {
|
||||
classDescriptor = [_descriptors objectForKey: NSStringFromClass(cls)];
|
||||
cls = [cls superclass];
|
||||
}
|
||||
|
||||
return classDescriptor;
|
||||
}
|
||||
|
||||
- (void)registerDescriptor:(SKNodeDescriptor *)descriptor forClass:(Class)cls {
|
||||
NSString *className = NSStringFromClass(cls);
|
||||
_descriptors[className] = descriptor;
|
||||
}
|
||||
|
||||
- (NSArray<SKNodeDescriptor *> *)allDescriptors {
|
||||
return [_descriptors allValues];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface SKHighlightOverlay : NSObject
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
+ (UIColor*)overlayColor;
|
||||
|
||||
- (void)mountInView:(UIView *)view withFrame:(CGRect)frame;
|
||||
- (void)unmount;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKHighlightOverlay.h"
|
||||
|
||||
@implementation SKHighlightOverlay
|
||||
{
|
||||
CALayer *_overlayLayer;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static SKHighlightOverlay *sharedInstance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedInstance = [self new];
|
||||
});
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_overlayLayer = [CALayer layer];
|
||||
_overlayLayer.backgroundColor = [SKHighlightOverlay overlayColor].CGColor;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)mountInView:(UIView *)view withFrame:(CGRect)frame {
|
||||
[CATransaction begin];
|
||||
[CATransaction setValue:(id)kCFBooleanTrue
|
||||
forKey:kCATransactionDisableActions];
|
||||
_overlayLayer.frame = frame;
|
||||
[view.layer addSublayer: _overlayLayer];
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
- (void)unmount {
|
||||
[_overlayLayer removeFromSuperlayer];
|
||||
}
|
||||
|
||||
+ (UIColor*)overlayColor {
|
||||
return [UIColor colorWithRed:136.0 / 255.0 green:117.0 / 255.0 blue:197.0 / 255.0 alpha:0.6];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol SKInvalidationDelegate
|
||||
|
||||
- (void)invalidateNode:(id<NSObject>)node;
|
||||
|
||||
- (void)updateNodeReference:(id<NSObject>)node;
|
||||
|
||||
@end
|
||||
|
||||
@interface SKInvalidation : NSObject
|
||||
|
||||
+ (instancetype)sharedInstance;
|
||||
|
||||
+ (void)enableInvalidations;
|
||||
|
||||
@property (nonatomic, weak) id<SKInvalidationDelegate> delegate;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "SKInvalidation.h"
|
||||
#import "UIView+SKInvalidation.h"
|
||||
#import "UICollectionView+SKInvalidation.h"
|
||||
|
||||
@implementation SKInvalidation
|
||||
|
||||
+ (instancetype)sharedInstance {
|
||||
static SKInvalidation *sInstance = nil;
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sInstance = [SKInvalidation new];
|
||||
});
|
||||
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
+ (void)enableInvalidations {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
[UIView enableInvalidation];
|
||||
[UICollectionView enableInvalidations];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(windowDidBecomeVisible:)
|
||||
name:UIWindowDidBecomeVisibleNotification
|
||||
object:nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(windowDidBecomeHidden:)
|
||||
name:UIWindowDidBecomeHiddenNotification
|
||||
object:nil];
|
||||
});
|
||||
}
|
||||
|
||||
+ (void)windowDidBecomeVisible:(NSNotification*)notification {
|
||||
[[SKInvalidation sharedInstance].delegate invalidateNode:[notification.object nextResponder]];
|
||||
}
|
||||
|
||||
+ (void)windowDidBecomeHidden:(NSNotification*)notification {
|
||||
[[SKInvalidation sharedInstance].delegate invalidateNode:[notification.object nextResponder]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface SKNamed<__covariant T> : NSObject
|
||||
|
||||
+ (instancetype)newWithName:(NSString *)name withValue:(T)value;
|
||||
|
||||
@property (nonatomic, readonly) NSString *name;
|
||||
@property (nonatomic, readonly) T value;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKNamed.h"
|
||||
|
||||
@implementation SKNamed
|
||||
|
||||
+ (instancetype)newWithName:(NSString *)name withValue:(id)value {
|
||||
return [[SKNamed alloc] initWithName: name withValue: value];
|
||||
}
|
||||
|
||||
- (instancetype)initWithName:(NSString *)name withValue:(id)value {
|
||||
if (self = [super init]) {
|
||||
_name = name;
|
||||
_value = value;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"%@: %@", _name, _value];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "SKDescriptorMapper.h"
|
||||
#import "SKNamed.h"
|
||||
#import "SKTouch.h"
|
||||
|
||||
typedef void (^SKNodeUpdateData)(id value);
|
||||
|
||||
/**
|
||||
A SKNodeDescriptor is an object which know how to expose an Object of type T
|
||||
to SonarKitLayoutPlugin. This class is the extension point for SonarKitLayoutPlugin and
|
||||
is how custom objects or data can be exposed to Sonar.
|
||||
*/
|
||||
@interface SKNodeDescriptor<__covariant T> : NSObject
|
||||
|
||||
/**
|
||||
If the descriptor class is dependent on some set-up, use this.
|
||||
This is invoked once Sonar connects.
|
||||
*/
|
||||
- (void)setUp;
|
||||
|
||||
/**
|
||||
Initializes the node-descriptor with a SKDescriptorMapper which contains mappings
|
||||
between Class -> SKNodeDescriptor<Class>.
|
||||
*/
|
||||
- (instancetype)initWithDescriptorMapper:(SKDescriptorMapper *)mapper;
|
||||
|
||||
/**
|
||||
Gets the node-descriptor registered for a specific class.
|
||||
*/
|
||||
- (SKNodeDescriptor *)descriptorForClass:(Class)cls;
|
||||
|
||||
/**
|
||||
A globally unique ID used to identify a node in the hierarchy. This is used
|
||||
in the communication between SonarKitLayoutPlugin and the Sonar desktop application
|
||||
in order to identify nodes.
|
||||
*/
|
||||
- (NSString *)identifierForNode:(T)node;
|
||||
|
||||
/**
|
||||
The name used to identify this node in the Sonar desktop application. This is what
|
||||
will be visible in the hierarchy.
|
||||
*/
|
||||
- (NSString *)nameForNode:(T)node;
|
||||
|
||||
/**
|
||||
The number of children this node exposes in the layout hierarchy.
|
||||
*/
|
||||
- (NSUInteger)childCountForNode:(T)node;
|
||||
|
||||
/**
|
||||
Get the child for a specific node at a specified index.
|
||||
*/
|
||||
- (id)childForNode:(T)node atIndex:(NSUInteger)index;
|
||||
|
||||
/**
|
||||
Get the data to show for this node in the sidebar of the Sonar application. The objects
|
||||
will be shown in order by SKNamed.name as their header.
|
||||
*/
|
||||
- (NSArray<SKNamed<NSDictionary *> *> *)dataForNode:(T)node;
|
||||
|
||||
/**
|
||||
Get the attributes for this node. Attributes will be showed in the Sonar application right
|
||||
next to the name of the node.
|
||||
*/
|
||||
- (NSArray<SKNamed<NSString *> *> *)attributesForNode:(T)node;
|
||||
|
||||
/**
|
||||
A mapping of the path for a specific value, and a block responsible for updating
|
||||
its corresponding value for a specific node.
|
||||
|
||||
The paths (string) is dependent on what `dataForNode` returns (e.g "SKNodeDescriptor.name").
|
||||
*/
|
||||
- (NSDictionary<NSString *, SKNodeUpdateData> *)dataMutationsForNode:(T)node;
|
||||
|
||||
/**
|
||||
This is used in order to highlight any specific node which is currently
|
||||
selected in the Sonar application. The plugin automatically takes care of de-selecting
|
||||
the previously highlighted node.
|
||||
*/
|
||||
- (void)setHighlighted:(BOOL)highlighted forNode:(T)node;
|
||||
|
||||
/**
|
||||
Perform hit testing on the given node. Either continue the search in
|
||||
one of the children of the node, or finish the hit testing on this
|
||||
node.
|
||||
*/
|
||||
- (void)hitTest:(SKTouch *)point forNode:(T)node;
|
||||
|
||||
/**
|
||||
Invalidate a specific node. This is called once a node is removed or added
|
||||
from or to the layout hierarchy.
|
||||
*/
|
||||
- (void)invalidateNode:(T)node;
|
||||
|
||||
/**
|
||||
The decoration for this node. Valid values are defined in the Sonar
|
||||
applictation.
|
||||
*/
|
||||
- (NSString *)decorationForNode:(T)node;
|
||||
|
||||
/**
|
||||
Whether the node matches the given query.
|
||||
Used for layout search.
|
||||
*/
|
||||
- (BOOL)matchesQuery:(NSString *)query forNode:(T)node;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKNodeDescriptor.h"
|
||||
|
||||
@implementation SKNodeDescriptor
|
||||
{
|
||||
SKDescriptorMapper *_mapper;
|
||||
}
|
||||
|
||||
- (void)setUp {
|
||||
}
|
||||
|
||||
- (instancetype)initWithDescriptorMapper:(SKDescriptorMapper *)mapper {
|
||||
if (self = [super init]) {
|
||||
_mapper = mapper;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (SKNodeDescriptor *)descriptorForClass:(Class)cls {
|
||||
return [_mapper descriptorForClass: cls];
|
||||
}
|
||||
|
||||
- (NSString *)identifierForNode:(id)node {
|
||||
@throw [NSString stringWithFormat:@"need to implement %@", NSStringFromSelector(_cmd)];
|
||||
}
|
||||
|
||||
- (NSString *)nameForNode:(id)node {
|
||||
return NSStringFromClass([node class]);
|
||||
}
|
||||
|
||||
- (NSUInteger)childCountForNode:(id)node {
|
||||
@throw [NSString stringWithFormat:@"need to implement %@", NSStringFromSelector(_cmd)];
|
||||
}
|
||||
|
||||
- (id)childForNode:(id)node atIndex:(NSUInteger)index {
|
||||
@throw [NSString stringWithFormat:@"need to implement %@", NSStringFromSelector(_cmd)];
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, SKNodeUpdateData> *)dataMutationsForNode:(id)node {
|
||||
return @{};
|
||||
}
|
||||
|
||||
- (NSArray<SKNamed<NSDictionary *> *> *)dataForNode:(id)node {
|
||||
return @[];
|
||||
}
|
||||
|
||||
- (NSArray<SKNamed<NSString *> *> *)attributesForNode:(id)node {
|
||||
return @[];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted forNode:(id)node {
|
||||
}
|
||||
|
||||
- (void)hitTest:(SKTouch *)point forNode:(id)node {
|
||||
}
|
||||
|
||||
- (void)invalidateNode:(id)node {
|
||||
}
|
||||
|
||||
- (NSString *)decorationForNode:(id)node {
|
||||
return @"";
|
||||
}
|
||||
|
||||
- (BOOL)matchesQuery:(NSString *)query forNode:(id)node {
|
||||
NSString *name = [self nameForNode: node];
|
||||
return [self string:name contains:query] || [self string:[self identifierForNode: node] contains: query];
|
||||
}
|
||||
|
||||
- (BOOL)string:(NSString *)string contains:(NSString *)substring {
|
||||
return string != nil && substring != nil && [string rangeOfString: substring options: NSCaseInsensitiveSearch].location != NSNotFound;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol SKSonarValueCoder
|
||||
|
||||
+ (instancetype)fromSonarValue:(id)sonarValue;
|
||||
|
||||
- (NSDictionary<NSString *, id<NSObject>> *)sonarValue;
|
||||
|
||||
@end
|
||||
|
||||
class SKObject {
|
||||
public:
|
||||
SKObject(CGRect rect);
|
||||
SKObject(CGSize size);
|
||||
SKObject(CGPoint point);
|
||||
SKObject(UIEdgeInsets insets);
|
||||
SKObject(CGAffineTransform transform);
|
||||
SKObject(id<SKSonarValueCoder> value);
|
||||
SKObject(id value);
|
||||
|
||||
operator id<NSObject> () const noexcept {
|
||||
return _actual ?: [NSNull null];
|
||||
}
|
||||
protected:
|
||||
id<NSObject> _actual;
|
||||
};
|
||||
|
||||
class SKMutableObject : public SKObject {
|
||||
public:
|
||||
SKMutableObject(CGRect rect) : SKObject(rect) { }
|
||||
SKMutableObject(CGSize size) : SKObject(size) { };
|
||||
SKMutableObject(CGPoint point) : SKObject(point) { };
|
||||
SKMutableObject(UIEdgeInsets insets) : SKObject(insets) { };
|
||||
SKMutableObject(CGAffineTransform transform) : SKObject(transform) { };
|
||||
SKMutableObject(id<SKSonarValueCoder> value) : SKObject(value) { };
|
||||
SKMutableObject(id value) : SKObject(value) { };
|
||||
|
||||
operator id<NSObject> () {
|
||||
convertToMutable();
|
||||
return _actual;
|
||||
}
|
||||
protected:
|
||||
BOOL _convertedToMutable = NO;
|
||||
void convertToMutable();
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKObject.h"
|
||||
|
||||
SKObject::SKObject(CGRect rect) {
|
||||
_actual = @{
|
||||
@"origin": SKObject(rect.origin),
|
||||
@"size": SKObject(rect.size)
|
||||
};
|
||||
}
|
||||
|
||||
SKObject::SKObject(CGSize size) {
|
||||
_actual = @{
|
||||
@"height": @(size.height),
|
||||
@"width": @(size.width)
|
||||
};
|
||||
}
|
||||
|
||||
SKObject::SKObject(CGPoint point) {
|
||||
_actual = @{
|
||||
@"x": @(point.x),
|
||||
@"y": @(point.y)
|
||||
};
|
||||
}
|
||||
|
||||
SKObject::SKObject(UIEdgeInsets insets) {
|
||||
_actual = @{
|
||||
@"top": @(insets.top),
|
||||
@"bottom": @(insets.bottom),
|
||||
@"left": @(insets.left),
|
||||
@"right": @(insets.right),
|
||||
};
|
||||
}
|
||||
|
||||
SKObject::SKObject(CGAffineTransform transform) {
|
||||
_actual = @{
|
||||
@"a": @(transform.a),
|
||||
@"b": @(transform.b),
|
||||
@"c": @(transform.c),
|
||||
@"d": @(transform.d),
|
||||
@"tx": @(transform.tx),
|
||||
@"ty": @(transform.ty),
|
||||
};
|
||||
}
|
||||
|
||||
SKObject::SKObject(id<SKSonarValueCoder> value) : _actual([value sonarValue]) { }
|
||||
|
||||
SKObject::SKObject(id value) : _actual(value) { }
|
||||
|
||||
static NSString *_objectType(id<NSObject> object) {
|
||||
if ([object isKindOfClass: [NSDictionary class]]) {
|
||||
return (NSString *)((NSDictionary *)object)[@"__type__"];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
static id<NSObject> _objectValue(id<NSObject> object) {
|
||||
if ([object isKindOfClass: [NSDictionary class]]) {
|
||||
return ((NSDictionary *)object)[@"value"];
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
static NSDictionary<NSString *, id<NSObject>> *_SKValue(id<NSObject> object, BOOL isMutable) {
|
||||
NSString *type = _objectType(object);
|
||||
id<NSObject> value = _objectValue(object);
|
||||
|
||||
return @{
|
||||
@"__type__": (type != nil ? type : @"auto"),
|
||||
@"__mutable__": @(isMutable),
|
||||
@"value": (value != nil ? value : [NSNull null]),
|
||||
};
|
||||
}
|
||||
|
||||
static NSDictionary *_SKMutable(const NSDictionary<NSString *, id<NSObject>> *skObject) {
|
||||
NSMutableDictionary *mutableObject = [NSMutableDictionary new];
|
||||
for (NSString *key: skObject) {
|
||||
id<NSObject> value = skObject[key];
|
||||
|
||||
if (_objectType(value) != nil) {
|
||||
mutableObject[key] = _SKValue(value, YES);
|
||||
} else if ([value isKindOfClass: [NSDictionary class]]) {
|
||||
auto objectValue = (NSDictionary<NSString *, id<NSObject>>*) value;
|
||||
mutableObject[key] = _SKMutable(objectValue);
|
||||
} else {
|
||||
mutableObject[key] = _SKValue(value, YES);
|
||||
}
|
||||
}
|
||||
|
||||
return mutableObject;
|
||||
}
|
||||
|
||||
void SKMutableObject::convertToMutable() {
|
||||
if (_convertedToMutable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_objectType(_actual) == nil && [_actual isKindOfClass: [NSDictionary class]]) {
|
||||
auto object = (const NSDictionary<NSString *, id<NSObject>> *)_actual;
|
||||
_actual = _SKMutable(object);
|
||||
} else {
|
||||
_actual = _SKValue(_actual, YES);
|
||||
}
|
||||
|
||||
_convertedToMutable = YES;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2004-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#ifndef SKSearchResultNode_h
|
||||
#define SKSearchResultNode_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface SKSearchResultNode : NSObject
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *nodeId;
|
||||
|
||||
- (instancetype)initWithNode:(NSString *)nodeId
|
||||
asMatch:(BOOL)isMatch
|
||||
withElement:(NSDictionary *)element
|
||||
andChildren:(NSArray<SKSearchResultNode *> *)children;
|
||||
|
||||
- (NSDictionary *)toNSDictionary;
|
||||
|
||||
@end
|
||||
#endif /* SKSearchResultNode_h */
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import "SKSearchResultNode.h"
|
||||
|
||||
@implementation SKSearchResultNode {
|
||||
NSString *_nodeId;
|
||||
BOOL _isMatch;
|
||||
NSDictionary *_element;
|
||||
NSArray<SKSearchResultNode *> *_children;
|
||||
}
|
||||
|
||||
- (instancetype)initWithNode:(NSString *)nodeId
|
||||
asMatch:(BOOL)isMatch
|
||||
withElement:(NSDictionary *)element
|
||||
andChildren:(NSArray<SKSearchResultNode *> *)children {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_nodeId = nodeId;
|
||||
_isMatch = isMatch;
|
||||
_element = element;
|
||||
_children = children;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSDictionary *)toNSDictionary {
|
||||
if (_element == nil) {
|
||||
return nil;
|
||||
}
|
||||
NSMutableArray<NSDictionary *> *childArray;
|
||||
if (_children) {
|
||||
childArray = [NSMutableArray new];
|
||||
for (SKSearchResultNode *child in _children) {
|
||||
NSDictionary *childDict = [child toNSDictionary];
|
||||
if (childDict) {
|
||||
[childArray addObject:childDict];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
childArray = nil;
|
||||
}
|
||||
return @{
|
||||
@"id": _nodeId,
|
||||
@"isMatch": @(_isMatch),
|
||||
@"element": _element,
|
||||
@"children": childArray ?: [NSNull null]
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef void (^SKTapReceiver)(CGPoint touchPoint);
|
||||
|
||||
@protocol SKTapListener
|
||||
|
||||
@property (nonatomic, readonly) BOOL isMounted;
|
||||
|
||||
- (void)mountWithFrame:(CGRect)frame;
|
||||
|
||||
- (void)unmount;
|
||||
|
||||
- (void)listenForTapWithBlock:(SKTapReceiver)receiver;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import "SKTapListener.h"
|
||||
|
||||
@interface SKTapListenerImpl : NSObject<SKTapListener, UIGestureRecognizerDelegate>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKTapListenerImpl.h"
|
||||
|
||||
#import "SKHighlightOverlay.h"
|
||||
#import "SKHiddenWindow.h"
|
||||
|
||||
@implementation SKTapListenerImpl
|
||||
{
|
||||
NSMutableArray<SKTapReceiver> *_receiversWaitingForInput;
|
||||
UITapGestureRecognizer *_gestureRecognizer;
|
||||
|
||||
SKHiddenWindow *_overlayWindow;
|
||||
}
|
||||
|
||||
@synthesize isMounted = _isMounted;
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_receiversWaitingForInput = [NSMutableArray new];
|
||||
|
||||
_gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget: self action: nil];
|
||||
_gestureRecognizer.delegate = self;
|
||||
|
||||
_isMounted = NO;
|
||||
|
||||
_overlayWindow = [SKHiddenWindow new];
|
||||
_overlayWindow.hidden = YES;
|
||||
_overlayWindow.windowLevel = UIWindowLevelAlert;
|
||||
_overlayWindow.backgroundColor = [SKHighlightOverlay overlayColor];
|
||||
|
||||
[_overlayWindow addGestureRecognizer: _gestureRecognizer];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)mountWithFrame:(CGRect)frame {
|
||||
if (_isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
[_overlayWindow setFrame: frame];
|
||||
[_overlayWindow makeKeyAndVisible];
|
||||
_overlayWindow.hidden = NO;
|
||||
|
||||
_isMounted = YES;
|
||||
}
|
||||
|
||||
- (void)unmount {
|
||||
if (!_isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
[_receiversWaitingForInput removeAllObjects];
|
||||
[_overlayWindow removeFromSuperview];
|
||||
_overlayWindow.hidden = YES;
|
||||
|
||||
_isMounted = NO;
|
||||
}
|
||||
|
||||
- (void)listenForTapWithBlock:(SKTapReceiver)receiver {
|
||||
[_receiversWaitingForInput addObject: receiver];
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
|
||||
if ([_receiversWaitingForInput count] == 0) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
CGPoint touchPoint = [touch locationInView: _overlayWindow];
|
||||
|
||||
for (SKTapReceiver recv in _receiversWaitingForInput) {
|
||||
recv(touchPoint);
|
||||
}
|
||||
|
||||
[_receiversWaitingForInput removeAllObjects];
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "SKDescriptorMapper.h"
|
||||
|
||||
typedef void (^SKTouchFinishDelegate)(NSArray<NSString *> *path);
|
||||
|
||||
@interface SKTouch : NSObject
|
||||
|
||||
- (instancetype)initWithTouchPoint:(CGPoint)touchPoint
|
||||
withRootNode:(id<NSObject>)node
|
||||
withDescriptorMapper:(SKDescriptorMapper *)mapper
|
||||
finishWithBlock:(SKTouchFinishDelegate)d;
|
||||
|
||||
- (void)continueWithChildIndex:(NSUInteger)childIndex
|
||||
withOffset:(CGPoint)offset;
|
||||
|
||||
- (void)finish;
|
||||
|
||||
- (BOOL)containedIn:(CGRect)bounds;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKTouch.h"
|
||||
#import "SKNodeDescriptor.h"
|
||||
|
||||
@implementation SKTouch
|
||||
{
|
||||
SKTouchFinishDelegate _onFinish;
|
||||
NSMutableArray<NSString *> *_path;
|
||||
|
||||
CGPoint _currentTouchPoint;
|
||||
id<NSObject> _currentNode;
|
||||
|
||||
SKDescriptorMapper *_descriptorMapper;
|
||||
}
|
||||
|
||||
- (instancetype)initWithTouchPoint:(CGPoint)touchPoint
|
||||
withRootNode:(id<NSObject>)node
|
||||
withDescriptorMapper:(SKDescriptorMapper *)mapper
|
||||
finishWithBlock:(SKTouchFinishDelegate)finishBlock {
|
||||
if (self = [super init]) {
|
||||
_onFinish = finishBlock;
|
||||
_currentTouchPoint = touchPoint;
|
||||
_currentNode = node;
|
||||
_descriptorMapper = mapper;
|
||||
_path = [NSMutableArray new];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)continueWithChildIndex:(NSUInteger)childIndex withOffset:(CGPoint)offset {
|
||||
_currentTouchPoint.x -= offset.x;
|
||||
_currentTouchPoint.y -= offset.y;
|
||||
|
||||
SKNodeDescriptor *descriptor = [_descriptorMapper descriptorForClass: [_currentNode class]];
|
||||
_currentNode = [descriptor childForNode: _currentNode atIndex: childIndex];
|
||||
|
||||
descriptor = [_descriptorMapper descriptorForClass: [_currentNode class]];
|
||||
[_path addObject: [descriptor identifierForNode: _currentNode]];
|
||||
|
||||
[descriptor hitTest: self forNode: _currentNode];
|
||||
}
|
||||
|
||||
- (void)finish {
|
||||
_onFinish(_path);
|
||||
}
|
||||
|
||||
- (BOOL)containedIn:(CGRect)bounds {
|
||||
return CGRectContainsPoint(bounds, _currentTouchPoint);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <SonarKit/SonarPlugin.h>
|
||||
|
||||
#import "SKTapListener.h"
|
||||
#import "SKInvalidation.h"
|
||||
#import "SKDescriptorMapper.h"
|
||||
|
||||
@interface SonarKitLayoutPlugin : NSObject<SonarPlugin, SKInvalidationDelegate>
|
||||
|
||||
- (instancetype)initWithRootNode:(id<NSObject>)rootNode
|
||||
withDescriptorMapper:(SKDescriptorMapper *)mapper;
|
||||
|
||||
- (instancetype)initWithRootNode:(id<NSObject>)rootNode
|
||||
withTapListener:(id<SKTapListener>)tapListener
|
||||
withDescriptorMapper:(SKDescriptorMapper *)mapper;
|
||||
|
||||
@property (nonatomic, readonly, strong) SKDescriptorMapper *descriptorMapper;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,414 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SonarKitLayoutPlugin.h"
|
||||
|
||||
#import <SonarKit/SonarClient.h>
|
||||
#import <SonarKit/SonarConnection.h>
|
||||
#import <SonarKit/SonarResponder.h>
|
||||
#import <SonarKit/SKMacros.h>
|
||||
#import "SKDescriptorMapper.h"
|
||||
#import "SKNodeDescriptor.h"
|
||||
#import "SKTapListener.h"
|
||||
#import "SKTapListenerImpl.h"
|
||||
#import "SKSearchResultNode.h"
|
||||
#import <mutex>
|
||||
|
||||
@implementation SonarKitLayoutPlugin
|
||||
{
|
||||
|
||||
NSMapTable<NSString *, id> *_trackedObjects;
|
||||
NSString *_lastHighlightedNode;
|
||||
NSMutableSet *_invalidObjects;
|
||||
Boolean _invalidateMessageQueued;
|
||||
NSDate *_lastInvalidateMessage;
|
||||
std::mutex invalidObjectsMutex;
|
||||
|
||||
id<NSObject> _rootNode;
|
||||
id<SKTapListener> _tapListener;
|
||||
|
||||
id<SonarConnection> _connection;
|
||||
|
||||
NSMutableSet *_registeredDelegates;
|
||||
}
|
||||
|
||||
- (instancetype)initWithRootNode:(id<NSObject>)rootNode
|
||||
withDescriptorMapper:(SKDescriptorMapper *)mapper{
|
||||
return [self initWithRootNode: rootNode
|
||||
withTapListener: [SKTapListenerImpl new]
|
||||
withDescriptorMapper: mapper];
|
||||
}
|
||||
|
||||
- (instancetype)initWithRootNode:(id<NSObject>)rootNode
|
||||
withTapListener:(id<SKTapListener>)tapListener
|
||||
withDescriptorMapper:(SKDescriptorMapper *)mapper {
|
||||
if (self = [super init]) {
|
||||
_descriptorMapper = mapper;
|
||||
_trackedObjects = [NSMapTable strongToWeakObjectsMapTable];
|
||||
_lastHighlightedNode = nil;
|
||||
_invalidObjects = [NSMutableSet new];
|
||||
_invalidateMessageQueued = false;
|
||||
_lastInvalidateMessage = [NSDate date];
|
||||
_rootNode = rootNode;
|
||||
_tapListener = tapListener;
|
||||
|
||||
_registeredDelegates = [NSMutableSet new];
|
||||
[SKInvalidation sharedInstance].delegate = self;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)identifier
|
||||
{
|
||||
return @"Inspector";
|
||||
}
|
||||
|
||||
- (void)didConnect:(id<SonarConnection>)connection {
|
||||
_connection = connection;
|
||||
|
||||
[SKInvalidation enableInvalidations];
|
||||
|
||||
// Run setup logic for each descriptor
|
||||
for (SKNodeDescriptor *descriptor in _descriptorMapper.allDescriptors) {
|
||||
[descriptor setUp];
|
||||
}
|
||||
|
||||
// In order to avoid a retain cycle (Connection -> Block -> SonarKitLayoutPlugin -> Connection ...)
|
||||
__weak SonarKitLayoutPlugin *weakSelf = self;
|
||||
|
||||
[connection receive:@"getRoot" withBlock:^(NSDictionary *params, id<SonarResponder> responder) {
|
||||
SonarPerformBlockOnMainThread(^{ [weakSelf onCallGetRoot: responder]; });
|
||||
}];
|
||||
|
||||
[connection receive:@"getNodes" withBlock:^(NSDictionary *params, id<SonarResponder> responder) {
|
||||
SonarPerformBlockOnMainThread(^{ [weakSelf onCallGetNodes: params[@"ids"] withResponder: responder]; });
|
||||
}];
|
||||
|
||||
[connection receive:@"setData" withBlock:^(NSDictionary *params, id<SonarResponder> responder) {
|
||||
SonarPerformBlockOnMainThread(^{
|
||||
[weakSelf onCallSetData: params[@"id"]
|
||||
withPath: params[@"path"]
|
||||
toValue: params[@"value"]
|
||||
withConnection: connection];
|
||||
});
|
||||
}];
|
||||
|
||||
[connection receive:@"setHighlighted" withBlock:^(NSDictionary *params, id<SonarResponder> responder) {
|
||||
SonarPerformBlockOnMainThread(^{ [weakSelf onCallSetHighlighted: params[@"id"] withResponder: responder]; });
|
||||
}];
|
||||
|
||||
[connection receive:@"setSearchActive" withBlock:^(NSDictionary *params, id<SonarResponder> responder) {
|
||||
SonarPerformBlockOnMainThread(^{ [weakSelf onCallSetSearchActive: [params[@"active"] boolValue] withConnection: connection]; });
|
||||
}];
|
||||
|
||||
[connection receive:@"isConsoleEnabled" withBlock:^(NSDictionary *params, id<SonarResponder> responder) {
|
||||
SonarPerformBlockOnMainThread(^{ [responder success: @{@"isEnabled": @NO}];});
|
||||
}];
|
||||
|
||||
[connection receive:@"getSearchResults" withBlock:^(NSDictionary *params, id<SonarResponder> responder) {
|
||||
SonarPerformBlockOnMainThread(^{ [weakSelf onCallGetSearchResults: params[@"query"] withResponder: responder]; });
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)didDisconnect {
|
||||
// Clear the last highlight if there is any
|
||||
[self onCallSetHighlighted: nil withResponder: nil];
|
||||
// Disable search if it is active
|
||||
[self onCallSetSearchActive: NO withConnection: nil];
|
||||
}
|
||||
|
||||
- (void)onCallGetRoot:(id<SonarResponder>)responder {
|
||||
const auto rootNode= [self getNode: [self trackObject: _rootNode]];
|
||||
|
||||
[responder success: rootNode];
|
||||
}
|
||||
|
||||
- (void)onCallGetNodes:(NSArray<NSDictionary *> *)nodeIds withResponder:(id<SonarResponder>)responder {
|
||||
NSMutableArray<NSDictionary *> *elements = [NSMutableArray new];
|
||||
|
||||
for (id nodeId in nodeIds) {
|
||||
const auto node = [self getNode: nodeId];
|
||||
if (node == nil) {
|
||||
continue;
|
||||
}
|
||||
[elements addObject: node];
|
||||
}
|
||||
|
||||
[responder success: @{ @"elements": elements }];
|
||||
}
|
||||
|
||||
- (void)onCallSetData:(NSString *)objectId
|
||||
withPath:(NSArray<NSString *> *)path
|
||||
toValue:(id<NSObject>)value
|
||||
withConnection:(id<SonarConnection>)connection {
|
||||
id node = [_trackedObjects objectForKey: objectId];
|
||||
if (node == nil) {
|
||||
SKLog(@"node is nil, trying to setData: \
|
||||
objectId: %@ \
|
||||
path: %@ \
|
||||
value: %@",
|
||||
objectId, path, value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sonar sends nil/NSNull on some values when the text-field
|
||||
// is empty, disregard these changes otherwise we'll crash.
|
||||
if (value == nil || [value isKindOfClass: [NSNull class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
SKNodeDescriptor *descriptor = [_descriptorMapper descriptorForClass: [node class]];
|
||||
|
||||
NSString *dotJoinedPath = [path componentsJoinedByString: @"."];
|
||||
SKNodeUpdateData updateDataForPath = [[descriptor dataMutationsForNode: node] objectForKey: dotJoinedPath];
|
||||
if (updateDataForPath != nil) {
|
||||
updateDataForPath(value);
|
||||
[connection send: @"invalidate" withParams: @{ @"id": [descriptor identifierForNode: node] }];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onCallGetSearchResults:(NSString *)query withResponder:(id<SonarResponder>)responder {
|
||||
const auto alreadyAddedElements = [NSMutableSet<NSString *> new];
|
||||
SKSearchResultNode *matchTree = [self searchForQuery:(NSString *)[query lowercaseString] fromNode:(id)_rootNode withElementsAlreadyAdded: alreadyAddedElements];
|
||||
|
||||
[responder success: @{
|
||||
@"results": [matchTree toNSDictionary] ?: [NSNull null],
|
||||
@"query": query
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
- (void)onCallSetHighlighted:(NSString *)objectId withResponder:(id<SonarResponder>)responder {
|
||||
if (_lastHighlightedNode != nil) {
|
||||
id lastHighlightedObject = [_trackedObjects objectForKey: _lastHighlightedNode];
|
||||
if (lastHighlightedObject == nil) {
|
||||
[responder error: @{ @"error": @"unable to get last highlighted object" }];
|
||||
return;
|
||||
}
|
||||
|
||||
SKNodeDescriptor *descriptor = [self->_descriptorMapper descriptorForClass: [lastHighlightedObject class]];
|
||||
[descriptor setHighlighted: NO forNode: lastHighlightedObject];
|
||||
|
||||
_lastHighlightedNode = nil;
|
||||
}
|
||||
|
||||
if (objectId == nil || [objectId isKindOfClass:[NSNull class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
id object = [_trackedObjects objectForKey: objectId];
|
||||
if (object == nil) {
|
||||
SKLog(@"tried to setHighlighted for untracked id, objectId: %@", objectId);
|
||||
return;
|
||||
}
|
||||
|
||||
SKNodeDescriptor *descriptor = [self->_descriptorMapper descriptorForClass: [object class]];
|
||||
[descriptor setHighlighted: YES forNode: object];
|
||||
|
||||
_lastHighlightedNode = objectId;
|
||||
}
|
||||
|
||||
- (void)onCallSetSearchActive:(BOOL)active withConnection:(id<SonarConnection>)connection {
|
||||
if (active) {
|
||||
[_tapListener mountWithFrame: [[UIScreen mainScreen] bounds]];
|
||||
__block id<NSObject> rootNode = _rootNode;
|
||||
|
||||
[_tapListener listenForTapWithBlock:^(CGPoint touchPoint) {
|
||||
SKTouch *touch =
|
||||
[[SKTouch alloc] initWithTouchPoint: touchPoint
|
||||
withRootNode: rootNode
|
||||
withDescriptorMapper: self->_descriptorMapper
|
||||
finishWithBlock:^(NSArray<NSString *> *path) {
|
||||
[connection send: @"select"
|
||||
withParams: @{ @"path": path }];
|
||||
}];
|
||||
|
||||
SKNodeDescriptor *descriptor = [self->_descriptorMapper descriptorForClass: [rootNode class]];
|
||||
[descriptor hitTest: touch forNode: rootNode];
|
||||
}];
|
||||
} else {
|
||||
[_tapListener unmount];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)invalidateNode:(id<NSObject>)node {
|
||||
SKNodeDescriptor *descriptor = [_descriptorMapper descriptorForClass: [node class]];
|
||||
if (descriptor == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *nodeId = [descriptor identifierForNode: node];
|
||||
if (![_trackedObjects objectForKey: nodeId]) {
|
||||
return;
|
||||
}
|
||||
[descriptor invalidateNode: node];
|
||||
|
||||
// Collect invalidate messages before sending in a batch
|
||||
std::lock_guard<std::mutex> lock(invalidObjectsMutex);
|
||||
[_invalidObjects addObject:nodeId];
|
||||
if (_invalidateMessageQueued) {
|
||||
return;
|
||||
}
|
||||
_invalidateMessageQueued = true;
|
||||
|
||||
if (_lastInvalidateMessage.timeIntervalSinceNow < -1) {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 500 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
|
||||
[self reportInvalidatedObjects];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reportInvalidatedObjects {
|
||||
std::lock_guard<std::mutex> lock(invalidObjectsMutex);
|
||||
NSMutableArray *nodes = [NSMutableArray new];
|
||||
for (NSString *nodeId in self->_invalidObjects) {
|
||||
[nodes addObject: [NSDictionary dictionaryWithObject: nodeId forKey: @"id"]];
|
||||
}
|
||||
[self->_connection send: @"invalidate" withParams: [NSDictionary dictionaryWithObject: nodes forKey: @"nodes"]];
|
||||
self->_lastInvalidateMessage = [NSDate date];
|
||||
self->_invalidObjects = [NSMutableSet new];
|
||||
self->_invalidateMessageQueued = false;
|
||||
return;
|
||||
}
|
||||
|
||||
- (void)updateNodeReference:(id<NSObject>)node {
|
||||
SKNodeDescriptor *descriptor = [_descriptorMapper descriptorForClass: [node class]];
|
||||
if (descriptor == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *nodeId = [descriptor identifierForNode: node];
|
||||
[_trackedObjects setObject:node forKey:nodeId];
|
||||
}
|
||||
|
||||
- (SKSearchResultNode *)searchForQuery:(NSString *)query fromNode:(id)node withElementsAlreadyAdded:(NSMutableSet<NSString *> *)alreadyAdded {
|
||||
SKNodeDescriptor *descriptor = [_descriptorMapper descriptorForClass: [node class]];
|
||||
if (node == nil || descriptor == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray<SKSearchResultNode *> *childTrees = nil;
|
||||
BOOL isMatch = [descriptor matchesQuery: query forNode: node];
|
||||
|
||||
NSString *nodeId = [self trackObject: node];
|
||||
|
||||
for (auto i = 0; i < [descriptor childCountForNode: node]; i++) {
|
||||
id child = [descriptor childForNode: node atIndex: i];
|
||||
if (child) {
|
||||
SKSearchResultNode *childTree = [self searchForQuery: query fromNode: child withElementsAlreadyAdded:alreadyAdded];
|
||||
if (childTree != nil) {
|
||||
if (childTrees == nil) {
|
||||
childTrees = [NSMutableArray new];
|
||||
}
|
||||
[childTrees addObject: childTree];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isMatch || childTrees != nil) {
|
||||
|
||||
NSDictionary *element = [self getNode: nodeId];
|
||||
if (nodeId == nil || element == nil) {
|
||||
return nil;
|
||||
}
|
||||
NSMutableArray<NSString *> *descriptorChildElements = [element objectForKey: @"children"];
|
||||
NSMutableDictionary *newElement = [element mutableCopy];
|
||||
|
||||
NSMutableArray<NSString *> *childElementsToReturn = [NSMutableArray new];
|
||||
for (NSString *child in descriptorChildElements) {
|
||||
if (![alreadyAdded containsObject: child]) {
|
||||
[alreadyAdded addObject: child]; //todo add all at end
|
||||
[childElementsToReturn addObject: child];
|
||||
}
|
||||
}
|
||||
[newElement setObject: childElementsToReturn forKey: @"children"];
|
||||
return [[SKSearchResultNode alloc] initWithNode: nodeId
|
||||
asMatch: isMatch
|
||||
withElement: newElement
|
||||
andChildren: childTrees];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSDictionary *)getNode:(NSString *)nodeId {
|
||||
id<NSObject> node = [_trackedObjects objectForKey: nodeId];
|
||||
if (node == nil) {
|
||||
SKLog(@"node is nil, no tracked node found for nodeId: %@", nodeId);
|
||||
return nil;
|
||||
}
|
||||
|
||||
SKNodeDescriptor *nodeDescriptor = [_descriptorMapper descriptorForClass: [node class]];
|
||||
if (nodeDescriptor == nil) {
|
||||
SKLog(@"No registered descriptor for class: %@", [node class]);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *attributes = [NSMutableArray new];
|
||||
NSMutableDictionary *data = [NSMutableDictionary new];
|
||||
|
||||
const auto *nodeAttributes = [nodeDescriptor attributesForNode: node];
|
||||
for (const SKNamed<NSString *> *namedPair in nodeAttributes) {
|
||||
const auto name = namedPair.name;
|
||||
if (name) {
|
||||
const NSDictionary *attribute = @{
|
||||
@"name": name,
|
||||
@"value": namedPair.value ?: [NSNull null],
|
||||
};
|
||||
[attributes addObject: attribute];
|
||||
}
|
||||
}
|
||||
|
||||
const auto *nodeData = [nodeDescriptor dataForNode: node];
|
||||
for (const SKNamed<NSDictionary *> *namedPair in nodeData) {
|
||||
data[namedPair.name] = namedPair.value;
|
||||
}
|
||||
|
||||
NSMutableArray *children = [NSMutableArray new];
|
||||
for (NSUInteger i = 0; i < [nodeDescriptor childCountForNode: node]; i++) {
|
||||
id childNode = [nodeDescriptor childForNode: node atIndex: i];
|
||||
|
||||
NSString *childIdentifier = [self trackObject: childNode];
|
||||
if (childIdentifier) {
|
||||
[children addObject: childIdentifier];
|
||||
}
|
||||
}
|
||||
|
||||
NSDictionary *nodeDic =
|
||||
@{
|
||||
// We shouldn't get nil for id/name/decoration, but let's not crash if we do.
|
||||
@"id": [nodeDescriptor identifierForNode: node] ?: @"(unknown)",
|
||||
@"name": [nodeDescriptor nameForNode: node] ?: @"(unknown)",
|
||||
@"children": children,
|
||||
@"attributes": attributes,
|
||||
@"data": data,
|
||||
@"decoration": [nodeDescriptor decorationForNode: node] ?: @"(unknown)",
|
||||
};
|
||||
|
||||
return nodeDic;
|
||||
}
|
||||
|
||||
- (NSString *)trackObject:(id)object {
|
||||
const SKNodeDescriptor *descriptor = [_descriptorMapper descriptorForClass: [object class]];
|
||||
NSString *objectIdentifier = [descriptor identifierForNode: object];
|
||||
|
||||
if (objectIdentifier == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (! [_trackedObjects objectForKey: objectIdentifier]) {
|
||||
[_trackedObjects setObject:object forKey:objectIdentifier];
|
||||
}
|
||||
|
||||
return objectIdentifier;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import <SonarKit/SKMacros.h>
|
||||
|
||||
FB_LINK_REQUIRE(UICollectionView_SKInvalidation)
|
||||
@interface UICollectionView (SKInvalidation)
|
||||
|
||||
+ (void)enableInvalidations;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "UICollectionView+SKInvalidation.h"
|
||||
|
||||
#import "SKInvalidation.h"
|
||||
#import "SKSwizzle.h"
|
||||
|
||||
FB_LINKABLE(UICollectionView_SKInvalidation)
|
||||
@implementation UICollectionView (SKInvalidation)
|
||||
|
||||
+ (void)enableInvalidations {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
swizzleMethods([self class], @selector(cellForItemAtIndexPath:), @selector(swizzle_cellForItemAtIndexPath:));
|
||||
});
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)swizzle_cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[SKInvalidation sharedInstance].delegate invalidateNode: self];
|
||||
});
|
||||
|
||||
return [self swizzle_cellForItemAtIndexPath: indexPath];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import <SonarKit/SKMacros.h>
|
||||
|
||||
#import "SKObject.h"
|
||||
|
||||
FB_LINK_REQUIRE(UIColor_SonarValueCoder)
|
||||
@interface UIColor (SonarValueCoder) <SKSonarValueCoder>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "UIColor+SKSonarValueCoder.h"
|
||||
|
||||
FB_LINKABLE(UIColor_SonarValueCoder)
|
||||
@implementation UIColor (SonarValueCoder)
|
||||
|
||||
+ (instancetype)fromSonarValue:(NSNumber *)sonarValue {
|
||||
NSUInteger intColor = [sonarValue integerValue];
|
||||
|
||||
CGFloat r, g, b, a;
|
||||
|
||||
b = CGFloat(intColor & 0xFF) / 255;
|
||||
g = CGFloat((intColor >> 8) & 0xFF) / 255;
|
||||
r = CGFloat((intColor >> 16) & 0xFF) / 255;
|
||||
a = CGFloat((intColor >> 24) & 0xFF) / 255;
|
||||
|
||||
return [[UIColor alloc] initWithRed: r green: g blue: b alpha: a];
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, id<NSObject>> *)sonarValue {
|
||||
CGColorSpaceRef colorSpace = CGColorGetColorSpace([self CGColor]);
|
||||
CGColorSpaceModel colorSpaceModel = CGColorSpaceGetModel(colorSpace);
|
||||
|
||||
NSUInteger red, green, blue, alpha;
|
||||
|
||||
switch (colorSpaceModel) {
|
||||
case kCGColorSpaceModelUnknown:
|
||||
case kCGColorSpaceModelRGB: {
|
||||
CGFloat r, g, b, a;
|
||||
[self getRed: &r green: &g blue: &b alpha: &a];
|
||||
|
||||
red = (NSUInteger)(r * 255) & 0xFF;
|
||||
green = (NSUInteger)(g * 255) & 0xFF;
|
||||
blue = (NSUInteger)(b * 255) & 0xFF;
|
||||
alpha = (NSUInteger)(a * 255) & 0xFF;
|
||||
} break;
|
||||
|
||||
case kCGColorSpaceModelMonochrome: {
|
||||
CGFloat a, w;
|
||||
[self getWhite: &w alpha: &a];
|
||||
|
||||
red = green = blue = (NSUInteger)(w * 255) & 0xFF;
|
||||
alpha = (NSUInteger)(a * 255) & 0xFF;
|
||||
} break;
|
||||
|
||||
default:
|
||||
red = green = blue = alpha = 0;
|
||||
}
|
||||
|
||||
NSUInteger intColor = (alpha << 24) | (red << 16) | (green << 8) | blue;
|
||||
return @{
|
||||
@"__type__": @"color",
|
||||
@"__mutable__": @NO,
|
||||
@"value": @(intColor)
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <SonarKit/SKMacros.h>
|
||||
|
||||
FB_LINK_REQUIRE(UIView_SKInvalidation)
|
||||
@interface UIView (SKInvalidation)
|
||||
|
||||
+ (void)enableInvalidation;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "SKInvalidation.h"
|
||||
#import "SKSwizzle.h"
|
||||
#import "UIView+SKInvalidation.h"
|
||||
|
||||
FB_LINKABLE(UIView_SKInvalidation)
|
||||
@implementation UIView (SKInvalidation)
|
||||
|
||||
+ (void)enableInvalidation {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
swizzleMethods([self class], @selector(setHidden:), @selector(swizzle_setHidden:));
|
||||
swizzleMethods([self class], @selector(addSubview:), @selector(swizzle_addSubview:));
|
||||
swizzleMethods([self class], @selector(removeFromSuperview), @selector(swizzle_removeFromSuperview));
|
||||
});
|
||||
}
|
||||
|
||||
- (void)swizzle_setHidden:(BOOL)hidden {
|
||||
[self swizzle_setHidden: hidden];
|
||||
|
||||
id<SKInvalidationDelegate> delegate = [SKInvalidation sharedInstance].delegate;
|
||||
if (delegate != nil) {
|
||||
[delegate invalidateNode: self.superview];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)swizzle_addSubview:(UIView *)view {
|
||||
[self swizzle_addSubview: view];
|
||||
|
||||
id<SKInvalidationDelegate> delegate = [SKInvalidation sharedInstance].delegate;
|
||||
if (delegate != nil) {
|
||||
[delegate invalidateNode: view];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)swizzle_removeFromSuperview {
|
||||
id<SKInvalidationDelegate> delegate = [SKInvalidation sharedInstance].delegate;
|
||||
if (delegate != nil && self.superview != nil) {
|
||||
[delegate invalidateNode: self.superview];
|
||||
}
|
||||
|
||||
[self swizzle_removeFromSuperview];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "SKNodeDescriptor.h"
|
||||
|
||||
@interface SKApplicationDescriptor : SKNodeDescriptor<UIApplication *>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKApplicationDescriptor.h"
|
||||
|
||||
#import "SKDescriptorMapper.h"
|
||||
#import "SKHiddenWindow.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation SKApplicationDescriptor
|
||||
|
||||
- (NSString *)identifierForNode:(UIApplication *)node {
|
||||
return [NSString stringWithFormat: @"%p", node];
|
||||
}
|
||||
|
||||
- (NSUInteger)childCountForNode:(UIApplication *)node {
|
||||
return [[self visibleChildrenForNode: node] count];
|
||||
}
|
||||
|
||||
- (id)childForNode:(UIApplication *)node atIndex:(NSUInteger)index {
|
||||
return [self visibleChildrenForNode: node][index];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted forNode:(UIApplication *)node {
|
||||
SKNodeDescriptor *windowDescriptor = [self descriptorForClass: [UIWindow class]];
|
||||
[windowDescriptor setHighlighted: highlighted forNode: [node keyWindow]];
|
||||
}
|
||||
|
||||
- (void)hitTest:(SKTouch *)touch forNode:(UIApplication *)node {
|
||||
for (NSInteger index = [self childCountForNode: node] - 1; index >= 0; index--) {
|
||||
UIWindow *child = [self childForNode: node atIndex: index];
|
||||
if (child.isHidden || child.alpha <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ([touch containedIn: child.frame]) {
|
||||
[touch continueWithChildIndex: index withOffset: child.frame.origin];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[touch finish];
|
||||
}
|
||||
|
||||
- (NSArray<UIWindow *> *)visibleChildrenForNode:(UIApplication *)node {
|
||||
NSMutableArray<UIWindow *> *children = [NSMutableArray new];
|
||||
for (UIWindow *window in node.windows) {
|
||||
if ([window isKindOfClass: [SKHiddenWindow class]]
|
||||
|| [window isKindOfClass:objc_lookUpClass("FBAccessibilityOverlayWindow")]
|
||||
|| [window isKindOfClass:objc_lookUpClass("UITextEffectsWindow")]) {
|
||||
continue;
|
||||
}
|
||||
[children addObject: window];
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "SKNodeDescriptor.h"
|
||||
|
||||
@class UIButton;
|
||||
|
||||
@interface SKButtonDescriptor : SKNodeDescriptor<UIButton *>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKButtonDescriptor.h"
|
||||
|
||||
#import "SKDescriptorMapper.h"
|
||||
#import "SKObject.h"
|
||||
#import "UIColor+SKSonarValueCoder.h"
|
||||
|
||||
@implementation SKButtonDescriptor
|
||||
|
||||
- (NSString *)identifierForNode:(UIButton *)node {
|
||||
return [NSString stringWithFormat: @"%p", node];
|
||||
}
|
||||
|
||||
- (NSUInteger)childCountForNode:(UIButton *)node {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (id)childForNode:(UIButton *)node atIndex:(NSUInteger)index {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray<SKNamed<NSDictionary *> *> *)dataForNode:(UIButton *)node {
|
||||
SKNodeDescriptor *viewDescriptor = [self descriptorForClass: [UIView class]];
|
||||
auto *viewData = [viewDescriptor dataForNode: node];
|
||||
|
||||
NSMutableArray *data = [NSMutableArray new];
|
||||
[data addObjectsFromArray: viewData];
|
||||
[data addObject:
|
||||
[SKNamed newWithName: @"UIButton"
|
||||
withValue: @{
|
||||
@"focused": @(node.focused),
|
||||
@"enabled": SKMutableObject(@(node.enabled)),
|
||||
@"highlighted": SKMutableObject(@(node.highlighted)),
|
||||
@"titleEdgeInsets": SKObject(node.titleEdgeInsets),
|
||||
@"titleLabel": SKMutableObject(node.titleLabel.attributedText.string.stringByStandardizingPath),
|
||||
@"currentTitleColor": SKMutableObject(node.currentTitleColor),
|
||||
}]
|
||||
];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, SKNodeUpdateData> *)dataMutationsForNode:(UIButton *)node {
|
||||
NSDictionary *buttonMutations = @{
|
||||
@"UIButton.titleLabel": ^(NSString *newValue) {
|
||||
[node setTitle: newValue forState: node.state];
|
||||
},
|
||||
@"UIButton.currentTitleColor": ^(NSNumber *newValue) {
|
||||
[node setTitleColor: [UIColor fromSonarValue: newValue] forState: node.state];
|
||||
},
|
||||
@"UIButton.highlighted": ^(NSNumber *highlighted) {
|
||||
[node setHighlighted: [highlighted boolValue]];
|
||||
},
|
||||
@"UIButton.enabled": ^(NSNumber *enabled) {
|
||||
[node setEnabled: [enabled boolValue]];
|
||||
}
|
||||
};
|
||||
|
||||
SKNodeDescriptor *viewDescriptor = [self descriptorForClass: [UIView class]];
|
||||
NSDictionary *viewMutations = [viewDescriptor dataMutationsForNode: node];
|
||||
|
||||
NSMutableDictionary *mutations = [NSMutableDictionary new];
|
||||
[mutations addEntriesFromDictionary: buttonMutations];
|
||||
[mutations addEntriesFromDictionary: viewMutations];
|
||||
|
||||
return mutations;
|
||||
}
|
||||
|
||||
- (NSArray<SKNamed<NSString *> *> *)attributesForNode:(UIScrollView *)node {
|
||||
SKNodeDescriptor *descriptor = [self descriptorForClass: [UIView class]];
|
||||
return [descriptor attributesForNode: node];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted forNode:(UIButton *)node {
|
||||
SKNodeDescriptor *viewDescriptor = [self descriptorForClass: [UIView class]];
|
||||
[viewDescriptor setHighlighted: highlighted forNode: node];
|
||||
}
|
||||
|
||||
- (void)hitTest:(SKTouch *)touch forNode:(UIButton *)node {
|
||||
[touch finish];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "SKNodeDescriptor.h"
|
||||
|
||||
@interface SKScrollViewDescriptor : SKNodeDescriptor<UIScrollView *>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKScrollViewDescriptor.h"
|
||||
|
||||
#import "SKDescriptorMapper.h"
|
||||
|
||||
@implementation SKScrollViewDescriptor
|
||||
|
||||
- (NSString *)identifierForNode:(UIScrollView *)node {
|
||||
SKNodeDescriptor *descriptor = [self descriptorForClass: [UIView class]];
|
||||
return [descriptor identifierForNode: node];
|
||||
}
|
||||
|
||||
- (NSUInteger)childCountForNode:(UIScrollView *)node {
|
||||
SKNodeDescriptor *descriptor = [self descriptorForClass: [UIView class]];
|
||||
return [descriptor childCountForNode: node];
|
||||
}
|
||||
|
||||
- (id)childForNode:(UIScrollView *)node atIndex:(NSUInteger)index {
|
||||
SKNodeDescriptor *descriptor = [self descriptorForClass: [UIView class]];
|
||||
return [descriptor childForNode: node atIndex: index];
|
||||
}
|
||||
|
||||
- (id)dataForNode:(UIScrollView *)node {
|
||||
SKNodeDescriptor *descriptor = [self descriptorForClass: [UIView class]];
|
||||
return [descriptor dataForNode:node];
|
||||
}
|
||||
|
||||
- (id)dataMutationsForNode:(UIScrollView *)node {
|
||||
SKNodeDescriptor *descriptor = [self descriptorForClass: [UIView class]];
|
||||
return [descriptor dataMutationsForNode:node];
|
||||
}
|
||||
|
||||
- (NSArray<SKNamed<NSString *> *> *)attributesForNode:(UIScrollView *)node {
|
||||
SKNodeDescriptor *descriptor = [self descriptorForClass: [UIView class]];
|
||||
return [descriptor attributesForNode: node];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted forNode:(UIScrollView *)node {
|
||||
SKNodeDescriptor *descriptor = [self descriptorForClass: [UIView class]];
|
||||
[descriptor setHighlighted: highlighted forNode: node];
|
||||
}
|
||||
|
||||
- (void)hitTest:(SKTouch *)touch forNode:(UIScrollView *)node {
|
||||
for (NSInteger index = [self childCountForNode: node] - 1; index >= 0; index--) {
|
||||
id<NSObject> childNode = [self childForNode: node atIndex: index];
|
||||
CGRect frame;
|
||||
|
||||
if ([childNode isKindOfClass: [UIViewController class]]) {
|
||||
UIViewController *child = (UIViewController *)childNode;
|
||||
if (child.view.isHidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
frame = child.view.frame;
|
||||
} else {
|
||||
UIView *child = (UIView *)childNode;
|
||||
if (child.isHidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
frame = child.frame;
|
||||
}
|
||||
|
||||
frame.origin.x -= node.contentOffset.x;
|
||||
frame.origin.y -= node.contentOffset.y;
|
||||
|
||||
if ([touch containedIn: frame]) {
|
||||
[touch continueWithChildIndex: index withOffset: frame.origin];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[touch finish];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "SKNodeDescriptor.h"
|
||||
|
||||
@interface SKViewControllerDescriptor : SKNodeDescriptor<UIViewController *>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKViewControllerDescriptor.h"
|
||||
|
||||
#import "SKDescriptorMapper.h"
|
||||
|
||||
@implementation SKViewControllerDescriptor
|
||||
|
||||
- (NSString *)identifierForNode:(UIViewController *)node {
|
||||
return [NSString stringWithFormat: @"%p", node];
|
||||
}
|
||||
|
||||
- (NSUInteger)childCountForNode:(UIViewController *)node {
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (id)childForNode:(UIViewController *)node atIndex:(NSUInteger)index {
|
||||
return node.view;
|
||||
}
|
||||
|
||||
- (void)setHighlightedForNode:(UIViewController *)node {
|
||||
}
|
||||
|
||||
- (NSArray<SKNamed<NSString *> *> *)attributesForNode:(UIViewController *)node {
|
||||
return @[
|
||||
[SKNamed newWithName: @"addr"
|
||||
withValue: [NSString stringWithFormat: @"%p", node]]
|
||||
];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted forNode:(UIViewController *)node {
|
||||
SKNodeDescriptor *descriptor = [self descriptorForClass: [UIView class]];
|
||||
[descriptor setHighlighted: highlighted forNode: node.view];
|
||||
}
|
||||
|
||||
- (void)hitTest:(SKTouch *)touch forNode:(UIViewController *)node {
|
||||
[touch continueWithChildIndex: 0 withOffset: (CGPoint){ 0, 0}];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "SKNodeDescriptor.h"
|
||||
|
||||
@interface SKViewDescriptor : SKNodeDescriptor<UIView *>
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,575 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKViewDescriptor.h"
|
||||
|
||||
#import "SKDescriptorMapper.h"
|
||||
#import "SKHighlightOverlay.h"
|
||||
#import "SKNamed.h"
|
||||
#import "SKObject.h"
|
||||
#import "SKYogaKitHelper.h"
|
||||
#import "UIColor+SKSonarValueCoder.h"
|
||||
#import <YogaKit/UIView+Yoga.h>
|
||||
|
||||
@implementation SKViewDescriptor
|
||||
|
||||
static NSDictionary *YGDirectionEnumMap = nil;
|
||||
static NSDictionary *YGFlexDirectionEnumMap = nil;
|
||||
static NSDictionary *YGJustifyEnumMap = nil;
|
||||
static NSDictionary *YGAlignEnumMap = nil;
|
||||
static NSDictionary *YGPositionTypeEnumMap = nil;
|
||||
static NSDictionary *YGWrapEnumMap = nil;
|
||||
static NSDictionary *YGOverflowEnumMap = nil;
|
||||
static NSDictionary *YGDisplayEnumMap = nil;
|
||||
static NSDictionary *YGUnitEnumMap = nil;
|
||||
|
||||
- (instancetype)initWithDescriptorMapper:(SKDescriptorMapper *)mapper {
|
||||
if (self = [super initWithDescriptorMapper: mapper]) {
|
||||
initEnumDictionaries();
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)identifierForNode:(UIView *)node {
|
||||
return [NSString stringWithFormat: @"%p", node];
|
||||
}
|
||||
|
||||
- (NSUInteger)childCountForNode:(UIView *)node {
|
||||
return [[self validChildrenForNode: node] count];
|
||||
}
|
||||
|
||||
- (id)childForNode:(UIView *)node atIndex:(NSUInteger)index {
|
||||
return [[self validChildrenForNode:node] objectAtIndex: index];
|
||||
}
|
||||
|
||||
- (NSArray *)validChildrenForNode:(UIView *)node {
|
||||
NSMutableArray *validChildren = [NSMutableArray new];
|
||||
|
||||
// Use UIViewControllers for children which responds to a different
|
||||
// viewController than their parent
|
||||
for (UIView *child in node.subviews) {
|
||||
BOOL responderIsUIViewController = [child.nextResponder isKindOfClass: [UIViewController class]];
|
||||
|
||||
if (!child.isHidden) {
|
||||
if (responderIsUIViewController && child.nextResponder != node.nextResponder) {
|
||||
[validChildren addObject: child.nextResponder];
|
||||
} else {
|
||||
[validChildren addObject: child];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validChildren;
|
||||
}
|
||||
|
||||
- (NSArray<SKNamed<NSDictionary *> *> *)dataForNode:(UIView *)node {
|
||||
return [NSArray arrayWithObjects:
|
||||
[SKNamed newWithName: @"UIView"
|
||||
withValue: @{
|
||||
@"frame": SKMutableObject(node.frame),
|
||||
@"bounds": SKObject(node.bounds),
|
||||
@"center": SKObject(node.center),
|
||||
@"layoutMargins": SKObject(node.layoutMargins),
|
||||
@"clipsToBounds": @(node.clipsToBounds),
|
||||
@"alpha": SKMutableObject(@(node.alpha)),
|
||||
@"tag": @(node.tag),
|
||||
@"backgroundColor": SKMutableObject(node.backgroundColor)
|
||||
}],
|
||||
[SKNamed newWithName: @"CALayer"
|
||||
withValue: @{
|
||||
@"shadowColor": SKMutableObject([UIColor colorWithCGColor:node.layer.shadowColor]),
|
||||
@"shadowOpacity": SKMutableObject(@(node.layer.shadowOpacity)),
|
||||
@"shadowRadius": SKMutableObject(@(node.layer.shadowRadius)),
|
||||
@"shadowOffset": SKMutableObject(node.layer.shadowOffset),
|
||||
@"backgroundColor": SKMutableObject([UIColor colorWithCGColor:node.layer.backgroundColor]),
|
||||
@"borderColor": SKMutableObject([UIColor colorWithCGColor:node.layer.borderColor]),
|
||||
@"borderWidth": SKMutableObject(@(node.layer.borderWidth)),
|
||||
@"cornerRadius": SKMutableObject(@(node.layer.cornerRadius)),
|
||||
@"masksToBounds": SKMutableObject(@(node.layer.masksToBounds)),
|
||||
}],
|
||||
[SKNamed newWithName: @"Accessibility"
|
||||
withValue: @{
|
||||
@"isAccessibilityElement": SKMutableObject(@(node.isAccessibilityElement)),
|
||||
@"accessibilityLabel": SKMutableObject(node.accessibilityLabel ?: @""),
|
||||
@"accessibilityValue": SKMutableObject(node.accessibilityValue ?: @""),
|
||||
@"accessibilityHint": SKMutableObject(node.accessibilityHint ?: @""),
|
||||
@"accessibilityTraits": AccessibilityTraitsDict(node.accessibilityTraits),
|
||||
@"accessibilityViewIsModal": SKMutableObject(@(node.accessibilityViewIsModal)),
|
||||
@"shouldGroupAccessibilityChildren": SKMutableObject(@(node.shouldGroupAccessibilityChildren)),
|
||||
}],
|
||||
!node.isYogaEnabled ? nil :
|
||||
[SKNamed newWithName: @"YGLayout"
|
||||
withValue: @{
|
||||
@"direction": SKMutableObject(YGDirectionEnumMap[@(node.yoga.direction)]),
|
||||
@"justifyContent": SKMutableObject(YGJustifyEnumMap[@(node.yoga.justifyContent)]),
|
||||
@"aligns": @{
|
||||
@"alignContent": SKMutableObject(YGAlignEnumMap[@(node.yoga.alignContent)]),
|
||||
@"alignItems": SKMutableObject(YGAlignEnumMap[@(node.yoga.alignItems)]),
|
||||
@"alignSelf": SKMutableObject(YGAlignEnumMap[@(node.yoga.alignSelf)]),
|
||||
},
|
||||
@"position": @{
|
||||
@"type": SKMutableObject(YGPositionTypeEnumMap[@(node.yoga.position)]),
|
||||
@"left": SKYGValueObject(node.yoga.left),
|
||||
@"top": SKYGValueObject(node.yoga.top),
|
||||
@"right": SKYGValueObject(node.yoga.right),
|
||||
@"bottom": SKYGValueObject(node.yoga.bottom),
|
||||
@"start": SKYGValueObject(node.yoga.start),
|
||||
@"end": SKYGValueObject(node.yoga.end),
|
||||
},
|
||||
@"overflow": SKMutableObject(YGOverflowEnumMap[@(node.yoga.overflow)]),
|
||||
@"display": SKMutableObject(YGDisplayEnumMap[@(node.yoga.display)]),
|
||||
@"flex": @{
|
||||
@"flexDirection": SKMutableObject(YGFlexDirectionEnumMap[@(node.yoga.flexDirection)]),
|
||||
@"flexWrap": SKMutableObject(YGWrapEnumMap[@(node.yoga.flexWrap)]),
|
||||
@"flexGrow": SKMutableObject(@(node.yoga.flexGrow)),
|
||||
@"flexShrink": SKMutableObject(@(node.yoga.flexShrink)),
|
||||
@"flexBasis": SKYGValueObject(node.yoga.flexBasis),
|
||||
},
|
||||
@"margin": @{
|
||||
@"left": SKYGValueObject(node.yoga.marginLeft),
|
||||
@"top": SKYGValueObject(node.yoga.marginTop),
|
||||
@"right": SKYGValueObject(node.yoga.marginRight),
|
||||
@"bottom": SKYGValueObject(node.yoga.marginBottom),
|
||||
@"start": SKYGValueObject(node.yoga.marginStart),
|
||||
@"end": SKYGValueObject(node.yoga.marginEnd),
|
||||
@"horizontal": SKYGValueObject(node.yoga.marginHorizontal),
|
||||
@"vertical": SKYGValueObject(node.yoga.marginVertical),
|
||||
@"all": SKYGValueObject(node.yoga.margin),
|
||||
},
|
||||
@"padding": @{
|
||||
@"left": SKYGValueObject(node.yoga.paddingLeft),
|
||||
@"top": SKYGValueObject(node.yoga.paddingTop),
|
||||
@"right": SKYGValueObject(node.yoga.paddingRight),
|
||||
@"bottom": SKYGValueObject(node.yoga.paddingBottom),
|
||||
@"start": SKYGValueObject(node.yoga.paddingStart),
|
||||
@"end": SKYGValueObject(node.yoga.paddingEnd),
|
||||
@"horizontal": SKYGValueObject(node.yoga.paddingHorizontal),
|
||||
@"vertical": SKYGValueObject(node.yoga.paddingVertical),
|
||||
@"all": SKYGValueObject(node.yoga.padding),
|
||||
},
|
||||
@"border": @{
|
||||
@"leftWidth": SKMutableObject(@(node.yoga.borderLeftWidth)),
|
||||
@"topWidth": SKMutableObject(@(node.yoga.borderTopWidth)),
|
||||
@"rightWidth": SKMutableObject(@(node.yoga.borderRightWidth)),
|
||||
@"bottomWidth": SKMutableObject(@(node.yoga.borderBottomWidth)),
|
||||
@"startWidth": SKMutableObject(@(node.yoga.borderStartWidth)),
|
||||
@"endWidth": SKMutableObject(@(node.yoga.borderEndWidth)),
|
||||
@"all": SKMutableObject(@(node.yoga.borderWidth)),
|
||||
},
|
||||
@"dimensions": @{
|
||||
@"width": SKYGValueObject(node.yoga.width),
|
||||
@"height": SKYGValueObject(node.yoga.height),
|
||||
@"minWidth": SKYGValueObject(node.yoga.minWidth),
|
||||
@"minHeight": SKYGValueObject(node.yoga.minHeight),
|
||||
@"maxWidth": SKYGValueObject(node.yoga.maxWidth),
|
||||
@"maxHeight": SKYGValueObject(node.yoga.maxHeight),
|
||||
},
|
||||
@"aspectRatio": SKMutableObject(@(node.yoga.aspectRatio)),
|
||||
@"resolvedDirection": SKObject(YGDirectionEnumMap[@(node.yoga.resolvedDirection)]),
|
||||
}],
|
||||
nil];
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, SKNodeUpdateData> *)dataMutationsForNode:(UIView *)node {
|
||||
return @{
|
||||
// UIView
|
||||
@"UIView.alpha": ^(NSNumber *value) {
|
||||
node.alpha = [value floatValue];
|
||||
},
|
||||
@"UIView.backgroundColor": ^(NSNumber *value) {
|
||||
node.backgroundColor = [UIColor fromSonarValue: value];
|
||||
},
|
||||
@"UIView.frame.origin.y": ^(NSNumber *value) {
|
||||
CGRect frame = node.frame;
|
||||
frame.origin.y = [value floatValue];
|
||||
node.frame = frame;
|
||||
},
|
||||
@"UIView.frame.origin.x": ^(NSNumber *value) {
|
||||
CGRect frame = node.frame;
|
||||
frame.origin.x = [value floatValue];
|
||||
node.frame = frame;
|
||||
},
|
||||
@"UIView.frame.size.width": ^(NSNumber *value) {
|
||||
CGRect frame = node.frame;
|
||||
frame.size.width = [value floatValue];
|
||||
node.frame = frame;
|
||||
},
|
||||
@"UIView.frame.size.height": ^(NSNumber *value) {
|
||||
CGRect frame = node.frame;
|
||||
frame.size.width = [value floatValue];
|
||||
node.frame = frame;
|
||||
},
|
||||
// CALayer
|
||||
@"CALayer.shadowColor": ^(NSNumber *value) {
|
||||
node.layer.shadowColor = [UIColor fromSonarValue:value].CGColor;
|
||||
},
|
||||
@"CALayer.shadowOpacity": ^(NSNumber *value) {
|
||||
node.layer.shadowOpacity = [value floatValue];
|
||||
},
|
||||
@"CALayer.shadowRadius": ^(NSNumber *value) {
|
||||
node.layer.shadowRadius = [value floatValue];
|
||||
},
|
||||
@"CALayer.shadowOffset.width": ^(NSNumber *value) {
|
||||
CGSize offset = node.layer.shadowOffset;
|
||||
offset.width = [value floatValue];
|
||||
node.layer.shadowOffset = offset;
|
||||
},
|
||||
@"CALayer.shadowOffset.height": ^(NSNumber *value) {
|
||||
CGSize offset = node.layer.shadowOffset;
|
||||
offset.height = [value floatValue];
|
||||
node.layer.shadowOffset = offset;
|
||||
},
|
||||
@"CALayer.backgroundColor": ^(NSNumber *value) {
|
||||
node.layer.backgroundColor = [UIColor fromSonarValue:value].CGColor;
|
||||
},
|
||||
@"CALayer.borderColor": ^(NSNumber *value) {
|
||||
node.layer.borderColor = [UIColor fromSonarValue:value].CGColor;
|
||||
},
|
||||
@"CALayer.borderWidth": ^(NSNumber *value) {
|
||||
node.layer.borderWidth = [value floatValue];
|
||||
},
|
||||
@"CALayer.cornerRadius": ^(NSNumber *value) {
|
||||
node.layer.cornerRadius = [value floatValue];
|
||||
},
|
||||
@"CALayer.masksToBounds": ^(NSNumber *value) {
|
||||
node.layer.masksToBounds = [value boolValue];
|
||||
},
|
||||
// YGLayout
|
||||
@"YGLayout.direction": APPLY_ENUM_TO_YOGA_PROPERTY(direction, YGDirection),
|
||||
@"YGLayout.justifyContent": APPLY_ENUM_TO_YOGA_PROPERTY(justifyContent, YGJustify),
|
||||
@"YGLayout.aligns.alignContent": APPLY_ENUM_TO_YOGA_PROPERTY(alignContent, YGAlign),
|
||||
@"YGLayout.aligns.alignItems": APPLY_ENUM_TO_YOGA_PROPERTY(alignItems, YGAlign),
|
||||
@"YGLayout.aligns.alignSelf": APPLY_ENUM_TO_YOGA_PROPERTY(alignSelf, YGAlign),
|
||||
@"YGLayout.position.type": APPLY_ENUM_TO_YOGA_PROPERTY(position, YGPositionType),
|
||||
@"YGLayout.position.left.value": APPLY_VALUE_TO_YGVALUE(left),
|
||||
@"YGLayout.position.left.unit": APPLY_UNIT_TO_YGVALUE(left, YGUnit),
|
||||
@"YGLayout.position.top.value": APPLY_VALUE_TO_YGVALUE(top),
|
||||
@"YGLayout.position.top.unit": APPLY_UNIT_TO_YGVALUE(top, YGUnit),
|
||||
@"YGLayout.position.right.value": APPLY_VALUE_TO_YGVALUE(right),
|
||||
@"YGLayout.position.right.unit": APPLY_UNIT_TO_YGVALUE(right, YGUnit),
|
||||
@"YGLayout.position.bottom.value": APPLY_VALUE_TO_YGVALUE(bottom),
|
||||
@"YGLayout.position.bottom.unit": APPLY_UNIT_TO_YGVALUE(bottom, YGUnit),
|
||||
@"YGLayout.position.start.value": APPLY_VALUE_TO_YGVALUE(start),
|
||||
@"YGLayout.position.start.unit": APPLY_UNIT_TO_YGVALUE(start, YGUnit),
|
||||
@"YGLayout.position.end.value": APPLY_VALUE_TO_YGVALUE(end),
|
||||
@"YGLayout.position.end.unit": APPLY_UNIT_TO_YGVALUE(end, YGUnit),
|
||||
@"YGLayout.overflow": APPLY_ENUM_TO_YOGA_PROPERTY(overflow, YGOverflow),
|
||||
@"YGLayout.display": APPLY_ENUM_TO_YOGA_PROPERTY(display, YGDisplay),
|
||||
@"YGLayout.flex.flexDirection": APPLY_ENUM_TO_YOGA_PROPERTY(flexDirection, YGFlexDirection),
|
||||
@"YGLayout.flex.flexWrap": APPLY_ENUM_TO_YOGA_PROPERTY(flexWrap, YGWrap),
|
||||
@"YGLayout.flex.flexGrow": ^(NSNumber *value) {
|
||||
node.yoga.flexGrow = [value floatValue];
|
||||
},
|
||||
@"YGLayout.flex.flexShrink": ^(NSNumber *value) {
|
||||
node.yoga.flexShrink = [value floatValue];
|
||||
},
|
||||
@"YGLayout.flex.flexBasis.value": APPLY_VALUE_TO_YGVALUE(flexBasis),
|
||||
@"YGLayout.flex.flexBasis.unit": APPLY_UNIT_TO_YGVALUE(flexBasis, YGUnit),
|
||||
@"YGLayout.margin.left.value": APPLY_VALUE_TO_YGVALUE(marginLeft),
|
||||
@"YGLayout.margin.left.unit": APPLY_UNIT_TO_YGVALUE(marginLeft, YGUnit),
|
||||
@"YGLayout.margin.top.value": APPLY_VALUE_TO_YGVALUE(marginTop),
|
||||
@"YGLayout.margin.top.unit": APPLY_UNIT_TO_YGVALUE(marginTop, YGUnit),
|
||||
@"YGLayout.margin.right.value": APPLY_VALUE_TO_YGVALUE(marginRight),
|
||||
@"YGLayout.margin.right.unit": APPLY_UNIT_TO_YGVALUE(marginRight, YGUnit),
|
||||
@"YGLayout.margin.bottom.value": APPLY_VALUE_TO_YGVALUE(marginBottom),
|
||||
@"YGLayout.margin.bottom.unit": APPLY_UNIT_TO_YGVALUE(marginBottom, YGUnit),
|
||||
@"YGLayout.margin.start.value": APPLY_VALUE_TO_YGVALUE(marginStart),
|
||||
@"YGLayout.margin.start.unit": APPLY_UNIT_TO_YGVALUE(marginStart, YGUnit),
|
||||
@"YGLayout.margin.end.value": APPLY_VALUE_TO_YGVALUE(marginEnd),
|
||||
@"YGLayout.margin.end.unit": APPLY_UNIT_TO_YGVALUE(marginEnd, YGUnit),
|
||||
@"YGLayout.margin.horizontal.value": APPLY_VALUE_TO_YGVALUE(marginHorizontal),
|
||||
@"YGLayout.margin.horizontal.unit": APPLY_UNIT_TO_YGVALUE(marginHorizontal, YGUnit),
|
||||
@"YGLayout.margin.vertical.value": APPLY_VALUE_TO_YGVALUE(marginVertical),
|
||||
@"YGLayout.margin.vertical.unit": APPLY_UNIT_TO_YGVALUE(marginVertical, YGUnit),
|
||||
@"YGLayout.margin.all.value": APPLY_VALUE_TO_YGVALUE(margin),
|
||||
@"YGLayout.margin.all.unit": APPLY_UNIT_TO_YGVALUE(margin, YGUnit),
|
||||
@"YGLayout.padding.left.value": APPLY_VALUE_TO_YGVALUE(paddingLeft),
|
||||
@"YGLayout.padding.left.unit": APPLY_UNIT_TO_YGVALUE(paddingLeft, YGUnit),
|
||||
@"YGLayout.padding.top.value": APPLY_VALUE_TO_YGVALUE(paddingTop),
|
||||
@"YGLayout.padding.top.unit": APPLY_UNIT_TO_YGVALUE(paddingTop, YGUnit),
|
||||
@"YGLayout.padding.right.value": APPLY_VALUE_TO_YGVALUE(paddingRight),
|
||||
@"YGLayout.padding.right.unit": APPLY_UNIT_TO_YGVALUE(paddingRight, YGUnit),
|
||||
@"YGLayout.padding.bottom.value": APPLY_VALUE_TO_YGVALUE(paddingBottom),
|
||||
@"YGLayout.padding.bottom.unit": APPLY_UNIT_TO_YGVALUE(paddingBottom, YGUnit),
|
||||
@"YGLayout.padding.start.value": APPLY_VALUE_TO_YGVALUE(paddingStart),
|
||||
@"YGLayout.padding.start.unit": APPLY_UNIT_TO_YGVALUE(paddingStart, YGUnit),
|
||||
@"YGLayout.padding.end.value": APPLY_VALUE_TO_YGVALUE(paddingEnd),
|
||||
@"YGLayout.padding.end.unit": APPLY_UNIT_TO_YGVALUE(paddingEnd, YGUnit),
|
||||
@"YGLayout.padding.horizontal.value": APPLY_VALUE_TO_YGVALUE(paddingHorizontal),
|
||||
@"YGLayout.padding.horizontal.unit": APPLY_UNIT_TO_YGVALUE(paddingHorizontal, YGUnit),
|
||||
@"YGLayout.padding.vertical.value": APPLY_VALUE_TO_YGVALUE(paddingVertical),
|
||||
@"YGLayout.padding.vertical.unit": APPLY_UNIT_TO_YGVALUE(paddingVertical, YGUnit),
|
||||
@"YGLayout.padding.all.value": APPLY_VALUE_TO_YGVALUE(padding),
|
||||
@"YGLayout.padding.all.unit": APPLY_UNIT_TO_YGVALUE(padding, YGUnit),
|
||||
@"YGLayout.border.leftWidth": ^(NSNumber *value) {
|
||||
node.yoga.borderLeftWidth = [value floatValue];
|
||||
},
|
||||
@"YGLayout.border.topWidth": ^(NSNumber *value) {
|
||||
node.yoga.borderTopWidth = [value floatValue];
|
||||
},
|
||||
@"YGLayout.border.rightWidth": ^(NSNumber *value) {
|
||||
node.yoga.borderRightWidth = [value floatValue];
|
||||
},
|
||||
@"YGLayout.border.bottomWidth": ^(NSNumber *value) {
|
||||
node.yoga.borderBottomWidth = [value floatValue];
|
||||
},
|
||||
@"YGLayout.border.startWidth": ^(NSNumber *value) {
|
||||
node.yoga.borderStartWidth = [value floatValue];
|
||||
},
|
||||
@"YGLayout.border.endWidth": ^(NSNumber *value) {
|
||||
node.yoga.borderEndWidth = [value floatValue];
|
||||
},
|
||||
@"YGLayout.border.all": ^(NSNumber *value) {
|
||||
node.yoga.borderWidth = [value floatValue];
|
||||
},
|
||||
@"YGLayout.dimensions.width.value": APPLY_VALUE_TO_YGVALUE(width),
|
||||
@"YGLayout.dimensions.width.unit": APPLY_UNIT_TO_YGVALUE(width, YGUnit),
|
||||
@"YGLayout.dimensions.height.value": APPLY_VALUE_TO_YGVALUE(height),
|
||||
@"YGLayout.dimensions.height.unit": APPLY_UNIT_TO_YGVALUE(height, YGUnit),
|
||||
@"YGLayout.dimensions.minWidth.value": APPLY_VALUE_TO_YGVALUE(minWidth),
|
||||
@"YGLayout.dimensions.minWidth.unit": APPLY_UNIT_TO_YGVALUE(minWidth, YGUnit),
|
||||
@"YGLayout.dimensions.minHeight.value": APPLY_VALUE_TO_YGVALUE(minHeight),
|
||||
@"YGLayout.dimensions.minHeight.unit": APPLY_UNIT_TO_YGVALUE(minHeight, YGUnit),
|
||||
@"YGLayout.dimensions.maxWidth.value": APPLY_VALUE_TO_YGVALUE(maxWidth),
|
||||
@"YGLayout.dimensions.maxWidth.unit": APPLY_UNIT_TO_YGVALUE(maxWidth, YGUnit),
|
||||
@"YGLayout.dimensions.maxHeight.value": APPLY_VALUE_TO_YGVALUE(maxHeight),
|
||||
@"YGLayout.dimensions.maxHeight.unit": APPLY_UNIT_TO_YGVALUE(maxHeight, YGUnit),
|
||||
@"YGLayout.aspectRatio": ^(NSNumber *value) {
|
||||
node.yoga.aspectRatio = [value floatValue];
|
||||
},
|
||||
// Accessibility
|
||||
@"Accessibility.isAccessibilityElement": ^(NSNumber *value) {
|
||||
node.isAccessibilityElement = [value boolValue];
|
||||
},
|
||||
@"Accessibility.accessibilityLabel": ^(NSString *value) {
|
||||
node.accessibilityLabel = value;
|
||||
},
|
||||
@"Accessibility.accessibilityValue": ^(NSString *value) {
|
||||
node.accessibilityValue = value;
|
||||
},
|
||||
@"Accessibility.accessibilityHint": ^(NSString *value) {
|
||||
node.accessibilityHint = value;
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitButton": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitButton, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitLink": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitLink, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitHeader": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitHeader, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitSearchField": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitSearchField, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitImage": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitImage, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitSelected": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitSelected, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitPlaysSound": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitPlaysSound, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitKeyboardKey": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitKeyboardKey, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitStaticText": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitStaticText, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitSummaryElement": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitSummaryElement, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitNotEnabled": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitNotEnabled, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitUpdatesFrequently": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitUpdatesFrequently, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitStartsMediaSession": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitStartsMediaSession, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitAdjustable": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitAdjustable, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitAllowsDirectInteraction": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitAllowsDirectInteraction, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitCausesPageTurn": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitCausesPageTurn, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityTraits.UIAccessibilityTraitTabBar": ^(NSNumber *value) {
|
||||
node.accessibilityTraits = AccessibilityTraitsToggle(node.accessibilityTraits, UIAccessibilityTraitTabBar, [value boolValue]);
|
||||
},
|
||||
@"Accessibility.accessibilityViewIsModal": ^(NSNumber *value) {
|
||||
node.accessibilityViewIsModal = [value boolValue];
|
||||
},
|
||||
@"Accessibility.shouldGroupAccessibilityChildren": ^(NSNumber *value) {
|
||||
node.shouldGroupAccessibilityChildren = [value boolValue];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
- (NSArray<SKNamed<NSString *> *> *)attributesForNode:(UIView *)node {
|
||||
return @[
|
||||
[SKNamed newWithName: @"addr"
|
||||
withValue: [NSString stringWithFormat: @"%p", node]]
|
||||
];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted forNode:(UIView *)node {
|
||||
SKHighlightOverlay *overlay = [SKHighlightOverlay sharedInstance];
|
||||
if (highlighted == YES) {
|
||||
[overlay mountInView: node withFrame: node.bounds];
|
||||
} else {
|
||||
[overlay unmount];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)hitTest:(SKTouch *)touch forNode:(UIView *)node {
|
||||
for (NSInteger index = [self childCountForNode: node] - 1; index >= 0; index--) {
|
||||
id<NSObject> childNode = [self childForNode: node atIndex: index];
|
||||
UIView *viewForNode = nil;
|
||||
|
||||
if ([childNode isKindOfClass: [UIViewController class]]) {
|
||||
UIViewController *child = (UIViewController *)childNode;
|
||||
viewForNode = child.view;
|
||||
} else {
|
||||
viewForNode = (UIView *)childNode;
|
||||
}
|
||||
|
||||
if (viewForNode.isHidden || viewForNode.alpha <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ([touch containedIn: viewForNode.frame]) {
|
||||
[touch continueWithChildIndex: index withOffset: viewForNode.frame.origin ];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[touch finish];
|
||||
}
|
||||
|
||||
static void initEnumDictionaries() {
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
YGDirectionEnumMap = @{
|
||||
@(YGDirectionInherit): @"inherit",
|
||||
@(YGDirectionLTR): @"LTR",
|
||||
@(YGDirectionRTL): @"RTL",
|
||||
};
|
||||
|
||||
YGFlexDirectionEnumMap = @{
|
||||
@(YGFlexDirectionColumn): @"column",
|
||||
@(YGFlexDirectionColumnReverse): @"column-reverse",
|
||||
@(YGFlexDirectionRow): @"row",
|
||||
@(YGFlexDirectionRowReverse): @"row-reverse",
|
||||
};
|
||||
|
||||
YGJustifyEnumMap = @{
|
||||
@(YGJustifyFlexStart): @"flex-start",
|
||||
@(YGJustifyCenter): @"center",
|
||||
@(YGJustifyFlexEnd): @"flex-end",
|
||||
@(YGJustifySpaceBetween): @"space-between",
|
||||
@(YGJustifySpaceAround): @"space-around",
|
||||
};
|
||||
|
||||
YGAlignEnumMap = @{
|
||||
@(YGAlignAuto): @"auto",
|
||||
@(YGAlignFlexStart): @"flex-start",
|
||||
@(YGAlignCenter): @"end",
|
||||
@(YGAlignFlexEnd): @"flex-end",
|
||||
@(YGAlignStretch): @"stretch",
|
||||
@(YGAlignBaseline): @"baseline",
|
||||
@(YGAlignSpaceBetween): @"space-between",
|
||||
@(YGAlignSpaceAround): @"space-around",
|
||||
};
|
||||
|
||||
YGPositionTypeEnumMap = @{
|
||||
@(YGPositionTypeRelative): @"relative",
|
||||
@(YGPositionTypeAbsolute): @"absolute",
|
||||
};
|
||||
|
||||
YGWrapEnumMap = @{
|
||||
@(YGWrapNoWrap): @"no-wrap",
|
||||
@(YGWrapWrap): @"wrap",
|
||||
@(YGWrapWrapReverse): @"wrap-reverse",
|
||||
};
|
||||
|
||||
YGOverflowEnumMap = @{
|
||||
@(YGOverflowVisible): @"visible",
|
||||
@(YGOverflowHidden): @"hidden",
|
||||
@(YGOverflowScroll): @"scroll",
|
||||
};
|
||||
|
||||
YGDisplayEnumMap = @{
|
||||
@(YGDisplayFlex): @"flex",
|
||||
@(YGDisplayNone): @"none",
|
||||
};
|
||||
|
||||
YGUnitEnumMap = @{
|
||||
@(YGUnitUndefined): @"undefined",
|
||||
@(YGUnitPoint): @"point",
|
||||
@(YGUnitPercent): @"percent",
|
||||
@(YGUnitAuto): @"auto",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static NSDictionary *SKYGValueObject(YGValue value) {
|
||||
return @{
|
||||
@"value": SKMutableObject(@(value.value)),
|
||||
@"unit": SKMutableObject(YGUnitEnumMap[@(value.unit)]),
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
Takes the originalTraits, and set all bits from toggleTraits to the toggleValue
|
||||
e.g. originalTraits = UIAccessibilityTraitButton | UIAccessibilityTraitSelected
|
||||
toggleTraits = UIAccessibilityTraitImage
|
||||
toggleValue = YES
|
||||
return value = UIAccessibilityTraitButton | UIAccessibilityTraitSelected | UIAccessibilityTraitImage
|
||||
*/
|
||||
static UIAccessibilityTraits AccessibilityTraitsToggle(UIAccessibilityTraits originalTraits, UIAccessibilityTraits toggleTraits, BOOL toggleValue) {
|
||||
// NEGATE all bits of toggleTraits from originalTraits and OR it against either toggleTraits or 0 (UIAccessibilityTraitNone) based on toggleValue
|
||||
UIAccessibilityTraits bitsValue = toggleValue ? toggleTraits : UIAccessibilityTraitNone;
|
||||
return (originalTraits & ~(toggleTraits)) | bitsValue;
|
||||
}
|
||||
|
||||
static NSDictionary *AccessibilityTraitsDict(UIAccessibilityTraits accessibilityTraits) {
|
||||
NSMutableDictionary *traitsDict = [NSMutableDictionary new];
|
||||
[traitsDict addEntriesFromDictionary:@{
|
||||
@"UIAccessibilityTraitButton": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitButton))),
|
||||
@"UIAccessibilityTraitLink": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitLink))),
|
||||
@"UIAccessibilityTraitHeader": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitHeader))),
|
||||
@"UIAccessibilityTraitSearchField": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitSearchField))),
|
||||
@"UIAccessibilityTraitImage": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitImage))),
|
||||
@"UIAccessibilityTraitSelected": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitSelected))),
|
||||
@"UIAccessibilityTraitPlaysSound": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitPlaysSound))),
|
||||
@"UIAccessibilityTraitKeyboardKey": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitKeyboardKey))),
|
||||
@"UIAccessibilityTraitStaticText": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitStaticText))),
|
||||
@"UIAccessibilityTraitSummaryElement": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitSummaryElement))),
|
||||
@"UIAccessibilityTraitNotEnabled": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitNotEnabled))),
|
||||
@"UIAccessibilityTraitUpdatesFrequently": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitUpdatesFrequently))),
|
||||
@"UIAccessibilityTraitStartsMediaSession": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitStartsMediaSession))),
|
||||
@"UIAccessibilityTraitAdjustable": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitAdjustable))),
|
||||
@"UIAccessibilityTraitAllowsDirectInteraction": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitAllowsDirectInteraction))),
|
||||
@"UIAccessibilityTraitCausesPageTurn": SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitCausesPageTurn))),
|
||||
}];
|
||||
if (@available(iOS 10.0, *)) {
|
||||
traitsDict[@"UIAccessibilityTraitTabBar"] = SKMutableObject(@(!!(accessibilityTraits & UIAccessibilityTraitTabBar)));
|
||||
}
|
||||
return traitsDict;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface SKHiddenWindow : UIWindow
|
||||
@end
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKHiddenWindow.h"
|
||||
|
||||
@implementation SKHiddenWindow
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/*
|
||||
A hash function is needed in order to use NSObject classes
|
||||
as keys in C++ STL
|
||||
*/
|
||||
class SKObjectHash {
|
||||
public:
|
||||
size_t operator()(const NSObject *x) const {
|
||||
return (size_t)[x hash];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void swizzleMethods(Class cls, SEL original, SEL swissled);
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKSwizzle.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
void swizzleMethods(Class cls, SEL original, SEL swissled) {
|
||||
Method originalMethod = class_getInstanceMethod(cls, original);
|
||||
Method swissledMethod = class_getInstanceMethod(cls, swissled);
|
||||
|
||||
BOOL didAddMethod = class_addMethod(cls, original,
|
||||
method_getImplementation(swissledMethod),
|
||||
method_getTypeEncoding(swissledMethod));
|
||||
|
||||
if (didAddMethod) {
|
||||
class_replaceMethod(cls, swissled,
|
||||
method_getImplementation(originalMethod),
|
||||
method_getTypeEncoding(originalMethod));
|
||||
} else {
|
||||
method_exchangeImplementations(originalMethod, swissledMethod);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#define APPLY_ENUM_TO_YOGA_PROPERTY(varName, enumName) \
|
||||
^(NSString *newValue) { \
|
||||
NSNumber *varName = [[enumName##EnumMap allKeysForObject:newValue] lastObject]; \
|
||||
if (varName == nil) { return; } \
|
||||
node.yoga.varName = (enumName)[varName unsignedIntegerValue]; \
|
||||
}
|
||||
|
||||
#define APPLY_VALUE_TO_YGVALUE(varName) \
|
||||
^(NSNumber *value) { \
|
||||
YGValue newValue = node.yoga.varName; \
|
||||
newValue.value = [value floatValue]; \
|
||||
node.yoga.varName = newValue; \
|
||||
}
|
||||
|
||||
#define APPLY_UNIT_TO_YGVALUE(varName, enumName) \
|
||||
^(NSString *value) { \
|
||||
NSNumber *varName = [[enumName##EnumMap allKeysForObject:value] lastObject]; \
|
||||
if (varName == nil) { return; } \
|
||||
YGValue newValue = node.yoga.varName; \
|
||||
newValue.unit = (enumName)[varName unsignedIntegerValue]; \
|
||||
node.yoga.varName = newValue; \
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <SonarKitLayoutPlugin/SKTapListener.h>
|
||||
|
||||
@interface SKTapListenerMock : NSObject<SKTapListener>
|
||||
|
||||
- (void)tapAt:(CGPoint)point;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import "SKTapListenerMock.h"
|
||||
|
||||
@implementation SKTapListenerMock
|
||||
{
|
||||
NSMutableArray<SKTapReceiver> *_tapReceivers;
|
||||
}
|
||||
|
||||
@synthesize isMounted;
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_tapReceivers = [NSMutableArray new];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)listenForTapWithBlock:(SKTapReceiver)receiver {
|
||||
[_tapReceivers addObject: receiver];
|
||||
}
|
||||
|
||||
- (void)tapAt:(CGPoint)point {
|
||||
for (SKTapReceiver recv in _tapReceivers) {
|
||||
recv(point);
|
||||
}
|
||||
|
||||
[_tapReceivers removeAllObjects];
|
||||
}
|
||||
|
||||
- (void)mountWithFrame:(CGRect)frame {
|
||||
isMounted = YES;
|
||||
}
|
||||
|
||||
- (void)unmount {
|
||||
isMounted = NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import <SonarKitLayoutPlugin/SKDescriptorMapper.h>
|
||||
#import <SonarKitLayoutPlugin/SKNodeDescriptor.h>
|
||||
#import <SonarKitLayoutPlugin/SonarKitLayoutPlugin.h>
|
||||
#import <SonarKitTestUtils/SonarConnectionMock.h>
|
||||
#import <SonarKitTestUtils/SonarResponderMock.h>
|
||||
|
||||
#import "SKTapListenerMock.h"
|
||||
#import "TestNode.h"
|
||||
#import "TestNodeDescriptor.h"
|
||||
|
||||
@interface SonarKitLayoutPluginTests : XCTestCase
|
||||
@end
|
||||
|
||||
@implementation SonarKitLayoutPluginTests
|
||||
{
|
||||
SKDescriptorMapper *_descriptorMapper;
|
||||
}
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
|
||||
_descriptorMapper = [[SKDescriptorMapper alloc] initWithDefaults];
|
||||
|
||||
[_descriptorMapper registerDescriptor: [TestNodeDescriptor new]
|
||||
forClass: [TestNode class]];
|
||||
}
|
||||
|
||||
- (void)testGetRoot {
|
||||
SonarKitLayoutPlugin *plugin = [[SonarKitLayoutPlugin alloc]
|
||||
initWithRootNode: [[TestNode alloc] initWithName: @"rootNode"]
|
||||
withTapListener: nil
|
||||
withDescriptorMapper: _descriptorMapper];
|
||||
|
||||
SonarConnectionMock *connection = [SonarConnectionMock new];
|
||||
SonarResponderMock *responder = [SonarResponderMock new];
|
||||
[plugin didConnect:connection];
|
||||
|
||||
SonarReceiver receiver = connection.receivers[@"getRoot"];
|
||||
receiver(@{}, responder);
|
||||
|
||||
XCTAssertTrue(([responder.successes containsObject: @{
|
||||
@"id": @"rootNode",
|
||||
@"name": @"TestNode",
|
||||
@"children": @[],
|
||||
@"attributes": @[],
|
||||
@"data": @{},
|
||||
@"decoration": @"",
|
||||
}]));
|
||||
}
|
||||
|
||||
- (void)testGetEmptyNodes {
|
||||
SonarKitLayoutPlugin *plugin = [SonarKitLayoutPlugin new];
|
||||
SonarConnectionMock *connection = [SonarConnectionMock new];
|
||||
SonarResponderMock *responder = [SonarResponderMock new];
|
||||
[plugin didConnect:connection];
|
||||
|
||||
SonarReceiver receiver = connection.receivers[@"getNodes"];
|
||||
receiver(@{@"ids": @[]}, responder);
|
||||
|
||||
XCTAssertTrue(([responder.successes containsObject:@{@"elements": @[]}]));
|
||||
}
|
||||
|
||||
- (void)testGetNodes {
|
||||
TestNode *rootNode = [[TestNode alloc] initWithName: @"rootNode"];
|
||||
NSArray *childNodes = @[
|
||||
[[TestNode alloc] initWithName: @"testNode1"],
|
||||
[[TestNode alloc] initWithName: @"testNode2"],
|
||||
[[TestNode alloc] initWithName: @"testNode3"],
|
||||
];
|
||||
|
||||
rootNode.children = childNodes;
|
||||
|
||||
SonarKitLayoutPlugin *plugin = [[SonarKitLayoutPlugin alloc] initWithRootNode: rootNode
|
||||
withTapListener: nil
|
||||
withDescriptorMapper: _descriptorMapper];
|
||||
|
||||
SonarConnectionMock *connection = [SonarConnectionMock new];
|
||||
SonarResponderMock *responder = [SonarResponderMock new];
|
||||
[plugin didConnect:connection];
|
||||
|
||||
// Ensure that nodes are tracked
|
||||
connection.receivers[@"getRoot"](@{}, responder);
|
||||
|
||||
SonarReceiver receiver = connection.receivers[@"getNodes"];
|
||||
receiver(@{@"ids": @[ @"testNode1", @"testNode2", @"testNode3" ]}, responder);
|
||||
|
||||
XCTAssertTrue(([responder.successes containsObject:@{@"elements": @[
|
||||
@{
|
||||
@"id": @"testNode1",
|
||||
@"name": @"TestNode",
|
||||
@"children": @[],
|
||||
@"attributes": @[],
|
||||
@"data": @{},
|
||||
@"decoration": @"",
|
||||
},
|
||||
@{
|
||||
@"id": @"testNode2",
|
||||
@"name": @"TestNode",
|
||||
@"children": @[],
|
||||
@"attributes": @[],
|
||||
@"data": @{},
|
||||
@"decoration": @"",
|
||||
},
|
||||
@{
|
||||
@"id": @"testNode3",
|
||||
@"name": @"TestNode",
|
||||
@"children": @[],
|
||||
@"attributes": @[],
|
||||
@"data": @{},
|
||||
@"decoration": @"",
|
||||
},
|
||||
]}]));
|
||||
}
|
||||
|
||||
- (void)testSetHighlighted {
|
||||
TestNode *rootNode = [[TestNode alloc] initWithName: @"rootNode"];
|
||||
|
||||
TestNode *testNode1 = [[TestNode alloc] initWithName: @"testNode1"];
|
||||
TestNode *testNode2 = [[TestNode alloc] initWithName: @"testNode2"];
|
||||
NSArray *childNodes = @[ testNode1, testNode2 ];
|
||||
|
||||
rootNode.children = childNodes;
|
||||
|
||||
SonarKitLayoutPlugin *plugin = [[SonarKitLayoutPlugin alloc] initWithRootNode: rootNode
|
||||
withTapListener: nil
|
||||
withDescriptorMapper: _descriptorMapper];
|
||||
|
||||
SonarConnectionMock *connection = [SonarConnectionMock new];
|
||||
SonarResponderMock *responder = [SonarResponderMock new];
|
||||
[plugin didConnect:connection];
|
||||
|
||||
// Setup in order to track nodes successfully
|
||||
connection.receivers[@"getRoot"](@{}, responder);
|
||||
SonarReceiver getNodesCall = connection.receivers[@"getNodes"];
|
||||
getNodesCall(@{@"ids": @[ @"testNode1", @"testNode2" ]}, responder);
|
||||
|
||||
SonarReceiver setHighlighted = connection.receivers[@"setHighlighted"];
|
||||
setHighlighted(@{@"id":@"testNode2"}, responder);
|
||||
|
||||
XCTAssertTrue(testNode2.highlighted);
|
||||
|
||||
setHighlighted(@{@"id":@"testNode1"}, responder);
|
||||
|
||||
// Ensure that old highlight was removed
|
||||
XCTAssertTrue(testNode1.highlighted && !testNode2.highlighted);
|
||||
}
|
||||
|
||||
- (void)testSetSearchActive {
|
||||
TestNode *rootNode = [[TestNode alloc] initWithName: @"rootNode"
|
||||
withFrame: CGRectMake(0, 0, 20, 60)];
|
||||
|
||||
TestNode *testNode1 = [[TestNode alloc] initWithName: @"testNode1"
|
||||
withFrame: CGRectMake(20, 20, 20, 20)];
|
||||
TestNode *testNode2 = [[TestNode alloc] initWithName: @"testNode2"
|
||||
withFrame: CGRectMake(20, 40, 20, 20)];
|
||||
TestNode *testNode3 = [[TestNode alloc] initWithName: @"testNode3"
|
||||
withFrame: CGRectMake(25, 42, 5, 5)];
|
||||
|
||||
rootNode.children = @[ testNode1, testNode2 ];
|
||||
testNode2.children = @[ testNode3 ];
|
||||
|
||||
SKTapListenerMock *tapListener = [SKTapListenerMock new];
|
||||
SonarKitLayoutPlugin *plugin = [[SonarKitLayoutPlugin alloc] initWithRootNode: rootNode
|
||||
withTapListener: tapListener
|
||||
withDescriptorMapper: _descriptorMapper];
|
||||
|
||||
SonarConnectionMock *connection = [SonarConnectionMock new];
|
||||
SonarResponderMock *responder = [SonarResponderMock new];
|
||||
[plugin didConnect:connection];
|
||||
|
||||
connection.receivers[@"setSearchActive"](@{@"active":@YES}, responder);
|
||||
|
||||
// Fake a tap at `testNode3`
|
||||
[tapListener tapAt: (CGPoint){ 26, 43 }];
|
||||
|
||||
XCTAssertTrue(([connection.sent[@"select"] containsObject: @{ @"path": @[ @"testNode2", @"testNode3" ] }]));
|
||||
}
|
||||
|
||||
- (void)testSetSearchActiveMountAndUnmount {
|
||||
TestNode *rootNode = [[TestNode alloc] initWithName: @"rootNode"];
|
||||
|
||||
SKTapListenerMock *tapListener = [SKTapListenerMock new];
|
||||
SonarKitLayoutPlugin *plugin = [[SonarKitLayoutPlugin alloc] initWithRootNode: rootNode
|
||||
withTapListener: tapListener
|
||||
withDescriptorMapper: _descriptorMapper];
|
||||
|
||||
SonarConnectionMock *connection = [SonarConnectionMock new];
|
||||
SonarResponderMock *responder = [SonarResponderMock new];
|
||||
[plugin didConnect:connection];
|
||||
|
||||
SonarReceiver setSearchActive = connection.receivers[@"setSearchActive"];
|
||||
setSearchActive(@{@"active":@YES}, responder);
|
||||
|
||||
XCTAssertTrue(tapListener.isMounted);
|
||||
|
||||
setSearchActive(@{@"active":@NO}, responder);
|
||||
XCTAssertTrue(! tapListener.isMounted);
|
||||
}
|
||||
|
||||
- (void)testSetData {
|
||||
TestNode *rootNode = [[TestNode alloc] initWithName: @"rootNode"
|
||||
withFrame: CGRectMake(0, 0, 20, 60)];
|
||||
|
||||
TestNode *testNode1 = [[TestNode alloc] initWithName: @"testNode1"
|
||||
withFrame: CGRectMake(20, 20, 20, 20)];
|
||||
TestNode *testNode2 = [[TestNode alloc] initWithName: @"testNode2"
|
||||
withFrame: CGRectMake(20, 40, 20, 20)];
|
||||
TestNode *testNode3 = [[TestNode alloc] initWithName: @"testNode3"
|
||||
withFrame: CGRectMake(25, 42, 5, 5)];
|
||||
|
||||
rootNode.children = @[ testNode1, testNode2 ];
|
||||
testNode2.children = @[ testNode3 ];
|
||||
|
||||
SKTapListenerMock *tapListener = [SKTapListenerMock new];
|
||||
SonarKitLayoutPlugin *plugin = [[SonarKitLayoutPlugin alloc] initWithRootNode: rootNode
|
||||
withTapListener: tapListener
|
||||
withDescriptorMapper: _descriptorMapper];
|
||||
|
||||
SonarConnectionMock *connection = [SonarConnectionMock new];
|
||||
SonarResponderMock *responder = [SonarResponderMock new];
|
||||
[plugin didConnect:connection];
|
||||
|
||||
// Setup in order to track nodes successfully
|
||||
connection.receivers[@"getRoot"](@{}, responder);
|
||||
connection.receivers[@"getNodes"](@{ @"ids": @[ @"testNode2" ] }, responder);
|
||||
|
||||
// Modify the name of testNode3
|
||||
connection.receivers[@"setData"](@{
|
||||
@"id": @"testNode3",
|
||||
@"path": @[ @"TestNode", @"name" ],
|
||||
@"value": @"changedNameForTestNode3",
|
||||
}, responder);
|
||||
|
||||
XCTAssertTrue([testNode3.nodeName isEqualToString: @"changedNameForTestNode3"]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface TestNode : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *nodeName;
|
||||
@property (nonatomic, copy) NSArray<TestNode *> *children;
|
||||
|
||||
@property (nonatomic, assign) BOOL highlighted;
|
||||
@property (nonatomic, assign) CGRect frame;
|
||||
|
||||
- (instancetype)initWithName:(NSString *)name;
|
||||
- (instancetype)initWithName:(NSString *)name withFrame:(CGRect)frame;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import "TestNode.h"
|
||||
|
||||
@implementation TestNode
|
||||
|
||||
- (instancetype)initWithName:(NSString *)name {
|
||||
return [self initWithName: name
|
||||
withFrame: CGRectZero];
|
||||
}
|
||||
|
||||
- (instancetype)initWithName:(NSString *)name withFrame:(CGRect)frame {
|
||||
if (self = [super init]) {
|
||||
_nodeName = name;
|
||||
_frame = frame;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <SonarKitLayoutPlugin/SKNodeDescriptor.h>
|
||||
|
||||
#import "TestNode.h"
|
||||
|
||||
@interface TestNodeDescriptor : SKNodeDescriptor<TestNode *>
|
||||
@end
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import "TestNodeDescriptor.h"
|
||||
|
||||
@implementation TestNodeDescriptor
|
||||
|
||||
- (NSString *)identifierForNode:(TestNode *)node {
|
||||
return node.nodeName;
|
||||
}
|
||||
|
||||
- (NSUInteger)childCountForNode:(TestNode *)node {
|
||||
return [node.children count];
|
||||
}
|
||||
|
||||
- (id)childForNode:(TestNode *)node atIndex:(NSUInteger)index {
|
||||
return [node.children objectAtIndex: index];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted forNode:(TestNode *)node {
|
||||
node.highlighted = highlighted;
|
||||
}
|
||||
|
||||
- (void)hitTest:(SKTouch *)touch forNode:(TestNode *)node {
|
||||
NSUInteger index = [node.children count] - 1;
|
||||
for (TestNode *childNode in [node.children reverseObjectEnumerator]) {
|
||||
if ([touch containedIn: childNode.frame]) {
|
||||
[touch continueWithChildIndex: index withOffset: node.frame.origin];
|
||||
return;
|
||||
}
|
||||
|
||||
index = index - 1;
|
||||
}
|
||||
|
||||
[touch finish];
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *, SKNodeUpdateData> *)dataMutationsForNode:(TestNode *)node {
|
||||
return @{
|
||||
@"TestNode.name": ^(NSString *newName) {
|
||||
node.nodeName = newName;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
//
|
||||
// FLEXNetworkObserver.h
|
||||
// Derived from:
|
||||
//
|
||||
// PDAFNetworkDomainController.h
|
||||
// PonyDebugger
|
||||
//
|
||||
// Created by Mike Lewis on 2/27/12.
|
||||
//
|
||||
// Licensed to Square, Inc. under one or more contributor license agreements.
|
||||
// See the LICENSE file distributed with this work for the terms under
|
||||
// which Square, Inc. licenses this file to you.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
FOUNDATION_EXTERN NSString *const kFLEXNetworkObserverEnabledStateChangedNotification;
|
||||
|
||||
/// This class swizzles NSURLConnection and NSURLSession delegate methods to observe events in the URL loading system.
|
||||
/// High level network events are sent to the default FLEXNetworkRecorder instance which maintains the request history and caches response bodies.
|
||||
@interface FLEXNetworkObserver : NSObject
|
||||
|
||||
+ (void)start;
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// FLEXNetworkRecorder.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 2/4/15.
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <SonarKitNetworkPlugin/SKNetworkReporter.h>
|
||||
|
||||
// Notifications posted when the record is updated
|
||||
extern NSString *const kFLEXNetworkRecorderNewTransactionNotification;
|
||||
extern NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification;
|
||||
extern NSString *const kFLEXNetworkRecorderUserInfoTransactionKey;
|
||||
extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
|
||||
|
||||
@class FLEXNetworkTransaction;
|
||||
|
||||
@interface FLEXNetworkRecorder : NSObject
|
||||
|
||||
/// In general, it only makes sense to have one recorder for the entire application.
|
||||
+ (instancetype)defaultRecorder;
|
||||
|
||||
@property (nonatomic, weak) id<SKNetworkReporterDelegate> delegate;
|
||||
|
||||
/// Defaults to 25 MB if never set. Values set here are presisted across launches of the app.
|
||||
@property (nonatomic, assign) NSUInteger responseCacheByteLimit;
|
||||
|
||||
/// If NO, the recorder not cache will not cache response for content types with an "image", "video", or "audio" prefix.
|
||||
@property (nonatomic, assign) BOOL shouldCacheMediaResponses;
|
||||
|
||||
@property (nonatomic, copy) NSArray<NSString *> *hostBlacklist;
|
||||
|
||||
|
||||
// Accessing recorded network activity
|
||||
|
||||
/// Array of FLEXNetworkTransaction objects ordered by start time with the newest first.
|
||||
- (NSArray<FLEXNetworkTransaction *> *)networkTransactions;
|
||||
|
||||
/// The full response data IFF it hasn't been purged due to memory pressure.
|
||||
- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction;
|
||||
|
||||
/// Dumps all network transactions and cached response bodies.
|
||||
- (void)clearRecordedActivity;
|
||||
|
||||
|
||||
// Recording network activity
|
||||
|
||||
/// Call when app is about to send HTTP request.
|
||||
- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID request:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
|
||||
|
||||
/// Call when HTTP response is available.
|
||||
- (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSURLResponse *)response;
|
||||
|
||||
/// Call when data chunk is received over the network.
|
||||
- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength;
|
||||
|
||||
/// Call when HTTP request has finished loading.
|
||||
- (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(NSData *)responseBody;
|
||||
|
||||
/// Call when HTTP request has failed to load.
|
||||
- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error;
|
||||
|
||||
/// Call to set the request mechanism anytime after recordRequestWillBeSent... has been called.
|
||||
/// This string can be set to anything useful about the API used to make the request.
|
||||
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,235 @@
|
||||
//
|
||||
// FLEXNetworkRecorder.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 2/4/15.
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
//
|
||||
|
||||
#import "FLEXNetworkRecorder.h"
|
||||
|
||||
#import "FLEXNetworkTransaction.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
NSString *const kFLEXNetworkRecorderNewTransactionNotification = @"kFLEXNetworkRecorderNewTransactionNotification";
|
||||
NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification = @"kFLEXNetworkRecorderTransactionUpdatedNotification";
|
||||
NSString *const kFLEXNetworkRecorderUserInfoTransactionKey = @"transaction";
|
||||
NSString *const kFLEXNetworkRecorderTransactionsClearedNotification = @"kFLEXNetworkRecorderTransactionsClearedNotification";
|
||||
|
||||
NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.responseCacheLimit";
|
||||
|
||||
@interface FLEXNetworkRecorder ()
|
||||
|
||||
@property (nonatomic, strong) NSCache *responseCache;
|
||||
@property (nonatomic, strong) NSMutableArray<FLEXNetworkTransaction *> *orderedTransactions;
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, FLEXNetworkTransaction *> *networkTransactionsForRequestIdentifiers;
|
||||
@property (nonatomic, strong) dispatch_queue_t queue;
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *identifierDict;
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkRecorder
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_responseCache = [NSCache new];
|
||||
NSUInteger responseCacheLimit = [[[NSUserDefaults standardUserDefaults] objectForKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey] unsignedIntegerValue];
|
||||
if (responseCacheLimit) {
|
||||
[_responseCache setTotalCostLimit:responseCacheLimit];
|
||||
} else {
|
||||
// Default to 25 MB max. The cache will purge earlier if there is memory pressure.
|
||||
[_responseCache setTotalCostLimit:25 * 1024 * 1024];
|
||||
}
|
||||
_orderedTransactions = [NSMutableArray array];
|
||||
_networkTransactionsForRequestIdentifiers = [NSMutableDictionary dictionary];
|
||||
|
||||
// Serial queue used because we use mutable objects that are not thread safe
|
||||
_queue = dispatch_queue_create("com.flex.FLEXNetworkRecorder", DISPATCH_QUEUE_SERIAL);
|
||||
_identifierDict = [NSMutableDictionary dictionary];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)defaultRecorder
|
||||
{
|
||||
static FLEXNetworkRecorder *defaultRecorder = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
defaultRecorder = [[[self class] alloc] init];
|
||||
});
|
||||
return defaultRecorder;
|
||||
}
|
||||
|
||||
#pragma mark - Public Data Access
|
||||
|
||||
- (void)setDelegate:(id<SKNetworkReporterDelegate>)delegate {
|
||||
_delegate = delegate;
|
||||
}
|
||||
|
||||
- (NSUInteger)responseCacheByteLimit
|
||||
{
|
||||
return [self.responseCache totalCostLimit];
|
||||
}
|
||||
|
||||
- (void)setResponseCacheByteLimit:(NSUInteger)responseCacheByteLimit
|
||||
{
|
||||
[self.responseCache setTotalCostLimit:responseCacheByteLimit];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:@(responseCacheByteLimit) forKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXNetworkTransaction *> *)networkTransactions
|
||||
{
|
||||
__block NSArray<FLEXNetworkTransaction *> *transactions = nil;
|
||||
dispatch_sync(self.queue, ^{
|
||||
transactions = [self.orderedTransactions copy];
|
||||
});
|
||||
return transactions;
|
||||
}
|
||||
|
||||
- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction
|
||||
{
|
||||
return [self.responseCache objectForKey:transaction.requestID];
|
||||
}
|
||||
|
||||
- (void)clearRecordedActivity
|
||||
{
|
||||
dispatch_async(self.queue, ^{
|
||||
[self.responseCache removeAllObjects];
|
||||
[self.orderedTransactions removeAllObjects];
|
||||
[self.networkTransactionsForRequestIdentifiers removeAllObjects];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Network Events
|
||||
|
||||
- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID request:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse
|
||||
{
|
||||
if (![self.identifierDict objectForKey:requestID]) {
|
||||
self.identifierDict[requestID] = [NSNumber random];
|
||||
}
|
||||
NSDate *startDate = [NSDate date];
|
||||
|
||||
if (redirectResponse) {
|
||||
[self recordResponseReceivedWithRequestID:requestID response:redirectResponse];
|
||||
[self recordLoadingFinishedWithRequestID:requestID responseBody:nil];
|
||||
}
|
||||
|
||||
dispatch_async(self.queue, ^{
|
||||
RequestInfo info = {
|
||||
.identifier = self.identifierDict[requestID].longLongValue,
|
||||
.timestamp = [NSDate timestamp],
|
||||
.request = request,
|
||||
};
|
||||
|
||||
info.setBody(request.HTTPBody);
|
||||
[self.delegate didObserveRequest:info];
|
||||
|
||||
FLEXNetworkTransaction *transaction = [FLEXNetworkTransaction new];
|
||||
transaction.requestID = requestID;
|
||||
transaction.request = request;
|
||||
transaction.startTime = startDate;
|
||||
|
||||
[self.orderedTransactions insertObject:transaction atIndex:0];
|
||||
[self.networkTransactionsForRequestIdentifiers setObject:transaction forKey:requestID];
|
||||
transaction.transactionState = FLEXNetworkTransactionStateAwaitingResponse;
|
||||
});
|
||||
}
|
||||
|
||||
/// Call when HTTP response is available.
|
||||
- (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSURLResponse *)response
|
||||
{
|
||||
NSDate *responseDate = [NSDate date];
|
||||
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
transaction.response = response;
|
||||
transaction.transactionState = FLEXNetworkTransactionStateReceivingData;
|
||||
transaction.latency = -[transaction.startTime timeIntervalSinceDate:responseDate];
|
||||
});
|
||||
}
|
||||
|
||||
/// Call when data chunk is received over the network.
|
||||
- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength
|
||||
{
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
transaction.receivedDataLength += dataLength;
|
||||
});
|
||||
}
|
||||
|
||||
/// Call when HTTP request has finished loading.
|
||||
- (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(NSData *)responseBody
|
||||
{
|
||||
NSDate *finishedDate = [NSDate date];
|
||||
dispatch_async(self.queue, ^{
|
||||
|
||||
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
transaction.transactionState = FLEXNetworkTransactionStateFinished;
|
||||
transaction.duration = -[transaction.startTime timeIntervalSinceDate:finishedDate];
|
||||
ResponseInfo responseInfo = {
|
||||
.identifier = self.identifierDict[requestID].longLongValue,
|
||||
.timestamp = [NSDate timestamp],
|
||||
.response = transaction.response,
|
||||
.body = nil,
|
||||
};
|
||||
responseInfo.setBody(responseBody);
|
||||
self.identifierDict[requestID] = nil; //Clear the entry
|
||||
[self.delegate didObserveResponse:responseInfo];
|
||||
|
||||
BOOL shouldCache = [responseBody length] > 0;
|
||||
if (!self.shouldCacheMediaResponses) {
|
||||
NSArray<NSString *> *ignoredMIMETypePrefixes = @[ @"audio", @"image", @"video" ];
|
||||
for (NSString *ignoredPrefix in ignoredMIMETypePrefixes) {
|
||||
shouldCache = shouldCache && ![transaction.response.MIMEType hasPrefix:ignoredPrefix];
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCache) {
|
||||
[self.responseCache setObject:responseBody forKey:requestID cost:[responseBody length]];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error
|
||||
{
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
ResponseInfo responseInfo = {
|
||||
.identifier = self.identifierDict[requestID].longLongValue,
|
||||
.timestamp = [NSDate timestamp],
|
||||
.response = transaction.response,
|
||||
.body = nil,
|
||||
};
|
||||
self.identifierDict[requestID] = nil; //Clear the entry
|
||||
[self.delegate didObserveResponse:responseInfo];
|
||||
transaction.transactionState = FLEXNetworkTransactionStateFailed;
|
||||
transaction.duration = -[transaction.startTime timeIntervalSinceNow];
|
||||
transaction.error = error;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID
|
||||
{
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
transaction.requestMechanism = mechanism;
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// FLEXNetworkTransaction.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 2/8/15.
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "UIKit/UIKit.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, FLEXNetworkTransactionState) {
|
||||
FLEXNetworkTransactionStateUnstarted,
|
||||
FLEXNetworkTransactionStateAwaitingResponse,
|
||||
FLEXNetworkTransactionStateReceivingData,
|
||||
FLEXNetworkTransactionStateFinished,
|
||||
FLEXNetworkTransactionStateFailed
|
||||
};
|
||||
|
||||
@interface FLEXNetworkTransaction : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *requestID;
|
||||
|
||||
@property (nonatomic, strong) NSURLRequest *request;
|
||||
@property (nonatomic, strong) NSURLResponse *response;
|
||||
@property (nonatomic, copy) NSString *requestMechanism;
|
||||
@property (nonatomic, assign) FLEXNetworkTransactionState transactionState;
|
||||
@property (nonatomic, strong) NSError *error;
|
||||
|
||||
@property (nonatomic, strong) NSDate *startTime;
|
||||
@property (nonatomic, assign) NSTimeInterval latency;
|
||||
@property (nonatomic, assign) NSTimeInterval duration;
|
||||
|
||||
@property (nonatomic, assign) int64_t receivedDataLength;
|
||||
|
||||
/// Only applicable for image downloads. A small thumbnail to preview the full response.
|
||||
@property (nonatomic, strong) UIImage *responseThumbnail;
|
||||
|
||||
/// Populated lazily. Handles both normal HTTPBody data and HTTPBodyStreams.
|
||||
@property (nonatomic, strong, readonly) NSData *cachedRequestBody;
|
||||
|
||||
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import "FLEXNetworkTransaction.h"
|
||||
|
||||
@interface FLEXNetworkTransaction ()
|
||||
|
||||
@property (nonatomic, strong, readwrite) NSData *cachedRequestBody;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkTransaction
|
||||
|
||||
- (NSString *)description
|
||||
{
|
||||
NSString *description = [super description];
|
||||
|
||||
description = [description stringByAppendingFormat:@" id = %@;", self.requestID];
|
||||
description = [description stringByAppendingFormat:@" url = %@;", self.request.URL];
|
||||
description = [description stringByAppendingFormat:@" duration = %f;", self.duration];
|
||||
description = [description stringByAppendingFormat:@" receivedDataLength = %lld", self.receivedDataLength];
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
- (NSData *)cachedRequestBody {
|
||||
if (!_cachedRequestBody) {
|
||||
if (self.request.HTTPBody != nil) {
|
||||
_cachedRequestBody = self.request.HTTPBody;
|
||||
} else if ([self.request.HTTPBodyStream conformsToProtocol:@protocol(NSCopying)]) {
|
||||
NSInputStream *bodyStream = [self.request.HTTPBodyStream copy];
|
||||
const NSUInteger bufferSize = 1024;
|
||||
uint8_t buffer[bufferSize];
|
||||
NSMutableData *data = [NSMutableData data];
|
||||
[bodyStream open];
|
||||
NSInteger readBytes = 0;
|
||||
do {
|
||||
readBytes = [bodyStream read:buffer maxLength:bufferSize];
|
||||
[data appendBytes:buffer length:readBytes];
|
||||
} while (readBytes > 0);
|
||||
[bodyStream close];
|
||||
_cachedRequestBody = data;
|
||||
}
|
||||
}
|
||||
return _cachedRequestBody;
|
||||
}
|
||||
|
||||
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state
|
||||
{
|
||||
NSString *readableString = nil;
|
||||
switch (state) {
|
||||
case FLEXNetworkTransactionStateUnstarted:
|
||||
readableString = @"Unstarted";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateAwaitingResponse:
|
||||
readableString = @"Awaiting Response";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateReceivingData:
|
||||
readableString = @"Receiving Data";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateFinished:
|
||||
readableString = @"Finished";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateFailed:
|
||||
readableString = @"Failed";
|
||||
break;
|
||||
}
|
||||
return readableString;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
|
||||
#import <Availability.h>
|
||||
#import <AvailabilityInternal.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#define FLEXFloor(x) (floor([[UIScreen mainScreen] scale] * (x)) / [[UIScreen mainScreen] scale])
|
||||
|
||||
#define FLEX_AT_LEAST_IOS11_SDK defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
|
||||
|
||||
@interface NSNumber (SonarUtility)
|
||||
|
||||
+ (NSNumber *)random;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSDate (SonarUtility)
|
||||
|
||||
+ (uint64_t)timestamp;
|
||||
@end
|
||||
|
||||
@interface FLEXUtility : NSObject
|
||||
|
||||
// Swizzling utilities
|
||||
|
||||
+ (SEL)swizzledSelectorForSelector:(SEL)selector;
|
||||
+ (BOOL)instanceRespondsButDoesNotImplementSelector:(SEL)selector class:(Class)cls;
|
||||
+ (void)replaceImplementationOfKnownSelector:(SEL)originalSelector onClass:(Class)className withBlock:(id)block swizzledSelector:(SEL)swizzledSelector;
|
||||
+ (void)replaceImplementationOfSelector:(SEL)selector withSelector:(SEL)swizzledSelector forClass:(Class)cls withMethodDescription:(struct objc_method_description)methodDescription implementationBlock:(id)implementationBlock undefinedBlock:(id)undefinedBlock;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// FLEXUtility.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/18/14.
|
||||
// Copyright 2004-present Facebook. All Rights Reserved.
|
||||
//
|
||||
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
#include <assert.h>
|
||||
#import <zlib.h>
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
#import <ImageIO/ImageIO.h>
|
||||
|
||||
@implementation FLEXUtility
|
||||
|
||||
+ (SEL)swizzledSelectorForSelector:(SEL)selector
|
||||
{
|
||||
return NSSelectorFromString([NSString stringWithFormat:@"_flex_swizzle_%x_%@", arc4random(), NSStringFromSelector(selector)]);
|
||||
}
|
||||
|
||||
+ (BOOL)instanceRespondsButDoesNotImplementSelector:(SEL)selector class:(Class)cls
|
||||
{
|
||||
if ([cls instancesRespondToSelector:selector]) {
|
||||
unsigned int numMethods = 0;
|
||||
Method *methods = class_copyMethodList(cls, &numMethods);
|
||||
|
||||
BOOL implementsSelector = NO;
|
||||
for (int index = 0; index < numMethods; index++) {
|
||||
SEL methodSelector = method_getName(methods[index]);
|
||||
if (selector == methodSelector) {
|
||||
implementsSelector = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(methods);
|
||||
|
||||
if (!implementsSelector) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (void)replaceImplementationOfKnownSelector:(SEL)originalSelector onClass:(Class)className withBlock:(id)block swizzledSelector:(SEL)swizzledSelector
|
||||
{
|
||||
// This method is only intended for swizzling methods that are know to exist on the class.
|
||||
// Bail if that isn't the case.
|
||||
Method originalMethod = class_getInstanceMethod(className, originalSelector);
|
||||
if (!originalMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
IMP implementation = imp_implementationWithBlock(block);
|
||||
class_addMethod(className, swizzledSelector, implementation, method_getTypeEncoding(originalMethod));
|
||||
Method newMethod = class_getInstanceMethod(className, swizzledSelector);
|
||||
method_exchangeImplementations(originalMethod, newMethod);
|
||||
}
|
||||
|
||||
+ (void)replaceImplementationOfSelector:(SEL)selector withSelector:(SEL)swizzledSelector forClass:(Class)cls withMethodDescription:(struct objc_method_description)methodDescription implementationBlock:(id)implementationBlock undefinedBlock:(id)undefinedBlock
|
||||
{
|
||||
if ([self instanceRespondsButDoesNotImplementSelector:selector class:cls]) {
|
||||
return;
|
||||
}
|
||||
|
||||
IMP implementation = imp_implementationWithBlock((id)([cls instancesRespondToSelector:selector] ? implementationBlock : undefinedBlock));
|
||||
|
||||
Method oldMethod = class_getInstanceMethod(cls, selector);
|
||||
if (oldMethod) {
|
||||
class_addMethod(cls, swizzledSelector, implementation, methodDescription.types);
|
||||
|
||||
Method newMethod = class_getInstanceMethod(cls, swizzledSelector);
|
||||
|
||||
method_exchangeImplementations(oldMethod, newMethod);
|
||||
} else {
|
||||
class_addMethod(cls, selector, implementation, methodDescription.types);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSNumber (SonarUtility)
|
||||
|
||||
+ (NSNumber *)random {
|
||||
int64_t identifier;
|
||||
arc4random_buf(&identifier, sizeof(int64_t));
|
||||
return @(identifier);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSDate (SonarUtility)
|
||||
|
||||
+ (uint64_t)getTimeNanoseconds
|
||||
{
|
||||
static struct mach_timebase_info tb_info = {0};
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
__unused int ret = mach_timebase_info(&tb_info);
|
||||
assert(0 == ret);
|
||||
});
|
||||
|
||||
return (mach_absolute_time() * tb_info.numer) / tb_info.denom;
|
||||
}
|
||||
|
||||
+ (uint64_t)timestamp {
|
||||
const uint64_t nowNanoSeconds = [self getTimeNanoseconds];
|
||||
return nowNanoSeconds / 1000000;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <SonarKitNetworkPlugin/SKNetworkReporter.h>
|
||||
|
||||
@interface SKIOSNetworkAdapter : NSObject<SKNetworkAdapterDelegate>
|
||||
- (instancetype)init NS_DESIGNATED_INITIALIZER;
|
||||
@property (weak, nonatomic) id<SKNetworkReporterDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import "SKIOSNetworkAdapter.h"
|
||||
#import "FLEXNetworkObserver.h"
|
||||
#import "FLEXNetworkRecorder.h"
|
||||
|
||||
@implementation SKIOSNetworkAdapter
|
||||
@synthesize delegate = _delegate;
|
||||
- (instancetype)init{
|
||||
if (self=[super init]){
|
||||
_delegate = nil;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setDelegate:(id<SKNetworkReporterDelegate>)delegate {
|
||||
_delegate = delegate;
|
||||
[FLEXNetworkObserver start];
|
||||
[FLEXNetworkRecorder defaultRecorder].delegate = _delegate;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <SonarKit/SonarPlugin.h>
|
||||
|
||||
#import <memory>
|
||||
|
||||
#import "SKDispatchQueue.h"
|
||||
|
||||
@interface SKBufferingPlugin : NSObject<SonarPlugin>
|
||||
|
||||
- (instancetype)initWithQueue:(const std::shared_ptr<facebook::sonar::DispatchQueue> &)queue NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (void)send:(NSString *)method sonarObject:(NSDictionary<NSString *, id> *)sonarObject;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#import <vector>
|
||||
|
||||
#import "SKBufferingPlugin.h"
|
||||
#import <SonarKit/SonarConnection.h>
|
||||
|
||||
struct CachedEvent {
|
||||
NSString *method;
|
||||
NSDictionary<NSString *, id> *sonarObject;
|
||||
};
|
||||
|
||||
static const NSUInteger bufferSize = 500;
|
||||
|
||||
@implementation SKBufferingPlugin
|
||||
{
|
||||
std::vector<CachedEvent> _ringBuffer;
|
||||
std::shared_ptr<facebook::sonar::DispatchQueue> _connectionAccessQueue;
|
||||
|
||||
id<SonarConnection> _connection;
|
||||
}
|
||||
|
||||
- (instancetype)initWithQueue:(const std::shared_ptr<facebook::sonar::DispatchQueue> &)queue {
|
||||
if (self = [super init]) {
|
||||
_ringBuffer.reserve(bufferSize);
|
||||
_connectionAccessQueue = queue;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)identifier {
|
||||
// Note: This must match with the javascript pulgin identifier!!
|
||||
return @"Network";
|
||||
}
|
||||
|
||||
- (void)didConnect:(id<SonarConnection>)connection {
|
||||
_connectionAccessQueue->async(^{
|
||||
self->_connection = connection;
|
||||
[self sendBufferedEvents];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didDisconnect {
|
||||
_connectionAccessQueue->async(^{
|
||||
self->_connection = nil;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)send:(NSString *)method
|
||||
sonarObject:(NSDictionary<NSString *, id> *)sonarObject {
|
||||
_connectionAccessQueue->async(^{
|
||||
if (self->_connection) {
|
||||
[self->_connection send:method withParams:sonarObject];
|
||||
} else {
|
||||
if (self->_ringBuffer.size() == bufferSize) {
|
||||
return;
|
||||
}
|
||||
self->_ringBuffer.push_back({
|
||||
.method = method,
|
||||
.sonarObject = sonarObject
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)sendBufferedEvents {
|
||||
NSAssert(_connection, @"connection object cannot be nil");
|
||||
for (const auto &event : _ringBuffer) {
|
||||
[_connection send:event.method withParams:event.sonarObject];
|
||||
}
|
||||
_ringBuffer.clear();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <dispatch/dispatch.h>
|
||||
|
||||
namespace facebook {
|
||||
namespace sonar {
|
||||
class DispatchQueue
|
||||
{
|
||||
public:
|
||||
virtual void async(dispatch_block_t block) = 0;
|
||||
};
|
||||
|
||||
class GCDQueue: public DispatchQueue
|
||||
{
|
||||
public:
|
||||
GCDQueue(dispatch_queue_t underlyingQueue)
|
||||
:_underlyingQueue(underlyingQueue) { }
|
||||
|
||||
void async(dispatch_block_t block) override
|
||||
{
|
||||
dispatch_async(_underlyingQueue, block);
|
||||
}
|
||||
|
||||
private:
|
||||
dispatch_queue_t _underlyingQueue;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
struct RequestInfo {
|
||||
int64_t identifier;
|
||||
uint64_t timestamp;
|
||||
NSURLRequest *request;
|
||||
NSString *body;
|
||||
|
||||
void setBody(NSData *data) {
|
||||
body = data ? [data base64EncodedStringWithOptions: 0]
|
||||
: [request.HTTPBody base64EncodedStringWithOptions: 0];
|
||||
}
|
||||
};
|
||||
|
||||
struct ResponseInfo {
|
||||
int64_t identifier;
|
||||
uint64_t timestamp;
|
||||
NSURLResponse *response;
|
||||
NSString *body;
|
||||
|
||||
bool shouldStripReponseBody() {
|
||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response;
|
||||
NSString *contentType = httpResponse.allHeaderFields[@"content-type"];
|
||||
if (!contentType) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return [contentType containsString:@"image/"] ||
|
||||
[contentType containsString:@"video/"] ||
|
||||
[contentType containsString:@"application/zip"];
|
||||
}
|
||||
|
||||
void setBody(NSData *data) {
|
||||
body = shouldStripReponseBody() ? nil : [data base64EncodedStringWithOptions: 0];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@protocol SKNetworkReporterDelegate
|
||||
|
||||
- (void)didObserveRequest:(RequestInfo)request;
|
||||
- (void)didObserveResponse:(ResponseInfo)response;
|
||||
|
||||
@end
|
||||
|
||||
@protocol SKNetworkAdapterDelegate
|
||||
|
||||
@property (weak, nonatomic) id<SKNetworkReporterDelegate> delegate;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <SonarKit/SonarPlugin.h>
|
||||
|
||||
#import "SKBufferingPlugin.h"
|
||||
#import "SKNetworkReporter.h"
|
||||
#import "SKDispatchQueue.h"
|
||||
|
||||
@interface SonarKitNetworkPlugin : SKBufferingPlugin <SKNetworkReporterDelegate>
|
||||
|
||||
- (instancetype)initWithNetworkAdapter:(id<SKNetworkAdapterDelegate>)adapter NS_DESIGNATED_INITIALIZER;
|
||||
- (instancetype)initWithNetworkAdapter:(id<SKNetworkAdapterDelegate>)adapter queue:(const std::shared_ptr<facebook::sonar::DispatchQueue> &)queue; //For test purposes
|
||||
|
||||
@property (strong, nonatomic) id<SKNetworkAdapterDelegate> adapter;
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (c) 2018-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the LICENSE
|
||||
* file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
#if FB_SONARKIT_ENABLED
|
||||
#import "SonarKitNetworkPlugin.h"
|
||||
#import "SKNetworkReporter.h"
|
||||
|
||||
@interface SonarKitNetworkPlugin ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation SonarKitNetworkPlugin
|
||||
|
||||
- (void)setAdapter:(id<SKNetworkAdapterDelegate>)adapter {
|
||||
_adapter = adapter;
|
||||
_adapter.delegate = self;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super initWithQueue:std::make_shared<facebook::sonar::GCDQueue>(dispatch_queue_create("com.sonarkit.network.buffer", DISPATCH_QUEUE_SERIAL))]) {
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithNetworkAdapter:(id<SKNetworkAdapterDelegate>)adapter {
|
||||
if (self = [super initWithQueue:std::make_shared<facebook::sonar::GCDQueue>(dispatch_queue_create("com.sonarkit.network.buffer", DISPATCH_QUEUE_SERIAL))]) {
|
||||
adapter.delegate = self;
|
||||
_adapter = adapter;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithNetworkAdapter:(id<SKNetworkAdapterDelegate>)adapter queue:(const std::shared_ptr<facebook::sonar::DispatchQueue> &)queue; {
|
||||
if (self = [super initWithQueue:queue]) {
|
||||
adapter.delegate = self;
|
||||
_adapter = adapter;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - SKNetworkReporterDelegate
|
||||
|
||||
|
||||
- (void)didObserveRequest:(RequestInfo)request;
|
||||
{
|
||||
NSMutableArray<NSDictionary<NSString *, id> *> *headers = [NSMutableArray new];
|
||||
for (NSString *key in [request.request.allHTTPHeaderFields allKeys]) {
|
||||
NSDictionary<NSString *, id> *header = @{
|
||||
@"key": key,
|
||||
@"value": request.request.allHTTPHeaderFields[key]
|
||||
};
|
||||
[headers addObject: header];
|
||||
}
|
||||
|
||||
NSString *body = request.body;
|
||||
|
||||
[self send:@"newRequest"
|
||||
sonarObject:@{
|
||||
@"id": @(request.identifier),
|
||||
@"timestamp": @(request.timestamp),
|
||||
@"method": request.request.HTTPMethod ?: [NSNull null],
|
||||
@"url": [request.request.URL absoluteString] ?: [NSNull null],
|
||||
@"headers": headers,
|
||||
@"data": body ? body : [NSNull null]
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)didObserveResponse:(ResponseInfo)response
|
||||
{
|
||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response.response;
|
||||
|
||||
NSMutableArray<NSDictionary<NSString *, id> *> *headers = [NSMutableArray new];
|
||||
for (NSString *key in [httpResponse.allHeaderFields allKeys]) {
|
||||
NSDictionary<NSString *, id> *header = @{
|
||||
@"key": key,
|
||||
@"value": httpResponse.allHeaderFields[key]
|
||||
};
|
||||
[headers addObject: header];
|
||||
}
|
||||
|
||||
NSString *body = response.body;
|
||||
|
||||
[self send:@"newResponse"
|
||||
sonarObject:@{
|
||||
@"id": @(response.identifier),
|
||||
@"timestamp": @(response.timestamp),
|
||||
@"status": @(httpResponse.statusCode),
|
||||
@"reason": [NSHTTPURLResponse localizedStringForStatusCode: httpResponse.statusCode] ?: [NSNull null],
|
||||
@"headers": headers,
|
||||
@"data": body ? body : [NSNull null]
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user