FHSSnapshotView.m 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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 = [KBSlider 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. _depthSlider = [FHSRangeSlider new];
  71. [self.depthSlider
  72. addTarget:self
  73. action:@selector(depthSliderDidChange:)
  74. forControlEvents:UIControlEventValueChanged
  75. ];
  76. }
  77. #pragma mark - Public
  78. - (void)setSelectedView:(FHSViewSnapshot *)view {
  79. // Ivar set in selectSnapshot:
  80. [self selectSnapshot:view ? self.nodesMap[view.view.identifier] : nil];
  81. }
  82. - (void)setSnapshots:(NSArray<FHSViewSnapshot *> *)snapshots {
  83. _snapshots = snapshots;
  84. // Create new scene (possibly discarding old scene)
  85. SCNScene *scene = [SCNScene new];
  86. scene.background.contents = FLEXColor.primaryBackgroundColor;
  87. self.sceneView.scene = scene;
  88. NSInteger depth = 0;
  89. NSMutableDictionary *nodesMap = [NSMutableDictionary new];
  90. // Add every root snapshot to the root scene node with increasing depths
  91. SCNNode *root = scene.rootNode;
  92. for (FHSViewSnapshot *snapshot in self.snapshots) {
  93. [SCNNode
  94. snapshot:snapshot
  95. parent:nil
  96. parentNode:nil
  97. root:root
  98. depth:&depth
  99. nodesMap:nodesMap
  100. hideHeaders:_hideHeaders
  101. ];
  102. }
  103. self.maxDepth = depth;
  104. self.nodesMap = nodesMap;
  105. }
  106. - (void)setHeaderExclusions:(NSArray<Class> *)headerExclusions {
  107. _headerExclusions = headerExclusions;
  108. if (headerExclusions.count) {
  109. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  110. if ([headerExclusions containsObject:nodes.snapshotItem.view.view.class]) {
  111. nodes.forceHideHeader = YES;
  112. } else {
  113. nodes.forceHideHeader = NO;
  114. }
  115. }
  116. }
  117. }
  118. - (void)emphasizeViews:(NSArray<UIView *> *)emphasizedViews {
  119. if (emphasizedViews.count) {
  120. [self emphasizeViews:emphasizedViews inSnapshots:self.snapshots];
  121. [self setNeedsLayout];
  122. }
  123. }
  124. - (void)emphasizeViews:(NSArray<UIView *> *)emphasizedViews inSnapshots:(NSArray<FHSViewSnapshot *> *)snapshots {
  125. for (FHSViewSnapshot *snapshot in snapshots) {
  126. FHSSnapshotNodes *nodes = self.nodesMap[snapshot.view.identifier];
  127. nodes.dimmed = ![emphasizedViews containsObject:snapshot.view.view];
  128. [self emphasizeViews:emphasizedViews inSnapshots:snapshot.children];
  129. }
  130. }
  131. - (void)toggleShowHeaders {
  132. self.hideHeaders = !self.hideHeaders;
  133. }
  134. - (void)toggleShowBorders {
  135. self.hideBorders = !self.hideBorders;
  136. }
  137. - (void)hideView:(FHSViewSnapshot *)view {
  138. NSParameterAssert(view);
  139. FHSSnapshotNodes *nodes = self.nodesMap[view.view.identifier];
  140. [nodes.snapshot removeFromParentNode];
  141. }
  142. #pragma mark - Helper
  143. - (BOOL)mustHideHeaders {
  144. return self.spacingSlider.value <= kFHSSmallZOffset;
  145. }
  146. - (void)setMaxDepth:(NSInteger)maxDepth {
  147. _maxDepth = maxDepth;
  148. self.depthSlider.allowedMinValue = 0;
  149. self.depthSlider.allowedMaxValue = maxDepth;
  150. self.depthSlider.maxValue = maxDepth;
  151. self.depthSlider.minValue = 0;
  152. }
  153. - (void)setHideHeaders:(BOOL)hideHeaders {
  154. if (_hideHeaders != hideHeaders) {
  155. _hideHeaders = hideHeaders;
  156. if (!self.mustHideHeaders) {
  157. if (hideHeaders) {
  158. [self hideHeaders];
  159. } else {
  160. [self unhideHeaders];
  161. }
  162. }
  163. }
  164. }
  165. - (void)setHideBorders:(BOOL)hideBorders {
  166. if (_hideBorders != hideBorders) {
  167. _hideBorders = hideBorders;
  168. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  169. nodes.border.hidden = hideBorders;
  170. }
  171. }
  172. }
  173. - (FHSSnapshotNodes *)nodesAtPoint:(CGPoint)point {
  174. NSArray<SCNHitTestResult *> *results = [self.sceneView hitTest:point options:nil];
  175. for (SCNHitTestResult *result in results) {
  176. SCNNode *nearestSnapshot = result.node.nearestAncestorSnapshot;
  177. if (nearestSnapshot) {
  178. return self.nodesMap[nearestSnapshot.name];
  179. }
  180. }
  181. return nil;
  182. }
  183. - (void)selectSnapshot:(FHSSnapshotNodes *)selected {
  184. // Notify delegate of de-select
  185. if (!selected && self.selectedView) {
  186. [self.delegate didDeselectView:self.selectedView];
  187. }
  188. _selectedView = selected.snapshotItem;
  189. // Case: selected the currently selected node
  190. if (selected == self.highlightedNodes) {
  191. return;
  192. }
  193. // No-op if nothng is selected (yay objc!)
  194. self.highlightedNodes.highlighted = NO;
  195. self.highlightedNodes = nil;
  196. // No node means we tapped the background
  197. if (selected) {
  198. selected.highlighted = YES;
  199. // TODO: update description text here
  200. self.highlightedNodes = selected;
  201. }
  202. // Notify delegate
  203. [self.delegate didSelectView:selected.snapshotItem];
  204. [self setNeedsLayout];
  205. }
  206. - (void)hideHeaders {
  207. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  208. nodes.header.hidden = YES;
  209. }
  210. }
  211. - (void)unhideHeaders {
  212. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  213. if (!nodes.forceHideHeader) {
  214. nodes.header.hidden = NO;
  215. }
  216. }
  217. }
  218. #pragma mark - Event Handlers
  219. - (void)handleTap:(UITapGestureRecognizer *)gesture {
  220. if (gesture.state == UIGestureRecognizerStateRecognized) {
  221. CGPoint tap = [gesture locationInView:self.sceneView];
  222. [self selectSnapshot:[self nodesAtPoint:tap]];
  223. }
  224. }
  225. - (void)spacingSliderDidChange:(KBSlider *)slider { //easier to make a KBSlider since they are API compatible - one less #if macro!
  226. // TODO: hiding the header when flat logic
  227. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  228. nodes.snapshot.position = ({
  229. SCNVector3 pos = nodes.snapshot.position;
  230. pos.z = MAX(slider.value, kFHSSmallZOffset) * nodes.depth;
  231. pos;
  232. });
  233. if (!self.wantsHideHeaders) {
  234. if (self.mustHideHeaders) {
  235. [self hideHeaders];
  236. } else {
  237. [self unhideHeaders];
  238. }
  239. }
  240. }
  241. }
  242. - (void)depthSliderDidChange:(FHSRangeSlider *)slider {
  243. CGFloat min = slider.minValue, max = slider.maxValue;
  244. for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) {
  245. CGFloat depth = nodes.depth;
  246. nodes.snapshot.hidden = depth < min || max < depth;
  247. }
  248. }
  249. @end