Files
FLEX/Classes/ObjectExplorers/FLEXObjectExplorerViewController.m
Tanner Bennett 6c83ddc2c7 Refactor FLEXHeapEnumerator and FLEXObjectRef
Move logic that was in FLEXLiveObjectsController into FLEXHeapEnumerator. Also adjust FLEXObjectRef initializers to reflect the type of reference you want to hold to the object. FLEXObjectRef now supports unsafe_unretained and retained references.
2021-10-17 17:31:12 -05:00

394 lines
14 KiB
Objective-C

//
// FLEXObjectExplorerViewController.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import "FLEXObjectExplorerViewController.h"
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "UIBarButtonItem+FLEX.h"
#import "FLEXMultilineTableViewCell.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXFieldEditorViewController.h"
#import "FLEXMethodCallingViewController.h"
#import "FLEXObjectListViewController.h"
#import "FLEXTabsViewController.h"
#import "FLEXBookmarkManager.h"
#import "FLEXTableView.h"
#import "FLEXResources.h"
#import "FLEXTableViewCell.h"
#import "FLEXScopeCarousel.h"
#import "FLEXMetadataSection.h"
#import "FLEXSingleRowSection.h"
#import "FLEXShortcutsSection.h"
#import "NSUserDefaults+FLEX.h"
#import <objc/runtime.h>
#pragma mark - Private properties
@interface FLEXObjectExplorerViewController () <UIGestureRecognizerDelegate>
@property (nonatomic, readonly) FLEXSingleRowSection *descriptionSection;
@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *customSections;
@property (nonatomic) NSIndexSet *customSectionVisibleIndexes;
@property (nonatomic, readonly) NSArray<NSString *> *observedNotifications;
@end
@implementation FLEXObjectExplorerViewController
#pragma mark - Initialization
+ (instancetype)exploringObject:(id)target {
return [self exploringObject:target customSection:[FLEXShortcutsSection forObject:target]];
}
+ (instancetype)exploringObject:(id)target customSection:(FLEXTableViewSection *)section {
return [self exploringObject:target customSections:@[section]];
}
+ (instancetype)exploringObject:(id)target customSections:(NSArray *)customSections {
return [[self alloc]
initWithObject:target
explorer:[FLEXObjectExplorer forObject:target]
customSections:customSections
];
}
- (id)initWithObject:(id)target
explorer:(__kindof FLEXObjectExplorer *)explorer
customSections:(NSArray<FLEXTableViewSection *> *)customSections {
NSParameterAssert(target);
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
_object = target;
_explorer = explorer;
_customSections = customSections;
}
return self;
}
- (NSArray<NSString *> *)observedNotifications {
return @[
kFLEXDefaultsHidePropertyIvarsKey,
kFLEXDefaultsHidePropertyMethodsKey,
kFLEXDefaultsHidePrivateMethodsKey,
kFLEXDefaultsShowMethodOverridesKey,
kFLEXDefaultsHideVariablePreviewsKey,
];
}
#pragma mark - View controller lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
self.showsShareToolbarItem = YES;
self.wantsSectionIndexTitles = YES;
// Use [object class] here rather than object_getClass
// to avoid the KVO prefix for observed objects
self.title = [FLEXRuntimeUtility safeClassNameForObject:self.object];
// Search
self.showsSearchBar = YES;
self.searchBarDebounceInterval = kFLEXDebounceInstant;
self.showsCarousel = YES;
// Carousel scope bar
[self.explorer reloadClassHierarchy];
self.carousel.items = [self.explorer.classHierarchyClasses flex_mapped:^id(Class cls, NSUInteger idx) {
return NSStringFromClass(cls);
}];
// ... button for extra options
[self addToolbarItems:@[[UIBarButtonItem
flex_itemWithImage:FLEXResources.moreIcon target:self action:@selector(moreButtonPressed:)
]]];
// Swipe gestures to swipe between classes in the hierarchy
UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc]
initWithTarget:self action:@selector(handleSwipeGesture:)
];
UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc]
initWithTarget:self action:@selector(handleSwipeGesture:)
];
leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
leftSwipe.delegate = self;
rightSwipe.delegate = self;
[self.tableView addGestureRecognizer:leftSwipe];
[self.tableView addGestureRecognizer:rightSwipe];
// Observe preferences which may change on other screens
//
// "If your app targets iOS 9.0 and later or macOS 10.11 and later,
// you don't need to unregister an observer in its dealloc method."
for (NSString *pref in self.observedNotifications) {
[NSNotificationCenter.defaultCenter
addObserver:self
selector:@selector(fullyReloadData)
name:pref
object:nil
];
}
}
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView {
[self.navigationController setToolbarHidden:NO animated:YES];
return YES;
}
#pragma mark - Overrides
/// Override to hide the description section when searching
- (NSArray<FLEXTableViewSection *> *)nonemptySections {
if (self.shouldShowDescription) {
return super.nonemptySections;
}
return [super.nonemptySections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
return section != self.descriptionSection;
}];
}
- (NSArray<FLEXTableViewSection *> *)makeSections {
FLEXObjectExplorer *explorer = self.explorer;
// Description section is only for instances
if (self.explorer.objectIsInstance) {
_descriptionSection = [FLEXSingleRowSection
title:@"Description" reuse:kFLEXMultilineCell cell:^(FLEXTableViewCell *cell) {
cell.titleLabel.font = UIFont.flex_defaultTableCellFont;
cell.titleLabel.text = explorer.objectDescription;
}
];
self.descriptionSection.filterMatcher = ^BOOL(NSString *filterText) {
return [explorer.objectDescription localizedCaseInsensitiveContainsString:filterText];
};
}
// Object graph section
FLEXSingleRowSection *referencesSection = [FLEXSingleRowSection
title:@"Object Graph" reuse:kFLEXDefaultCell cell:^(FLEXTableViewCell *cell) {
cell.titleLabel.text = @"See Objects with References to This Object";
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
];
referencesSection.selectionAction = ^(UIViewController *host) {
UIViewController *references = [FLEXObjectListViewController
objectsWithReferencesToObject:explorer.object
retained:NO
];
[host.navigationController pushViewController:references animated:YES];
};
NSMutableArray *sections = [NSMutableArray arrayWithArray:@[
[FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindProperties],
[FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindClassProperties],
[FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindIvars],
[FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindMethods],
[FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindClassMethods],
[FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindClassHierarchy],
[FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindProtocols],
[FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindOther],
referencesSection
]];
if (self.customSections) {
[sections insertObjects:self.customSections atIndexes:[NSIndexSet
indexSetWithIndexesInRange:NSMakeRange(0, self.customSections.count)
]];
}
if (self.descriptionSection) {
[sections insertObject:self.descriptionSection atIndex:0];
}
return sections.copy;
}
/// In our case, all this does is reload the table view,
/// or reload the sections' data if we changed places
/// in the class hierarchy. Doesn't refresh \c self.explorer
- (void)reloadData {
// Check to see if class scope changed, update accordingly
if (self.explorer.classScope != self.selectedScope) {
self.explorer.classScope = self.selectedScope;
[self reloadSections];
}
[super reloadData];
}
- (void)shareButtonPressed:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.button(@"Add to Bookmarks").handler(^(NSArray<NSString *> *strings) {
[FLEXBookmarkManager.bookmarks addObject:self.object];
});
make.button(@"Copy Description").handler(^(NSArray<NSString *> *strings) {
UIPasteboard.generalPasteboard.string = self.explorer.objectDescription;
});
make.button(@"Copy Address").handler(^(NSArray<NSString *> *strings) {
UIPasteboard.generalPasteboard.string = [FLEXUtility addressOfObject:self.object];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
}
#pragma mark - Private
/// Unlike \c -reloadData, this refreshes everything, including the explorer.
- (void)fullyReloadData {
[self.explorer reloadMetadata];
[self reloadSections];
[self reloadData];
}
- (void)handleSwipeGesture:(UISwipeGestureRecognizer *)gesture {
if (gesture.state == UIGestureRecognizerStateEnded) {
switch (gesture.direction) {
case UISwipeGestureRecognizerDirectionRight:
if (self.selectedScope > 0) {
self.selectedScope -= 1;
}
break;
case UISwipeGestureRecognizerDirectionLeft:
if (self.selectedScope != self.explorer.classHierarchy.count - 1) {
self.selectedScope += 1;
}
break;
default:
break;
}
}
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)g2 {
// Prioritize important pan gestures over our swipe gesture
if ([g2 isKindOfClass:[UIPanGestureRecognizer class]]) {
if (g2 == self.navigationController.interactivePopGestureRecognizer) {
return NO;
}
if (g2 == self.tableView.panGestureRecognizer) {
return NO;
}
}
return YES;
}
- (BOOL)gestureRecognizerShouldBegin:(UISwipeGestureRecognizer *)gesture {
// Don't allow swiping from the carousel
CGPoint location = [gesture locationInView:self.tableView];
if ([self.carousel hitTest:location withEvent:nil]) {
return NO;
}
return YES;
}
- (void)moreButtonPressed:(UIBarButtonItem *)sender {
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
// Maps preference keys to a description of what they affect
NSDictionary<NSString *, NSString *> *explorerToggles = @{
kFLEXDefaultsHidePropertyIvarsKey: @"Property-Backing Ivars",
kFLEXDefaultsHidePropertyMethodsKey: @"Property-Backing Methods",
kFLEXDefaultsHidePrivateMethodsKey: @"Likely Private Methods",
kFLEXDefaultsShowMethodOverridesKey: @"Method Overrides",
kFLEXDefaultsHideVariablePreviewsKey: @"Variable Previews"
};
// Maps the key of the action itself to a map of a description
// of the action ("hide X") mapped to the current state.
//
// So keys that are hidden by default have NO mapped to "Show"
NSDictionary<NSString *, NSDictionary *> *nextStateDescriptions = @{
kFLEXDefaultsHidePropertyIvarsKey: @{ @NO: @"Hide ", @YES: @"Show " },
kFLEXDefaultsHidePropertyMethodsKey: @{ @NO: @"Hide ", @YES: @"Show " },
kFLEXDefaultsHidePrivateMethodsKey: @{ @NO: @"Hide ", @YES: @"Show " },
kFLEXDefaultsShowMethodOverridesKey: @{ @NO: @"Show ", @YES: @"Hide " },
kFLEXDefaultsHideVariablePreviewsKey: @{ @NO: @"Hide ", @YES: @"Show " },
};
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"Options");
for (NSString *option in explorerToggles.allKeys) {
BOOL current = [defaults boolForKey:option];
NSString *title = [nextStateDescriptions[option][@(current)]
stringByAppendingString:explorerToggles[option]
];
make.button(title).handler(^(NSArray<NSString *> *strings) {
[NSUserDefaults.standardUserDefaults flex_toggleBoolForKey:option];
[self fullyReloadData];
});
}
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
}
#pragma mark - Description
- (BOOL)shouldShowDescription {
// Hide if we have filter text; it is rarely
// useful to see the description when searching
// since it's already at the top of the screen
if (self.filterText.length) {
return NO;
}
return YES;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// For the description section, we want that nice slim/snug looking row.
// Other rows use the automatic size.
FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
if (section == self.descriptionSection) {
NSAttributedString *attributedText = [[NSAttributedString alloc]
initWithString:self.explorer.objectDescription
attributes:@{ NSFontAttributeName : UIFont.flex_defaultTableCellFont }
];
return [FLEXMultilineTableViewCell
preferredHeightWithAttributedText:attributedText
maxWidth:tableView.frame.size.width - tableView.separatorInset.right
style:tableView.style
showsAccessory:NO
];
}
return UITableViewAutomaticDimension;
}
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.filterDelegate.sections[indexPath.section] == self.descriptionSection;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
// Only the description section has "actions"
if (self.filterDelegate.sections[indexPath.section] == self.descriptionSection) {
return action == @selector(copy:);
}
return NO;
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
if (action == @selector(copy:)) {
UIPasteboard.generalPasteboard.string = self.explorer.objectDescription;
}
}
@end