FLEXObjectExplorerViewController.m 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104
  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, assign) objc_property_t property;
  29. @end
  30. @implementation FLEXPropertyBox
  31. @end
  32. @interface FLEXIvarBox : NSObject
  33. @property (nonatomic, assign) Ivar ivar;
  34. @end
  35. @implementation FLEXIvarBox
  36. @end
  37. @interface FLEXMethodBox : NSObject
  38. @property (nonatomic, assign) Method method;
  39. @end
  40. @implementation FLEXMethodBox
  41. @end
  42. @interface FLEXObjectExplorerViewController ()
  43. @property (nonatomic, strong) NSMutableArray<NSArray<FLEXPropertyBox *> *> *properties;
  44. @property (nonatomic, strong) NSArray<FLEXPropertyBox *> *filteredProperties;
  45. @property (nonatomic, strong) NSMutableArray<NSArray<FLEXIvarBox *> *> *ivars;
  46. @property (nonatomic, strong) NSArray<FLEXIvarBox *> *filteredIvars;
  47. @property (nonatomic, strong) NSMutableArray<NSArray<FLEXMethodBox *> *> *methods;
  48. @property (nonatomic, strong) NSArray<FLEXMethodBox *> *filteredMethods;
  49. @property (nonatomic, strong) NSMutableArray<NSArray<FLEXMethodBox *> *> *classMethods;
  50. @property (nonatomic, strong) NSArray<FLEXMethodBox *> *filteredClassMethods;
  51. @property (nonatomic, copy) NSArray<Class> *classHierarchy;
  52. @property (nonatomic, copy) NSArray<Class> *filteredSuperclasses;
  53. @property (nonatomic, strong) NSArray *cachedCustomSectionRowCookies;
  54. @property (nonatomic, strong) NSIndexSet *customSectionVisibleIndexes;
  55. @property (nonatomic, strong) 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. /// Skips NSObject
  208. + (NSArray<FLEXPropertyBox *> *)inheritedPropertiesForClass:(Class)class
  209. {
  210. NSMutableArray<FLEXPropertyBox *> *inheritedProperties = [NSMutableArray array];
  211. while ((class = [class superclass]) && class != [NSObject class]) {
  212. [inheritedProperties addObjectsFromArray:[self propertiesForClass:class]];
  213. }
  214. return inheritedProperties;
  215. }
  216. - (void)updateFilteredProperties
  217. {
  218. NSArray<FLEXPropertyBox *> *candidateProperties = [self metadata:FLEXMetadataKindProperties forClassAtIndex:self.selectedScope];
  219. NSArray<FLEXPropertyBox *> *unsortedFilteredProperties = nil;
  220. if (self.filterText.length > 0) {
  221. NSMutableArray<FLEXPropertyBox *> *mutableUnsortedFilteredProperties = [NSMutableArray array];
  222. for (FLEXPropertyBox *propertyBox in candidateProperties) {
  223. NSString *prettyName = [FLEXRuntimeUtility prettyNameForProperty:propertyBox.property];
  224. if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
  225. [mutableUnsortedFilteredProperties addObject:propertyBox];
  226. }
  227. }
  228. unsortedFilteredProperties = mutableUnsortedFilteredProperties;
  229. } else {
  230. unsortedFilteredProperties = candidateProperties;
  231. }
  232. self.filteredProperties = [unsortedFilteredProperties sortedArrayUsingComparator:^NSComparisonResult(FLEXPropertyBox *propertyBox1, FLEXPropertyBox *propertyBox2) {
  233. NSString *name1 = [NSString stringWithUTF8String:property_getName(propertyBox1.property)];
  234. NSString *name2 = [NSString stringWithUTF8String:property_getName(propertyBox2.property)];
  235. return [name1 caseInsensitiveCompare:name2];
  236. }];
  237. }
  238. - (NSString *)titleForPropertyAtIndex:(NSInteger)index
  239. {
  240. FLEXPropertyBox *propertyBox = self.filteredProperties[index];
  241. return [FLEXRuntimeUtility prettyNameForProperty:propertyBox.property];
  242. }
  243. - (id)valueForPropertyAtIndex:(NSInteger)index
  244. {
  245. id value = nil;
  246. if ([self canHaveInstanceState]) {
  247. FLEXPropertyBox *propertyBox = self.filteredProperties[index];
  248. NSString *typeString = [FLEXRuntimeUtility typeEncodingForProperty:propertyBox.property];
  249. const FLEXTypeEncoding *encoding = [typeString cStringUsingEncoding:NSUTF8StringEncoding];
  250. value = [FLEXRuntimeUtility valueForProperty:propertyBox.property onObject:self.object];
  251. value = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:value type:encoding];
  252. }
  253. return value;
  254. }
  255. #pragma mark - Ivars
  256. + (NSArray<FLEXIvarBox *> *)ivarsForClass:(Class)class
  257. {
  258. if (!class) {
  259. return @[];
  260. }
  261. NSMutableArray<FLEXIvarBox *> *boxedIvars = [NSMutableArray array];
  262. unsigned int ivarCount = 0;
  263. Ivar *ivarList = class_copyIvarList(class, &ivarCount);
  264. if (ivarList) {
  265. for (unsigned int i = 0; i < ivarCount; i++) {
  266. FLEXIvarBox *ivarBox = [FLEXIvarBox new];
  267. ivarBox.ivar = ivarList[i];
  268. [boxedIvars addObject:ivarBox];
  269. }
  270. free(ivarList);
  271. }
  272. return boxedIvars;
  273. }
  274. /// Skips NSObject
  275. + (NSArray<FLEXIvarBox *> *)inheritedIvarsForClass:(Class)class
  276. {
  277. NSMutableArray<FLEXIvarBox *> *inheritedIvars = [NSMutableArray array];
  278. while ((class = [class superclass]) && class != [NSObject class]) {
  279. [inheritedIvars addObjectsFromArray:[self ivarsForClass:class]];
  280. }
  281. return inheritedIvars;
  282. }
  283. - (void)updateFilteredIvars
  284. {
  285. NSArray<FLEXIvarBox *> *candidateIvars = [self metadata:FLEXMetadataKindIvars forClassAtIndex:self.selectedScope];
  286. NSArray<FLEXIvarBox *> *unsortedFilteredIvars = nil;
  287. if (self.filterText.length > 0) {
  288. NSMutableArray<FLEXIvarBox *> *mutableUnsortedFilteredIvars = [NSMutableArray array];
  289. for (FLEXIvarBox *ivarBox in candidateIvars) {
  290. NSString *prettyName = [FLEXRuntimeUtility prettyNameForIvar:ivarBox.ivar];
  291. if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
  292. [mutableUnsortedFilteredIvars addObject:ivarBox];
  293. }
  294. }
  295. unsortedFilteredIvars = mutableUnsortedFilteredIvars;
  296. } else {
  297. unsortedFilteredIvars = candidateIvars;
  298. }
  299. self.filteredIvars = [unsortedFilteredIvars sortedArrayUsingComparator:^NSComparisonResult(FLEXIvarBox *ivarBox1, FLEXIvarBox *ivarBox2) {
  300. NSString *name1 = [NSString stringWithUTF8String:ivar_getName(ivarBox1.ivar)];
  301. NSString *name2 = [NSString stringWithUTF8String:ivar_getName(ivarBox2.ivar)];
  302. return [name1 caseInsensitiveCompare:name2];
  303. }];
  304. }
  305. - (NSString *)titleForIvarAtIndex:(NSInteger)index
  306. {
  307. FLEXIvarBox *ivarBox = self.filteredIvars[index];
  308. return [FLEXRuntimeUtility prettyNameForIvar:ivarBox.ivar];
  309. }
  310. - (id)valueForIvarAtIndex:(NSInteger)index
  311. {
  312. id value = nil;
  313. if ([self canHaveInstanceState]) {
  314. FLEXIvarBox *ivarBox = self.filteredIvars[index];
  315. const FLEXTypeEncoding *encoding = ivar_getTypeEncoding(ivarBox.ivar);
  316. value = [FLEXRuntimeUtility valueForIvar:ivarBox.ivar onObject:self.object];
  317. value = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:value type:encoding];
  318. }
  319. return value;
  320. }
  321. #pragma mark - Methods
  322. - (void)updateFilteredMethods
  323. {
  324. NSArray<FLEXMethodBox *> *candidateMethods = [self metadata:FLEXMetadataKindMethods forClassAtIndex:self.selectedScope];
  325. self.filteredMethods = [self filteredMethodsFromMethods:candidateMethods areClassMethods:NO];
  326. }
  327. - (void)updateFilteredClassMethods
  328. {
  329. NSArray<FLEXMethodBox *> *candidateMethods = [self metadata:FLEXMetadataKindClassMethods forClassAtIndex:self.selectedScope];
  330. self.filteredClassMethods = [self filteredMethodsFromMethods:candidateMethods areClassMethods:YES];
  331. }
  332. + (NSArray<FLEXMethodBox *> *)methodsForClass:(Class)class
  333. {
  334. if (!class) {
  335. return @[];
  336. }
  337. NSMutableArray<FLEXMethodBox *> *boxedMethods = [NSMutableArray array];
  338. unsigned int methodCount = 0;
  339. Method *methodList = class_copyMethodList(class, &methodCount);
  340. if (methodList) {
  341. for (unsigned int i = 0; i < methodCount; i++) {
  342. FLEXMethodBox *methodBox = [FLEXMethodBox new];
  343. methodBox.method = methodList[i];
  344. [boxedMethods addObject:methodBox];
  345. }
  346. free(methodList);
  347. }
  348. return boxedMethods;
  349. }
  350. /// Skips NSObject
  351. + (NSArray<FLEXMethodBox *> *)inheritedMethodsForClass:(Class)class
  352. {
  353. NSMutableArray<FLEXMethodBox *> *inheritedMethods = [NSMutableArray array];
  354. while ((class = [class superclass]) && class != [NSObject class]) {
  355. [inheritedMethods addObjectsFromArray:[self methodsForClass:class]];
  356. }
  357. return inheritedMethods;
  358. }
  359. - (NSArray<FLEXMethodBox *> *)filteredMethodsFromMethods:(NSArray<FLEXMethodBox *> *)methods areClassMethods:(BOOL)areClassMethods
  360. {
  361. NSArray<FLEXMethodBox *> *candidateMethods = methods;
  362. NSArray<FLEXMethodBox *> *unsortedFilteredMethods = nil;
  363. if (self.filterText.length > 0) {
  364. NSMutableArray<FLEXMethodBox *> *mutableUnsortedFilteredMethods = [NSMutableArray array];
  365. for (FLEXMethodBox *methodBox in candidateMethods) {
  366. NSString *prettyName = [FLEXRuntimeUtility prettyNameForMethod:methodBox.method isClassMethod:areClassMethods];
  367. if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
  368. [mutableUnsortedFilteredMethods addObject:methodBox];
  369. }
  370. }
  371. unsortedFilteredMethods = mutableUnsortedFilteredMethods;
  372. } else {
  373. unsortedFilteredMethods = candidateMethods;
  374. }
  375. NSArray<FLEXMethodBox *> *sortedFilteredMethods = [unsortedFilteredMethods sortedArrayUsingComparator:^NSComparisonResult(FLEXMethodBox *methodBox1, FLEXMethodBox *methodBox2) {
  376. NSString *name1 = NSStringFromSelector(method_getName(methodBox1.method));
  377. NSString *name2 = NSStringFromSelector(method_getName(methodBox2.method));
  378. return [name1 caseInsensitiveCompare:name2];
  379. }];
  380. return sortedFilteredMethods;
  381. }
  382. - (NSString *)titleForMethodAtIndex:(NSInteger)index
  383. {
  384. FLEXMethodBox *methodBox = self.filteredMethods[index];
  385. return [FLEXRuntimeUtility prettyNameForMethod:methodBox.method isClassMethod:NO];
  386. }
  387. - (NSString *)titleForClassMethodAtIndex:(NSInteger)index
  388. {
  389. FLEXMethodBox *classMethodBox = self.filteredClassMethods[index];
  390. return [FLEXRuntimeUtility prettyNameForMethod:classMethodBox.method isClassMethod:YES];
  391. }
  392. - (objc_property_t)viewPropertyForName:(NSString *)propertyName
  393. {
  394. return class_getProperty([self.object class], propertyName.UTF8String);
  395. }
  396. #pragma mark - Superclasses
  397. - (void)updateSuperclasses
  398. {
  399. self.classHierarchy = [FLEXRuntimeUtility classHierarchyOfObject:self.object];
  400. }
  401. - (void)updateFilteredSuperclasses
  402. {
  403. if (self.filterText.length > 0) {
  404. NSMutableArray<Class> *filteredSuperclasses = [NSMutableArray array];
  405. for (Class superclass in self.classHierarchy) {
  406. if ([NSStringFromClass(superclass) localizedCaseInsensitiveContainsString:self.filterText]) {
  407. [filteredSuperclasses addObject:superclass];
  408. }
  409. }
  410. self.filteredSuperclasses = filteredSuperclasses;
  411. } else {
  412. self.filteredSuperclasses = self.classHierarchy;
  413. }
  414. }
  415. #pragma mark - Table View Data Helpers
  416. - (NSArray<NSNumber *> *)possibleExplorerSections
  417. {
  418. static NSArray<NSNumber *> *possibleSections = nil;
  419. static dispatch_once_t onceToken;
  420. dispatch_once(&onceToken, ^{
  421. possibleSections = @[@(FLEXObjectExplorerSectionDescription),
  422. @(FLEXObjectExplorerSectionCustom),
  423. @(FLEXObjectExplorerSectionProperties),
  424. @(FLEXObjectExplorerSectionIvars),
  425. @(FLEXObjectExplorerSectionMethods),
  426. @(FLEXObjectExplorerSectionClassMethods),
  427. @(FLEXObjectExplorerSectionSuperclasses),
  428. @(FLEXObjectExplorerSectionReferencingInstances)];
  429. });
  430. return possibleSections;
  431. }
  432. - (NSArray<NSNumber *> *)visibleExplorerSections
  433. {
  434. NSMutableArray<NSNumber *> *visibleSections = [NSMutableArray array];
  435. for (NSNumber *possibleSection in [self possibleExplorerSections]) {
  436. FLEXObjectExplorerSection explorerSection = [possibleSection unsignedIntegerValue];
  437. if ([self numberOfRowsForExplorerSection:explorerSection] > 0) {
  438. [visibleSections addObject:possibleSection];
  439. }
  440. }
  441. return visibleSections;
  442. }
  443. - (NSString *)sectionTitleWithBaseName:(NSString *)baseName totalCount:(NSUInteger)totalCount filteredCount:(NSUInteger)filteredCount
  444. {
  445. NSString *sectionTitle = nil;
  446. if (totalCount == filteredCount) {
  447. sectionTitle = [baseName stringByAppendingFormat:@" (%lu)", (unsigned long)totalCount];
  448. } else {
  449. sectionTitle = [baseName stringByAppendingFormat:@" (%lu of %lu)", (unsigned long)filteredCount, (unsigned long)totalCount];
  450. }
  451. return sectionTitle;
  452. }
  453. - (FLEXObjectExplorerSection)explorerSectionAtIndex:(NSInteger)sectionIndex
  454. {
  455. return [[[self visibleExplorerSections] objectAtIndex:sectionIndex] unsignedIntegerValue];
  456. }
  457. - (NSInteger)numberOfRowsForExplorerSection:(FLEXObjectExplorerSection)section
  458. {
  459. NSInteger numberOfRows = 0;
  460. switch (section) {
  461. case FLEXObjectExplorerSectionDescription:
  462. numberOfRows = [self shouldShowDescription] ? 1 : 0;
  463. break;
  464. case FLEXObjectExplorerSectionCustom:
  465. numberOfRows = self.customSectionVisibleIndexes.count;
  466. break;
  467. case FLEXObjectExplorerSectionProperties:
  468. numberOfRows = self.filteredProperties.count;
  469. break;
  470. case FLEXObjectExplorerSectionIvars:
  471. numberOfRows = self.filteredIvars.count;
  472. break;
  473. case FLEXObjectExplorerSectionMethods:
  474. numberOfRows = self.filteredMethods.count;
  475. break;
  476. case FLEXObjectExplorerSectionClassMethods:
  477. numberOfRows = self.filteredClassMethods.count;
  478. break;
  479. case FLEXObjectExplorerSectionSuperclasses:
  480. numberOfRows = self.filteredSuperclasses.count;
  481. break;
  482. case FLEXObjectExplorerSectionReferencingInstances:
  483. // Hide this section if there is fliter text since there's nothing searchable (only 1 row, always the same).
  484. numberOfRows = self.filterText.length == 0 ? 1 : 0;
  485. break;
  486. }
  487. return numberOfRows;
  488. }
  489. - (NSString *)titleForRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
  490. {
  491. NSString *title = nil;
  492. switch (section) {
  493. case FLEXObjectExplorerSectionDescription:
  494. title = [self displayedObjectDescription];
  495. break;
  496. case FLEXObjectExplorerSectionCustom:
  497. title = [self customSectionTitleForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
  498. break;
  499. case FLEXObjectExplorerSectionProperties:
  500. title = [self titleForPropertyAtIndex:row];
  501. break;
  502. case FLEXObjectExplorerSectionIvars:
  503. title = [self titleForIvarAtIndex:row];
  504. break;
  505. case FLEXObjectExplorerSectionMethods:
  506. title = [self titleForMethodAtIndex:row];
  507. break;
  508. case FLEXObjectExplorerSectionClassMethods:
  509. title = [self titleForClassMethodAtIndex:row];
  510. break;
  511. case FLEXObjectExplorerSectionSuperclasses:
  512. title = NSStringFromClass(self.filteredSuperclasses[row]);
  513. break;
  514. case FLEXObjectExplorerSectionReferencingInstances:
  515. title = @"Other objects with ivars referencing this object";
  516. break;
  517. }
  518. return title;
  519. }
  520. - (NSString *)subtitleForRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
  521. {
  522. NSString *subtitle = nil;
  523. switch (section) {
  524. case FLEXObjectExplorerSectionDescription:
  525. break;
  526. case FLEXObjectExplorerSectionCustom:
  527. subtitle = [self customSectionSubtitleForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
  528. break;
  529. case FLEXObjectExplorerSectionProperties:
  530. subtitle = [self canHaveInstanceState] ? [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:[self valueForPropertyAtIndex:row]] : nil;
  531. break;
  532. case FLEXObjectExplorerSectionIvars:
  533. subtitle = [self canHaveInstanceState] ? [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:[self valueForIvarAtIndex:row]] : nil;
  534. break;
  535. case FLEXObjectExplorerSectionMethods:
  536. break;
  537. case FLEXObjectExplorerSectionClassMethods:
  538. break;
  539. case FLEXObjectExplorerSectionSuperclasses:
  540. break;
  541. case FLEXObjectExplorerSectionReferencingInstances:
  542. break;
  543. }
  544. return subtitle;
  545. }
  546. - (BOOL)canDrillInToRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
  547. {
  548. BOOL canDrillIn = NO;
  549. switch (section) {
  550. case FLEXObjectExplorerSectionDescription:
  551. break;
  552. case FLEXObjectExplorerSectionCustom:
  553. canDrillIn = [self customSectionCanDrillIntoRowWithCookie:[self customSectionRowCookieForVisibleRow:row]];
  554. break;
  555. case FLEXObjectExplorerSectionProperties: {
  556. if ([self canHaveInstanceState]) {
  557. FLEXPropertyBox *propertyBox = self.filteredProperties[row];
  558. objc_property_t property = propertyBox.property;
  559. id currentValue = [self valueForPropertyAtIndex:row];
  560. BOOL canEdit = [FLEXPropertyEditorViewController canEditProperty:property onObject:self.object currentValue:currentValue];
  561. BOOL canExplore = currentValue != nil;
  562. canDrillIn = canEdit || canExplore;
  563. }
  564. } break;
  565. case FLEXObjectExplorerSectionIvars: {
  566. if ([self canHaveInstanceState]) {
  567. FLEXIvarBox *ivarBox = self.filteredIvars[row];
  568. Ivar ivar = ivarBox.ivar;
  569. id currentValue = [self valueForIvarAtIndex:row];
  570. BOOL canEdit = [FLEXIvarEditorViewController canEditIvar:ivar currentValue:currentValue];
  571. BOOL canExplore = currentValue != nil;
  572. canDrillIn = canEdit || canExplore;
  573. }
  574. } break;
  575. case FLEXObjectExplorerSectionMethods:
  576. canDrillIn = [self canCallInstanceMethods];
  577. break;
  578. case FLEXObjectExplorerSectionClassMethods:
  579. canDrillIn = YES;
  580. break;
  581. case FLEXObjectExplorerSectionSuperclasses:
  582. canDrillIn = YES;
  583. break;
  584. case FLEXObjectExplorerSectionReferencingInstances:
  585. canDrillIn = YES;
  586. break;
  587. }
  588. return canDrillIn;
  589. }
  590. - (BOOL)sectionHasActions:(NSInteger)section
  591. {
  592. return [self explorerSectionAtIndex:section] == FLEXObjectExplorerSectionDescription;
  593. }
  594. - (NSString *)titleForExplorerSection:(FLEXObjectExplorerSection)section
  595. {
  596. NSString *title = nil;
  597. switch (section) {
  598. case FLEXObjectExplorerSectionDescription: {
  599. title = @"Description";
  600. } break;
  601. case FLEXObjectExplorerSectionCustom: {
  602. title = [self customSectionTitle];
  603. } break;
  604. case FLEXObjectExplorerSectionProperties: {
  605. NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindProperties forClassAtIndex:self.selectedScope];
  606. title = [self sectionTitleWithBaseName:@"Properties" totalCount:totalCount filteredCount:self.filteredProperties.count];
  607. } break;
  608. case FLEXObjectExplorerSectionIvars: {
  609. NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindIvars forClassAtIndex:self.selectedScope];
  610. title = [self sectionTitleWithBaseName:@"Ivars" totalCount:totalCount filteredCount:self.filteredIvars.count];
  611. } break;
  612. case FLEXObjectExplorerSectionMethods: {
  613. NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindMethods forClassAtIndex:self.selectedScope];
  614. title = [self sectionTitleWithBaseName:@"Methods" totalCount:totalCount filteredCount:self.filteredMethods.count];
  615. } break;
  616. case FLEXObjectExplorerSectionClassMethods: {
  617. NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindClassMethods forClassAtIndex:self.selectedScope];
  618. title = [self sectionTitleWithBaseName:@"Class Methods" totalCount:totalCount filteredCount:self.filteredClassMethods.count];
  619. } break;
  620. case FLEXObjectExplorerSectionSuperclasses: {
  621. title = [self sectionTitleWithBaseName:@"Superclasses" totalCount:self.classHierarchy.count filteredCount:self.filteredSuperclasses.count];
  622. } break;
  623. case FLEXObjectExplorerSectionReferencingInstances: {
  624. title = @"Object Graph";
  625. } break;
  626. }
  627. return title;
  628. }
  629. - (UIViewController *)drillInViewControllerForRow:(NSUInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
  630. {
  631. UIViewController *viewController = nil;
  632. switch (section) {
  633. case FLEXObjectExplorerSectionDescription:
  634. break;
  635. case FLEXObjectExplorerSectionCustom:
  636. viewController = [self customSectionDrillInViewControllerForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
  637. break;
  638. case FLEXObjectExplorerSectionProperties: {
  639. FLEXPropertyBox *propertyBox = self.filteredProperties[row];
  640. objc_property_t property = propertyBox.property;
  641. id currentValue = [self valueForPropertyAtIndex:row];
  642. if ([FLEXPropertyEditorViewController canEditProperty:property onObject:self.object currentValue:currentValue]) {
  643. viewController = [[FLEXPropertyEditorViewController alloc] initWithTarget:self.object property:property];
  644. } else if (currentValue) {
  645. viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:currentValue];
  646. }
  647. } break;
  648. case FLEXObjectExplorerSectionIvars: {
  649. FLEXIvarBox *ivarBox = self.filteredIvars[row];
  650. Ivar ivar = ivarBox.ivar;
  651. id currentValue = [self valueForIvarAtIndex:row];
  652. if ([FLEXIvarEditorViewController canEditIvar:ivar currentValue:currentValue]) {
  653. viewController = [[FLEXIvarEditorViewController alloc] initWithTarget:self.object ivar:ivar];
  654. } else if (currentValue) {
  655. viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:currentValue];
  656. }
  657. } break;
  658. case FLEXObjectExplorerSectionMethods: {
  659. FLEXMethodBox *methodBox = self.filteredMethods[row];
  660. Method method = methodBox.method;
  661. viewController = [[FLEXMethodCallingViewController alloc] initWithTarget:self.object method:method];
  662. } break;
  663. case FLEXObjectExplorerSectionClassMethods: {
  664. FLEXMethodBox *methodBox = self.filteredClassMethods[row];
  665. Method method = methodBox.method;
  666. viewController = [[FLEXMethodCallingViewController alloc] initWithTarget:[self.object class] method:method];
  667. } break;
  668. case FLEXObjectExplorerSectionSuperclasses: {
  669. Class superclass = self.filteredSuperclasses[row];
  670. viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:superclass];
  671. } break;
  672. case FLEXObjectExplorerSectionReferencingInstances: {
  673. viewController = [FLEXInstancesTableViewController instancesTableViewControllerForInstancesReferencingObject:self.object];
  674. } break;
  675. }
  676. return viewController;
  677. }
  678. #pragma mark - Table View Data Source
  679. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
  680. {
  681. return [self visibleExplorerSections].count;
  682. }
  683. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  684. {
  685. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:section];
  686. return [self numberOfRowsForExplorerSection:explorerSection];
  687. }
  688. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
  689. {
  690. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:section];
  691. return [self titleForExplorerSection:explorerSection];
  692. }
  693. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  694. {
  695. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  696. BOOL isCustomSection = explorerSection == FLEXObjectExplorerSectionCustom;
  697. BOOL useDescriptionCell = explorerSection == FLEXObjectExplorerSectionDescription;
  698. NSString *cellIdentifier = useDescriptionCell ? kFLEXMultilineTableViewCellIdentifier : @"cell";
  699. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
  700. if (!cell) {
  701. if (useDescriptionCell) {
  702. cell = [[FLEXMultilineTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
  703. cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
  704. } else {
  705. cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
  706. UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
  707. cell.textLabel.font = cellFont;
  708. cell.detailTextLabel.font = cellFont;
  709. cell.detailTextLabel.textColor = UIColor.grayColor;
  710. }
  711. }
  712. UIView *customView;
  713. if (isCustomSection) {
  714. customView = [self customViewForRowCookie:[self customSectionRowCookieForVisibleRow:indexPath.row]];
  715. if (customView) {
  716. [cell.contentView addSubview:customView];
  717. }
  718. }
  719. cell.textLabel.text = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
  720. cell.detailTextLabel.text = [self subtitleForRow:indexPath.row inExplorerSection:explorerSection];
  721. cell.accessoryType = [self canDrillInToRow:indexPath.row inExplorerSection:explorerSection] ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
  722. return cell;
  723. }
  724. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
  725. {
  726. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  727. CGFloat height = self.tableView.rowHeight;
  728. if (explorerSection == FLEXObjectExplorerSectionDescription) {
  729. NSString *text = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
  730. NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:@{ NSFontAttributeName : [FLEXUtility defaultTableViewCellLabelFont] }];
  731. CGFloat preferredHeight = [FLEXMultilineTableViewCell preferredHeightWithAttributedText:attributedText inTableViewWidth:self.tableView.frame.size.width style:tableView.style showsAccessory:NO];
  732. height = MAX(height, preferredHeight);
  733. } else if (explorerSection == FLEXObjectExplorerSectionCustom) {
  734. id cookie = [self customSectionRowCookieForVisibleRow:indexPath.row];
  735. height = [self heightForCustomViewRowForRowCookie:cookie];
  736. }
  737. return height;
  738. }
  739. #pragma mark - Table View Delegate
  740. - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath
  741. {
  742. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  743. return [self canDrillInToRow:indexPath.row inExplorerSection:explorerSection];
  744. }
  745. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
  746. {
  747. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  748. UIViewController *detailViewController = [self drillInViewControllerForRow:indexPath.row inExplorerSection:explorerSection];
  749. if (detailViewController) {
  750. [self.navigationController pushViewController:detailViewController animated:YES];
  751. } else {
  752. [tableView deselectRowAtIndexPath:indexPath animated:YES];
  753. }
  754. }
  755. - (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
  756. {
  757. return [self sectionHasActions:indexPath.section];
  758. }
  759. - (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
  760. {
  761. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  762. switch (explorerSection) {
  763. case FLEXObjectExplorerSectionDescription:
  764. return action == @selector(copy:) || action == @selector(copyObjectAddress:);
  765. default:
  766. return NO;
  767. }
  768. }
  769. - (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
  770. {
  771. #pragma clang diagnostic push
  772. #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
  773. [self performSelector:action withObject:indexPath];
  774. #pragma clang diagnostic pop
  775. }
  776. #pragma mark - UIMenuController
  777. /// Prevent the search bar from trying to use us as a responder
  778. ///
  779. /// Our table cells will use the UITableViewDelegate methods
  780. /// to make sure we can perform the actions we want to
  781. - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
  782. {
  783. return NO;
  784. }
  785. - (void)copy:(NSIndexPath *)indexPath
  786. {
  787. FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
  788. NSString *stringToCopy = @"";
  789. NSString *title = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
  790. if (title.length) {
  791. stringToCopy = [stringToCopy stringByAppendingString:title];
  792. }
  793. NSString *subtitle = [self subtitleForRow:indexPath.row inExplorerSection:explorerSection];
  794. if (subtitle.length) {
  795. if (stringToCopy.length) {
  796. stringToCopy = [stringToCopy stringByAppendingString:@"\n\n"];
  797. }
  798. stringToCopy = [stringToCopy stringByAppendingString:subtitle];
  799. }
  800. UIPasteboard.generalPasteboard.string = stringToCopy;
  801. }
  802. - (void)copyObjectAddress:(NSIndexPath *)indexPath
  803. {
  804. UIPasteboard.generalPasteboard.string = [FLEXUtility addressOfObject:self.object];
  805. }
  806. #pragma mark - Custom Section
  807. - (void)updateCustomData
  808. {
  809. self.cachedCustomSectionRowCookies = [self customSectionRowCookies];
  810. }
  811. - (void)updateFilteredCustomData
  812. {
  813. NSIndexSet *filteredIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.cachedCustomSectionRowCookies.count)];
  814. if (self.filterText.length > 0) {
  815. filteredIndexSet = [filteredIndexSet indexesPassingTest:^BOOL(NSUInteger index, BOOL *stop) {
  816. BOOL matches = NO;
  817. NSString *rowTitle = [self customSectionTitleForRowCookie:self.cachedCustomSectionRowCookies[index]];
  818. if ([rowTitle rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
  819. matches = YES;
  820. }
  821. return matches;
  822. }];
  823. }
  824. self.customSectionVisibleIndexes = filteredIndexSet;
  825. }
  826. - (id)customSectionRowCookieForVisibleRow:(NSUInteger)row
  827. {
  828. return [[self.cachedCustomSectionRowCookies objectsAtIndexes:self.customSectionVisibleIndexes] objectAtIndex:row];
  829. }
  830. #pragma mark - Subclasses Can Override
  831. - (NSString *)customSectionTitle
  832. {
  833. return self.shortcutPropertyNames.count ? @"Shortcuts" : nil;
  834. }
  835. - (NSArray *)customSectionRowCookies
  836. {
  837. return self.shortcutPropertyNames;
  838. }
  839. - (NSString *)customSectionTitleForRowCookie:(id)rowCookie
  840. {
  841. if ([rowCookie isKindOfClass:[NSString class]]) {
  842. objc_property_t property = [self viewPropertyForName:rowCookie];
  843. if (property) {
  844. NSString *prettyPropertyName = [FLEXRuntimeUtility prettyNameForProperty:property];
  845. // Since we're outside of the "properties" section, prepend @property for clarity.
  846. return [@"@property " stringByAppendingString:prettyPropertyName];
  847. } else if ([rowCookie respondsToSelector:@selector(description)]) {
  848. return [@"No property found for object: " stringByAppendingString:[rowCookie description]];
  849. } else {
  850. NSString *cls = NSStringFromClass([rowCookie class]);
  851. return [@"No property found for object of class " stringByAppendingString:cls];
  852. }
  853. }
  854. return nil;
  855. }
  856. - (NSString *)customSectionSubtitleForRowCookie:(id)rowCookie
  857. {
  858. if ([rowCookie isKindOfClass:[NSString class]]) {
  859. objc_property_t property = [self viewPropertyForName:rowCookie];
  860. if (property) {
  861. id value = [FLEXRuntimeUtility valueForProperty:property onObject:self.object];
  862. return [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:value];
  863. } else {
  864. return nil;
  865. }
  866. }
  867. return nil;
  868. }
  869. - (BOOL)customSectionCanDrillIntoRowWithCookie:(id)rowCookie
  870. {
  871. return YES;
  872. }
  873. - (UIViewController *)customSectionDrillInViewControllerForRowCookie:(id)rowCookie
  874. {
  875. if ([rowCookie isKindOfClass:[NSString class]]) {
  876. objc_property_t property = [self viewPropertyForName:rowCookie];
  877. if (property) {
  878. id currentValue = [FLEXRuntimeUtility valueForProperty:property onObject:self.object];
  879. if ([FLEXPropertyEditorViewController canEditProperty:property onObject:self.object currentValue:currentValue]) {
  880. return [[FLEXPropertyEditorViewController alloc] initWithTarget:self.object property:property];
  881. } else {
  882. return [FLEXObjectExplorerFactory explorerViewControllerForObject:currentValue];
  883. }
  884. } else {
  885. [NSException raise:NSInternalInconsistencyException
  886. format:@"Cannot drill into row for cookie: %@", rowCookie];
  887. return nil;
  888. }
  889. }
  890. return nil;
  891. }
  892. - (UIView *)customViewForRowCookie:(id)rowCookie
  893. {
  894. return nil;
  895. }
  896. - (CGFloat)heightForCustomViewRowForRowCookie:(id)rowCookie
  897. {
  898. return self.tableView.rowHeight;
  899. }
  900. - (BOOL)canHaveInstanceState
  901. {
  902. return YES;
  903. }
  904. - (BOOL)canCallInstanceMethods
  905. {
  906. return YES;
  907. }
  908. @end
  909. @implementation FLEXObjectExplorerViewController (Shortcuts)
  910. - (NSArray<NSString *> *)shortcutPropertyNames { return @[]; }
  911. @end