FLEXRuntime+UIKitHelpers.m 18 KB

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