FLEXArgumentInputObjectView.m 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. //
  2. // FLEXArgumentInputJSONObjectView.m
  3. // Flipboard
  4. //
  5. // Created by Ryan Olson on 6/15/14.
  6. // Copyright (c) 2020 FLEX Team. All rights reserved.
  7. //
  8. #import "FLEXArgumentInputObjectView.h"
  9. #import "FLEXRuntimeUtility.h"
  10. static const CGFloat kSegmentInputMargin = 10;
  11. typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
  12. FLEXArgInputObjectTypeJSON,
  13. FLEXArgInputObjectTypeAddress
  14. };
  15. @interface FLEXArgumentInputObjectView ()
  16. @property (nonatomic) UISegmentedControl *objectTypeSegmentControl;
  17. @property (nonatomic) FLEXArgInputObjectType inputType;
  18. @end
  19. @implementation FLEXArgumentInputObjectView
  20. - (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
  21. self = [super initWithArgumentTypeEncoding:typeEncoding];
  22. if (self) {
  23. // Start with the numbers and punctuation keyboard since quotes, curly braces, or
  24. // square brackets are likely to be the first characters type for the JSON.
  25. self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
  26. self.targetSize = FLEXArgumentInputViewSizeLarge;
  27. self.objectTypeSegmentControl = [[UISegmentedControl alloc] initWithItems:@[@"Value", @"Address"]];
  28. [self.objectTypeSegmentControl addTarget:self action:@selector(didChangeType) forControlEvents:UIControlEventValueChanged];
  29. self.objectTypeSegmentControl.selectedSegmentIndex = 0;
  30. [self addSubview:self.objectTypeSegmentControl];
  31. self.inputType = [[self class] preferredDefaultTypeForObjCType:typeEncoding withCurrentValue:nil];
  32. self.objectTypeSegmentControl.selectedSegmentIndex = self.inputType;
  33. }
  34. return self;
  35. }
  36. - (void)didChangeType {
  37. self.inputType = self.objectTypeSegmentControl.selectedSegmentIndex;
  38. if (super.inputValue) {
  39. // Trigger an update to the text field to show
  40. // the address of the stored object we were given,
  41. // or to show a JSON representation of the object
  42. [self populateTextAreaFromValue:super.inputValue];
  43. } else {
  44. // Clear the text field
  45. [self populateTextAreaFromValue:nil];
  46. }
  47. }
  48. - (void)setInputType:(FLEXArgInputObjectType)inputType {
  49. if (_inputType == inputType) return;
  50. _inputType = inputType;
  51. // Resize input view
  52. switch (inputType) {
  53. case FLEXArgInputObjectTypeJSON:
  54. self.targetSize = FLEXArgumentInputViewSizeLarge;
  55. break;
  56. case FLEXArgInputObjectTypeAddress:
  57. self.targetSize = FLEXArgumentInputViewSizeSmall;
  58. break;
  59. }
  60. // Change placeholder
  61. switch (inputType) {
  62. case FLEXArgInputObjectTypeJSON:
  63. self.inputPlaceholderText =
  64. @"You can put any valid JSON here, such as a string, number, array, or dictionary:"
  65. "\n\"This is a string\""
  66. "\n1234"
  67. "\n{ \"name\": \"Bob\", \"age\": 47 }"
  68. "\n["
  69. "\n 1, 2, 3"
  70. "\n]";
  71. break;
  72. case FLEXArgInputObjectTypeAddress:
  73. self.inputPlaceholderText = @"0x0000deadb33f";
  74. break;
  75. }
  76. [self setNeedsLayout];
  77. [self.superview setNeedsLayout];
  78. }
  79. - (void)setInputValue:(id)inputValue {
  80. super.inputValue = inputValue;
  81. [self populateTextAreaFromValue:inputValue];
  82. }
  83. - (id)inputValue {
  84. switch (self.inputType) {
  85. case FLEXArgInputObjectTypeJSON:
  86. return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
  87. case FLEXArgInputObjectTypeAddress: {
  88. NSScanner *scanner = [NSScanner scannerWithString:self.inputTextView.text];
  89. unsigned long long objectPointerValue;
  90. if ([scanner scanHexLongLong:&objectPointerValue]) {
  91. return (__bridge id)(void *)objectPointerValue;
  92. }
  93. return nil;
  94. }
  95. }
  96. }
  97. - (void)populateTextAreaFromValue:(id)value {
  98. if (!value) {
  99. self.inputTextView.text = nil;
  100. } else {
  101. if (self.inputType == FLEXArgInputObjectTypeJSON) {
  102. self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:value];
  103. } else if (self.inputType == FLEXArgInputObjectTypeAddress) {
  104. self.inputTextView.text = [NSString stringWithFormat:@"%p", value];
  105. }
  106. }
  107. // Delegate methods are not called for programmatic changes
  108. [self textViewDidChange:self.inputTextView];
  109. }
  110. - (CGSize)sizeThatFits:(CGSize)size {
  111. CGSize fitSize = [super sizeThatFits:size];
  112. fitSize.height += [self.objectTypeSegmentControl sizeThatFits:size].height + kSegmentInputMargin;
  113. return fitSize;
  114. }
  115. - (void)layoutSubviews {
  116. CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
  117. self.objectTypeSegmentControl.frame = CGRectMake(
  118. 0.0,
  119. // Our segmented control is taking the position
  120. // of the text view, as far as super is concerned,
  121. // and we override this property to be different
  122. super.topInputFieldVerticalLayoutGuide,
  123. self.frame.size.width,
  124. segmentHeight
  125. );
  126. [super layoutSubviews];
  127. }
  128. - (CGFloat)topInputFieldVerticalLayoutGuide {
  129. // Our text view is offset from the segmented control
  130. CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
  131. return segmentHeight + super.topInputFieldVerticalLayoutGuide + kSegmentInputMargin;
  132. }
  133. + (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
  134. NSParameterAssert(type);
  135. // Must be object type
  136. return type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass;
  137. }
  138. + (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value {
  139. NSParameterAssert(type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass);
  140. if (value) {
  141. // If there's a current value, it must be serializable to JSON
  142. // to display the JSON editor. Otherwise display the address field.
  143. if ([FLEXRuntimeUtility editableJSONStringForObject:value]) {
  144. return FLEXArgInputObjectTypeJSON;
  145. } else {
  146. return FLEXArgInputObjectTypeAddress;
  147. }
  148. } else {
  149. // Otherwise, see if we have more type information than just 'id'.
  150. // If we do, make sure the encoding is something serializable to JSON.
  151. // Properties and ivars keep more detailed type encoding information than method arguments.
  152. if (strcmp(type, @encode(id)) != 0) {
  153. BOOL isJSONSerializableType = NO;
  154. // Parse class name out of the string,
  155. // which is in the form `@"ClassName"`
  156. Class cls = NSClassFromString(({
  157. NSString *className = nil;
  158. NSScanner *scan = [NSScanner scannerWithString:@(type)];
  159. NSCharacterSet *allowed = [NSCharacterSet
  160. characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$"
  161. ];
  162. // Skip over the @" then scan the name
  163. if ([scan scanString:@"@\"" intoString:nil]) {
  164. [scan scanCharactersFromSet:allowed intoString:&className];
  165. }
  166. className;
  167. }));
  168. // Note: we can't use @encode(NSString) here because that drops
  169. // the class information and just goes to @encode(id).
  170. NSArray<Class> *jsonTypes = @[
  171. [NSString class],
  172. [NSNumber class],
  173. [NSArray class],
  174. [NSDictionary class],
  175. ];
  176. // Look for matching types
  177. for (Class jsonClass in jsonTypes) {
  178. if ([cls isSubclassOfClass:jsonClass]) {
  179. isJSONSerializableType = YES;
  180. break;
  181. }
  182. }
  183. if (isJSONSerializableType) {
  184. return FLEXArgInputObjectTypeJSON;
  185. } else {
  186. return FLEXArgInputObjectTypeAddress;
  187. }
  188. } else {
  189. return FLEXArgInputObjectTypeAddress;
  190. }
  191. }
  192. }
  193. @end