FHSSnapshotView.m 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. //
  2. // FHSSnapshotView.m
  3. // FLEX
  4. //
  5. // Created by Tanner Bennett on 1/7/20.
  6. // Copyright © 2020 FLEX Team. All rights reserved.
  7. //
  8. #import "FHSSnapshotView.h"
  9. #import "FHSSnapshotNodes.h"
  10. #import "SceneKit+Snapshot.h"
  11. #import "FLEXColor.h"
  12. @interface FHSSnapshotView ()
  13. @property (nonatomic, readonly) SCNView *sceneView;
  14. @property (nonatomic) NSString *currentSummary;
  15. /// Maps nodes by snapshot IDs
  16. @property (nonatomic) NSDictionary<NSString *, FHSSnapshotNodes *> *nodesMap;
  17. @property (nonatomic) NSInteger maxDepth;
  18. @property (nonatomic) FHSSnapshotNodes *highlightedNodes;
  19. @property (nonatomic, getter=wantsHideHeaders) BOOL hideHeaders;
  20. @property (nonatomic, getter=wantsHideBorders) BOOL hideBorders;
  21. @property (nonatomic) BOOL suppressSelectionEvents;
  22. @property (nonatomic, readonly) BOOL mustHideHeaders;
  23. @end
  24. @implementation FHSSnapshotView
  25. #pragma mark - Initialization
  26. + (instancetype)delegate:(id<FHSSnapshotViewDelegate>)delegate {
  27. FHSSnapshotView *view = [self new];
  28. view.delegate = delegate;
  29. return view;
  30. }
  31. - (id)initWithFrame:(CGRect)frame {
  32. self = [super initWithFrame:CGRectZero];
  33. if (self) {
  34. [self initSpacingSlider];
  35. [self initDepthSlider];
  36. [self initSceneView]; // Must be last; calls setMaxDepth
  37. // self.hideHeaders = YES;
  38. // Self
  39. self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  40. // Scene
  41. self.sceneView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  42. [self addGestureRecognizer:[[UITapGestureRecognizer alloc]
  43. initWithTarget:self action:@selector(handleTap:)
  44. ]];
  45. }
  46. return self;
  47. }
  48. - (void)initSceneView {
  49. _sceneView = [SCNView new];
  50. self.sceneView.allowsCameraControl = YES;
  51. [self addSubview:self.sceneView];
  52. }
  53. - (void)initSpacingSlider {
  54. #if !TARGET_OS_TV
  55. _spacingSlider = [UISlider new];
  56. #else
  57. _spacingSlider = [UIFakeSlider new];
  58. #endif
  59. self.spacingSlider.minimumValue = 0;
  60. self.spacingSlider.maximumValue = 100;
  61. self.spacingSlider.continuous = YES;
  62. [self.spacingSlider
  63. addTarget:self
  64. action:@selector(spacingSliderDidChange:)
  65. forControlEvents:UIControlEventValueChanged
  66. ];
  67. self.spacingSlider.value = 50;
  68. }
  69. - (void)initDepthSlider {
  70. #if !TARGET_OS_TV
  71. _depthSlider = [FHSRangeSlider new];
  72. #else
  73. _depthSlider = [UIFakeSlider new];
  74. #endif
  75. [self.depthSlider
  76. addTarget:self
  77. action:@selector(depthSliderDidChange:)
  78. forControlEvents:UIControlEventValueChanged
  79. ];
  80. }
  81. #pragma mark - Public
  82. - (void)setSelectedView:(FHSViewSnapshot *)view {
  83. // Ivar set in selectSnapshot:
  84. [self selectSnapshot:view ? self.nodesMap[view.view.identifier] : nil];
  85. }
  86. - (void)setSnapshots:(NSArray<FHSViewSnapshot *> *)snapshots {
  87. _snapshots = snapshots;
  88. // Create new scene (possibly discarding old scene)
  89. SCNScene *scene = [SCNScene new];
  90. scene.background.contents = FLEXColor.primaryBackgroundColor;
  91. self.sceneView.scene = scene;
  92. NSInteger depth = 0;
  93. NSMutableDictionary *nodesMap = [NSMutableDictionary new];
  94. // Add every root snapshot to the root scene node with increasing depths
  95. SCNNode *root = scene.rootNode;
  96. for (FHSViewSnapshot *snapshot in self.snapshots) {
  97. [SCNNode
  98. snapshot:snapshot
  99. parent:nil
  100. parentNode:nil
  101. root:root
  102. depth:&depth
  103. nodesMap:nodesMap
  104. hideHeaders:_hideHeaders
  105. ];
  106. }
  107. self.maxDepth = depth;
  108. self.nodesMap = nodesMap;
  109. }
  110. - (void)setHeaderExclusions:(NSArray<Class> *)headerExclusions {
  111. _headerExclusions = headerExclusions;
  112. if (headerExclusions.count) {
  113. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  114. if ([headerExclusions containsObject:nodes.snapshotItem.view.view.class]) {
  115. nodes.forceHideHeader = YES;
  116. } else {
  117. nodes.forceHideHeader = NO;
  118. }
  119. }
  120. }
  121. }
  122. - (void)emphasizeViews:(NSArray<UIView *> *)emphasizedViews {
  123. if (emphasizedViews.count) {
  124. [self emphasizeViews:emphasizedViews inSnapshots:self.snapshots];
  125. [self setNeedsLayout];
  126. }
  127. }
  128. - (void)emphasizeViews:(NSArray<UIView *> *)emphasizedViews inSnapshots:(NSArray<FHSViewSnapshot *> *)snapshots {
  129. for (FHSViewSnapshot *snapshot in snapshots) {
  130. FHSSnapshotNodes *nodes = self.nodesMap[snapshot.view.identifier];
  131. nodes.dimmed = ![emphasizedViews containsObject:snapshot.view.view];
  132. [self emphasizeViews:emphasizedViews inSnapshots:snapshot.children];
  133. }
  134. }
  135. - (void)toggleShowHeaders {
  136. self.hideHeaders = !self.hideHeaders;
  137. }
  138. - (void)toggleShowBorders {
  139. self.hideBorders = !self.hideBorders;
  140. }
  141. - (void)hideView:(FHSViewSnapshot *)view {
  142. NSParameterAssert(view);
  143. FHSSnapshotNodes *nodes = self.nodesMap[view.view.identifier];
  144. [nodes.snapshot removeFromParentNode];
  145. }
  146. #pragma mark - Helper
  147. - (BOOL)mustHideHeaders {
  148. return self.spacingSlider.value <= kFHSSmallZOffset;
  149. }
  150. - (void)setMaxDepth:(NSInteger)maxDepth {
  151. _maxDepth = maxDepth;
  152. self.depthSlider.allowedMinValue = 0;
  153. self.depthSlider.allowedMaxValue = maxDepth;
  154. self.depthSlider.maxValue = maxDepth;
  155. self.depthSlider.minValue = 0;
  156. }
  157. - (void)setHideHeaders:(BOOL)hideHeaders {
  158. if (_hideHeaders != hideHeaders) {
  159. _hideHeaders = hideHeaders;
  160. if (!self.mustHideHeaders) {
  161. if (hideHeaders) {
  162. [self hideHeaders];
  163. } else {
  164. [self unhideHeaders];
  165. }
  166. }
  167. }
  168. }
  169. - (void)setHideBorders:(BOOL)hideBorders {
  170. if (_hideBorders != hideBorders) {
  171. _hideBorders = hideBorders;
  172. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  173. nodes.border.hidden = hideBorders;
  174. }
  175. }
  176. }
  177. - (FHSSnapshotNodes *)nodesAtPoint:(CGPoint)point {
  178. NSArray<SCNHitTestResult *> *results = [self.sceneView hitTest:point options:nil];
  179. for (SCNHitTestResult *result in results) {
  180. SCNNode *nearestSnapshot = result.node.nearestAncestorSnapshot;
  181. if (nearestSnapshot) {
  182. return self.nodesMap[nearestSnapshot.name];
  183. }
  184. }
  185. return nil;
  186. }
  187. - (void)selectSnapshot:(FHSSnapshotNodes *)selected {
  188. // Notify delegate of de-select
  189. if (!selected && self.selectedView) {
  190. [self.delegate didDeselectView:self.selectedView];
  191. }
  192. _selectedView = selected.snapshotItem;
  193. // Case: selected the currently selected node
  194. if (selected == self.highlightedNodes) {
  195. return;
  196. }
  197. // No-op if nothng is selected (yay objc!)
  198. self.highlightedNodes.highlighted = NO;
  199. self.highlightedNodes = nil;
  200. // No node means we tapped the background
  201. if (selected) {
  202. selected.highlighted = YES;
  203. // TODO: update description text here
  204. self.highlightedNodes = selected;
  205. }
  206. // Notify delegate
  207. [self.delegate didSelectView:selected.snapshotItem];
  208. [self setNeedsLayout];
  209. }
  210. - (void)hideHeaders {
  211. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  212. nodes.header.hidden = YES;
  213. }
  214. }
  215. - (void)unhideHeaders {
  216. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  217. if (!nodes.forceHideHeader) {
  218. nodes.header.hidden = NO;
  219. }
  220. }
  221. }
  222. #pragma mark - Event Handlers
  223. - (void)handleTap:(UITapGestureRecognizer *)gesture {
  224. if (gesture.state == UIGestureRecognizerStateRecognized) {
  225. CGPoint tap = [gesture locationInView:self.sceneView];
  226. [self selectSnapshot:[self nodesAtPoint:tap]];
  227. }
  228. }
  229. - (void)spacingSliderDidChange:(UIFakeSlider *)slider {
  230. // TODO: hiding the header when flat logic
  231. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  232. nodes.snapshot.position = ({
  233. SCNVector3 pos = nodes.snapshot.position;
  234. pos.z = MAX(slider.value, kFHSSmallZOffset) * nodes.depth;
  235. pos;
  236. });
  237. if (!self.wantsHideHeaders) {
  238. if (self.mustHideHeaders) {
  239. [self hideHeaders];
  240. } else {
  241. [self unhideHeaders];
  242. }
  243. }
  244. }
  245. }
  246. - (void)depthSliderDidChange:(UIFakeSlider *)slider {
  247. CGFloat min = slider.minValue, max = slider.maxValue;
  248. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  249. CGFloat depth = nodes.depth;
  250. nodes.snapshot.hidden = depth < min || max < depth;
  251. }
  252. }
  253. @end