Files
FLEX/Classes/ObjectExplorers/Controllers/FLEXObjectExplorerViewController.m
T
Tanner Bennett 3fe31e8628 Always display a description
Except while searching of course
2019-04-18 09:34:08 -05:00

1173 lines
43 KiB
Objective-C

//
// FLEXObjectExplorerViewController.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXObjectExplorerViewController.h"
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXMultilineTableViewCell.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXPropertyEditorViewController.h"
#import "FLEXIvarEditorViewController.h"
#import "FLEXMethodCallingViewController.h"
#import "FLEXInstancesTableViewController.h"
#import "FLEXTableView.h"
#import <objc/runtime.h>
typedef NS_ENUM(NSUInteger, FLEXObjectExplorerScope) {
FLEXObjectExplorerScopeNoInheritance,
FLEXObjectExplorerScopeWithParent,
FLEXObjectExplorerScopeAllButNSObject,
FLEXObjectExplorerScopeNSObjectOnly
};
typedef NS_ENUM(NSUInteger, FLEXMetadataKind) {
FLEXMetadataKindProperties,
FLEXMetadataKindIvars,
FLEXMetadataKindMethods,
FLEXMetadataKindClassMethods
};
// Convenience boxes to keep runtime properties, ivars, and methods in foundation collections.
@interface FLEXPropertyBox : NSObject
@property (nonatomic, assign) objc_property_t property;
@end
@implementation FLEXPropertyBox
@end
@interface FLEXIvarBox : NSObject
@property (nonatomic, assign) Ivar ivar;
@end
@implementation FLEXIvarBox
@end
@interface FLEXMethodBox : NSObject
@property (nonatomic, assign) Method method;
@end
@implementation FLEXMethodBox
@end
@interface FLEXObjectExplorerViewController () <UISearchBarDelegate>
@property (nonatomic, strong) NSArray<FLEXPropertyBox *> *properties;
@property (nonatomic, strong) NSArray<FLEXPropertyBox *> *propertiesWithParent;
@property (nonatomic, strong) NSArray<FLEXPropertyBox *> *inheritedProperties;
@property (nonatomic, strong) NSArray<FLEXPropertyBox *> *NSObjectProperties;
@property (nonatomic, strong) NSArray<FLEXPropertyBox *> *filteredProperties;
@property (nonatomic, strong) NSArray<FLEXIvarBox *> *ivars;
@property (nonatomic, strong) NSArray<FLEXIvarBox *> *ivarsWithParent;
@property (nonatomic, strong) NSArray<FLEXIvarBox *> *inheritedIvars;
@property (nonatomic, strong) NSArray<FLEXIvarBox *> *NSObjectIvars;
@property (nonatomic, strong) NSArray<FLEXIvarBox *> *filteredIvars;
@property (nonatomic, strong) NSArray<FLEXMethodBox *> *methods;
@property (nonatomic, strong) NSArray<FLEXMethodBox *> *methodsWithParent;
@property (nonatomic, strong) NSArray<FLEXMethodBox *> *inheritedMethods;
@property (nonatomic, strong) NSArray<FLEXMethodBox *> *NSObjectMethods;
@property (nonatomic, strong) NSArray<FLEXMethodBox *> *filteredMethods;
@property (nonatomic, strong) NSArray<FLEXMethodBox *> *classMethods;
@property (nonatomic, strong) NSArray<FLEXMethodBox *> *classMethodsWithParent;
@property (nonatomic, strong) NSArray<FLEXMethodBox *> *inheritedClassMethods;
@property (nonatomic, strong) NSArray<FLEXMethodBox *> *NSObjectClassMethods;
@property (nonatomic, strong) NSArray<FLEXMethodBox *> *filteredClassMethods;
@property (nonatomic, strong) NSArray<Class> *superclasses;
@property (nonatomic, strong) NSArray<Class> *filteredSuperclasses;
@property (nonatomic, strong) NSArray *cachedCustomSectionRowCookies;
@property (nonatomic, strong) NSIndexSet *customSectionVisibleIndexes;
@property (nonatomic, strong) UISearchBar *searchBar;
@property (nonatomic, strong) NSString *filterText;
@property (nonatomic, assign) FLEXObjectExplorerScope scope;
@end
@implementation FLEXObjectExplorerViewController
+ (void)initialize
{
if (self == [FLEXObjectExplorerViewController class]) {
// Initialize custom menu items for entire app
UIMenuItem *copyObjectAddress = [[UIMenuItem alloc] initWithTitle:@"Copy Address" action:@selector(copyObjectAddress:)];
[UIMenuController sharedMenuController].menuItems = @[copyObjectAddress];
[[UIMenuController sharedMenuController] update];
}
}
- (void)loadView
{
self.tableView = [[FLEXTableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.searchBar = [[UISearchBar alloc] init];
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
self.searchBar.delegate = self;
self.searchBar.showsScopeBar = YES;
[self refreshScopeTitles];
self.tableView.tableHeaderView = self.searchBar;
self.refreshControl = [[UIRefreshControl alloc] init];
[self.refreshControl addTarget:self action:@selector(refreshControlDidRefresh:) forControlEvents:UIControlEventValueChanged];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// Reload the entire table view rather than just the visible cells because the filtered rows
// may have changed (i.e. a change in the description row that causes it to get filtered out).
[self updateTableData];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self.searchBar endEditing:YES];
}
- (void)refreshControlDidRefresh:(id)sender
{
[self updateTableData];
[self.refreshControl endRefreshing];
}
#pragma mark - Search
- (void)refreshScopeTitles
{
if (!self.searchBar) return;
Class parent = [self.object superclass];
Class parentSuper = [parent superclass];
NSMutableArray *scopes = [NSMutableArray arrayWithObject:@"Base"];
if (parent) {
[scopes addObject:@"+ Parent"];
}
if (parentSuper && parentSuper != [NSObject class]) {
[scopes addObject:@"+ Inherited"];
}
if ([self.object isKindOfClass:[NSObject class]]) {
[scopes addObject:@"NSObject"];
}
self.searchBar.scopeButtonTitles = scopes;
[self.searchBar sizeToFit];
[self updateTableData];
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
self.filterText = searchText;
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
{
self.scope = selectedScope;
[self updateDisplayedData];
}
- (NSArray *)metadata:(FLEXMetadataKind)metadataKind forScope:(FLEXObjectExplorerScope)scope
{
switch (metadataKind) {
case FLEXMetadataKindProperties:
switch (self.scope) {
case FLEXObjectExplorerScopeNoInheritance:
return self.properties;
case FLEXObjectExplorerScopeWithParent:
return self.propertiesWithParent;
case FLEXObjectExplorerScopeAllButNSObject:
return self.inheritedProperties;
case FLEXObjectExplorerScopeNSObjectOnly:
return self.NSObjectProperties;
}
case FLEXMetadataKindIvars:
switch (self.scope) {
case FLEXObjectExplorerScopeNoInheritance:
return self.ivars;
case FLEXObjectExplorerScopeWithParent:
return self.ivarsWithParent;
case FLEXObjectExplorerScopeAllButNSObject:
return self.inheritedIvars;
case FLEXObjectExplorerScopeNSObjectOnly:
return self.NSObjectIvars;
}
case FLEXMetadataKindMethods:
switch (self.scope) {
case FLEXObjectExplorerScopeNoInheritance:
return self.methods;
case FLEXObjectExplorerScopeWithParent:
return self.methodsWithParent;
case FLEXObjectExplorerScopeAllButNSObject:
return self.inheritedMethods;
case FLEXObjectExplorerScopeNSObjectOnly:
return self.NSObjectMethods;
}
case FLEXMetadataKindClassMethods:
switch (self.scope) {
case FLEXObjectExplorerScopeNoInheritance:
return self.classMethods;
case FLEXObjectExplorerScopeWithParent:
return self.classMethodsWithParent;
case FLEXObjectExplorerScopeAllButNSObject:
return self.inheritedClassMethods;
case FLEXObjectExplorerScopeNSObjectOnly:
return self.NSObjectClassMethods;
}
}
}
- (NSInteger)totalCountOfMetadata:(FLEXMetadataKind)metadataKind forScope:(FLEXObjectExplorerScope)scope
{
return [self metadata:metadataKind forScope:scope].count;
}
#pragma mark - Setter overrides
- (void)setObject:(id)object
{
_object = object;
// Use [object class] here rather than object_getClass because we don't want to show the KVO prefix for observed objects.
self.title = [[object class] description];
[self refreshScopeTitles];
}
- (void)setFilterText:(NSString *)filterText
{
if (_filterText != filterText || ![_filterText isEqual:filterText]) {
_filterText = filterText;
[self updateDisplayedData];
}
}
#pragma mark - Reloading
- (void)updateTableData
{
[self updateCustomData];
[self updateProperties];
[self updateIvars];
[self updateMethods];
[self updateClassMethods];
[self updateSuperclasses];
[self updateDisplayedData];
}
- (void)updateDisplayedData
{
[self updateFilteredCustomData];
[self updateFilteredProperties];
[self updateFilteredIvars];
[self updateFilteredMethods];
[self updateFilteredClassMethods];
[self updateFilteredSuperclasses];
if (self.isViewLoaded) {
[self.tableView reloadData];
}
}
- (BOOL)shouldShowDescription
{
// Not if we have filter text that doesn't match the desctiption.
if (self.filterText.length) {
NSString *description = [self displayedObjectDescription];
return [description rangeOfString:self.filterText options:NSCaseInsensitiveSearch].length > 0;
}
return YES;
}
- (NSString *)displayedObjectDescription {
NSString *desc = [FLEXUtility safeDescriptionForObject:self.object];
if (!desc.length) {
NSString *address = [FLEXUtility addressOfObject:self.object];
desc = [NSString stringWithFormat:@"Object at %@ returned empty description", address];
}
return desc;
}
#pragma mark - Properties
- (void)updateProperties
{
Class class = [self.object class];
self.properties = [[self class] propertiesForClass:class];
self.propertiesWithParent = [self.properties arrayByAddingObjectsFromArray:[[self class] propertiesForClass:[class superclass]]];
self.inheritedProperties = [self.properties arrayByAddingObjectsFromArray:[[self class] inheritedPropertiesForClass:class]];
self.NSObjectProperties = [[self class] propertiesForClass:[NSObject class]];
}
+ (NSArray<FLEXPropertyBox *> *)propertiesForClass:(Class)class
{
if (!class) {
return @[];
}
NSMutableArray<FLEXPropertyBox *> *boxedProperties = [NSMutableArray array];
unsigned int propertyCount = 0;
objc_property_t *propertyList = class_copyPropertyList(class, &propertyCount);
if (propertyList) {
for (unsigned int i = 0; i < propertyCount; i++) {
FLEXPropertyBox *propertyBox = [[FLEXPropertyBox alloc] init];
propertyBox.property = propertyList[i];
[boxedProperties addObject:propertyBox];
}
free(propertyList);
}
return boxedProperties;
}
/// Skips NSObject
+ (NSArray<FLEXPropertyBox *> *)inheritedPropertiesForClass:(Class)class
{
NSMutableArray<FLEXPropertyBox *> *inheritedProperties = [NSMutableArray array];
while ((class = [class superclass]) && class != [NSObject class]) {
[inheritedProperties addObjectsFromArray:[self propertiesForClass:class]];
}
return inheritedProperties;
}
- (void)updateFilteredProperties
{
NSArray<FLEXPropertyBox *> *candidateProperties = [self metadata:FLEXMetadataKindProperties forScope:self.scope];
NSArray<FLEXPropertyBox *> *unsortedFilteredProperties = nil;
if ([self.filterText length] > 0) {
NSMutableArray<FLEXPropertyBox *> *mutableUnsortedFilteredProperties = [NSMutableArray array];
for (FLEXPropertyBox *propertyBox in candidateProperties) {
NSString *prettyName = [FLEXRuntimeUtility prettyNameForProperty:propertyBox.property];
if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
[mutableUnsortedFilteredProperties addObject:propertyBox];
}
}
unsortedFilteredProperties = mutableUnsortedFilteredProperties;
} else {
unsortedFilteredProperties = candidateProperties;
}
self.filteredProperties = [unsortedFilteredProperties sortedArrayUsingComparator:^NSComparisonResult(FLEXPropertyBox *propertyBox1, FLEXPropertyBox *propertyBox2) {
NSString *name1 = [NSString stringWithUTF8String:property_getName(propertyBox1.property)];
NSString *name2 = [NSString stringWithUTF8String:property_getName(propertyBox2.property)];
return [name1 caseInsensitiveCompare:name2];
}];
}
- (NSString *)titleForPropertyAtIndex:(NSInteger)index
{
FLEXPropertyBox *propertyBox = self.filteredProperties[index];
return [FLEXRuntimeUtility prettyNameForProperty:propertyBox.property];
}
- (id)valueForPropertyAtIndex:(NSInteger)index
{
id value = nil;
if ([self canHaveInstanceState]) {
FLEXPropertyBox *propertyBox = self.filteredProperties[index];
NSString *typeString = [FLEXRuntimeUtility typeEncodingForProperty:propertyBox.property];
const FLEXTypeEncoding *encoding = [typeString cStringUsingEncoding:NSUTF8StringEncoding];
value = [FLEXRuntimeUtility valueForProperty:propertyBox.property onObject:self.object];
value = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:value type:encoding];
}
return value;
}
#pragma mark - Ivars
- (void)updateIvars
{
Class class = [self.object class];
self.ivars = [[self class] ivarsForClass:class];
self.ivarsWithParent = [self.ivars arrayByAddingObjectsFromArray:[[self class] ivarsForClass:[class superclass]]];
self.inheritedIvars = [self.ivars arrayByAddingObjectsFromArray:[[self class] inheritedIvarsForClass:class]];
self.NSObjectIvars = [[self class] ivarsForClass:[NSObject class]];
}
+ (NSArray<FLEXIvarBox *> *)ivarsForClass:(Class)class
{
if (!class) {
return @[];
}
NSMutableArray<FLEXIvarBox *> *boxedIvars = [NSMutableArray array];
unsigned int ivarCount = 0;
Ivar *ivarList = class_copyIvarList(class, &ivarCount);
if (ivarList) {
for (unsigned int i = 0; i < ivarCount; i++) {
FLEXIvarBox *ivarBox = [[FLEXIvarBox alloc] init];
ivarBox.ivar = ivarList[i];
[boxedIvars addObject:ivarBox];
}
free(ivarList);
}
return boxedIvars;
}
/// Skips NSObject
+ (NSArray<FLEXIvarBox *> *)inheritedIvarsForClass:(Class)class
{
NSMutableArray<FLEXIvarBox *> *inheritedIvars = [NSMutableArray array];
while ((class = [class superclass]) && class != [NSObject class]) {
[inheritedIvars addObjectsFromArray:[self ivarsForClass:class]];
}
return inheritedIvars;
}
- (void)updateFilteredIvars
{
NSArray<FLEXIvarBox *> *candidateIvars = [self metadata:FLEXMetadataKindIvars forScope:self.scope];
NSArray<FLEXIvarBox *> *unsortedFilteredIvars = nil;
if ([self.filterText length] > 0) {
NSMutableArray<FLEXIvarBox *> *mutableUnsortedFilteredIvars = [NSMutableArray array];
for (FLEXIvarBox *ivarBox in candidateIvars) {
NSString *prettyName = [FLEXRuntimeUtility prettyNameForIvar:ivarBox.ivar];
if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
[mutableUnsortedFilteredIvars addObject:ivarBox];
}
}
unsortedFilteredIvars = mutableUnsortedFilteredIvars;
} else {
unsortedFilteredIvars = candidateIvars;
}
self.filteredIvars = [unsortedFilteredIvars sortedArrayUsingComparator:^NSComparisonResult(FLEXIvarBox *ivarBox1, FLEXIvarBox *ivarBox2) {
NSString *name1 = [NSString stringWithUTF8String:ivar_getName(ivarBox1.ivar)];
NSString *name2 = [NSString stringWithUTF8String:ivar_getName(ivarBox2.ivar)];
return [name1 caseInsensitiveCompare:name2];
}];
}
- (NSString *)titleForIvarAtIndex:(NSInteger)index
{
FLEXIvarBox *ivarBox = self.filteredIvars[index];
return [FLEXRuntimeUtility prettyNameForIvar:ivarBox.ivar];
}
- (id)valueForIvarAtIndex:(NSInteger)index
{
id value = nil;
if ([self canHaveInstanceState]) {
FLEXIvarBox *ivarBox = self.filteredIvars[index];
const FLEXTypeEncoding *encoding = ivar_getTypeEncoding(ivarBox.ivar);
value = [FLEXRuntimeUtility valueForIvar:ivarBox.ivar onObject:self.object];
value = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:value type:encoding];
}
return value;
}
#pragma mark - Methods
- (void)updateMethods
{
Class class = [self.object class];
self.methods = [[self class] methodsForClass:class];
self.methodsWithParent = [self.methods arrayByAddingObjectsFromArray:[[self class] methodsForClass:[class superclass]]];
self.inheritedMethods = [self.methods arrayByAddingObjectsFromArray:[[self class] inheritedMethodsForClass:class]];
self.NSObjectMethods = [[self class] methodsForClass:[NSObject class]];
}
- (void)updateFilteredMethods
{
NSArray<FLEXMethodBox *> *candidateMethods = [self metadata:FLEXMetadataKindMethods forScope:self.scope];
self.filteredMethods = [self filteredMethodsFromMethods:candidateMethods areClassMethods:NO];
}
- (void)updateClassMethods
{
const char *className = [NSStringFromClass([self.object class]) UTF8String];
Class metaClass = objc_getMetaClass(className);
self.classMethods = [[self class] methodsForClass:metaClass];
self.classMethodsWithParent = [self.classMethods arrayByAddingObjectsFromArray:[[self class] methodsForClass:[metaClass superclass]]];
self.inheritedClassMethods = [self.classMethods arrayByAddingObjectsFromArray:[[self class] inheritedMethodsForClass:metaClass]];
self.NSObjectClassMethods = [[self class] methodsForClass:[NSObject class]];
}
- (void)updateFilteredClassMethods
{
NSArray<FLEXMethodBox *> *candidateMethods = [self metadata:FLEXMetadataKindClassMethods forScope:self.scope];
self.filteredClassMethods = [self filteredMethodsFromMethods:candidateMethods areClassMethods:YES];
}
+ (NSArray<FLEXMethodBox *> *)methodsForClass:(Class)class
{
if (!class) {
return @[];
}
NSMutableArray<FLEXMethodBox *> *boxedMethods = [NSMutableArray array];
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(class, &methodCount);
if (methodList) {
for (unsigned int i = 0; i < methodCount; i++) {
FLEXMethodBox *methodBox = [[FLEXMethodBox alloc] init];
methodBox.method = methodList[i];
[boxedMethods addObject:methodBox];
}
free(methodList);
}
return boxedMethods;
}
/// Skips NSObject
+ (NSArray<FLEXMethodBox *> *)inheritedMethodsForClass:(Class)class
{
NSMutableArray<FLEXMethodBox *> *inheritedMethods = [NSMutableArray array];
while ((class = [class superclass]) && class != [NSObject class]) {
[inheritedMethods addObjectsFromArray:[self methodsForClass:class]];
}
return inheritedMethods;
}
- (NSArray<FLEXMethodBox *> *)filteredMethodsFromMethods:(NSArray<FLEXMethodBox *> *)methods areClassMethods:(BOOL)areClassMethods
{
NSArray<FLEXMethodBox *> *candidateMethods = methods;
NSArray<FLEXMethodBox *> *unsortedFilteredMethods = nil;
if ([self.filterText length] > 0) {
NSMutableArray<FLEXMethodBox *> *mutableUnsortedFilteredMethods = [NSMutableArray array];
for (FLEXMethodBox *methodBox in candidateMethods) {
NSString *prettyName = [FLEXRuntimeUtility prettyNameForMethod:methodBox.method isClassMethod:areClassMethods];
if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
[mutableUnsortedFilteredMethods addObject:methodBox];
}
}
unsortedFilteredMethods = mutableUnsortedFilteredMethods;
} else {
unsortedFilteredMethods = candidateMethods;
}
NSArray<FLEXMethodBox *> *sortedFilteredMethods = [unsortedFilteredMethods sortedArrayUsingComparator:^NSComparisonResult(FLEXMethodBox *methodBox1, FLEXMethodBox *methodBox2) {
NSString *name1 = NSStringFromSelector(method_getName(methodBox1.method));
NSString *name2 = NSStringFromSelector(method_getName(methodBox2.method));
return [name1 caseInsensitiveCompare:name2];
}];
return sortedFilteredMethods;
}
- (NSString *)titleForMethodAtIndex:(NSInteger)index
{
FLEXMethodBox *methodBox = self.filteredMethods[index];
return [FLEXRuntimeUtility prettyNameForMethod:methodBox.method isClassMethod:NO];
}
- (NSString *)titleForClassMethodAtIndex:(NSInteger)index
{
FLEXMethodBox *classMethodBox = self.filteredClassMethods[index];
return [FLEXRuntimeUtility prettyNameForMethod:classMethodBox.method isClassMethod:YES];
}
#pragma mark - Superclasses
+ (NSArray<Class> *)superclassesForClass:(Class)class
{
NSMutableArray<Class> *superClasses = [NSMutableArray array];
while ((class = [class superclass])) {
[superClasses addObject:class];
}
return superClasses;
}
- (void)updateSuperclasses
{
self.superclasses = [[self class] superclassesForClass:[self.object class]];
}
- (void)updateFilteredSuperclasses
{
if ([self.filterText length] > 0) {
NSMutableArray<Class> *filteredSuperclasses = [NSMutableArray array];
for (Class superclass in self.superclasses) {
if ([NSStringFromClass(superclass) rangeOfString:self.filterText options:NSCaseInsensitiveSearch].length > 0) {
[filteredSuperclasses addObject:superclass];
}
}
self.filteredSuperclasses = filteredSuperclasses;
} else {
self.filteredSuperclasses = self.superclasses;
}
}
#pragma mark - Table View Data Helpers
- (NSArray<NSNumber *> *)possibleExplorerSections
{
static NSArray<NSNumber *> *possibleSections = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
possibleSections = @[@(FLEXObjectExplorerSectionDescription),
@(FLEXObjectExplorerSectionCustom),
@(FLEXObjectExplorerSectionProperties),
@(FLEXObjectExplorerSectionIvars),
@(FLEXObjectExplorerSectionMethods),
@(FLEXObjectExplorerSectionClassMethods),
@(FLEXObjectExplorerSectionSuperclasses),
@(FLEXObjectExplorerSectionReferencingInstances)];
});
return possibleSections;
}
- (NSArray<NSNumber *> *)visibleExplorerSections
{
NSMutableArray<NSNumber *> *visibleSections = [NSMutableArray array];
for (NSNumber *possibleSection in [self possibleExplorerSections]) {
FLEXObjectExplorerSection explorerSection = [possibleSection unsignedIntegerValue];
if ([self numberOfRowsForExplorerSection:explorerSection] > 0) {
[visibleSections addObject:possibleSection];
}
}
return visibleSections;
}
- (NSString *)sectionTitleWithBaseName:(NSString *)baseName totalCount:(NSUInteger)totalCount filteredCount:(NSUInteger)filteredCount
{
NSString *sectionTitle = nil;
if (totalCount == filteredCount) {
sectionTitle = [baseName stringByAppendingFormat:@" (%lu)", (unsigned long)totalCount];
} else {
sectionTitle = [baseName stringByAppendingFormat:@" (%lu of %lu)", (unsigned long)filteredCount, (unsigned long)totalCount];
}
return sectionTitle;
}
- (FLEXObjectExplorerSection)explorerSectionAtIndex:(NSInteger)sectionIndex
{
return [[[self visibleExplorerSections] objectAtIndex:sectionIndex] unsignedIntegerValue];
}
- (NSInteger)numberOfRowsForExplorerSection:(FLEXObjectExplorerSection)section
{
NSInteger numberOfRows = 0;
switch (section) {
case FLEXObjectExplorerSectionDescription:
numberOfRows = [self shouldShowDescription] ? 1 : 0;
break;
case FLEXObjectExplorerSectionCustom:
numberOfRows = [self.customSectionVisibleIndexes count];
break;
case FLEXObjectExplorerSectionProperties:
numberOfRows = [self.filteredProperties count];
break;
case FLEXObjectExplorerSectionIvars:
numberOfRows = [self.filteredIvars count];
break;
case FLEXObjectExplorerSectionMethods:
numberOfRows = [self.filteredMethods count];
break;
case FLEXObjectExplorerSectionClassMethods:
numberOfRows = [self.filteredClassMethods count];
break;
case FLEXObjectExplorerSectionSuperclasses:
numberOfRows = [self.filteredSuperclasses count];
break;
case FLEXObjectExplorerSectionReferencingInstances:
// Hide this section if there is fliter text since there's nothing searchable (only 1 row, always the same).
numberOfRows = [self.filterText length] == 0 ? 1 : 0;
break;
}
return numberOfRows;
}
- (NSString *)titleForRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
{
NSString *title = nil;
switch (section) {
case FLEXObjectExplorerSectionDescription:
title = [self displayedObjectDescription];
break;
case FLEXObjectExplorerSectionCustom:
title = [self customSectionTitleForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
break;
case FLEXObjectExplorerSectionProperties:
title = [self titleForPropertyAtIndex:row];
break;
case FLEXObjectExplorerSectionIvars:
title = [self titleForIvarAtIndex:row];
break;
case FLEXObjectExplorerSectionMethods:
title = [self titleForMethodAtIndex:row];
break;
case FLEXObjectExplorerSectionClassMethods:
title = [self titleForClassMethodAtIndex:row];
break;
case FLEXObjectExplorerSectionSuperclasses:
title = NSStringFromClass(self.filteredSuperclasses[row]);
break;
case FLEXObjectExplorerSectionReferencingInstances:
title = @"Other objects with ivars referencing this object";
break;
}
return title;
}
- (NSString *)subtitleForRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
{
NSString *subtitle = nil;
switch (section) {
case FLEXObjectExplorerSectionDescription:
break;
case FLEXObjectExplorerSectionCustom:
subtitle = [self customSectionSubtitleForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
break;
case FLEXObjectExplorerSectionProperties:
subtitle = [self canHaveInstanceState] ? [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:[self valueForPropertyAtIndex:row]] : nil;
break;
case FLEXObjectExplorerSectionIvars:
subtitle = [self canHaveInstanceState] ? [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:[self valueForIvarAtIndex:row]] : nil;
break;
case FLEXObjectExplorerSectionMethods:
break;
case FLEXObjectExplorerSectionClassMethods:
break;
case FLEXObjectExplorerSectionSuperclasses:
break;
case FLEXObjectExplorerSectionReferencingInstances:
break;
}
return subtitle;
}
- (BOOL)canDrillInToRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
{
BOOL canDrillIn = NO;
switch (section) {
case FLEXObjectExplorerSectionDescription:
break;
case FLEXObjectExplorerSectionCustom:
canDrillIn = [self customSectionCanDrillIntoRowWithCookie:[self customSectionRowCookieForVisibleRow:row]];
break;
case FLEXObjectExplorerSectionProperties: {
if ([self canHaveInstanceState]) {
FLEXPropertyBox *propertyBox = self.filteredProperties[row];
objc_property_t property = propertyBox.property;
id currentValue = [self valueForPropertyAtIndex:row];
BOOL canEdit = [FLEXPropertyEditorViewController canEditProperty:property currentValue:currentValue];
BOOL canExplore = currentValue != nil;
canDrillIn = canEdit || canExplore;
}
} break;
case FLEXObjectExplorerSectionIvars: {
if ([self canHaveInstanceState]) {
FLEXIvarBox *ivarBox = self.filteredIvars[row];
Ivar ivar = ivarBox.ivar;
id currentValue = [self valueForIvarAtIndex:row];
BOOL canEdit = [FLEXIvarEditorViewController canEditIvar:ivar currentValue:currentValue];
BOOL canExplore = currentValue != nil;
canDrillIn = canEdit || canExplore;
}
} break;
case FLEXObjectExplorerSectionMethods:
canDrillIn = [self canCallInstanceMethods];
break;
case FLEXObjectExplorerSectionClassMethods:
canDrillIn = YES;
break;
case FLEXObjectExplorerSectionSuperclasses:
canDrillIn = YES;
break;
case FLEXObjectExplorerSectionReferencingInstances:
canDrillIn = YES;
break;
}
return canDrillIn;
}
- (BOOL)sectionHasActions:(NSInteger)section
{
return [self explorerSectionAtIndex:section] == FLEXObjectExplorerSectionDescription;
}
- (NSString *)titleForExplorerSection:(FLEXObjectExplorerSection)section
{
NSString *title = nil;
switch (section) {
case FLEXObjectExplorerSectionDescription: {
title = @"Description";
} break;
case FLEXObjectExplorerSectionCustom: {
title = [self customSectionTitle];
} break;
case FLEXObjectExplorerSectionProperties: {
NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindProperties forScope:self.scope];
title = [self sectionTitleWithBaseName:@"Properties" totalCount:totalCount filteredCount:[self.filteredProperties count]];
} break;
case FLEXObjectExplorerSectionIvars: {
NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindIvars forScope:self.scope];
title = [self sectionTitleWithBaseName:@"Ivars" totalCount:totalCount filteredCount:[self.filteredIvars count]];
} break;
case FLEXObjectExplorerSectionMethods: {
NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindMethods forScope:self.scope];
title = [self sectionTitleWithBaseName:@"Methods" totalCount:totalCount filteredCount:[self.filteredMethods count]];
} break;
case FLEXObjectExplorerSectionClassMethods: {
NSUInteger totalCount = [self totalCountOfMetadata:FLEXMetadataKindClassMethods forScope:self.scope];
title = [self sectionTitleWithBaseName:@"Class Methods" totalCount:totalCount filteredCount:[self.filteredClassMethods count]];
} break;
case FLEXObjectExplorerSectionSuperclasses: {
title = [self sectionTitleWithBaseName:@"Superclasses" totalCount:[self.superclasses count] filteredCount:[self.filteredSuperclasses count]];
} break;
case FLEXObjectExplorerSectionReferencingInstances: {
title = @"Object Graph";
} break;
}
return title;
}
- (UIViewController *)drillInViewControllerForRow:(NSUInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
{
UIViewController *viewController = nil;
switch (section) {
case FLEXObjectExplorerSectionDescription:
break;
case FLEXObjectExplorerSectionCustom:
viewController = [self customSectionDrillInViewControllerForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
break;
case FLEXObjectExplorerSectionProperties: {
FLEXPropertyBox *propertyBox = self.filteredProperties[row];
objc_property_t property = propertyBox.property;
id currentValue = [self valueForPropertyAtIndex:row];
if ([FLEXPropertyEditorViewController canEditProperty:property currentValue:currentValue]) {
viewController = [[FLEXPropertyEditorViewController alloc] initWithTarget:self.object property:property];
} else if (currentValue) {
viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:currentValue];
}
} break;
case FLEXObjectExplorerSectionIvars: {
FLEXIvarBox *ivarBox = self.filteredIvars[row];
Ivar ivar = ivarBox.ivar;
id currentValue = [self valueForIvarAtIndex:row];
if ([FLEXIvarEditorViewController canEditIvar:ivar currentValue:currentValue]) {
viewController = [[FLEXIvarEditorViewController alloc] initWithTarget:self.object ivar:ivar];
} else if (currentValue) {
viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:currentValue];
}
} break;
case FLEXObjectExplorerSectionMethods: {
FLEXMethodBox *methodBox = self.filteredMethods[row];
Method method = methodBox.method;
viewController = [[FLEXMethodCallingViewController alloc] initWithTarget:self.object method:method];
} break;
case FLEXObjectExplorerSectionClassMethods: {
FLEXMethodBox *methodBox = self.filteredClassMethods[row];
Method method = methodBox.method;
viewController = [[FLEXMethodCallingViewController alloc] initWithTarget:[self.object class] method:method];
} break;
case FLEXObjectExplorerSectionSuperclasses: {
Class superclass = self.filteredSuperclasses[row];
viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:superclass];
} break;
case FLEXObjectExplorerSectionReferencingInstances: {
viewController = [FLEXInstancesTableViewController instancesTableViewControllerForInstancesReferencingObject:self.object];
} break;
}
return viewController;
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [[self visibleExplorerSections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:section];
return [self numberOfRowsForExplorerSection:explorerSection];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:section];
return [self titleForExplorerSection:explorerSection];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
BOOL isCustomSection = explorerSection == FLEXObjectExplorerSectionCustom;
BOOL useDescriptionCell = explorerSection == FLEXObjectExplorerSectionDescription;
NSString *cellIdentifier = useDescriptionCell ? kFLEXMultilineTableViewCellIdentifier : @"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
if (useDescriptionCell) {
cell = [[FLEXMultilineTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
} else {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
cell.textLabel.font = cellFont;
cell.detailTextLabel.font = cellFont;
cell.detailTextLabel.textColor = [UIColor grayColor];
}
}
UIView *customView;
if (isCustomSection) {
customView = [self customViewForRowCookie:[self customSectionRowCookieForVisibleRow:indexPath.row]];
if (customView) {
[cell.contentView addSubview:customView];
}
}
cell.textLabel.text = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
cell.detailTextLabel.text = [self subtitleForRow:indexPath.row inExplorerSection:explorerSection];
cell.accessoryType = [self canDrillInToRow:indexPath.row inExplorerSection:explorerSection] ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
CGFloat height = self.tableView.rowHeight;
if (explorerSection == FLEXObjectExplorerSectionDescription) {
NSString *text = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:@{ NSFontAttributeName : [FLEXUtility defaultTableViewCellLabelFont] }];
CGFloat preferredHeight = [FLEXMultilineTableViewCell preferredHeightWithAttributedText:attributedText inTableViewWidth:self.tableView.frame.size.width style:tableView.style showsAccessory:NO];
height = MAX(height, preferredHeight);
} else if (explorerSection == FLEXObjectExplorerSectionCustom) {
id cookie = [self customSectionRowCookieForVisibleRow:indexPath.row];
height = [self heightForCustomViewRowForRowCookie:cookie];
}
return height;
}
#pragma mark - Table View Delegate
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
return [self canDrillInToRow:indexPath.row inExplorerSection:explorerSection];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
UIViewController *detailViewController = [self drillInViewControllerForRow:indexPath.row inExplorerSection:explorerSection];
if (detailViewController) {
[self.navigationController pushViewController:detailViewController animated:YES];
} else {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
}
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [self sectionHasActions:indexPath.section];
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
switch (explorerSection) {
case FLEXObjectExplorerSectionDescription:
return action == @selector(copy:) || action == @selector(copyObjectAddress:);
default:
return NO;
}
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:action withObject:indexPath];
#pragma clang diagnostic pop
}
#pragma mark - UIMenuController
/// Prevent the search bar from trying to use us as a responder
///
/// Our table cells will use the UITableViewDelegate methods
/// to make sure we can perform the actions we want to
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
return NO;
}
- (void)copy:(NSIndexPath *)indexPath
{
FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
NSString *stringToCopy = @"";
NSString *title = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
if (title.length) {
stringToCopy = [stringToCopy stringByAppendingString:title];
}
NSString *subtitle = [self subtitleForRow:indexPath.row inExplorerSection:explorerSection];
if (subtitle.length) {
if (stringToCopy.length) {
stringToCopy = [stringToCopy stringByAppendingString:@"\n\n"];
}
stringToCopy = [stringToCopy stringByAppendingString:subtitle];
}
[UIPasteboard generalPasteboard].string = stringToCopy;
}
- (void)copyObjectAddress:(NSIndexPath *)indexPath
{
[UIPasteboard generalPasteboard].string = [FLEXUtility addressOfObject:self.object];
}
#pragma mark - Custom Section
- (void)updateCustomData
{
self.cachedCustomSectionRowCookies = [self customSectionRowCookies];
}
- (void)updateFilteredCustomData
{
NSIndexSet *filteredIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self.cachedCustomSectionRowCookies count])];
if ([self.filterText length] > 0) {
filteredIndexSet = [filteredIndexSet indexesPassingTest:^BOOL(NSUInteger index, BOOL *stop) {
BOOL matches = NO;
NSString *rowTitle = [self customSectionTitleForRowCookie:self.cachedCustomSectionRowCookies[index]];
if ([rowTitle rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
matches = YES;
}
return matches;
}];
}
self.customSectionVisibleIndexes = filteredIndexSet;
}
- (id)customSectionRowCookieForVisibleRow:(NSUInteger)row
{
return [[self.cachedCustomSectionRowCookies objectsAtIndexes:self.customSectionVisibleIndexes] objectAtIndex:row];
}
#pragma mark - Subclasses Can Override
- (NSString *)customSectionTitle
{
return nil;
}
- (NSArray *)customSectionRowCookies
{
return nil;
}
- (NSString *)customSectionTitleForRowCookie:(id)rowCookie
{
return nil;
}
- (NSString *)customSectionSubtitleForRowCookie:(id)rowCookie
{
return nil;
}
- (BOOL)customSectionCanDrillIntoRowWithCookie:(id)rowCookie
{
return NO;
}
- (UIViewController *)customSectionDrillInViewControllerForRowCookie:(id)rowCookie
{
return nil;
}
- (UIView *)customViewForRowCookie:(id)rowCookie
{
return nil;
}
- (CGFloat)heightForCustomViewRowForRowCookie:(id)rowCookie
{
return self.tableView.rowHeight;
}
- (BOOL)canHaveInstanceState
{
return YES;
}
- (BOOL)canCallInstanceMethods
{
return YES;
}
@end