123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- //
- // FLEXArgumentInputJSONObjectView.m
- // Flipboard
- //
- // Created by Ryan Olson on 6/15/14.
- // Copyright (c) 2020 FLEX Team. 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] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass;
- }
- + (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value {
- NSParameterAssert(type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass);
- 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
|