You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
7.8 KiB
243 lines
7.8 KiB
//
|
|
// FLEXArgumentInputJSONObjectView.m
|
|
// Flipboard
|
|
//
|
|
// Created by Ryan Olson on 6/15/14.
|
|
// Copyright (c) 2014 Flipboard. All rights reserved.
|
|
//
|
|
|
|
#import "FLEXArgumentInputObjectView.h"
|
|
#import "FLEXRuntimeUtility.h"
|
|
|
|
static const CGFloat kSegmentInputMargin = 10;
|
|
|
|
typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
|
|
FLEXArgInputObjectTypeJSON,
|
|
FLEXArgInputObjectTypeAddress
|
|
};
|
|
|
|
@interface FLEXArgumentInputObjectView ()
|
|
|
|
@property (nonatomic) UISegmentedControl *objectTypeSegmentControl;
|
|
@property (nonatomic) FLEXArgInputObjectType inputType;
|
|
|
|
@end
|
|
|
|
@implementation FLEXArgumentInputObjectView
|
|
|
|
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
|
{
|
|
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
|
if (self) {
|
|
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
|
|
// square brackets are likely to be the first characters type for the JSON.
|
|
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
|
|
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
|
|
|
self.objectTypeSegmentControl = [[UISegmentedControl alloc] initWithItems:@[@"Value", @"Address"]];
|
|
[self.objectTypeSegmentControl addTarget:self action:@selector(didChangeType) forControlEvents:UIControlEventValueChanged];
|
|
self.objectTypeSegmentControl.selectedSegmentIndex = 0;
|
|
[self addSubview:self.objectTypeSegmentControl];
|
|
|
|
self.inputType = [[self class] preferredDefaultTypeForObjCType:typeEncoding withCurrentValue:nil];
|
|
self.objectTypeSegmentControl.selectedSegmentIndex = self.inputType;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)didChangeType
|
|
{
|
|
self.inputType = self.objectTypeSegmentControl.selectedSegmentIndex;
|
|
|
|
if (super.inputValue) {
|
|
// Trigger an update to the text field to show
|
|
// the address of the stored object we were given,
|
|
// or to show a JSON representation of the object
|
|
[self populateTextAreaFromValue:super.inputValue];
|
|
} else {
|
|
// Clear the text field
|
|
[self populateTextAreaFromValue:nil];
|
|
}
|
|
}
|
|
|
|
- (void)setInputType:(FLEXArgInputObjectType)inputType
|
|
{
|
|
if (_inputType == inputType) return;
|
|
|
|
_inputType = inputType;
|
|
|
|
// Resize input view
|
|
switch (inputType) {
|
|
case FLEXArgInputObjectTypeJSON:
|
|
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
|
break;
|
|
case FLEXArgInputObjectTypeAddress:
|
|
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
|
break;
|
|
}
|
|
|
|
// Change placeholder
|
|
switch (inputType) {
|
|
case FLEXArgInputObjectTypeJSON:
|
|
self.inputPlaceholderText =
|
|
@"You can put any valid JSON here, such as a string, number, array, or dictionary:"
|
|
"\n\"This is a string\""
|
|
"\n1234"
|
|
"\n{ \"name\": \"Bob\", \"age\": 47 }"
|
|
"\n["
|
|
"\n 1, 2, 3"
|
|
"\n]";
|
|
break;
|
|
case FLEXArgInputObjectTypeAddress:
|
|
self.inputPlaceholderText = @"0x0000deadb33f";
|
|
break;
|
|
}
|
|
|
|
[self setNeedsLayout];
|
|
[self.superview setNeedsLayout];
|
|
}
|
|
|
|
- (void)setInputValue:(id)inputValue
|
|
{
|
|
super.inputValue = inputValue;
|
|
[self populateTextAreaFromValue:inputValue];
|
|
}
|
|
|
|
- (id)inputValue
|
|
{
|
|
switch (self.inputType) {
|
|
case FLEXArgInputObjectTypeJSON:
|
|
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
|
|
case FLEXArgInputObjectTypeAddress: {
|
|
NSScanner *scanner = [NSScanner scannerWithString:self.inputTextView.text];
|
|
|
|
unsigned long long objectPointerValue;
|
|
if ([scanner scanHexLongLong:&objectPointerValue]) {
|
|
return (__bridge id)(void *)objectPointerValue;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)populateTextAreaFromValue:(id)value
|
|
{
|
|
if (!value) {
|
|
self.inputTextView.text = nil;
|
|
} else {
|
|
if (self.inputType == FLEXArgInputObjectTypeJSON) {
|
|
self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:value];
|
|
} else if (self.inputType == FLEXArgInputObjectTypeAddress) {
|
|
self.inputTextView.text = [NSString stringWithFormat:@"%p", value];
|
|
}
|
|
}
|
|
|
|
// Delegate methods are not called for programmatic changes
|
|
[self textViewDidChange:self.inputTextView];
|
|
}
|
|
|
|
- (CGSize)sizeThatFits:(CGSize)size
|
|
{
|
|
CGSize fitSize = [super sizeThatFits:size];
|
|
fitSize.height += [self.objectTypeSegmentControl sizeThatFits:size].height + kSegmentInputMargin;
|
|
|
|
return fitSize;
|
|
}
|
|
|
|
- (void)layoutSubviews
|
|
{
|
|
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
|
|
self.objectTypeSegmentControl.frame = CGRectMake(
|
|
0.0,
|
|
// Our segmented control is taking the position
|
|
// of the text view, as far as super is concerned,
|
|
// and we override this property to be different
|
|
super.topInputFieldVerticalLayoutGuide,
|
|
self.frame.size.width,
|
|
segmentHeight
|
|
);
|
|
|
|
[super layoutSubviews];
|
|
}
|
|
|
|
- (CGFloat)topInputFieldVerticalLayoutGuide
|
|
{
|
|
// Our text view is offset from the segmented control
|
|
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
|
|
return segmentHeight + super.topInputFieldVerticalLayoutGuide + kSegmentInputMargin;
|
|
}
|
|
|
|
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
|
{
|
|
NSParameterAssert(type);
|
|
// Must be object type
|
|
return type[0] == '@';
|
|
}
|
|
|
|
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value
|
|
{
|
|
NSParameterAssert(type[0] == '@');
|
|
|
|
if (value) {
|
|
// If there's a current value, it must be serializable to JSON
|
|
// to display the JSON editor. Otherwise display the address field.
|
|
if ([FLEXRuntimeUtility editableJSONStringForObject:value]) {
|
|
return FLEXArgInputObjectTypeJSON;
|
|
} else {
|
|
return FLEXArgInputObjectTypeAddress;
|
|
}
|
|
} else {
|
|
// Otherwise, see if we have more type information than just 'id'.
|
|
// If we do, make sure the encoding is something serializable to JSON.
|
|
// Properties and ivars keep more detailed type encoding information than method arguments.
|
|
if (strcmp(type, @encode(id)) != 0) {
|
|
BOOL isJSONSerializableType = NO;
|
|
|
|
// Parse class name out of the string,
|
|
// which is in the form `@"ClassName"`
|
|
Class cls = NSClassFromString(({
|
|
NSString *className = nil;
|
|
NSScanner *scan = [NSScanner scannerWithString:@(type)];
|
|
NSCharacterSet *allowed = [NSCharacterSet
|
|
characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$"
|
|
];
|
|
|
|
// Skip over the @" then scan the name
|
|
if ([scan scanString:@"@\"" intoString:nil]) {
|
|
[scan scanCharactersFromSet:allowed intoString:&className];
|
|
}
|
|
|
|
className;
|
|
}));
|
|
|
|
// Note: we can't use @encode(NSString) here because that drops
|
|
// the class information and just goes to @encode(id).
|
|
NSArray<Class> *jsonTypes = @[
|
|
[NSString class],
|
|
[NSNumber class],
|
|
[NSArray class],
|
|
[NSDictionary class],
|
|
];
|
|
|
|
// Look for matching types
|
|
for (Class jsonClass in jsonTypes) {
|
|
if ([cls isSubclassOfClass:jsonClass]) {
|
|
isJSONSerializableType = YES;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isJSONSerializableType) {
|
|
return FLEXArgInputObjectTypeJSON;
|
|
} else {
|
|
return FLEXArgInputObjectTypeAddress;
|
|
}
|
|
} else {
|
|
return FLEXArgInputObjectTypeAddress;
|
|
}
|
|
}
|
|
}
|
|
|
|
@end
|