FLEXExplorerViewController.m 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043
  1. //
  2. // FLEXExplorerViewController.m
  3. // Flipboard
  4. //
  5. // Created by Ryan Olson on 4/4/14.
  6. // Copyright (c) 2020 FLEX Team. All rights reserved.
  7. //
  8. #import "FLEXExplorerViewController.h"
  9. #import "FLEXExplorerToolbarItem.h"
  10. #import "FLEXUtility.h"
  11. #import "FLEXWindow.h"
  12. #import "FLEXTabList.h"
  13. #import "FLEXNavigationController.h"
  14. #import "FLEXHierarchyViewController.h"
  15. #import "FLEXGlobalsViewController.h"
  16. #import "FLEXObjectExplorerViewController.h"
  17. #import "FLEXObjectExplorerFactory.h"
  18. #import "FLEXNetworkMITMViewController.h"
  19. #import "FLEXTabsViewController.h"
  20. #import "FLEXWindowManagerController.h"
  21. #import "FLEXViewControllersViewController.h"
  22. #import "NSUserDefaults+FLEX.h"
  23. typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
  24. FLEXExplorerModeDefault,
  25. FLEXExplorerModeSelect,
  26. FLEXExplorerModeMove
  27. };
  28. @interface FLEXExplorerViewController () <FLEXHierarchyDelegate, UIAdaptivePresentationControllerDelegate>
  29. /// Tracks the currently active tool/mode
  30. @property (nonatomic) FLEXExplorerMode currentMode;
  31. /// Gesture recognizer for dragging a view in move mode
  32. @property (nonatomic) UIPanGestureRecognizer *movePanGR;
  33. /// Gesture recognizer for showing additional details on the selected view
  34. @property (nonatomic) UITapGestureRecognizer *detailsTapGR;
  35. /// Only valid while a move pan gesture is in progress.
  36. @property (nonatomic) CGRect selectedViewFrameBeforeDragging;
  37. /// Only valid while a toolbar drag pan gesture is in progress.
  38. @property (nonatomic) CGRect toolbarFrameBeforeDragging;
  39. /// Only valid while a selected view pan gesture is in progress.
  40. @property (nonatomic) CGFloat selectedViewLastPanX;
  41. /// Borders of all the visible views in the hierarchy at the selection point.
  42. /// The keys are NSValues with the corresponding view (nonretained).
  43. @property (nonatomic) NSDictionary<NSValue *, UIView *> *outlineViewsForVisibleViews;
  44. /// The actual views at the selection point with the deepest view last.
  45. @property (nonatomic) NSArray<UIView *> *viewsAtTapPoint;
  46. /// The view that we're currently highlighting with an overlay and displaying details for.
  47. @property (nonatomic) UIView *selectedView;
  48. /// A colored transparent overlay to indicate that the view is selected.
  49. @property (nonatomic) UIView *selectedViewOverlay;
  50. /// Used to actuate changes in view selection on iOS 10+
  51. @property (nonatomic, readonly) UISelectionFeedbackGenerator *selectionFBG API_AVAILABLE(ios(10.0));
  52. /// self.view.window as a \c FLEXWindow
  53. @property (nonatomic, readonly) FLEXWindow *window;
  54. /// All views that we're KVOing. Used to help us clean up properly.
  55. @property (nonatomic) NSMutableSet<UIView *> *observedViews;
  56. /// Used to preserve the target app's UIMenuController items.
  57. @property (nonatomic) NSArray<UIMenuItem *> *appMenuItems;
  58. @end
  59. @implementation FLEXExplorerViewController
  60. - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
  61. self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  62. if (self) {
  63. self.observedViews = [NSMutableSet new];
  64. }
  65. return self;
  66. }
  67. - (void)dealloc {
  68. for (UIView *view in _observedViews) {
  69. [self stopObservingView:view];
  70. }
  71. }
  72. - (void)viewDidLoad {
  73. [super viewDidLoad];
  74. // Toolbar
  75. _explorerToolbar = [FLEXExplorerToolbar new];
  76. // Start the toolbar off below any bars that may be at the top of the view.
  77. CGFloat toolbarOriginY = NSUserDefaults.standardUserDefaults.flex_toolbarTopMargin;
  78. CGRect safeArea = [self viewSafeArea];
  79. CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(
  80. CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea)
  81. )];
  82. [self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(
  83. CGRectGetMinX(safeArea), toolbarOriginY, toolbarSize.width, toolbarSize.height
  84. )];
  85. self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth |
  86. UIViewAutoresizingFlexibleBottomMargin |
  87. UIViewAutoresizingFlexibleTopMargin;
  88. [self.view addSubview:self.explorerToolbar];
  89. [self setupToolbarActions];
  90. [self setupToolbarGestures];
  91. // View selection
  92. UITapGestureRecognizer *selectionTapGR = [[UITapGestureRecognizer alloc]
  93. initWithTarget:self action:@selector(handleSelectionTap:)
  94. ];
  95. [self.view addGestureRecognizer:selectionTapGR];
  96. // View moving
  97. self.movePanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePan:)];
  98. self.movePanGR.enabled = self.currentMode == FLEXExplorerModeMove;
  99. [self.view addGestureRecognizer:self.movePanGR];
  100. // Feedback
  101. if (@available(iOS 10.0, *)) {
  102. _selectionFBG = [UISelectionFeedbackGenerator new];
  103. }
  104. }
  105. - (void)viewWillAppear:(BOOL)animated {
  106. [super viewWillAppear:animated];
  107. [self updateButtonStates];
  108. }
  109. #pragma mark - Rotation
  110. - (UIViewController *)viewControllerForRotationAndOrientation {
  111. UIViewController *viewController = FLEXUtility.appKeyWindow.rootViewController;
  112. // Obfuscating selector _viewControllerForSupportedInterfaceOrientations
  113. NSString *viewControllerSelectorString = [@[
  114. @"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"
  115. ] componentsJoinedByString:@""];
  116. SEL viewControllerSelector = NSSelectorFromString(viewControllerSelectorString);
  117. if ([viewController respondsToSelector:viewControllerSelector]) {
  118. viewController = [viewController valueForKey:viewControllerSelectorString];
  119. }
  120. return viewController;
  121. }
  122. - (UIInterfaceOrientationMask)supportedInterfaceOrientations {
  123. // Commenting this out until I can figure out a better way to solve this
  124. // if (self.window.isKeyWindow) {
  125. // [self.window resignKeyWindow];
  126. // }
  127. UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
  128. UIInterfaceOrientationMask supportedOrientations = FLEXUtility.infoPlistSupportedInterfaceOrientationsMask;
  129. if (viewControllerToAsk && ![viewControllerToAsk isKindOfClass:[self class]]) {
  130. supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
  131. }
  132. // The UIViewController docs state that this method must not return zero.
  133. // If we weren't able to get a valid value for the supported interface
  134. // orientations, default to all supported.
  135. if (supportedOrientations == 0) {
  136. supportedOrientations = UIInterfaceOrientationMaskAll;
  137. }
  138. return supportedOrientations;
  139. }
  140. - (BOOL)shouldAutorotate {
  141. UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
  142. BOOL shouldAutorotate = YES;
  143. if (viewControllerToAsk && viewControllerToAsk != self) {
  144. shouldAutorotate = [viewControllerToAsk shouldAutorotate];
  145. }
  146. return shouldAutorotate;
  147. }
  148. - (void)viewWillTransitionToSize:(CGSize)size
  149. withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
  150. [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
  151. [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
  152. for (UIView *outlineView in self.outlineViewsForVisibleViews.allValues) {
  153. outlineView.hidden = YES;
  154. }
  155. self.selectedViewOverlay.hidden = YES;
  156. } completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
  157. for (UIView *view in self.viewsAtTapPoint) {
  158. NSValue *key = [NSValue valueWithNonretainedObject:view];
  159. UIView *outlineView = self.outlineViewsForVisibleViews[key];
  160. outlineView.frame = [self frameInLocalCoordinatesForView:view];
  161. if (self.currentMode == FLEXExplorerModeSelect) {
  162. outlineView.hidden = NO;
  163. }
  164. }
  165. if (self.selectedView) {
  166. self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
  167. self.selectedViewOverlay.hidden = NO;
  168. }
  169. }];
  170. }
  171. #pragma mark - Setter Overrides
  172. - (void)setSelectedView:(UIView *)selectedView {
  173. if (![_selectedView isEqual:selectedView]) {
  174. if (![self.viewsAtTapPoint containsObject:_selectedView]) {
  175. [self stopObservingView:_selectedView];
  176. }
  177. _selectedView = selectedView;
  178. [self beginObservingView:selectedView];
  179. // Update the toolbar and selected overlay
  180. self.explorerToolbar.selectedViewDescription = [FLEXUtility
  181. descriptionForView:selectedView includingFrame:YES
  182. ];
  183. self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility
  184. consistentRandomColorForObject:selectedView
  185. ];
  186. if (selectedView) {
  187. if (!self.selectedViewOverlay) {
  188. self.selectedViewOverlay = [UIView new];
  189. [self.view addSubview:self.selectedViewOverlay];
  190. self.selectedViewOverlay.layer.borderWidth = 1.0;
  191. }
  192. UIColor *outlineColor = [FLEXUtility consistentRandomColorForObject:selectedView];
  193. self.selectedViewOverlay.backgroundColor = [outlineColor colorWithAlphaComponent:0.2];
  194. self.selectedViewOverlay.layer.borderColor = outlineColor.CGColor;
  195. self.selectedViewOverlay.frame = [self.view convertRect:selectedView.bounds fromView:selectedView];
  196. // Make sure the selected overlay is in front of all the other subviews
  197. // except the toolbar, which should always stay on top.
  198. [self.view bringSubviewToFront:self.selectedViewOverlay];
  199. [self.view bringSubviewToFront:self.explorerToolbar];
  200. } else {
  201. [self.selectedViewOverlay removeFromSuperview];
  202. self.selectedViewOverlay = nil;
  203. }
  204. // Some of the button states depend on whether we have a selected view.
  205. [self updateButtonStates];
  206. }
  207. }
  208. - (void)setViewsAtTapPoint:(NSArray<UIView *> *)viewsAtTapPoint {
  209. if (![_viewsAtTapPoint isEqual:viewsAtTapPoint]) {
  210. for (UIView *view in _viewsAtTapPoint) {
  211. if (view != self.selectedView) {
  212. [self stopObservingView:view];
  213. }
  214. }
  215. _viewsAtTapPoint = viewsAtTapPoint;
  216. for (UIView *view in viewsAtTapPoint) {
  217. [self beginObservingView:view];
  218. }
  219. }
  220. }
  221. - (void)setCurrentMode:(FLEXExplorerMode)currentMode {
  222. if (_currentMode != currentMode) {
  223. _currentMode = currentMode;
  224. switch (currentMode) {
  225. case FLEXExplorerModeDefault:
  226. [self removeAndClearOutlineViews];
  227. self.viewsAtTapPoint = nil;
  228. self.selectedView = nil;
  229. break;
  230. case FLEXExplorerModeSelect:
  231. // Make sure the outline views are unhidden in case we came from the move mode.
  232. for (NSValue *key in self.outlineViewsForVisibleViews) {
  233. UIView *outlineView = self.outlineViewsForVisibleViews[key];
  234. outlineView.hidden = NO;
  235. }
  236. break;
  237. case FLEXExplorerModeMove:
  238. // Hide all the outline views to focus on the selected view,
  239. // which is the only one that will move.
  240. for (NSValue *key in self.outlineViewsForVisibleViews) {
  241. UIView *outlineView = self.outlineViewsForVisibleViews[key];
  242. outlineView.hidden = YES;
  243. }
  244. break;
  245. }
  246. self.movePanGR.enabled = currentMode == FLEXExplorerModeMove;
  247. [self updateButtonStates];
  248. }
  249. }
  250. #pragma mark - View Tracking
  251. - (void)beginObservingView:(UIView *)view {
  252. // Bail if we're already observing this view or if there's nothing to observe.
  253. if (!view || [self.observedViews containsObject:view]) {
  254. return;
  255. }
  256. for (NSString *keyPath in self.viewKeyPathsToTrack) {
  257. [view addObserver:self forKeyPath:keyPath options:0 context:NULL];
  258. }
  259. [self.observedViews addObject:view];
  260. }
  261. - (void)stopObservingView:(UIView *)view {
  262. if (!view) {
  263. return;
  264. }
  265. for (NSString *keyPath in self.viewKeyPathsToTrack) {
  266. [view removeObserver:self forKeyPath:keyPath];
  267. }
  268. [self.observedViews removeObject:view];
  269. }
  270. - (NSArray<NSString *> *)viewKeyPathsToTrack {
  271. static NSArray<NSString *> *trackedViewKeyPaths = nil;
  272. static dispatch_once_t onceToken;
  273. dispatch_once(&onceToken, ^{
  274. NSString *frameKeyPath = NSStringFromSelector(@selector(frame));
  275. trackedViewKeyPaths = @[frameKeyPath];
  276. });
  277. return trackedViewKeyPaths;
  278. }
  279. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
  280. change:(NSDictionary<NSString *, id> *)change
  281. context:(void *)context {
  282. [self updateOverlayAndDescriptionForObjectIfNeeded:object];
  283. }
  284. - (void)updateOverlayAndDescriptionForObjectIfNeeded:(id)object {
  285. NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
  286. if (indexOfView != NSNotFound) {
  287. UIView *view = self.viewsAtTapPoint[indexOfView];
  288. NSValue *key = [NSValue valueWithNonretainedObject:view];
  289. UIView *outline = self.outlineViewsForVisibleViews[key];
  290. if (outline) {
  291. outline.frame = [self frameInLocalCoordinatesForView:view];
  292. }
  293. }
  294. if (object == self.selectedView) {
  295. // Update the selected view description since we show the frame value there.
  296. self.explorerToolbar.selectedViewDescription = [FLEXUtility
  297. descriptionForView:self.selectedView includingFrame:YES
  298. ];
  299. CGRect selectedViewOutlineFrame = [self frameInLocalCoordinatesForView:self.selectedView];
  300. self.selectedViewOverlay.frame = selectedViewOutlineFrame;
  301. }
  302. }
  303. - (CGRect)frameInLocalCoordinatesForView:(UIView *)view {
  304. // Convert to window coordinates since the view may be in a different window than our view
  305. CGRect frameInWindow = [view convertRect:view.bounds toView:nil];
  306. // Convert from the window to our view's coordinate space
  307. return [self.view convertRect:frameInWindow fromView:nil];
  308. }
  309. #pragma mark - Toolbar Buttons
  310. - (void)setupToolbarActions {
  311. FLEXExplorerToolbar *toolbar = self.explorerToolbar;
  312. NSDictionary<NSString *, FLEXExplorerToolbarItem *> *actionsToItems = @{
  313. NSStringFromSelector(@selector(selectButtonTapped:)): toolbar.selectItem,
  314. NSStringFromSelector(@selector(hierarchyButtonTapped:)): toolbar.hierarchyItem,
  315. NSStringFromSelector(@selector(recentButtonTapped:)): toolbar.recentItem,
  316. NSStringFromSelector(@selector(moveButtonTapped:)): toolbar.moveItem,
  317. NSStringFromSelector(@selector(globalsButtonTapped:)): toolbar.globalsItem,
  318. NSStringFromSelector(@selector(closeButtonTapped:)): toolbar.closeItem,
  319. };
  320. [actionsToItems enumerateKeysAndObjectsUsingBlock:^(NSString *sel, FLEXExplorerToolbarItem *item, BOOL *stop) {
  321. [item addTarget:self action:NSSelectorFromString(sel) forControlEvents:UIControlEventTouchUpInside];
  322. }];
  323. }
  324. - (void)selectButtonTapped:(FLEXExplorerToolbarItem *)sender {
  325. [self toggleSelectTool];
  326. }
  327. - (void)hierarchyButtonTapped:(FLEXExplorerToolbarItem *)sender {
  328. [self toggleViewsTool];
  329. }
  330. - (UIWindow *)statusWindow {
  331. NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
  332. return [UIApplication.sharedApplication valueForKey:statusBarString];
  333. }
  334. - (void)recentButtonTapped:(FLEXExplorerToolbarItem *)sender {
  335. NSAssert(FLEXTabList.sharedList.activeTab, @"Must have active tab");
  336. [self presentViewController:FLEXTabList.sharedList.activeTab animated:YES completion:nil];
  337. }
  338. - (void)moveButtonTapped:(FLEXExplorerToolbarItem *)sender {
  339. [self toggleMoveTool];
  340. }
  341. - (void)globalsButtonTapped:(FLEXExplorerToolbarItem *)sender {
  342. [self toggleMenuTool];
  343. }
  344. - (void)closeButtonTapped:(FLEXExplorerToolbarItem *)sender {
  345. self.currentMode = FLEXExplorerModeDefault;
  346. [self.delegate explorerViewControllerDidFinish:self];
  347. }
  348. - (void)updateButtonStates {
  349. FLEXExplorerToolbar *toolbar = self.explorerToolbar;
  350. toolbar.selectItem.selected = self.currentMode == FLEXExplorerModeSelect;
  351. // Move only enabled when an object is selected.
  352. BOOL hasSelectedObject = self.selectedView != nil;
  353. toolbar.moveItem.enabled = hasSelectedObject;
  354. toolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
  355. // Recent only enabled when we have a last active tab
  356. toolbar.recentItem.enabled = FLEXTabList.sharedList.activeTab != nil;
  357. }
  358. #pragma mark - Toolbar Dragging
  359. - (void)setupToolbarGestures {
  360. FLEXExplorerToolbar *toolbar = self.explorerToolbar;
  361. // Pan gesture for dragging.
  362. [toolbar.dragHandle addGestureRecognizer:[[UIPanGestureRecognizer alloc]
  363. initWithTarget:self action:@selector(handleToolbarPanGesture:)
  364. ]];
  365. // Tap gesture for hinting.
  366. [toolbar.dragHandle addGestureRecognizer:[[UITapGestureRecognizer alloc]
  367. initWithTarget:self action:@selector(handleToolbarHintTapGesture:)
  368. ]];
  369. // Tap gesture for showing additional details
  370. self.detailsTapGR = [[UITapGestureRecognizer alloc]
  371. initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)
  372. ];
  373. [toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
  374. // Swipe gestures for selecting deeper / higher views at a point
  375. UIPanGestureRecognizer *leftSwipe = [[UIPanGestureRecognizer alloc]
  376. initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
  377. ];
  378. // UIPanGestureRecognizer *rightSwipe = [[UIPanGestureRecognizer alloc]
  379. // initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
  380. // ];
  381. // leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
  382. // rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
  383. [toolbar.selectedViewDescriptionContainer addGestureRecognizer:leftSwipe];
  384. // [toolbar.selectedViewDescriptionContainer addGestureRecognizer:rightSwipe];
  385. // Long press gesture to present tabs manager
  386. [toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
  387. initWithTarget:self action:@selector(handleToolbarShowTabsGesture:)
  388. ]];
  389. // Long press gesture to present window manager
  390. [toolbar.selectItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
  391. initWithTarget:self action:@selector(handleToolbarWindowManagerGesture:)
  392. ]];
  393. // Long press gesture to present view controllers at tap
  394. [toolbar.hierarchyItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
  395. initWithTarget:self action:@selector(handleToolbarShowViewControllersGesture:)
  396. ]];
  397. }
  398. - (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR {
  399. switch (panGR.state) {
  400. case UIGestureRecognizerStateBegan:
  401. self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
  402. [self updateToolbarPositionWithDragGesture:panGR];
  403. break;
  404. case UIGestureRecognizerStateChanged:
  405. case UIGestureRecognizerStateEnded:
  406. [self updateToolbarPositionWithDragGesture:panGR];
  407. break;
  408. default:
  409. break;
  410. }
  411. }
  412. - (void)updateToolbarPositionWithDragGesture:(UIPanGestureRecognizer *)panGR {
  413. CGPoint translation = [panGR translationInView:self.view];
  414. CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
  415. newToolbarFrame.origin.y += translation.y;
  416. [self updateToolbarPositionWithUnconstrainedFrame:newToolbarFrame];
  417. }
  418. - (void)updateToolbarPositionWithUnconstrainedFrame:(CGRect)unconstrainedFrame {
  419. CGRect safeArea = [self viewSafeArea];
  420. // We only constrain the Y-axis because we want the toolbar
  421. // to handle the X-axis safeArea layout by itself
  422. CGFloat minY = CGRectGetMinY(safeArea);
  423. CGFloat maxY = CGRectGetMaxY(safeArea) - unconstrainedFrame.size.height;
  424. if (unconstrainedFrame.origin.y < minY) {
  425. unconstrainedFrame.origin.y = minY;
  426. } else if (unconstrainedFrame.origin.y > maxY) {
  427. unconstrainedFrame.origin.y = maxY;
  428. }
  429. self.explorerToolbar.frame = unconstrainedFrame;
  430. NSUserDefaults.standardUserDefaults.flex_toolbarTopMargin = unconstrainedFrame.origin.y;
  431. }
  432. - (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR {
  433. // Bounce the toolbar to indicate that it is draggable.
  434. // TODO: make it bouncier.
  435. if (tapGR.state == UIGestureRecognizerStateRecognized) {
  436. CGRect originalToolbarFrame = self.explorerToolbar.frame;
  437. const NSTimeInterval kHalfwayDuration = 0.2;
  438. const CGFloat kVerticalOffset = 30.0;
  439. [UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
  440. CGRect newToolbarFrame = self.explorerToolbar.frame;
  441. newToolbarFrame.origin.y += kVerticalOffset;
  442. self.explorerToolbar.frame = newToolbarFrame;
  443. } completion:^(BOOL finished) {
  444. [UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
  445. self.explorerToolbar.frame = originalToolbarFrame;
  446. } completion:nil];
  447. }];
  448. }
  449. }
  450. - (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR {
  451. if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
  452. UIViewController *topStackVC = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
  453. [self presentViewController:
  454. [FLEXNavigationController withRootViewController:topStackVC]
  455. animated:YES completion:nil];
  456. }
  457. }
  458. - (void)handleToolbarShowTabsGesture:(UILongPressGestureRecognizer *)sender {
  459. if (sender.state == UIGestureRecognizerStateBegan) {
  460. // Back up the UIMenuController items since dismissViewController: will attempt to replace them
  461. self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
  462. // Don't use FLEXNavigationController because the tab viewer itself is not a tab
  463. [super presentViewController:[[UINavigationController alloc]
  464. initWithRootViewController:[FLEXTabsViewController new]
  465. ] animated:YES completion:nil];
  466. }
  467. }
  468. - (void)handleToolbarWindowManagerGesture:(UILongPressGestureRecognizer *)sender {
  469. if (sender.state == UIGestureRecognizerStateBegan) {
  470. // Back up the UIMenuController items since dismissViewController: will attempt to replace them
  471. self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
  472. [super presentViewController:[FLEXNavigationController
  473. withRootViewController:[FLEXWindowManagerController new]
  474. ] animated:YES completion:nil];
  475. }
  476. }
  477. - (void)handleToolbarShowViewControllersGesture:(UILongPressGestureRecognizer *)sender {
  478. if (sender.state == UIGestureRecognizerStateBegan && self.viewsAtTapPoint.count) {
  479. // Back up the UIMenuController items since dismissViewController: will attempt to replace them
  480. self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
  481. UIViewController *list = [FLEXViewControllersViewController
  482. controllersForViews:self.viewsAtTapPoint
  483. ];
  484. [self presentViewController:
  485. [FLEXNavigationController withRootViewController:list
  486. ] animated:YES completion:nil];
  487. }
  488. }
  489. #pragma mark - View Selection
  490. - (void)handleSelectionTap:(UITapGestureRecognizer *)tapGR {
  491. // Only if we're in selection mode
  492. if (self.currentMode == FLEXExplorerModeSelect && tapGR.state == UIGestureRecognizerStateRecognized) {
  493. // Note that [tapGR locationInView:nil] is broken in iOS 8,
  494. // so we have to do a two step conversion to window coordinates.
  495. // Thanks to @lascorbe for finding this: https://github.com/Flipboard/FLEX/pull/31
  496. CGPoint tapPointInView = [tapGR locationInView:self.view];
  497. CGPoint tapPointInWindow = [self.view convertPoint:tapPointInView toView:nil];
  498. [self updateOutlineViewsForSelectionPoint:tapPointInWindow];
  499. }
  500. }
  501. - (void)handleChangeViewAtPointGesture:(UIPanGestureRecognizer *)sender {
  502. NSInteger max = self.viewsAtTapPoint.count - 1;
  503. NSInteger currentIdx = [self.viewsAtTapPoint indexOfObject:self.selectedView];
  504. CGFloat locationX = [sender locationInView:self.view].x;
  505. // Track the pan gesture: every N points we move along the X axis,
  506. // actuate some haptic feedback and move up or down the hierarchy.
  507. // We only store the "last" location when we've met the threshold.
  508. // We only change the view and actuate feedback if the view selection
  509. // changes; that is, as long as we don't go outside or under the array.
  510. switch (sender.state) {
  511. case UIGestureRecognizerStateBegan: {
  512. self.selectedViewLastPanX = locationX;
  513. break;
  514. }
  515. case UIGestureRecognizerStateChanged: {
  516. static CGFloat kNextLevelThreshold = 20.f;
  517. CGFloat lastX = self.selectedViewLastPanX;
  518. NSInteger newSelection = currentIdx;
  519. // Left, go down the hierarchy
  520. if (locationX < lastX && (lastX - locationX) >= kNextLevelThreshold) {
  521. // Choose a new view index up to the max index
  522. newSelection = MIN(max, currentIdx + 1);
  523. self.selectedViewLastPanX = locationX;
  524. }
  525. // Right, go up the hierarchy
  526. else if (lastX < locationX && (locationX - lastX) >= kNextLevelThreshold) {
  527. // Choose a new view index down to the min index
  528. newSelection = MAX(0, currentIdx - 1);
  529. self.selectedViewLastPanX = locationX;
  530. }
  531. if (currentIdx != newSelection) {
  532. self.selectedView = self.viewsAtTapPoint[newSelection];
  533. [self actuateSelectionChangedFeedback];
  534. }
  535. break;
  536. }
  537. default: break;
  538. }
  539. }
  540. - (void)actuateSelectionChangedFeedback {
  541. if (@available(iOS 10.0, *)) {
  542. [self.selectionFBG selectionChanged];
  543. }
  544. }
  545. - (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow {
  546. [self removeAndClearOutlineViews];
  547. // Include hidden views in the "viewsAtTapPoint" array so we can show them in the hierarchy list.
  548. self.viewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:NO];
  549. // For outlined views and the selected view, only use visible views.
  550. // Outlining hidden views adds clutter and makes the selection behavior confusing.
  551. NSArray<UIView *> *visibleViewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:YES];
  552. NSMutableDictionary<NSValue *, UIView *> *newOutlineViewsForVisibleViews = [NSMutableDictionary new];
  553. for (UIView *view in visibleViewsAtTapPoint) {
  554. UIView *outlineView = [self outlineViewForView:view];
  555. [self.view addSubview:outlineView];
  556. NSValue *key = [NSValue valueWithNonretainedObject:view];
  557. [newOutlineViewsForVisibleViews setObject:outlineView forKey:key];
  558. }
  559. self.outlineViewsForVisibleViews = newOutlineViewsForVisibleViews;
  560. self.selectedView = [self viewForSelectionAtPoint:selectionPointInWindow];
  561. // Make sure the explorer toolbar doesn't end up behind the newly added outline views.
  562. [self.view bringSubviewToFront:self.explorerToolbar];
  563. [self updateButtonStates];
  564. }
  565. - (UIView *)outlineViewForView:(UIView *)view {
  566. CGRect outlineFrame = [self frameInLocalCoordinatesForView:view];
  567. UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
  568. outlineView.backgroundColor = UIColor.clearColor;
  569. outlineView.layer.borderColor = [FLEXUtility consistentRandomColorForObject:view].CGColor;
  570. outlineView.layer.borderWidth = 1.0;
  571. return outlineView;
  572. }
  573. - (void)removeAndClearOutlineViews {
  574. for (NSValue *key in self.outlineViewsForVisibleViews) {
  575. UIView *outlineView = self.outlineViewsForVisibleViews[key];
  576. [outlineView removeFromSuperview];
  577. }
  578. self.outlineViewsForVisibleViews = nil;
  579. }
  580. - (NSArray<UIView *> *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden {
  581. NSMutableArray<UIView *> *views = [NSMutableArray new];
  582. for (UIWindow *window in FLEXUtility.allWindows) {
  583. // Don't include the explorer's own window or subviews.
  584. if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
  585. [views addObject:window];
  586. [views addObjectsFromArray:[self
  587. recursiveSubviewsAtPoint:tapPointInWindow inView:window skipHiddenViews:skipHidden
  588. ]];
  589. }
  590. }
  591. return views;
  592. }
  593. - (UIView *)viewForSelectionAtPoint:(CGPoint)tapPointInWindow {
  594. // Select in the window that would handle the touch, but don't just use the result of
  595. // hitTest:withEvent: so we can still select views with interaction disabled.
  596. // Default to the the application's key window if none of the windows want the touch.
  597. UIWindow *windowForSelection = UIApplication.sharedApplication.keyWindow;
  598. for (UIWindow *window in FLEXUtility.allWindows.reverseObjectEnumerator) {
  599. // Ignore the explorer's own window.
  600. if (window != self.view.window) {
  601. if ([window hitTest:tapPointInWindow withEvent:nil]) {
  602. windowForSelection = window;
  603. break;
  604. }
  605. }
  606. }
  607. // Select the deepest visible view at the tap point. This generally corresponds to what the user wants to select.
  608. return [self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES].lastObject;
  609. }
  610. - (NSArray<UIView *> *)recursiveSubviewsAtPoint:(CGPoint)pointInView
  611. inView:(UIView *)view
  612. skipHiddenViews:(BOOL)skipHidden {
  613. NSMutableArray<UIView *> *subviewsAtPoint = [NSMutableArray new];
  614. for (UIView *subview in view.subviews) {
  615. BOOL isHidden = subview.hidden || subview.alpha < 0.01;
  616. if (skipHidden && isHidden) {
  617. continue;
  618. }
  619. BOOL subviewContainsPoint = CGRectContainsPoint(subview.frame, pointInView);
  620. if (subviewContainsPoint) {
  621. [subviewsAtPoint addObject:subview];
  622. }
  623. // If this view doesn't clip to its bounds, we need to check its subviews even if it
  624. // doesn't contain the selection point. They may be visible and contain the selection point.
  625. if (subviewContainsPoint || !subview.clipsToBounds) {
  626. CGPoint pointInSubview = [view convertPoint:pointInView toView:subview];
  627. [subviewsAtPoint addObjectsFromArray:[self
  628. recursiveSubviewsAtPoint:pointInSubview inView:subview skipHiddenViews:skipHidden
  629. ]];
  630. }
  631. }
  632. return subviewsAtPoint;
  633. }
  634. #pragma mark - Selected View Moving
  635. - (void)handleMovePan:(UIPanGestureRecognizer *)movePanGR {
  636. switch (movePanGR.state) {
  637. case UIGestureRecognizerStateBegan:
  638. self.selectedViewFrameBeforeDragging = self.selectedView.frame;
  639. [self updateSelectedViewPositionWithDragGesture:movePanGR];
  640. break;
  641. case UIGestureRecognizerStateChanged:
  642. case UIGestureRecognizerStateEnded:
  643. [self updateSelectedViewPositionWithDragGesture:movePanGR];
  644. break;
  645. default:
  646. break;
  647. }
  648. }
  649. - (void)updateSelectedViewPositionWithDragGesture:(UIPanGestureRecognizer *)movePanGR {
  650. CGPoint translation = [movePanGR translationInView:self.selectedView.superview];
  651. CGRect newSelectedViewFrame = self.selectedViewFrameBeforeDragging;
  652. newSelectedViewFrame.origin.x = FLEXFloor(newSelectedViewFrame.origin.x + translation.x);
  653. newSelectedViewFrame.origin.y = FLEXFloor(newSelectedViewFrame.origin.y + translation.y);
  654. self.selectedView.frame = newSelectedViewFrame;
  655. }
  656. #pragma mark - Safe Area Handling
  657. - (CGRect)viewSafeArea {
  658. CGRect safeArea = self.view.bounds;
  659. if (@available(iOS 11.0, *)) {
  660. safeArea = UIEdgeInsetsInsetRect(self.view.bounds, self.view.safeAreaInsets);
  661. }
  662. return safeArea;
  663. }
  664. - (void)viewSafeAreaInsetsDidChange {
  665. if (@available(iOS 11.0, *)) {
  666. [super viewSafeAreaInsetsDidChange];
  667. CGRect safeArea = [self viewSafeArea];
  668. CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(
  669. CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea)
  670. )];
  671. [self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(
  672. CGRectGetMinX(self.explorerToolbar.frame),
  673. CGRectGetMinY(self.explorerToolbar.frame),
  674. toolbarSize.width,
  675. toolbarSize.height)
  676. ];
  677. }
  678. }
  679. #pragma mark - Touch Handling
  680. - (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates {
  681. BOOL shouldReceiveTouch = NO;
  682. CGPoint pointInLocalCoordinates = [self.view convertPoint:pointInWindowCoordinates fromView:nil];
  683. // Always if it's on the toolbar
  684. if (CGRectContainsPoint(self.explorerToolbar.frame, pointInLocalCoordinates)) {
  685. shouldReceiveTouch = YES;
  686. }
  687. // Always if we're in selection mode
  688. if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeSelect) {
  689. shouldReceiveTouch = YES;
  690. }
  691. // Always in move mode too
  692. if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeMove) {
  693. shouldReceiveTouch = YES;
  694. }
  695. // Always if we have a modal presented
  696. if (!shouldReceiveTouch && self.presentedViewController) {
  697. shouldReceiveTouch = YES;
  698. }
  699. return shouldReceiveTouch;
  700. }
  701. #pragma mark - FLEXHierarchyDelegate
  702. - (void)viewHierarchyDidDismiss:(UIView *)selectedView {
  703. // Note that we need to wait until the view controller is dismissed to calculate the frame
  704. // of the outline view, otherwise the coordinate conversion doesn't give the correct result.
  705. [self toggleViewsToolWithCompletion:^{
  706. // If the selected view is outside of the tap point array (selected from "Full Hierarchy"),
  707. // then clear out the tap point array and remove all the outline views.
  708. if (![self.viewsAtTapPoint containsObject:selectedView]) {
  709. self.viewsAtTapPoint = nil;
  710. [self removeAndClearOutlineViews];
  711. }
  712. // If we now have a selected view and we didn't have one previously, go to "select" mode.
  713. if (self.currentMode == FLEXExplorerModeDefault && selectedView) {
  714. self.currentMode = FLEXExplorerModeSelect;
  715. }
  716. // The selected view setter will also update the selected view overlay appropriately.
  717. self.selectedView = selectedView;
  718. }];
  719. }
  720. #pragma mark - Modal Presentation and Window Management
  721. - (void)presentViewController:(UIViewController *)toPresent
  722. animated:(BOOL)animated
  723. completion:(void (^)(void))completion {
  724. // Make our window key to correctly handle input.
  725. [self.view.window makeKeyWindow];
  726. // Move the status bar on top of FLEX so we can get scroll to top behavior for taps.
  727. if (!@available(iOS 13, *)) {
  728. [self statusWindow].windowLevel = self.view.window.windowLevel + 1.0;
  729. }
  730. // Back up and replace the UIMenuController items
  731. // Edit: no longer replacing the items, but still backing them
  732. // up in case we start replacing them again in the future
  733. self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
  734. // Show the view controller
  735. [super presentViewController:toPresent animated:animated completion:completion];
  736. }
  737. - (void)dismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion {
  738. UIWindow *appWindow = self.window.previousKeyWindow;
  739. [appWindow makeKeyWindow];
  740. [appWindow.rootViewController setNeedsStatusBarAppearanceUpdate];
  741. // Restore previous UIMenuController items
  742. // Back up and replace the UIMenuController items
  743. UIMenuController.sharedMenuController.menuItems = self.appMenuItems;
  744. [UIMenuController.sharedMenuController update];
  745. self.appMenuItems = nil;
  746. // Restore the status bar window's normal window level.
  747. // We want it above FLEX while a modal is presented for
  748. // scroll to top, but below FLEX otherwise for exploration.
  749. [self statusWindow].windowLevel = UIWindowLevelStatusBar;
  750. [self updateButtonStates];
  751. [super dismissViewControllerAnimated:animated completion:completion];
  752. }
  753. - (BOOL)wantsWindowToBecomeKey
  754. {
  755. return self.window.previousKeyWindow != nil;
  756. }
  757. - (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
  758. completion:(void(^)(void))completion {
  759. if (self.presentedViewController) {
  760. [self dismissViewControllerAnimated:YES completion:completion];
  761. } else if (future) {
  762. [self presentViewController:future() animated:YES completion:completion];
  763. }
  764. }
  765. - (FLEXWindow *)window {
  766. return (id)self.view.window;
  767. }
  768. #pragma mark - Keyboard Shortcut Helpers
  769. - (void)toggleSelectTool {
  770. if (self.currentMode == FLEXExplorerModeSelect) {
  771. self.currentMode = FLEXExplorerModeDefault;
  772. } else {
  773. self.currentMode = FLEXExplorerModeSelect;
  774. }
  775. }
  776. - (void)toggleMoveTool {
  777. if (self.currentMode == FLEXExplorerModeMove) {
  778. self.currentMode = FLEXExplorerModeSelect;
  779. } else if (self.currentMode == FLEXExplorerModeSelect && self.selectedView) {
  780. self.currentMode = FLEXExplorerModeMove;
  781. }
  782. }
  783. - (void)toggleViewsTool {
  784. [self toggleViewsToolWithCompletion:nil];
  785. }
  786. - (void)toggleViewsToolWithCompletion:(void(^)(void))completion {
  787. [self toggleToolWithViewControllerProvider:^UINavigationController *{
  788. if (self.selectedView) {
  789. return [FLEXHierarchyViewController
  790. delegate:self
  791. viewsAtTap:self.viewsAtTapPoint
  792. selectedView:self.selectedView
  793. ];
  794. } else {
  795. return [FLEXHierarchyViewController delegate:self];
  796. }
  797. } completion:^{
  798. if (completion) {
  799. completion();
  800. }
  801. }];
  802. }
  803. - (void)toggleMenuTool {
  804. [self toggleToolWithViewControllerProvider:^UINavigationController *{
  805. return [FLEXNavigationController withRootViewController:[FLEXGlobalsViewController new]];
  806. } completion:nil];
  807. }
  808. - (BOOL)handleDownArrowKeyPressed {
  809. if (self.currentMode == FLEXExplorerModeMove) {
  810. CGRect frame = self.selectedView.frame;
  811. frame.origin.y += 1.0 / UIScreen.mainScreen.scale;
  812. self.selectedView.frame = frame;
  813. } else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
  814. NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
  815. if (selectedViewIndex > 0) {
  816. self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex - 1];
  817. }
  818. } else {
  819. return NO;
  820. }
  821. return YES;
  822. }
  823. - (BOOL)handleUpArrowKeyPressed {
  824. if (self.currentMode == FLEXExplorerModeMove) {
  825. CGRect frame = self.selectedView.frame;
  826. frame.origin.y -= 1.0 / UIScreen.mainScreen.scale;
  827. self.selectedView.frame = frame;
  828. } else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
  829. NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
  830. if (selectedViewIndex < self.viewsAtTapPoint.count - 1) {
  831. self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex + 1];
  832. }
  833. } else {
  834. return NO;
  835. }
  836. return YES;
  837. }
  838. - (BOOL)handleRightArrowKeyPressed {
  839. if (self.currentMode == FLEXExplorerModeMove) {
  840. CGRect frame = self.selectedView.frame;
  841. frame.origin.x += 1.0 / UIScreen.mainScreen.scale;
  842. self.selectedView.frame = frame;
  843. return YES;
  844. }
  845. return NO;
  846. }
  847. - (BOOL)handleLeftArrowKeyPressed {
  848. if (self.currentMode == FLEXExplorerModeMove) {
  849. CGRect frame = self.selectedView.frame;
  850. frame.origin.x -= 1.0 / UIScreen.mainScreen.scale;
  851. self.selectedView.frame = frame;
  852. return YES;
  853. }
  854. return NO;
  855. }
  856. @end