FHSViewController.m 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. //
  2. // FHSViewController.m
  3. // FLEX
  4. //
  5. // Created by Tanner Bennett on 1/6/20.
  6. // Copyright © 2020 FLEX Team. All rights reserved.
  7. //
  8. #import "FHSViewController.h"
  9. #import "FHSSnapshotView.h"
  10. #import "FLEXHierarchyViewController.h"
  11. #import "FLEXColor.h"
  12. #import "FLEXAlert.h"
  13. #import "FLEXWindow.h"
  14. #import "FLEXResources.h"
  15. #import "NSArray+FLEX.h"
  16. #import "UIBarButtonItem+FLEX.h"
  17. BOOL const kFHSViewControllerExcludeFLEXWindows = YES;
  18. @interface FHSViewController () <FHSSnapshotViewDelegate>
  19. /// An array of only the target views whose hierarchies
  20. /// we wish to snapshot, not every view in the snapshot.
  21. @property (nonatomic, readonly) NSArray<UIView *> *targetViews;
  22. @property (nonatomic, readonly) NSArray<FHSView *> *views;
  23. @property (nonatomic ) NSArray<FHSViewSnapshot *> *snapshots;
  24. @property (nonatomic, ) FHSSnapshotView *snapshotView;
  25. @property (nonatomic, readonly) UIView *containerView;
  26. @property (nonatomic, readonly) NSArray<UIView *> *viewsAtTap;
  27. @property (nonatomic, readonly) NSMutableSet<Class> *forceHideHeaders;
  28. @end
  29. @implementation FHSViewController
  30. @synthesize views = _views;
  31. @synthesize snapshotView = _snapshotView;
  32. #pragma mark - Initialization
  33. + (instancetype)snapshotWindows:(NSArray<UIWindow *> *)windows {
  34. return [[self alloc] initWithViews:windows viewsAtTap:nil selectedView:nil];
  35. }
  36. + (instancetype)snapshotView:(UIView *)view {
  37. return [[self alloc] initWithViews:@[view] viewsAtTap:nil selectedView:nil];
  38. }
  39. + (instancetype)snapshotViewsAtTap:(NSArray<UIView *> *)viewsAtTap selectedView:(UIView *)view {
  40. NSParameterAssert(viewsAtTap.count);
  41. NSParameterAssert(view.window);
  42. return [[self alloc] initWithViews:@[view.window] viewsAtTap:viewsAtTap selectedView:view];
  43. }
  44. - (id)initWithViews:(NSArray<UIView *> *)views
  45. viewsAtTap:(NSArray<UIView *> *)viewsAtTap
  46. selectedView:(UIView *)view {
  47. NSParameterAssert(views.count);
  48. self = [super init];
  49. if (self) {
  50. _forceHideHeaders = [NSMutableSet setWithObject:NSClassFromString(@"_UITableViewCellSeparatorView")];
  51. _selectedView = view;
  52. _viewsAtTap = viewsAtTap;
  53. if (!viewsAtTap && kFHSViewControllerExcludeFLEXWindows) {
  54. Class flexwindow = [FLEXWindow class];
  55. views = [views flex_filtered:^BOOL(UIView *view, NSUInteger idx) {
  56. return [view class] != flexwindow;
  57. }];
  58. }
  59. _targetViews = views;
  60. _views = [views flex_mapped:^id(UIView *view, NSUInteger idx) {
  61. BOOL isScrollView = [view.superview isKindOfClass:[UIScrollView class]];
  62. return [FHSView forView:view isInScrollView:isScrollView];
  63. }];
  64. }
  65. return self;
  66. }
  67. - (void)refreshSnapshotView {
  68. // Alert view to block interaction while we load everything
  69. UIAlertController *loading = [FLEXAlert makeAlert:^(FLEXAlert *make) {
  70. make.title(@"Please Wait").message(@"Generating snapshot…");
  71. }];
  72. [self presentViewController:loading animated:YES completion:^{
  73. self.snapshots = [self.views flex_mapped:^id(FHSView *view, NSUInteger idx) {
  74. return [FHSViewSnapshot snapshotWithView:view];
  75. }];
  76. FHSSnapshotView *newSnapshotView = [FHSSnapshotView delegate:self];
  77. // This work is highly intensive so we do it on a background thread first
  78. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
  79. // Setting the snapshots computes lots of SCNNodes, takes several seconds
  80. newSnapshotView.snapshots = self.snapshots;
  81. // After we finish generating all the model objects and scene nodes, display the view
  82. dispatch_async(dispatch_get_main_queue(), ^{
  83. // Dismiss alert
  84. [loading dismissViewControllerAnimated:YES completion:nil];
  85. self.snapshotView = newSnapshotView;
  86. });
  87. });
  88. }];
  89. }
  90. #pragma mark - View Controller Lifecycle
  91. - (void)loadView {
  92. [super loadView];
  93. self.view.backgroundColor = FLEXColor.primaryBackgroundColor;
  94. }
  95. - (void)viewDidLoad {
  96. [super viewDidLoad];
  97. // Initialize back bar button item for 3D view to look like a button
  98. #if !TARGET_OS_TV
  99. self.navigationItem.hidesBackButton = YES;
  100. #endif
  101. self.navigationItem.leftBarButtonItem = [UIBarButtonItem
  102. flex_itemWithImage:FLEXResources.toggle2DIcon
  103. target:self.navigationController
  104. action:@selector(toggleHierarchyMode)
  105. ];
  106. }
  107. - (void)viewDidAppear:(BOOL)animated {
  108. [super viewDidAppear:animated];
  109. if (!_snapshotView) {
  110. [self refreshSnapshotView];
  111. }
  112. }
  113. #pragma mark - Public
  114. - (void)setSelectedView:(UIView *)view {
  115. _selectedView = view;
  116. self.snapshotView.selectedView = view ? [self snapshotForView:view] : nil;
  117. }
  118. #pragma mark - Private
  119. #pragma mark Properties
  120. - (FHSSnapshotView *)snapshotView {
  121. return self.isViewLoaded ? _snapshotView : nil;
  122. }
  123. - (void)setSnapshotView:(FHSSnapshotView *)snapshotView {
  124. NSParameterAssert(snapshotView);
  125. _snapshotView = snapshotView;
  126. #if !TARGET_OS_TV
  127. // Initialize our toolbar items
  128. self.toolbarItems = @[
  129. [UIBarButtonItem flex_itemWithCustomView:snapshotView.spacingSlider],
  130. UIBarButtonItem.flex_flexibleSpace,
  131. [UIBarButtonItem
  132. flex_itemWithImage:FLEXResources.moreIcon
  133. target:self action:@selector(didPressOptionsButton:)
  134. ],
  135. UIBarButtonItem.flex_flexibleSpace,
  136. [UIBarButtonItem flex_itemWithCustomView:snapshotView.depthSlider]
  137. ];
  138. [self resizeToolbarItems:self.view.frame.size];
  139. #endif
  140. // If we have views-at-tap, dim the other views
  141. [snapshotView emphasizeViews:self.viewsAtTap];
  142. // Set the selected view, if any
  143. snapshotView.selectedView = [self snapshotForView:self.selectedView];
  144. snapshotView.headerExclusions = self.forceHideHeaders.allObjects;
  145. [snapshotView setNeedsLayout];
  146. // Remove old snapshot, if any, and add the new one
  147. [_snapshotView removeFromSuperview];
  148. snapshotView.frame = self.containerView.bounds;
  149. [self.containerView addSubview:snapshotView];
  150. }
  151. - (UIView *)containerView {
  152. return self.view;
  153. }
  154. #pragma mark Helper
  155. - (FHSViewSnapshot *)snapshotForView:(UIView *)view {
  156. if (!view || !self.snapshots.count) return nil;
  157. for (FHSViewSnapshot *snapshot in self.snapshots) {
  158. FHSViewSnapshot *found = [snapshot snapshotForView:view];
  159. if (found) {
  160. return found;
  161. }
  162. }
  163. // Error: we have snapshots but the view we requested is not in one
  164. @throw NSInternalInconsistencyException;
  165. return nil;
  166. }
  167. #pragma mark Events
  168. - (void)didPressOptionsButton:(UIBarButtonItem *)sender {
  169. [FLEXAlert makeSheet:^(FLEXAlert *make) {
  170. if (self.selectedView) {
  171. make.button(@"Hide selected view").handler(^(NSArray<NSString *> *strings) {
  172. [self.snapshotView hideView:[self snapshotForView:self.selectedView]];
  173. });
  174. make.button(@"Hide headers for views like this").handler(^(NSArray<NSString *> *strings) {
  175. Class cls = [self.selectedView class];
  176. if (![self.forceHideHeaders containsObject:cls]) {
  177. [self.forceHideHeaders addObject:[self.selectedView class]];
  178. self.snapshotView.headerExclusions = self.forceHideHeaders.allObjects;
  179. }
  180. });
  181. }
  182. make.title(@"Options");
  183. make.button(@"Toggle headers").handler(^(NSArray<NSString *> *strings) {
  184. [self.snapshotView toggleShowHeaders];
  185. });
  186. make.button(@"Toggle outlines").handler(^(NSArray<NSString *> *strings) {
  187. [self.snapshotView toggleShowBorders];
  188. });
  189. make.button(@"Cancel").cancelStyle();
  190. } showFrom:self source:sender];
  191. }
  192. - (void)resizeToolbarItems:(CGSize)viewSize {
  193. #if !TARGET_OS_TV
  194. CGFloat sliderHeights = self.snapshotView.spacingSlider.bounds.size.height;
  195. CGFloat sliderWidths = viewSize.width / 3.f;
  196. CGRect frame = CGRectMake(0, 0, sliderWidths, sliderHeights);
  197. self.snapshotView.spacingSlider.frame = frame;
  198. self.snapshotView.depthSlider.frame = frame;
  199. [self.navigationController.toolbar setNeedsLayout];
  200. #endif
  201. }
  202. - (void)viewWillTransitionToSize:(CGSize)size
  203. withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
  204. [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
  205. [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
  206. [self resizeToolbarItems:self.view.frame.size];
  207. } completion:nil];
  208. }
  209. #pragma mark FHSSnapshotViewDelegate
  210. - (void)didDeselectView:(FHSViewSnapshot *)snapshot {
  211. // Our setter would also call the setter for the snapshot view,
  212. // which we don't need to do here since it is already selected
  213. _selectedView = nil;
  214. }
  215. - (void)didLongPressView:(FHSViewSnapshot *)snapshot {
  216. }
  217. - (void)didSelectView:(FHSViewSnapshot *)snapshot {
  218. // Our setter would also call the setter for the snapshot view,
  219. // which we don't need to do here since it is already selected
  220. _selectedView = snapshot.view.view;
  221. }
  222. @end