FLEXArgumentInputObjectView.m 7.4 KB

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