FHSSnapshotView.m 8.2 KB

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