FLEXObjectExplorerViewController.m 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074
  1. //
  2. // FLEXObjectExplorerViewController.m
  3. // Flipboard
  4. //
  5. // Created by Ryan Olson on 2014-05-03.
  6. // Copyright (c) 2014 Flipboard. All rights reserved.
  7. //
  8. #import "FLEXObjectExplorerViewController.h"
  9. #import "FLEXUtility.h"
  10. #import "FLEXRuntimeUtility.h"
  11. #import "FLEXMultilineTableViewCell.h"
  12. #import "FLEXObjectExplorerFactory.h"
  13. #import "FLEXPropertyEditorViewController.h"
  14. #import "FLEXIvarEditorViewController.h"
  15. #import "FLEXMethodCallingViewController.h"
  16. #import "FLEXInstancesTableViewController.h"
  17. #import "FLEXTableView.h"
  18. #import "FLEXScopeCarousel.h"
  19. #import <objc/runtime.h>
  20. typedef NS_ENUM(NSUInteger, FLEXMetadataKind) {
  21. FLEXMetadataKindProperties,
  22. FLEXMetadataKindIvars,
  23. FLEXMetadataKindMethods,
  24. FLEXMetadataKindClassMethods
  25. };
  26. // Convenience boxes to keep runtime properties, ivars, and methods in foundation collections.
  27. @interface FLEXPropertyBox : NSObject
  28. @property (nonatomic) objc_property_t property;
  29. @end
  30. @implementation FLEXPropertyBox
  31. @end
  32. @interface FLEXIvarBox : NSObject
  33. @property (nonatomic) Ivar ivar;
  34. @end
  35. @implementation FLEXIvarBox
  36. @end
  37. @interface FLEXMethodBox : NSObject
  38. @property (nonatomic) Method method;
  39. @end
  40. @implementation FLEXMethodBox
  41. @end
  42. @interface FLEXObjectExplorerViewController ()
  43. @property (nonatomic) NSMutableArray<NSArray<FLEXPropertyBox *> *> *properties;
  44. @property (nonatomic) NSArray<FLEXPropertyBox *> *filteredProperties;
  45. @property (nonatomic) NSMutableArray<NSArray<FLEXIvarBox *> *> *ivars;
  46. @property (nonatomic) NSArray<FLEXIvarBox *> *filteredIvars;
  47. @property (nonatomic) NSMutableArray<NSArray<FLEXMethodBox *> *> *methods;
  48. @property (nonatomic) NSArray<FLEXMethodBox *> *filteredMethods;
  49. @property (nonatomic) NSMutableArray<NSArray<FLEXMethodBox *> *> *classMethods;
  50. @property (nonatomic) NSArray<FLEXMethodBox *> *filteredClassMethods;
  51. @property (nonatomic, copy) NSArray<Class> *classHierarchy;
  52. @property (nonatomic, copy) NSArray<Class> *filteredSuperclasses;
  53. @property (nonatomic) NSArray *cachedCustomSectionRowCookies;
  54. @property (nonatomic) NSIndexSet *customSectionVisibleIndexes;
  55. @property (nonatomic) NSString *filterText;
  56. /// An index into the `classHierarchy` array
  57. @property (nonatomic) NSInteger classScope;
  58. @end
  59. @implementation FLEXObjectExplorerViewController
  60. + (void)initialize
  61. {
  62. if (self == [FLEXObjectExplorerViewController class]) {
  63. // Initialize custom menu items for entire app
  64. UIMenuItem *copyObjectAddress = [[UIMenuItem alloc] initWithTitle:@"Copy Address" action:@selector(copyObjectAddress:)];
  65. UIMenuController.sharedMenuController.menuItems = @[copyObjectAddress];
  66. [UIMenuController.sharedMenuController update];
  67. }
  68. }
  69. - (void)loadView
  70. {
  71. self.tableView = [[FLEXTableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
  72. }
  73. - (void)viewDidLoad
  74. {
  75. [super viewDidLoad];
  76. self.showsSearchBar = YES;
  77. self.searchBarDebounceInterval = kFLEXDebounceInstant;
  78. self.showsCarousel = YES;
  79. [self refreshScopeTitles];
  80. self.refreshControl = [UIRefreshControl new];
  81. [self.refreshControl addTarget:self action:@selector(refreshControlDidRefresh:) forControlEvents:UIControlEventValueChanged];
  82. }
  83. - (void)viewWillAppear:(BOOL)animated
  84. {
  85. [super viewWillAppear:animated];
  86. // Reload the entire table view rather than just the visible cells because the filtered rows
  87. // may have changed (i.e. a change in the description row that causes it to get filtered out).
  88. [self updateTableData];
  89. }
  90. - (void)refreshControlDidRefresh:(id)sender
  91. {
  92. [self updateTableData];
  93. [self.refreshControl endRefreshing];
  94. }
  95. - (BOOL)shouldShowDescription
  96. {
  97. // Not if we have filter text that doesn't match the desctiption.
  98. if (self.filterText.length) {
  99. NSString *description = [self displayedObjectDescription];
  100. return [description rangeOfString:self.filterText options:NSCaseInsensitiveSearch].length > 0;
  101. }
  102. return YES;
  103. }
  104. - (NSString *)displayedObjectDescription
  105. {
  106. NSString *desc = [FLEXUtility safeDescriptionForObject:self.object];
  107. if (!desc.length) {
  108. NSString *address = [FLEXUtility addressOfObject:self.object];
  109. desc = [NSString stringWithFormat:@"Object at %@ returned empty description", address];
  110. }
  111. return desc;
  112. }
  113. #pragma mark - Search
  114. - (void)refreshScopeTitles
  115. {
  116. [self updateSuperclasses];
  117. self.carousel.items = [FLEXUtility map:self.classHierarchy block:^id(Class cls, NSUInteger idx) {
  118. return NSStringFromClass(cls);
  119. }];
  120. [self updateTableData];
  121. }
  122. - (void)updateSearchResults:(NSString *)newText;
  123. {
  124. self.filterText = newText;
  125. [self updateDisplayedData];
  126. }
  127. - (NSArray *)metadata:(FLEXMetadataKind)metadataKind forClassAtIndex:(NSUInteger)idx
  128. {
  129. switch (metadataKind) {
  130. case FLEXMetadataKindProperties:
  131. return self.properties[idx];
  132. case FLEXMetadataKindIvars:
  133. return self.ivars[idx];
  134. case FLEXMetadataKindMethods:
  135. return self.methods[idx];
  136. case FLEXMetadataKindClassMethods:
  137. return self.classMethods[idx];
  138. }
  139. }
  140. - (NSInteger)totalCountOfMetadata:(FLEXMetadataKind)metadataKind forClassAtIndex:(NSUInteger)idx
  141. {
  142. return [self metadata:metadataKind forClassAtIndex:idx].count;
  143. }
  144. #pragma mark - Setter overrides
  145. - (void)setObject:(id)object
  146. {
  147. _object = object;
  148. // Use [object class] here rather than object_getClass because we don't want to show the KVO prefix for observed objects.
  149. self.title = [[object class] description];
  150. // Only refresh if the view has appeared
  151. // TODO: make .object readonly so we don't have to deal with this...
  152. if (self.showsCarousel) {
  153. [self refreshScopeTitles];
  154. }
  155. }
  156. #pragma mark - Reloading
  157. - (void)updateTableData
  158. {
  159. [self updateCustomData];
  160. [self updateMetadata];
  161. [self updateDisplayedData];
  162. }
  163. - (void)updateDisplayedData
  164. {
  165. [self updateFilteredCustomData];
  166. [self updateFilteredProperties];
  167. [self updateFilteredIvars];
  168. [self updateFilteredMethods];
  169. [self updateFilteredClassMethods];
  170. [self updateFilteredSuperclasses];
  171. if (self.isViewLoaded) {
  172. [self.tableView reloadData];
  173. }
  174. }
  175. - (void)updateMetadata
  176. {
  177. self.properties = [NSMutableArray new];
  178. self.ivars = [NSMutableArray new];
  179. self.methods = [NSMutableArray new];
  180. self.classMethods = [NSMutableArray new];
  181. for (Class cls in self.classHierarchy) {
  182. [self.properties addObject:[[self class] propertiesForClass:cls]];
  183. [self.ivars addObject:[[self class] ivarsForClass:cls]];
  184. [self.methods addObject:[[self class] methodsForClass:cls]];
  185. [self.classMethods addObject:[[self class] methodsForClass:object_getClass(cls)]];
  186. }
  187. }
  188. #pragma mark - Properties
  189. + (NSArray<FLEXPropertyBox *> *)propertiesForClass:(Class)class
  190. {
  191. if (!class) {
  192. return @[];
  193. }
  194. NSMutableArray<FLEXPropertyBox *> *boxedProperties = [NSMutableArray array];
  195. unsigned int propertyCount = 0;
  196. objc_property_t *propertyList = class_copyPropertyList(class, &propertyCount);
  197. if (propertyList) {
  198. for (unsigned int i = 0; i < propertyCount; i++) {
  199. FLEXPropertyBox *propertyBox = [FLEXPropertyBox new];
  200. propertyBox.property = propertyList[i];
  201. [boxedProperties addObject:propertyBox];
  202. }
  203. free(propertyList);
  204. }
  205. return boxedProperties;
  206. }
  207. - (void)updateFilteredProperties
  208. {
  209. NSArray<FLEXPropertyBox *> *candidateProperties = [self metadata:FLEXMetadataKindProperties forClassAtIndex:self.selectedScope];
  210. NSArray<FLEXPropertyBox *> *unsortedFilteredProperties = nil;
  211. if (self.filterText.length > 0) {
  212. NSMutableArray<FLEXPropertyBox *> *mutableUnsortedFilteredProperties = [NSMutableArray array];
  213. for (FLEXPropertyBox *propertyBox in candidateProperties) {
  214. NSString *prettyName = [FLEXRuntimeUtility prettyNameForProperty:propertyBox.property];
  215. if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
  216. [mutableUnsortedFilteredProperties addObject:propertyBox];
  217. }
  218. }
  219. unsortedFilteredProperties = mutableUnsortedFilteredProperties;
  220. } else {
  221. unsortedFilteredProperties = candidateProperties;
  222. }
  223. self.filteredProperties = [unsortedFilteredProperties sortedArrayUsingComparator:^NSComparisonResult(FLEXPropertyBox *propertyBox1, FLEXPropertyBox *propertyBox2) {
  224. NSString *name1 = [NSString stringWithUTF8String:property_getName(propertyBox1.property)];
  225. NSString *name2 = [NSString stringWithUTF8String:property_getName(propertyBox2.property)];
  226. return [name1 caseInsensitiveCompare:name2];
  227. }];
  228. }
  229. - (NSString *)titleForPropertyAtIndex:(NSInteger)index
  230. {
  231. FLEXPropertyBox *propertyBox = self.filteredProperties[index];
  232. return [FLEXRuntimeUtility prettyNameForProperty:propertyBox.property];
  233. }
  234. - (id)valueForPropertyAtIndex:(NSInteger)index
  235. {
  236. id value = nil;
  237. if ([self canHaveInstanceState]) {
  238. FLEXPropertyBox *propertyBox = self.filteredProperties[index];
  239. NSString *typeString = [FLEXRuntimeUtility typeEncodingForProperty:propertyBox.property];
  240. const FLEXTypeEncoding *encoding = [typeString cStringUsingEncoding:NSUTF8StringEncoding];
  241. value = [FLEXRuntimeUtility valueForProperty:propertyBox.property onObject:self.object];
  242. value = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:value type:encoding];
  243. }
  244. return value;
  245. }
  246. #pragma mark - Ivars
  247. + (NSArray<FLEXIvarBox *> *)ivarsForClass:(Class)class
  248. {
  249. if (!class) {
  250. return @[];
  251. }
  252. NSMutableArray<FLEXIvarBox *> *boxedIvars = [NSMutableArray array];
  253. unsigned int ivarCount = 0;
  254. Ivar *ivarList = class_copyIvarList(class, &ivarCount);
  255. if (ivarList) {
  256. for (unsigned int i = 0; i < ivarCount; i++) {
  257. FLEXIvarBox *ivarBox = [FLEXIvarBox new];
  258. ivarBox.ivar = ivarList[i];
  259. [boxedIvars addObject:ivarBox];
  260. }
  261. free(ivarList);
  262. }
  263. return boxedIvars;
  264. }
  265. - (void)updateFilteredIvars
  266. {
  267. NSArray<FLEXIvarBox *> *candidateIvars = [self metadata:FLEXMetadataKindIvars forClassAtIndex:self.selectedScope];
  268. NSArray<FLEXIvarBox *> *unsortedFilteredIvars = nil;
  269. if (self.filterText.length > 0) {
  270. NSMutableArray<FLEXIvarBox *> *mutableUnsortedFilteredIvars = [NSMutableArray array];
  271. for (FLEXIvarBox *ivarBox in candidateIvars) {
  272. NSString *prettyName = [FLEXRuntimeUtility prettyNameForIvar:ivarBox.ivar];
  273. if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
  274. [mutableUnsortedFilteredIvars addObject:ivarBox];
  275. }
  276. }
  277. unsortedFilteredIvars = mutableUnsortedFilteredIvars;
  278. } else {
  279. unsortedFilteredIvars = candidateIvars;
  280. }
  281. self.filteredIvars = [unsortedFilteredIvars sortedArrayUsingComparator:^NSComparisonResult(FLEXIvarBox *ivarBox1, FLEXIvarBox *ivarBox2) {
  282. NSString *name1 = [NSString stringWithUTF8String:ivar_getName(ivarBox1.ivar)];
  283. NSString *name2 = [NSString stringWithUTF8String:ivar_getName(ivarBox2.ivar)];
  284. return [name1 caseInsensitiveCompare:name2];
  285. }];
  286. }
  287. - (NSString *)titleForIvarAtIndex:(NSInteger)index
  288. {
  289. FLEXIvarBox *ivarBox = self.filteredIvars[index];
  290. return [FLEXRuntimeUtility prettyNameForIvar:ivarBox.ivar];
  291. }
  292. - (id)valueForIvarAtIndex:(NSInteger)index
  293. {
  294. id value = nil;
  295. if ([self canHaveInstanceState]) {
  296. FLEXIvarBox *ivarBox = self.filteredIvars[index];
  297. const FLEXTypeEncoding *encoding = ivar_getTypeEncoding(ivarBox.ivar);
  298. value = [FLEXRuntimeUtility valueForIvar:ivarBox.ivar onObject:self.object];
  299. value = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:value type:encoding];
  300. }
  301. return value;
  302. }
  303. #pragma mark - Methods
  304. - (void)updateFilteredMethods
  305. {
  306. NSArray<FLEXMethodBox *> *candidateMethods = [self metadata:FLEXMetadataKindMethods forClassAtIndex:self.selectedScope];
  307. self.filteredMethods = [self filteredMethodsFromMethods:candidateMethods areClassMethods:NO];
  308. }
  309. - (void)updateFilteredClassMethods
  310. {
  311. NSArray<FLEXMethodBox *> *candidateMethods = [self metadata:FLEXMetadataKindClassMethods forClassAtIndex:self.selectedScope];
  312. self.filteredClassMethods = [self filteredMethodsFromMethods:candidateMethods areClassMethods:YES];
  313. }
  314. + (NSArray<FLEXMethodBox *> *)methodsForClass:(Class)class
  315. {
  316. if (!class) {
  317. return @[];
  318. }
  319. NSMutableArray<FLEXMethodBox *> *boxedMethods = [NSMutableArray array];
  320. unsigned int methodCount = 0;
  321. Method *methodList = class_copyMethodList(class, &methodCount);
  322. if (methodList) {
  323. for (unsigned int i = 0; i < methodCount; i++) {
  324. FLEXMethodBox *methodBox = [FLEXMethodBox new];
  325. methodBox.method = methodList[i];
  326. [boxedMethods addObject:methodBox];
  327. }
  328. free(methodList);
  329. }
  330. return boxedMethods;
  331. }
  332. - (NSArray<FLEXMethodBox *> *)filteredMethodsFromMethods:(NSArray<FLEXMethodBox *> *)methods areClassMethods:(BOOL)areClassMethods
  333. {
  334. NSArray<FLEXMethodBox *> *candidateMethods = methods;
  335. NSArray<FLEXMethodBox *> *unsortedFilteredMethods = nil;
  336. if (self.filterText.length > 0) {
  337. NSMutableArray<FLEXMethodBox *> *mutableUnsortedFilteredMethods = [NSMutableArray array];
  338. for (FLEXMethodBox *methodBox in candidateMethods) {
  339. NSString *prettyName = [FLEXRuntimeUtility prettyNameForMethod:methodBox.method isClassMethod:areClassMethods];
  340. if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
  341. [mutableUnsortedFilteredMethods addObject:methodBox];
  342. }
  343. }
  344. unsortedFilteredMethods = mutableUnsortedFilteredMethods;
  345. } else {
  346. unsortedFilteredMethods = candidateMethods;
  347. }
  348. NSArray<FLEXMethodBox *> *sortedFilteredMethods = [unsortedFilteredMethods sortedArrayUsingComparator:^NSComparisonResult(FLEXMethodBox *methodBox1, FLEXMethodBox *methodBox2) {
  349. NSString *name1 = NSStringFromSelector(method_getName(methodBox1.method));
  350. NSString *name2 = NSStringFromSelector(method_getName(methodBox2.method));
  351. return [name1 caseInsensitiveCompare:name2];
  352. }];
  353. return sortedFilteredMethods;
  354. }
  355. - (NSString *)titleForMethodAtIndex:(NSInteger)index
  356. {
  357. FLEXMethodBox *methodBox = self.filteredMethods[index];
  358. return [FLEXRuntimeUtility prettyNameForMethod:methodBox.method isClassMethod:NO];
  359. }
  360. - (NSString *)titleForClassMethodAtIndex:(NSInteger)index
  361. {
  362. FLEXMethodBox *classMethodBox = self.filteredClassMethods[index];
  363. return [FLEXRuntimeUtility prettyNameForMethod:classMethodBox.method isClassMethod:YES];
  364. }
  365. - (objc_property_t)viewPropertyForName:(NSString *)propertyName
  366. {
  367. return class_getProperty([self.object class], propertyName.UTF8String);
  368. }
  369. #pragma mark - Superclasses
  370. - (void)updateSuperclasses
  371. {
  372. self.classHierarchy = [FLEXRuntimeUtility classHierarchyOfObject:self.object];
  373. }
  374. - (void)updateFilteredSuperclasses
  375. {
  376. if (self.filterText.length > 0) {
  377. NSMutableArray<Class> *filteredSuperclasses = [NSMutableArray array];
  378. for (Class superclass in self.classHierarchy) {
  379. if ([NSStringFromClass(superclass) localizedCaseInsensitiveContainsString:self.filterText]) {
  380. [filteredSuperclasses addObject:superclass];
  381. }
  382. }
  383. self.filteredSuperclasses = filteredSuperclasses;
  384. } else {
  385. self.filteredSuperclasses = self.classHierarchy;
  386. }
  387. }
  388. #pragma mark - Table View Data Helpers
  389. - (NSArray<NSNumber *> *)possibleExplorerSections
  390. {
  391. static NSArray<NSNumber *> *possibleSections = nil;
  392. static dispatch_once_t onceToken;
  393. dispatch_once(&onceToken, ^{
  394. possibleSections = @[@(FLEXObjectExplorerSectionDescription),
  395. @(FLEXObjectExplorerSectionCustom),
  396. @(FLEXObjectExplorerSectionProperties),
  397. @(FLEXObjectExplorerSectionIvars),
  398. @(FLEXObjectExplorerSectionMethods),
  399. @(FLEXObjectExplorerSectionClassMethods),
  400. @(FLEXObjectExplorerSectionSuperclasses),
  401. @(FLEXObjectExplorerSectionReferencingInstances)];
  402. });
  403. return possibleSections;
  404. }
  405. - (NSArray<NSNumber *> *)visibleExplorerSections
  406. {
  407. NSMutableArray<NSNumber *> *visibleSections = [NSMutableArray array];
  408. for (NSNumber *possibleSection in [self possibleExplorerSections]) {
  409. FLEXObjectExplorerSection explorerSection = [possibleSection unsignedIntegerValue];
  410. if ([self numberOfRowsForExplorerSection:explorerSection] > 0) {
  411. [visibleSections addObject:possibleSection];
  412. }
  413. }
  414. return visibleSections;
  415. }
  416. - (NSString *)sectionTitleWithBaseName:(NSString *)baseName totalCount:(NSUInteger)totalCount filteredCount:(NSUInteger)filteredCount
  417. {
  418. NSString *sectionTitle = nil;
  419. if (totalCount == filteredCount) {
  420. sectionTitle = [baseName stringByAppendingFormat:@" (%lu)", (unsigned long)totalCount];
  421. } else {
  422. sectionTitle = [baseName stringByAppendingFormat:@" (%lu of %lu)", (unsigned long)filteredCount, (unsigned long)totalCount];
  423. }
  424. return sectionTitle;
  425. }
  426. - (FLEXObjectExplorerSection)explorerSectionAtIndex:(NSInteger)sectionIndex
  427. {
  428. return [[[self visibleExplorerSections] objectAtIndex:sectionIndex] unsignedIntegerValue];
  429. }
  430. - (NSInteger)numberOfRowsForExplorerSection:(FLEXObjectExplorerSection)section
  431. {
  432. NSInteger numberOfRows = 0;
  433. switch (section) {
  434. case FLEXObjectExplorerSectionDescription:
  435. numberOfRows = [self shouldShowDescription] ? 1 : 0;
  436. break;
  437. case FLEXObjectExplorerSectionCustom:
  438. numberOfRows = self.customSectionVisibleIndexes.count;
  439. break;
  440. case FLEXObjectExplorerSectionProperties:
  441. numberOfRows = self.filteredProperties.count;
  442. break;
  443. case FLEXObjectExplorerSectionIvars:
  444. numberOfRows = self.filteredIvars.count;
  445. break;
  446. case FLEXObjectExplorerSectionMethods:
  447. numberOfRows = self.filteredMethods.count;
  448. break;
  449. case FLEXObjectExplorerSectionClassMethods:
  450. numberOfRows = self.filteredClassMethods.count;
  451. break;
  452. case FLEXObjectExplorerSectionSuperclasses:
  453. numberOfRows = self.filteredSuperclasses.count;
  454. break;
  455. case FLEXObjectExplorerSectionReferencingInstances:
  456. // Hide this section if there is fliter text since there's nothing searchable (only 1 row, always the same).
  457. numberOfRows = self.filterText.length == 0 ? 1 : 0;
  458. break;
  459. }
  460. return numberOfRows;
  461. }
  462. - (NSString *)titleForRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
  463. {
  464. NSString *title = nil;
  465. switch (section) {
  466. case FLEXObjectExplorerSectionDescription:
  467. title = [self displayedObjectDescription];
  468. break;
  469. case FLEXObjectExplorerSectionCustom:
  470. title = [self customSectionTitleForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
  471. break;
  472. case FLEXObjectExplorerSectionProperties:
  473. title = [self titleForPropertyAtIndex:row];
  474. break;
  475. case FLEXObjectExplorerSectionIvars:
  476. title = [self titleForIvarAtIndex:row];
  477. break;
  478. case FLEXObjectExplorerSectionMethods:
  479. title = [self titleForMethodAtIndex:row];
  480. break;
  481. case FLEXObjectExplorerSectionClassMethods:
  482. title = [self titleForClassMethodAtIndex:row];
  483. break;
  484. case FLEXObjectExplorerSectionSuperclasses:
  485. title = NSStringFromClass(self.filteredSuperclasses[row]);
  486. break;
  487. case FLEXObjectExplorerSectionReferencingInstances:
  488. title = @"Other objects with ivars referencing this object";
  489. break;
  490. }
  491. return title;
  492. }
  493. - (NSString *)subtitleForRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
  494. {
  495. NSString *subtitle = nil;
  496. switch (section) {
  497. case FLEXObjectExplorerSectionDescription:
  498. break;
  499. case FLEXObjectExplorerSectionCustom:
  500. subtitle = [self customSectionSubtitleForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
  501. break;
  502. case FLEXObjectExplorerSectionProperties:
  503. subtitle = [self canHaveInstanceState] ? [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:[self valueForPropertyAtIndex:row]] : nil;
  504. break;
  505. case FLEXObjectExplorerSectionIvars:
  506. subtitle = [self canHaveInstanceState] ? [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:[self valueForIvarAtIndex:row]] : nil;
  507. break;
  508. case FLEXObjectExplorerSectionMethods:
  509. break;
  510. case FLEXObjectExplorerSectionClassMethods:
  511. break;
  512. case FLEXObjectExplorerSectionSuperclasses:
  513. break;
  514. case FLEXObjectExplorerSectionReferencingInstances:
  515. break;
  516. }
  517. return subtitle;
  518. }
  519. - (BOOL)canDrillInToRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
  520. {
  521. BOOL canDrillIn = NO;
  522. switch (section) {
  523. case FLEXObjectExplorerSectionDescription:
  524. break;
  525. case FLEXObjectExplorerSectionCustom:
  526. canDrillIn = [self customSectionCanDrillIntoRowWithCookie:[self customSectionRowCookieForVisibleRow:row]];
  527. break;
  528. case FLEXObjectExplorerSectionProperties: {
  529. if ([self canHaveInstanceState]) {
  530. FLEXPropertyBox *propertyBox = self.filteredProperties[row];
  531. objc_property_t property = propertyBox.property;
  532. id currentValue = [self valueForPropertyAtIndex:row];
  533. BOOL canEdit = [FLEXPropertyEditorViewController canEditProperty:property onObject:self.object currentValue:currentValue];
  534. BOOL canExplore = currentValue != nil;
  535. canDrillIn = canEdit || canExplore;
  536. }
  537. } break;
  538. case FLEXObjectExplorerSectionIvars: {
  539. if ([self canHaveInstanceState]) {
  540. FLEXIvarBox *ivarBox = self.filteredIvars[row];
  541. Ivar ivar = ivarBox.ivar;
  542. id currentValue = [self valueForIvarAtIndex:row];
  543. BOOL canEdit = [FLEXIvarEditorViewController canEditIvar:ivar currentValue:currentValue];
  544. BOOL canExplore = currentValue != nil;
  545. canDrillIn = canEdit || canExplore;
  546. }
  547. } break;
  548. case FLEXObjectExplorerSectionMethods:
  549. canDrillIn = [self canCallInstanceMethods];
  550. break;
  551. case FLEXObjectExplorerSectionClassMethods:
  552. canDrillIn = YES;
  553. break;
  554. case FLEXObjectExplorerSectionSuperclasses:
  555. canDrillIn = YES;
  556. break;
  557. case FLEXObjectExplorerSectionReferencingInstances:
  558. canDrillIn = YES;
  559. break;
  560. }
  561. return canDrillIn;
  562. }
  563. - (BOOL)sectionHasActions:(NSInteger)section
  564. {
  565. return [self explorerSectionAtIndex:section] == FLEXObjectExplorerSectionDescription;
  566. }
  567. - (NSString *)titleForExplorerSection:(FLEXObjectExplorerSection)section
  568. {
  569. NSString *title = nil;
  570. switch (section) {
  571. case FLEXObjectExplorerSectionDescription: {
  572. title = @"Description";
  573. } break;
  574. case FLEXObjectExplorerSectionCustom: {
  575. title = [self customSectionTitle];
  576. } break;
  577. case FLEXObjectExplorerSectionProperties: {
  578. NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindProperties forClassAtIndex:self.selectedScope];
  579. title = [self sectionTitleWithBaseName:@"Properties" totalCount:totalCount filteredCount:self.filteredProperties.count];
  580. } break;
  581. case FLEXObjectExplorerSectionIvars: {
  582. NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindIvars forClassAtIndex:self.selectedScope];
  583. title = [self sectionTitleWithBaseName:@"Ivars" totalCount:totalCount filteredCount:self.filteredIvars.count];
  584. } break;
  585. case FLEXObjectExplorerSectionMethods: {
  586. NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindMethods forClassAtIndex:self.selectedScope];
  587. title = [self sectionTitleWithBaseName:@"Methods" totalCount:totalCount filteredCount:self.filteredMethods.count];
  588. } break;
  589. case FLEXObjectExplorerSectionClassMethods: {
  590. NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindClassMethods forClassAtIndex:self.selectedScope];
  591. title = [self sectionTitleWithBaseName:@"Class Methods" totalCount:totalCount filteredCount:self.filteredClassMethods.count];
  592. } break;
  593. case FLEXObjectExplorerSectionSuperclasses: {
  594. title = [self sectionTitleWithBaseName:@"Superclasses" totalCount:self.classHierarchy.count filteredCount:self.filteredSuperclasses.count];
  595. } break;
  596. case FLEXObjectExplorerSectionReferencingInstances: {
  597. title = @"Object Graph";
  598. } break;
  599. }
  600. return title;
  601. }
  602. - (UIViewController *)drillInViewControllerForRow:(NSUInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
  603. {
  604. UIViewController *viewController = nil;
  605. switch (section) {
  606. case FLEXObjectExplorerSectionDescription:
  607. break;
  608. case FLEXObjectExplorerSectionCustom:
  609. viewController = [self customSectionDrillInViewControllerForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
  610. break;
  611. case FLEXObjectExplorerSectionProperties: {
  612. FLEXPropertyBox *propertyBox = self.filteredProperties[row];
  613. objc_property_t property = propertyBox.property;
  614. id currentValue = [self valueForPropertyAtIndex:row];
  615. if ([FLEXPropertyEditorViewController canEditProperty:property onObject:self.object currentValue:currentValue]) {
  616. viewController = [[FLEXPropertyEditorViewController alloc] initWithTarget:self.object property:property];
  617. } else if (currentValue) {
  618. viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:currentValue];
  619. }
  620. } break;
  621. case FLEXObjectExplorerSectionIvars: {
  622. FLEXIvarBox *ivarBox = self.filteredIvars[row];
  623. Ivar ivar = ivarBox.ivar;
  624. id currentValue = [self valueForIvarAtIndex:row];
  625. if ([FLEXIvarEditorViewController canEditIvar:ivar currentValue:currentValue]) {
  626. viewController = [[FLEXIvarEditorViewController alloc] initWithTarget:self.object ivar:ivar];
  627. } else if (currentValue) {
  628. viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:currentValue];
  629. }
  630. } break;
  631. case FLEXObjectExplorerSectionMethods: {
  632. FLEXMethodBox *methodBox = self.filteredMethods[row];
  633. Method method = methodBox.method;
  634. viewController = [[FLEXMethodCallingViewController alloc] initWithTarget:self.object method:method];
  635. } break;
  636. case FLEXObjectExplorerSectionClassMethods: {
  637. FLEXMethodBox *methodBox = self.filteredClassMethods[row];
  638. Method method = methodBox.method;
  639. viewController = [[FLEXMethodCallingViewController alloc] initWithTarget:[self.object class] method:method];
  640. } break;
  641. case FLEXObjectExplorerSectionSuperclasses: {
  642. Class superclass = self.filteredSuperclasses[row];
  643. viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:superclass];
  644. } break;
  645. case FLEXObjectExplorerSectionReferencingInstances: {
  646. viewController = [FLEXInstancesTableViewController instancesTableViewControllerForInstancesReferencingObject:self.object];
  647. } break;
  648. }
  649. return viewController;
  650. }
  651. #pragma mark - Table View Data Source
  652. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
  653. {
  654. return [self visibleExplorerSections].count;
  655. }
  656. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  657. {
  658. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:section];
  659. return [self numberOfRowsForExplorerSection:explorerSection];
  660. }
  661. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
  662. {
  663. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:section];
  664. return [self titleForExplorerSection:explorerSection];
  665. }
  666. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  667. {
  668. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  669. BOOL isCustomSection = explorerSection == FLEXObjectExplorerSectionCustom;
  670. BOOL useDescriptionCell = explorerSection == FLEXObjectExplorerSectionDescription;
  671. NSString *cellIdentifier = useDescriptionCell ? kFLEXMultilineCell : @"cell";
  672. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
  673. if (!cell) {
  674. if (useDescriptionCell) {
  675. cell = [[FLEXMultilineTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
  676. cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
  677. } else {
  678. cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
  679. UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
  680. cell.textLabel.font = cellFont;
  681. cell.detailTextLabel.font = cellFont;
  682. cell.detailTextLabel.textColor = UIColor.grayColor;
  683. }
  684. }
  685. UIView *customView;
  686. if (isCustomSection) {
  687. customView = [self customViewForRowCookie:[self customSectionRowCookieForVisibleRow:indexPath.row]];
  688. if (customView) {
  689. [cell.contentView addSubview:customView];
  690. }
  691. }
  692. cell.textLabel.text = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
  693. cell.detailTextLabel.text = [self subtitleForRow:indexPath.row inExplorerSection:explorerSection];
  694. cell.accessoryType = [self canDrillInToRow:indexPath.row inExplorerSection:explorerSection] ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
  695. return cell;
  696. }
  697. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
  698. {
  699. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  700. CGFloat height = self.tableView.rowHeight;
  701. if (explorerSection == FLEXObjectExplorerSectionDescription) {
  702. NSString *text = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
  703. NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:@{ NSFontAttributeName : [FLEXUtility defaultTableViewCellLabelFont] }];
  704. CGFloat preferredHeight = [FLEXMultilineTableViewCell preferredHeightWithAttributedText:attributedText inTableViewWidth:self.tableView.frame.size.width style:tableView.style showsAccessory:NO];
  705. height = MAX(height, preferredHeight);
  706. } else if (explorerSection == FLEXObjectExplorerSectionCustom) {
  707. id cookie = [self customSectionRowCookieForVisibleRow:indexPath.row];
  708. height = [self heightForCustomViewRowForRowCookie:cookie];
  709. }
  710. return height;
  711. }
  712. #pragma mark - Table View Delegate
  713. - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath
  714. {
  715. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  716. return [self canDrillInToRow:indexPath.row inExplorerSection:explorerSection];
  717. }
  718. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
  719. {
  720. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  721. UIViewController *detailViewController = [self drillInViewControllerForRow:indexPath.row inExplorerSection:explorerSection];
  722. if (detailViewController) {
  723. [self.navigationController pushViewController:detailViewController animated:YES];
  724. } else {
  725. [tableView deselectRowAtIndexPath:indexPath animated:YES];
  726. }
  727. }
  728. - (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
  729. {
  730. return [self sectionHasActions:indexPath.section];
  731. }
  732. - (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
  733. {
  734. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  735. switch (explorerSection) {
  736. case FLEXObjectExplorerSectionDescription:
  737. return action == @selector(copy:) || action == @selector(copyObjectAddress:);
  738. default:
  739. return NO;
  740. }
  741. }
  742. - (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
  743. {
  744. #pragma clang diagnostic push
  745. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  746. [self performSelector:action withObject:indexPath];
  747. #pragma clang diagnostic pop
  748. }
  749. #pragma mark - UIMenuController
  750. /// Prevent the search bar from trying to use us as a responder
  751. ///
  752. /// Our table cells will use the UITableViewDelegate methods
  753. /// to make sure we can perform the actions we want to
  754. - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
  755. {
  756. return NO;
  757. }
  758. - (void)copy:(NSIndexPath *)indexPath
  759. {
  760. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  761. NSString *stringToCopy = @"";
  762. NSString *title = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
  763. if (title.length) {
  764. stringToCopy = [stringToCopy stringByAppendingString:title];
  765. }
  766. NSString *subtitle = [self subtitleForRow:indexPath.row inExplorerSection:explorerSection];
  767. if (subtitle.length) {
  768. if (stringToCopy.length) {
  769. stringToCopy = [stringToCopy stringByAppendingString:@"\n\n"];
  770. }
  771. stringToCopy = [stringToCopy stringByAppendingString:subtitle];
  772. }
  773. UIPasteboard.generalPasteboard.string = stringToCopy;
  774. }
  775. - (void)copyObjectAddress:(NSIndexPath *)indexPath
  776. {
  777. UIPasteboard.generalPasteboard.string = [FLEXUtility addressOfObject:self.object];
  778. }
  779. #pragma mark - Custom Section
  780. - (void)updateCustomData
  781. {
  782. self.cachedCustomSectionRowCookies = [self customSectionRowCookies];
  783. }
  784. - (void)updateFilteredCustomData
  785. {
  786. NSIndexSet *filteredIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.cachedCustomSectionRowCookies.count)];
  787. if (self.filterText.length > 0) {
  788. filteredIndexSet = [filteredIndexSet indexesPassingTest:^BOOL(NSUInteger index, BOOL *stop) {
  789. BOOL matches = NO;
  790. NSString *rowTitle = [self customSectionTitleForRowCookie:self.cachedCustomSectionRowCookies[index]];
  791. if ([rowTitle rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
  792. matches = YES;
  793. }
  794. return matches;
  795. }];
  796. }
  797. self.customSectionVisibleIndexes = filteredIndexSet;
  798. }
  799. - (id)customSectionRowCookieForVisibleRow:(NSUInteger)row
  800. {
  801. return [[self.cachedCustomSectionRowCookies objectsAtIndexes:self.customSectionVisibleIndexes] objectAtIndex:row];
  802. }
  803. #pragma mark - Subclasses Can Override
  804. - (NSString *)customSectionTitle
  805. {
  806. return self.shortcutPropertyNames.count ? @"Shortcuts" : nil;
  807. }
  808. - (NSArray *)customSectionRowCookies
  809. {
  810. return self.shortcutPropertyNames;
  811. }
  812. - (NSString *)customSectionTitleForRowCookie:(id)rowCookie
  813. {
  814. if ([rowCookie isKindOfClass:[NSString class]]) {
  815. objc_property_t property = [self viewPropertyForName:rowCookie];
  816. if (property) {
  817. NSString *prettyPropertyName = [FLEXRuntimeUtility prettyNameForProperty:property];
  818. // Since we're outside of the "properties" section, prepend @property for clarity.
  819. return [@"@property " stringByAppendingString:prettyPropertyName];
  820. } else if ([rowCookie respondsToSelector:@selector(description)]) {
  821. return [@"No property found for object: " stringByAppendingString:[rowCookie description]];
  822. } else {
  823. NSString *cls = NSStringFromClass([rowCookie class]);
  824. return [@"No property found for object of class " stringByAppendingString:cls];
  825. }
  826. }
  827. return nil;
  828. }
  829. - (NSString *)customSectionSubtitleForRowCookie:(id)rowCookie
  830. {
  831. if ([rowCookie isKindOfClass:[NSString class]]) {
  832. objc_property_t property = [self viewPropertyForName:rowCookie];
  833. if (property) {
  834. id value = [FLEXRuntimeUtility valueForProperty:property onObject:self.object];
  835. return [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:value];
  836. } else {
  837. return nil;
  838. }
  839. }
  840. return nil;
  841. }
  842. - (BOOL)customSectionCanDrillIntoRowWithCookie:(id)rowCookie
  843. {
  844. return YES;
  845. }
  846. - (UIViewController *)customSectionDrillInViewControllerForRowCookie:(id)rowCookie
  847. {
  848. if ([rowCookie isKindOfClass:[NSString class]]) {
  849. objc_property_t property = [self viewPropertyForName:rowCookie];
  850. if (property) {
  851. id currentValue = [FLEXRuntimeUtility valueForProperty:property onObject:self.object];
  852. if ([FLEXPropertyEditorViewController canEditProperty:property onObject:self.object currentValue:currentValue]) {
  853. return [[FLEXPropertyEditorViewController alloc] initWithTarget:self.object property:property];
  854. } else {
  855. return [FLEXObjectExplorerFactory explorerViewControllerForObject:currentValue];
  856. }
  857. } else {
  858. [NSException raise:NSInternalInconsistencyException
  859. format:@"Cannot drill into row for cookie: %@", rowCookie];
  860. return nil;
  861. }
  862. }
  863. return nil;
  864. }
  865. - (UIView *)customViewForRowCookie:(id)rowCookie
  866. {
  867. return nil;
  868. }
  869. - (CGFloat)heightForCustomViewRowForRowCookie:(id)rowCookie
  870. {
  871. return self.tableView.rowHeight;
  872. }
  873. - (BOOL)canHaveInstanceState
  874. {
  875. return YES;
  876. }
  877. - (BOOL)canCallInstanceMethods
  878. {
  879. return YES;
  880. }
  881. @end
  882. @implementation FLEXObjectExplorerViewController (Shortcuts)
  883. - (NSArray<NSString *> *)shortcutPropertyNames { return @[]; }
  884. @end