FLEXRuntime+UIKitHelpers.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. //
  2. // FLEXRuntime+UIKitHelpers.m
  3. // FLEX
  4. //
  5. // Created by Tanner Bennett on 12/16/19.
  6. // Copyright © 2019 Flipboard. All rights reserved.
  7. //
  8. #import "FLEXRuntime+UIKitHelpers.h"
  9. #import "FLEXRuntimeUtility.h"
  10. #import "FLEXPropertyAttributes.h"
  11. #import "FLEXArgumentInputViewFactory.h"
  12. #import "FLEXObjectExplorerFactory.h"
  13. #import "FLEXFieldEditorViewController.h"
  14. #import "FLEXMethodCallingViewController.h"
  15. #import "FLEXTableView.h"
  16. #import "FLEXUtility.h"
  17. #import "NSArray+Functional.h"
  18. #import "NSString+FLEX.h"
  19. #pragma mark FLEXProperty
  20. @implementation FLEXProperty (UIKitHelpers)
  21. /// Decide whether to use potentialTarget or [potentialTarget class] to get or set property
  22. - (id)appropriateTargetForPropertyType:(id)potentialTarget {
  23. if (!object_isClass(potentialTarget)) {
  24. if (self.isClassProperty) {
  25. return [potentialTarget class];
  26. } else {
  27. return potentialTarget;
  28. }
  29. } else {
  30. if (self.isClassProperty) {
  31. return potentialTarget;
  32. } else {
  33. // Instance property with a class object
  34. return nil;
  35. }
  36. }
  37. }
  38. - (BOOL)isEditable {
  39. if (self.attributes.isReadOnly) {
  40. return self.likelySetterExists;
  41. }
  42. const FLEXTypeEncoding *typeEncoding = self.attributes.typeEncoding.UTF8String;
  43. return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:nil];
  44. }
  45. - (BOOL)isCallable {
  46. return YES;
  47. }
  48. - (id)currentValueWithTarget:(id)object {
  49. return [self getPotentiallyUnboxedValue:
  50. [self appropriateTargetForPropertyType:object]
  51. ];
  52. }
  53. - (id)currentValueBeforeUnboxingWithTarget:(id)object {
  54. return [self getValue:
  55. [self appropriateTargetForPropertyType:object]
  56. ];
  57. }
  58. - (NSString *)previewWithTarget:(id)object {
  59. if (object_isClass(object) && !self.isClassProperty) {
  60. return self.attributes.fullDeclaration;
  61. } else {
  62. return [FLEXRuntimeUtility
  63. summaryForObject:[self currentValueWithTarget:object]
  64. ];
  65. }
  66. }
  67. - (UIViewController *)viewerWithTarget:(id)object {
  68. id value = [self currentValueWithTarget:object];
  69. return [FLEXObjectExplorerFactory explorerViewControllerForObject:value];
  70. }
  71. - (UIViewController *)editorWithTarget:(id)object {
  72. id target = [self appropriateTargetForPropertyType:object];
  73. return [FLEXFieldEditorViewController target:target property:self];
  74. }
  75. - (UITableViewCellAccessoryType)suggestedAccessoryTypeWithTarget:(id)object {
  76. id targetForValueCheck = [self appropriateTargetForPropertyType:object];
  77. if (!targetForValueCheck) {
  78. // Instance property with a class object
  79. return UITableViewCellAccessoryNone;
  80. }
  81. // We use .tag to store the cached value of .isEditable that is
  82. // initialized by FLEXObjectExplorer in -reloadMetada
  83. if ([self getPotentiallyUnboxedValue:targetForValueCheck]) {
  84. if (self.tag) {
  85. // Editable non-nil value, both
  86. return UITableViewCellAccessoryDetailDisclosureButton;
  87. } else {
  88. // Uneditable non-nil value, chevron only
  89. return UITableViewCellAccessoryDisclosureIndicator;
  90. }
  91. } else {
  92. if (self.tag) {
  93. // Editable nil value, just (i)
  94. return UITableViewCellAccessoryDetailButton;
  95. } else {
  96. // Non-editable nil value, neither
  97. return UITableViewCellAccessoryNone;
  98. }
  99. }
  100. }
  101. - (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
  102. #if FLEX_AT_LEAST_IOS13_SDK
  103. - (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
  104. Class propertyClass = self.attributes.typeEncoding.flex_typeClass;
  105. // "Explore PropertyClass" for properties with a concrete class name
  106. if (propertyClass) {
  107. NSString *title = [NSString stringWithFormat:@"Explore %@", NSStringFromClass(propertyClass)];
  108. return @[[UIAction actionWithTitle:title image:nil identifier:nil handler:^(UIAction *action) {
  109. UIViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:propertyClass];
  110. [sender.navigationController pushViewController:explorer animated:YES];
  111. }]];
  112. }
  113. return nil;
  114. }
  115. - (NSArray<NSString *> *)copiableMetadataWithTarget:(id)object {
  116. BOOL returnsObject = self.attributes.typeEncoding.flex_typeIsObjectOrClass;
  117. BOOL targetNotNil = [self appropriateTargetForPropertyType:object] != nil;
  118. NSMutableArray *items = [NSMutableArray arrayWithArray:@[
  119. @"Name", self.name ?: @"",
  120. @"Type", self.attributes.typeEncoding ?: @"",
  121. @"Declaration", self.fullDescription ?: @"",
  122. ]];
  123. if (targetNotNil) {
  124. id value = [self currentValueBeforeUnboxingWithTarget:object];
  125. [items addObjectsFromArray:@[
  126. @"Value Preview", [self previewWithTarget:object],
  127. @"Value Address", returnsObject ? [FLEXUtility addressOfObject:value] : @"",
  128. ]];
  129. }
  130. [items addObjectsFromArray:@[
  131. @"Getter", NSStringFromSelector(self.likelyGetter) ?: @"",
  132. @"Setter", self.likelySetterExists ? NSStringFromSelector(self.likelySetter) : @"",
  133. @"Image Name", self.imageName ?: @"",
  134. @"Attributes", self.attributes.string ?: @"",
  135. @"objc_property", [FLEXUtility pointerToString:self.objc_property],
  136. @"objc_property_attribute_t", [FLEXUtility pointerToString:self.attributes.list],
  137. ]];
  138. return items;
  139. }
  140. - (NSString *)contextualSubtitleWithTarget:(id)object {
  141. id target = [self appropriateTargetForPropertyType:object];
  142. if (target && self.attributes.typeEncoding.flex_typeIsObjectOrClass) {
  143. return [FLEXUtility addressOfObject:[self currentValueBeforeUnboxingWithTarget:target]];
  144. }
  145. return nil;
  146. }
  147. #endif
  148. @end
  149. #pragma mark FLEXIvar
  150. @implementation FLEXIvar (UIKitHelpers)
  151. - (BOOL)isEditable {
  152. const FLEXTypeEncoding *typeEncoding = self.typeEncoding.UTF8String;
  153. return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:nil];
  154. }
  155. - (BOOL)isCallable {
  156. return NO;
  157. }
  158. - (id)currentValueWithTarget:(id)object {
  159. if (!object_isClass(object)) {
  160. return [self getPotentiallyUnboxedValue:object];
  161. }
  162. return nil;
  163. }
  164. - (NSString *)previewWithTarget:(id)object {
  165. if (object_isClass(object)) {
  166. return self.details;
  167. }
  168. return [FLEXRuntimeUtility
  169. summaryForObject:[self currentValueWithTarget:object]
  170. ];
  171. }
  172. - (UIViewController *)viewerWithTarget:(id)object {
  173. NSAssert(!object_isClass(object), @"Unreachable state: viewing ivar on class object");
  174. id value = [self currentValueWithTarget:object];
  175. return [FLEXObjectExplorerFactory explorerViewControllerForObject:value];
  176. }
  177. - (UIViewController *)editorWithTarget:(id)object {
  178. NSAssert(!object_isClass(object), @"Unreachable state: editing ivar on class object");
  179. return [FLEXFieldEditorViewController target:object ivar:self];
  180. }
  181. - (UITableViewCellAccessoryType)suggestedAccessoryTypeWithTarget:(id)object {
  182. if (object_isClass(object)) {
  183. return UITableViewCellAccessoryNone;
  184. }
  185. // Could use .isEditable here, but we use .tag for speed since it is cached
  186. if ([self getPotentiallyUnboxedValue:object]) {
  187. if (self.tag) {
  188. // Editable non-nil value, both
  189. return UITableViewCellAccessoryDetailDisclosureButton;
  190. } else {
  191. // Uneditable non-nil value, chevron only
  192. return UITableViewCellAccessoryDisclosureIndicator;
  193. }
  194. } else {
  195. if (self.tag) {
  196. // Editable nil value, just (i)
  197. return UITableViewCellAccessoryDetailButton;
  198. } else {
  199. // Non-editable nil value, neither
  200. return UITableViewCellAccessoryNone;
  201. }
  202. }
  203. }
  204. - (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
  205. #if FLEX_AT_LEAST_IOS13_SDK
  206. - (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
  207. Class ivarClass = self.typeEncoding.flex_typeClass;
  208. // "Explore PropertyClass" for properties with a concrete class name
  209. if (ivarClass) {
  210. NSString *title = [NSString stringWithFormat:@"Explore %@", NSStringFromClass(ivarClass)];
  211. return @[[UIAction actionWithTitle:title image:nil identifier:nil handler:^(UIAction *action) {
  212. UIViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:ivarClass];
  213. [sender.navigationController pushViewController:explorer animated:YES];
  214. }]];
  215. }
  216. return nil;
  217. }
  218. - (NSArray<NSString *> *)copiableMetadataWithTarget:(id)object {
  219. BOOL isInstance = !object_isClass(object);
  220. BOOL returnsObject = self.typeEncoding.flex_typeIsObjectOrClass;
  221. id value = isInstance ? [self getValue:object] : nil;
  222. NSMutableArray *items = [NSMutableArray arrayWithArray:@[
  223. @"Name", self.name ?: @"",
  224. @"Type", self.typeEncoding ?: @"",
  225. @"Declaration", self.description ?: @"",
  226. ]];
  227. if (isInstance) {
  228. [items addObjectsFromArray:@[
  229. @"Value Preview", isInstance ? [self previewWithTarget:object] : @"",
  230. @"Value Address", returnsObject ? [FLEXUtility addressOfObject:value] : @"",
  231. ]];
  232. }
  233. [items addObjectsFromArray:@[
  234. @"Size", @(self.size).stringValue,
  235. @"Offset", @(self.offset).stringValue,
  236. @"objc_ivar", [FLEXUtility pointerToString:self.objc_ivar],
  237. ]];
  238. return items;
  239. }
  240. - (NSString *)contextualSubtitleWithTarget:(id)object {
  241. if (!object_isClass(object) && self.typeEncoding.flex_typeIsObjectOrClass) {
  242. return [FLEXUtility addressOfObject:[self getValue:object]];
  243. }
  244. return nil;
  245. }
  246. #endif
  247. @end
  248. #pragma mark FLEXMethod
  249. @implementation FLEXMethodBase (UIKitHelpers)
  250. - (BOOL)isEditable {
  251. return NO;
  252. }
  253. - (BOOL)isCallable {
  254. return NO;
  255. }
  256. - (id)currentValueWithTarget:(id)object {
  257. // Methods can't be "edited" and have no "value"
  258. return nil;
  259. }
  260. - (NSString *)previewWithTarget:(id)object {
  261. return [self.selectorString stringByAppendingFormat:@" — %@", self.typeEncoding];
  262. }
  263. - (UIViewController *)viewerWithTarget:(id)object {
  264. // We disallow calling of FLEXMethodBase methods
  265. @throw NSInternalInconsistencyException;
  266. return nil;
  267. }
  268. - (UIViewController *)editorWithTarget:(id)object {
  269. // Methods cannot be edited
  270. @throw NSInternalInconsistencyException;
  271. return nil;
  272. }
  273. - (UITableViewCellAccessoryType)suggestedAccessoryTypeWithTarget:(id)object {
  274. // We shouldn't be using any FLEXMethodBase objects for this
  275. @throw NSInternalInconsistencyException;
  276. return UITableViewCellAccessoryNone;
  277. }
  278. - (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
  279. #if FLEX_AT_LEAST_IOS13_SDK
  280. - (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
  281. return nil;
  282. }
  283. - (NSArray<NSString *> *)copiableMetadataWithTarget:(id)object {
  284. return @[
  285. @"Selector", self.name ?: @"",
  286. @"Type Encoding", self.typeEncoding ?: @"",
  287. @"Declaration", self.description ?: @"",
  288. ];
  289. }
  290. - (NSString *)contextualSubtitleWithTarget:(id)object {
  291. return nil;
  292. }
  293. #endif
  294. @end
  295. @implementation FLEXMethod (UIKitHelpers)
  296. - (BOOL)isCallable {
  297. return self.signature != nil;
  298. }
  299. - (UIViewController *)viewerWithTarget:(id)object {
  300. object = self.isInstanceMethod ? object : (object_isClass(object) ? object : [object class]);
  301. return [FLEXMethodCallingViewController target:object method:self];
  302. }
  303. - (UITableViewCellAccessoryType)suggestedAccessoryTypeWithTarget:(id)object {
  304. if (self.isInstanceMethod) {
  305. if (object_isClass(object)) {
  306. // Instance method from class, can't call
  307. return UITableViewCellAccessoryNone;
  308. } else {
  309. // Instance method from instance, can call
  310. return UITableViewCellAccessoryDisclosureIndicator;
  311. }
  312. } else {
  313. return UITableViewCellAccessoryDisclosureIndicator;
  314. }
  315. }
  316. - (NSArray<NSString *> *)copiableMetadataWithTarget:(id)object {
  317. return [[super copiableMetadataWithTarget:object] arrayByAddingObjectsFromArray:@[
  318. @"NSMethodSignature *", [FLEXUtility addressOfObject:self.signature],
  319. @"Signature String", self.signatureString ?: @"",
  320. @"Number of Arguments", @(self.numberOfArguments).stringValue,
  321. @"Return Type", @(self.returnType ?: ""),
  322. @"Return Size", @(self.returnSize).stringValue,
  323. @"objc_method", [FLEXUtility pointerToString:self.objc_method],
  324. ]];
  325. }
  326. @end
  327. #pragma mark FLEXProtocol
  328. @implementation FLEXProtocol (UIKitHelpers)
  329. - (BOOL)isEditable {
  330. return NO;
  331. }
  332. - (BOOL)isCallable {
  333. return NO;
  334. }
  335. - (id)currentValueWithTarget:(id)object {
  336. return nil;
  337. }
  338. - (NSString *)previewWithTarget:(id)object {
  339. return nil;
  340. }
  341. - (UIViewController *)viewerWithTarget:(id)object {
  342. return [FLEXObjectExplorerFactory explorerViewControllerForObject:self];
  343. }
  344. - (UIViewController *)editorWithTarget:(id)object {
  345. return nil;
  346. }
  347. - (UITableViewCellAccessoryType)suggestedAccessoryTypeWithTarget:(id)object {
  348. return UITableViewCellAccessoryDisclosureIndicator;
  349. }
  350. - (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
  351. #if FLEX_AT_LEAST_IOS13_SDK
  352. - (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
  353. return nil;
  354. }
  355. - (NSArray<NSString *> *)copiableMetadataWithTarget:(id)object {
  356. NSArray<NSString *> *conformanceNames = [self.protocols valueForKeyPath:@"name"];
  357. NSString *conformances = [conformanceNames componentsJoinedByString:@"\n"];
  358. return @[
  359. @"Name", self.name ?: @"",
  360. @"Conformances", conformances,
  361. ];
  362. }
  363. - (NSString *)contextualSubtitleWithTarget:(id)object {
  364. return nil;
  365. }
  366. #endif
  367. @end
  368. #pragma mark FLEXStaticMetadata
  369. @interface FLEXStaticMetadata () {
  370. @protected
  371. NSString *_name;
  372. }
  373. @property (nonatomic) FLEXTableViewCellReuseIdentifier reuse;
  374. @property (nonatomic) NSString *subtitle;
  375. @property (nonatomic) id metadata;
  376. @end
  377. @interface FLEXStaticMetadata_Class : FLEXStaticMetadata
  378. + (instancetype)withClass:(Class)cls;
  379. @end
  380. @implementation FLEXStaticMetadata
  381. @synthesize name = _name;
  382. @synthesize tag = _tag;
  383. + (NSArray<FLEXStaticMetadata *> *)classHierarchy:(NSArray<Class> *)classes {
  384. return [classes flex_mapped:^id(Class cls, NSUInteger idx) {
  385. return [FLEXStaticMetadata_Class withClass:cls];
  386. }];
  387. }
  388. + (instancetype)style:(FLEXStaticMetadataRowStyle)style title:(NSString *)title string:(NSString *)string {
  389. return [[self alloc] initWithStyle:style title:title subtitle:string];
  390. }
  391. + (instancetype)style:(FLEXStaticMetadataRowStyle)style title:(NSString *)title number:(NSNumber *)number {
  392. return [[self alloc] initWithStyle:style title:title subtitle:number.stringValue];
  393. }
  394. - (id)initWithStyle:(FLEXStaticMetadataRowStyle)style title:(NSString *)title subtitle:(NSString *)subtitle {
  395. self = [super init];
  396. if (self) {
  397. if (style == FLEXStaticMetadataRowStyleKeyValue) {
  398. _reuse = kFLEXKeyValueCell;
  399. } else {
  400. _reuse = kFLEXMultilineDetailCell;
  401. }
  402. _name = title;
  403. _subtitle = subtitle;
  404. }
  405. return self;
  406. }
  407. - (NSString *)description {
  408. return self.name;
  409. }
  410. - (NSString *)reuseIdentifierWithTarget:(id)object {
  411. return self.reuse;
  412. }
  413. - (BOOL)isEditable {
  414. return NO;
  415. }
  416. - (BOOL)isCallable {
  417. return NO;
  418. }
  419. - (id)currentValueWithTarget:(id)object {
  420. return nil;
  421. }
  422. - (NSString *)previewWithTarget:(id)object {
  423. return self.subtitle;
  424. }
  425. - (UIViewController *)viewerWithTarget:(id)object {
  426. return nil;
  427. }
  428. - (UIViewController *)editorWithTarget:(id)object {
  429. return nil;
  430. }
  431. - (UITableViewCellAccessoryType)suggestedAccessoryTypeWithTarget:(id)object {
  432. return UITableViewCellAccessoryNone;
  433. }
  434. #if FLEX_AT_LEAST_IOS13_SDK
  435. - (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
  436. return nil;
  437. }
  438. - (NSArray<NSString *> *)copiableMetadataWithTarget:(id)object {
  439. return @[self.name, self.subtitle];
  440. }
  441. - (NSString *)contextualSubtitleWithTarget:(id)object {
  442. return nil;
  443. }
  444. #endif
  445. @end
  446. #pragma mark FLEXStaticMetadata_Class
  447. @implementation FLEXStaticMetadata_Class
  448. + (instancetype)withClass:(Class)cls {
  449. NSParameterAssert(cls);
  450. FLEXStaticMetadata_Class *metadata = [self new];
  451. metadata.metadata = cls;
  452. metadata->_name = NSStringFromClass(cls);
  453. metadata.reuse = kFLEXDefaultCell;
  454. return metadata;
  455. }
  456. - (id)initWithStyle:(FLEXStaticMetadataRowStyle)style title:(NSString *)title subtitle:(NSString *)subtitle {
  457. @throw NSInternalInconsistencyException;
  458. return nil;
  459. }
  460. - (UIViewController *)viewerWithTarget:(id)object {
  461. return [FLEXObjectExplorerFactory explorerViewControllerForObject:self.metadata];
  462. }
  463. - (UITableViewCellAccessoryType)suggestedAccessoryTypeWithTarget:(id)object {
  464. return UITableViewCellAccessoryDisclosureIndicator;
  465. }
  466. - (NSArray<NSString *> *)copiableMetadataWithTarget:(id)object {
  467. return @[
  468. @"Class Name", self.name,
  469. @"Class", [FLEXUtility addressOfObject:self.metadata]
  470. ];
  471. }
  472. - (NSString *)contextualSubtitleWithTarget:(id)object {
  473. return [FLEXUtility addressOfObject:self.metadata];
  474. }
  475. @end