mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-31 04:44:14 -04:00 
			
		
		
		
	added files via upload
This commit is contained in:
		
							
								
								
									
										22
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSRangeSlider.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSRangeSlider.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| // | ||||
| //  FHSRangeSlider.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/7/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FHSRangeSlider : UIControl | ||||
|  | ||||
| @property (nonatomic) CGFloat allowedMinValue; | ||||
| @property (nonatomic) CGFloat allowedMaxValue; | ||||
| @property (nonatomic) CGFloat minValue; | ||||
| @property (nonatomic) CGFloat maxValue; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										201
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSRangeSlider.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSRangeSlider.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,201 @@ | ||||
| // | ||||
| //  FHSRangeSlider.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/7/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FHSRangeSlider.h" | ||||
| #import "FLEXResources.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| @interface FHSRangeSlider () | ||||
| @property (nonatomic, readonly) UIImageView *track; | ||||
| @property (nonatomic, readonly) UIImageView *fill; | ||||
| @property (nonatomic, readonly) UIImageView *leftHandle; | ||||
| @property (nonatomic, readonly) UIImageView *rightHandle; | ||||
|  | ||||
| @property (nonatomic, getter=isTrackingLeftHandle) BOOL trackingLeftHandle; | ||||
| @property (nonatomic, getter=isTrackingRightHandle) BOOL trackingRightHandle; | ||||
| @end | ||||
|  | ||||
| @implementation FHSRangeSlider | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| - (instancetype)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:frame]; | ||||
|     if (self) { | ||||
|         _allowedMaxValue = 1.f; | ||||
|         _maxValue = 1.f; | ||||
|         [self initSubviews]; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)initSubviews { | ||||
|     self.userInteractionEnabled = YES; | ||||
|     UIImageView * (^newSubviewImageView)(UIImage *) = ^UIImageView *(UIImage *image) { | ||||
|         UIImageView *iv = [UIImageView new]; | ||||
|         iv.image = image; | ||||
| //        iv.userInteractionEnabled = YES; | ||||
|         [self addSubview:iv]; | ||||
|         return iv; | ||||
|     }; | ||||
|  | ||||
|     _track = newSubviewImageView(FLEXResources.rangeSliderTrack); | ||||
|     _fill = newSubviewImageView(FLEXResources.rangeSliderFill); | ||||
|     _leftHandle = newSubviewImageView(FLEXResources.rangeSliderLeftHandle); | ||||
|     _rightHandle = newSubviewImageView(FLEXResources.rangeSliderRightHandle); | ||||
| } | ||||
|  | ||||
| #pragma mark - Setters / Private | ||||
|  | ||||
| - (CGFloat)valueAt:(CGFloat)x { | ||||
|     CGFloat minX = self.leftHandle.image.size.width; | ||||
|     CGFloat maxX = self.bounds.size.width - self.rightHandle.image.size.width; | ||||
|     CGFloat cappedX = MIN(MAX(x, minX), maxX); | ||||
|     CGFloat delta = maxX - minX; | ||||
|     CGFloat maxDelta = self.allowedMaxValue - self.allowedMinValue; | ||||
|  | ||||
|     return ((delta > 0) ? (cappedX - minX) / delta : 0) * maxDelta + self.allowedMinValue; | ||||
| } | ||||
|  | ||||
| - (void)setAllowedMinValue:(CGFloat)allowedMinValue { | ||||
|     _allowedMinValue = allowedMinValue; | ||||
|  | ||||
|     if (self.minValue < self.allowedMaxValue) { | ||||
|         self.minValue = self.allowedMaxValue; | ||||
|     } else { | ||||
|         [self setNeedsLayout]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setAllowedMaxValue:(CGFloat)allowedMaxValue { | ||||
|     _allowedMaxValue = allowedMaxValue; | ||||
|  | ||||
|     if (self.maxValue > self.allowedMaxValue) { | ||||
|         self.maxValue = self.allowedMaxValue; | ||||
|     } else { | ||||
|         [self valuesChanged:NO]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setMinValue:(CGFloat)minValue { | ||||
|     _minValue = minValue; | ||||
|     [self valuesChanged:YES]; | ||||
| } | ||||
|  | ||||
| - (void)setMaxValue:(CGFloat)maxValue { | ||||
|     _maxValue = maxValue; | ||||
|     [self valuesChanged:YES]; | ||||
| } | ||||
|  | ||||
| - (void)valuesChanged:(BOOL)sendActions { | ||||
|     if (NSThread.isMainThread) { | ||||
|         if (sendActions) { | ||||
|             [self sendActionsForControlEvents:UIControlEventValueChanged]; | ||||
|         } | ||||
|         [self setNeedsLayout]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| - (CGSize)intrinsicContentSize { | ||||
|     return CGSizeMake(UIViewNoIntrinsicMetric, self.leftHandle.image.size.height); | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|  | ||||
|     CGSize lhs = self.leftHandle.image.size; | ||||
|     CGSize rhs = self.rightHandle.image.size; | ||||
|     CGSize trackSize = self.track.image.size; | ||||
|  | ||||
|     CGFloat delta = self.allowedMaxValue - self.allowedMinValue; | ||||
|     CGFloat minPercent, maxPercent; | ||||
|  | ||||
|     if (delta <= 0) { | ||||
|         minPercent = maxPercent = 0; | ||||
|     } else { | ||||
|         minPercent = MAX(0, (self.minValue - self.allowedMinValue) / delta); | ||||
|         maxPercent = MAX(minPercent, (self.maxValue - self.allowedMinValue) / delta); | ||||
|     } | ||||
|  | ||||
|     CGFloat rangeSliderWidth = self.bounds.size.width - lhs.width - rhs.width; | ||||
|  | ||||
|     self.leftHandle.frame = FLEXRectMake( | ||||
|         rangeSliderWidth * minPercent, | ||||
|         CGRectGetMidY(self.bounds) - (lhs.height / 2.f) + 3.f, | ||||
|         lhs.width, | ||||
|         lhs.height | ||||
|     ); | ||||
|  | ||||
|     self.rightHandle.frame = FLEXRectMake( | ||||
|         lhs.width + (rangeSliderWidth * maxPercent), | ||||
|         CGRectGetMidY(self.bounds) - (rhs.height / 2.f) + 3.f, | ||||
|         rhs.width, | ||||
|         rhs.height | ||||
|     ); | ||||
|  | ||||
|     self.track.frame = FLEXRectMake( | ||||
|         lhs.width / 2.f, | ||||
|         CGRectGetMidY(self.bounds) - trackSize.height / 2.f, | ||||
|         self.bounds.size.width - (lhs.width / 2.f) - (rhs.width / 2.f), | ||||
|         trackSize.height | ||||
|     ); | ||||
|  | ||||
|     self.fill.frame = FLEXRectMake( | ||||
|         CGRectGetMidX(self.leftHandle.frame), | ||||
|         CGRectGetMinY(self.track.frame), | ||||
|         CGRectGetMidX(self.rightHandle.frame) - CGRectGetMidX(self.leftHandle.frame), | ||||
|         self.track.frame.size.height | ||||
|     ); | ||||
| } | ||||
|  | ||||
| - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event { | ||||
|     CGPoint loc = [touch locationInView:self]; | ||||
|  | ||||
|     if (CGRectContainsPoint(self.leftHandle.frame, loc)) { | ||||
|         self.trackingLeftHandle = YES; | ||||
|         self.trackingRightHandle = NO; | ||||
|     } else if (CGRectContainsPoint(self.rightHandle.frame, loc)) { | ||||
|         self.trackingLeftHandle = NO; | ||||
|         self.trackingRightHandle = YES; | ||||
|     } else { | ||||
|         return NO; | ||||
|     } | ||||
|  | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event { | ||||
|     CGPoint loc = [touch locationInView:self]; | ||||
|  | ||||
|     if (self.isTrackingLeftHandle) { | ||||
|         self.minValue = MIN(MAX(self.allowedMinValue, [self valueAt:loc.x]), self.maxValue); | ||||
|     } else if (self.isTrackingRightHandle) { | ||||
|         self.maxValue = MAX(MIN(self.allowedMaxValue, [self valueAt:loc.x]), self.minValue); | ||||
|     } else { | ||||
|         return NO; | ||||
|     } | ||||
|  | ||||
|     [self setNeedsLayout]; | ||||
|     [self layoutIfNeeded]; | ||||
|  | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event { | ||||
|     self.trackingLeftHandle = NO; | ||||
|     self.trackingRightHandle = NO; | ||||
| } | ||||
|  | ||||
| - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										46
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSSnapshotView.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSSnapshotView.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| // | ||||
| //  FHSSnapshotView.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/7/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FHSViewSnapshot.h" | ||||
| #import "FHSRangeSlider.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @protocol FHSSnapshotViewDelegate <NSObject> | ||||
|  | ||||
| - (void)didSelectView:(FHSViewSnapshot *)snapshot; | ||||
| - (void)didDeselectView:(FHSViewSnapshot *)snapshot; | ||||
| - (void)didLongPressView:(FHSViewSnapshot *)snapshot; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @interface FHSSnapshotView : UIView | ||||
|  | ||||
| + (instancetype)delegate:(id<FHSSnapshotViewDelegate>)delegate; | ||||
|  | ||||
| @property (nonatomic, weak) id<FHSSnapshotViewDelegate> delegate; | ||||
|  | ||||
| @property (nonatomic) NSArray<FHSViewSnapshot *> *snapshots; | ||||
| @property (nonatomic, nullable) FHSViewSnapshot *selectedView; | ||||
|  | ||||
| /// Views of these classes will have their headers hidden | ||||
| @property (nonatomic) NSArray<Class> *headerExclusions; | ||||
|  | ||||
| @property (nonatomic, readonly) UISlider *spacingSlider; | ||||
| @property (nonatomic, readonly) FHSRangeSlider *depthSlider; | ||||
|  | ||||
| - (void)emphasizeViews:(NSArray<UIView *> *)emphasizedViews; | ||||
|  | ||||
| - (void)toggleShowHeaders; | ||||
| - (void)toggleShowBorders; | ||||
|  | ||||
| - (void)hideView:(FHSViewSnapshot *)view; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										304
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSSnapshotView.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										304
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSSnapshotView.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,304 @@ | ||||
| // | ||||
| //  FHSSnapshotView.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/7/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FHSSnapshotView.h" | ||||
| #import "FHSSnapshotNodes.h" | ||||
| #import "SceneKit+Snapshot.h" | ||||
| #import "FLEXColor.h" | ||||
|  | ||||
| @interface FHSSnapshotView () | ||||
| @property (nonatomic, readonly) SCNView *sceneView; | ||||
| @property (nonatomic) NSString *currentSummary; | ||||
|  | ||||
| /// Maps nodes by snapshot IDs | ||||
| @property (nonatomic) NSDictionary<NSString *, FHSSnapshotNodes *> *nodesMap; | ||||
| @property (nonatomic) NSInteger maxDepth; | ||||
|  | ||||
| @property (nonatomic) FHSSnapshotNodes *highlightedNodes; | ||||
| @property (nonatomic, getter=wantsHideHeaders) BOOL hideHeaders; | ||||
| @property (nonatomic, getter=wantsHideBorders) BOOL hideBorders; | ||||
| @property (nonatomic) BOOL suppressSelectionEvents; | ||||
|  | ||||
| @property (nonatomic, readonly) BOOL mustHideHeaders; | ||||
| @end | ||||
|  | ||||
| @implementation FHSSnapshotView | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (instancetype)delegate:(id<FHSSnapshotViewDelegate>)delegate { | ||||
|     FHSSnapshotView *view = [self new]; | ||||
|     view.delegate = delegate; | ||||
|     return view; | ||||
| } | ||||
|  | ||||
| - (id)initWithFrame:(CGRect)frame { | ||||
|     self = [super initWithFrame:CGRectZero]; | ||||
|     if (self) { | ||||
|         [self initSpacingSlider]; | ||||
|         [self initDepthSlider]; | ||||
|         [self initSceneView]; // Must be last; calls setMaxDepth | ||||
| //        self.hideHeaders = YES; | ||||
|          | ||||
|             // Self | ||||
|         self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | ||||
|  | ||||
|         // Scene | ||||
|         self.sceneView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | ||||
|         [self addGestureRecognizer:[[UITapGestureRecognizer alloc] | ||||
|             initWithTarget:self action:@selector(handleTap:) | ||||
|         ]]; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)initSceneView { | ||||
|     _sceneView = [SCNView new]; | ||||
|     self.sceneView.allowsCameraControl = YES; | ||||
|  | ||||
|     [self addSubview:self.sceneView]; | ||||
| } | ||||
|  | ||||
| - (void)initSpacingSlider { | ||||
|     _spacingSlider = [UISlider new]; | ||||
|     self.spacingSlider.minimumValue = 0; | ||||
|     self.spacingSlider.maximumValue = 100; | ||||
|     self.spacingSlider.continuous = YES; | ||||
|     [self.spacingSlider | ||||
|         addTarget:self | ||||
|         action:@selector(spacingSliderDidChange:) | ||||
|         forControlEvents:UIControlEventValueChanged | ||||
|     ]; | ||||
|  | ||||
|     self.spacingSlider.value = 50; | ||||
| } | ||||
|  | ||||
| - (void)initDepthSlider { | ||||
|     _depthSlider = [FHSRangeSlider new]; | ||||
|     [self.depthSlider | ||||
|         addTarget:self | ||||
|         action:@selector(depthSliderDidChange:) | ||||
|         forControlEvents:UIControlEventValueChanged | ||||
|     ]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (void)setSelectedView:(FHSViewSnapshot *)view { | ||||
|     // Ivar set in selectSnapshot: | ||||
|     [self selectSnapshot:view ? self.nodesMap[view.view.identifier] : nil]; | ||||
| } | ||||
|  | ||||
| - (void)setSnapshots:(NSArray<FHSViewSnapshot *> *)snapshots { | ||||
|     _snapshots = snapshots; | ||||
|  | ||||
|     // Create new scene (possibly discarding old scene) | ||||
|     SCNScene *scene = [SCNScene new]; | ||||
|     scene.background.contents = FLEXColor.primaryBackgroundColor; | ||||
|     self.sceneView.scene = scene; | ||||
|  | ||||
|     NSInteger depth = 0; | ||||
|     NSMutableDictionary *nodesMap = [NSMutableDictionary new]; | ||||
|  | ||||
|     // Add every root snapshot to the root scene node with increasing depths | ||||
|     SCNNode *root = scene.rootNode; | ||||
|     for (FHSViewSnapshot *snapshot in self.snapshots) { | ||||
|         [SCNNode | ||||
|             snapshot:snapshot | ||||
|             parent:nil | ||||
|             parentNode:nil | ||||
|             root:root | ||||
|             depth:&depth | ||||
|             nodesMap:nodesMap | ||||
|             hideHeaders:_hideHeaders | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     self.maxDepth = depth; | ||||
|     self.nodesMap = nodesMap; | ||||
| } | ||||
|  | ||||
| - (void)setHeaderExclusions:(NSArray<Class> *)headerExclusions { | ||||
|     _headerExclusions = headerExclusions; | ||||
|  | ||||
|     if (headerExclusions.count) { | ||||
|         for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) { | ||||
|             if ([headerExclusions containsObject:nodes.snapshotItem.view.view.class]) { | ||||
|                 nodes.forceHideHeader = YES; | ||||
|             } else { | ||||
|                 nodes.forceHideHeader = NO; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)emphasizeViews:(NSArray<UIView *> *)emphasizedViews { | ||||
|     if (emphasizedViews.count) { | ||||
|         [self emphasizeViews:emphasizedViews inSnapshots:self.snapshots]; | ||||
|         [self setNeedsLayout]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)emphasizeViews:(NSArray<UIView *> *)emphasizedViews inSnapshots:(NSArray<FHSViewSnapshot *> *)snapshots { | ||||
|     for (FHSViewSnapshot *snapshot in snapshots) { | ||||
|         FHSSnapshotNodes *nodes = self.nodesMap[snapshot.view.identifier]; | ||||
|         nodes.dimmed = ![emphasizedViews containsObject:snapshot.view.view]; | ||||
|         [self emphasizeViews:emphasizedViews inSnapshots:snapshot.children]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)toggleShowHeaders { | ||||
|     self.hideHeaders = !self.hideHeaders; | ||||
| } | ||||
|  | ||||
| - (void)toggleShowBorders { | ||||
|     self.hideBorders = !self.hideBorders; | ||||
| } | ||||
|  | ||||
| - (void)hideView:(FHSViewSnapshot *)view { | ||||
|     NSParameterAssert(view); | ||||
|     FHSSnapshotNodes *nodes = self.nodesMap[view.view.identifier]; | ||||
|     [nodes.snapshot removeFromParentNode]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Helper | ||||
|  | ||||
| - (BOOL)mustHideHeaders { | ||||
|     return self.spacingSlider.value <= kFHSSmallZOffset; | ||||
| } | ||||
|  | ||||
| - (void)setMaxDepth:(NSInteger)maxDepth { | ||||
|     _maxDepth = maxDepth; | ||||
|  | ||||
|     self.depthSlider.allowedMinValue = 0; | ||||
|     self.depthSlider.allowedMaxValue = maxDepth; | ||||
|     self.depthSlider.maxValue = maxDepth; | ||||
|     self.depthSlider.minValue = 0; | ||||
| } | ||||
|  | ||||
| - (void)setHideHeaders:(BOOL)hideHeaders { | ||||
|     if (_hideHeaders != hideHeaders) { | ||||
|         _hideHeaders = hideHeaders; | ||||
|  | ||||
|         if (!self.mustHideHeaders) { | ||||
|             if (hideHeaders) { | ||||
|                 [self hideHeaders]; | ||||
|             } else { | ||||
|                 [self unhideHeaders]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setHideBorders:(BOOL)hideBorders { | ||||
|     if (_hideBorders != hideBorders) { | ||||
|         _hideBorders = hideBorders; | ||||
|  | ||||
|         for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) { | ||||
|             nodes.border.hidden = hideBorders; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (FHSSnapshotNodes *)nodesAtPoint:(CGPoint)point { | ||||
|     NSArray<SCNHitTestResult *> *results = [self.sceneView hitTest:point options:nil]; | ||||
|     for (SCNHitTestResult *result in results) { | ||||
|         SCNNode *nearestSnapshot = result.node.nearestAncestorSnapshot; | ||||
|         if (nearestSnapshot) { | ||||
|             return self.nodesMap[nearestSnapshot.name]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (void)selectSnapshot:(FHSSnapshotNodes *)selected { | ||||
|     // Notify delegate of de-select | ||||
|     if (!selected && self.selectedView) { | ||||
|         [self.delegate didDeselectView:self.selectedView]; | ||||
|     } | ||||
|  | ||||
|     _selectedView = selected.snapshotItem; | ||||
|  | ||||
|     // Case: selected the currently selected node | ||||
|     if (selected == self.highlightedNodes) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // No-op if nothng is selected (yay objc!) | ||||
|     self.highlightedNodes.highlighted = NO; | ||||
|     self.highlightedNodes = nil; | ||||
|  | ||||
|     // No node means we tapped the background | ||||
|     if (selected) { | ||||
|         selected.highlighted = YES; | ||||
|         // TODO: update description text here | ||||
|         self.highlightedNodes = selected; | ||||
|     } | ||||
|  | ||||
|     // Notify delegate | ||||
|     [self.delegate didSelectView:selected.snapshotItem]; | ||||
|  | ||||
|     [self setNeedsLayout]; | ||||
| } | ||||
|  | ||||
| - (void)hideHeaders { | ||||
|     for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) { | ||||
|         nodes.header.hidden = YES; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)unhideHeaders { | ||||
|     for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) { | ||||
|         if (!nodes.forceHideHeader) { | ||||
|             nodes.header.hidden = NO; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Event Handlers | ||||
|  | ||||
| - (void)handleTap:(UITapGestureRecognizer *)gesture { | ||||
|     if (gesture.state == UIGestureRecognizerStateRecognized) { | ||||
|         CGPoint tap = [gesture locationInView:self.sceneView]; | ||||
|         [self selectSnapshot:[self nodesAtPoint:tap]]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)spacingSliderDidChange:(UISlider *)slider { | ||||
|     // TODO: hiding the header when flat logic | ||||
|  | ||||
|     for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) { | ||||
|         nodes.snapshot.position = ({ | ||||
|             SCNVector3 pos = nodes.snapshot.position; | ||||
|             pos.z = MAX(slider.value, kFHSSmallZOffset) * nodes.depth; | ||||
|             pos; | ||||
|         }); | ||||
|  | ||||
|         if (!self.wantsHideHeaders) { | ||||
|             if (self.mustHideHeaders) { | ||||
|                 [self hideHeaders]; | ||||
|             } else { | ||||
|                 [self unhideHeaders]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)depthSliderDidChange:(FHSRangeSlider *)slider { | ||||
|     CGFloat min = slider.minValue, max = slider.maxValue; | ||||
|     for (FHSSnapshotNodes *nodes in self.nodesMap.allValues) { | ||||
|         CGFloat depth = nodes.depth; | ||||
|         nodes.snapshot.hidden = depth < min || max < depth; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										35
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSView.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSView.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| // | ||||
| //  FHSView.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/6/20. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @interface FHSView : NSObject { | ||||
|     @private | ||||
|     BOOL _inScrollView; | ||||
| } | ||||
|  | ||||
| + (instancetype)forView:(UIView *)view isInScrollView:(BOOL)inScrollView; | ||||
|  | ||||
| /// Intentionally not weak | ||||
| @property (nonatomic, readonly) UIView *view; | ||||
| @property (nonatomic, readonly) NSString *identifier; | ||||
|  | ||||
| @property (nonatomic, readonly) NSString *title; | ||||
| /// Whether or not this view item should be visually distinguished | ||||
| @property (nonatomic, readwrite) BOOL important; | ||||
|  | ||||
| @property (nonatomic, readonly) CGRect frame; | ||||
| @property (nonatomic, readonly) BOOL hidden; | ||||
| @property (nonatomic, readonly) UIImage *snapshotImage; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<FHSView *> *children; | ||||
| @property (nonatomic, readonly) NSString *summary; | ||||
|  | ||||
| /// @return importantAttr if .important, otherwise normalAttr | ||||
| //- (id)ifImportant:(id)importantAttr ifNormal:(id)normalAttr; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										194
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSView.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSView.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,194 @@ | ||||
| // | ||||
| //  FHSView.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/6/20. | ||||
| // | ||||
|  | ||||
| #import "FHSView.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "NSArray+FLEX.h" | ||||
|  | ||||
| @interface FHSView (Snapshotting) | ||||
| + (UIImage *)_snapshotView:(UIView *)view; | ||||
| @end | ||||
|  | ||||
| @implementation FHSView | ||||
|  | ||||
| + (instancetype)forView:(UIView *)view isInScrollView:(BOOL)inScrollView { | ||||
|     return [[self alloc] initWithView:view isInScrollView:inScrollView]; | ||||
| } | ||||
|  | ||||
| - (id)initWithView:(UIView *)view isInScrollView:(BOOL)inScrollView { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _view = view; | ||||
|         _inScrollView = inScrollView; | ||||
|         _identifier = NSUUID.UUID.UUIDString; | ||||
|  | ||||
|         UIViewController *controller = [FLEXUtility viewControllerForView:view]; | ||||
|         if (controller) { | ||||
|             _important = YES; | ||||
|             _title = [NSString stringWithFormat: | ||||
|                 @"%@ (for %@)", | ||||
|                 NSStringFromClass([controller class]), | ||||
|                 NSStringFromClass([view class]) | ||||
|             ]; | ||||
|         } else { | ||||
|             _title = NSStringFromClass([view class]); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (CGRect)frame { | ||||
|     if (_inScrollView) { | ||||
|         CGPoint offset = [(UIScrollView *)self.view.superview contentOffset]; | ||||
|         return CGRectOffset(self.view.frame, -offset.x, -offset.y); | ||||
|     } else { | ||||
|         return self.view.frame; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (BOOL)hidden { | ||||
|     return self.view.isHidden; | ||||
| } | ||||
|  | ||||
| - (UIImage *)snapshotImage { | ||||
|     return [FHSView _snapshotView:self.view]; | ||||
| } | ||||
|  | ||||
| - (NSArray<FHSView *> *)children { | ||||
|     BOOL isScrollView = [self.view isKindOfClass:[UIScrollView class]]; | ||||
|     return [self.view.subviews flex_mapped:^id(UIView *subview, NSUInteger idx) { | ||||
|         return [FHSView forView:subview isInScrollView:isScrollView]; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (NSString *)summary { | ||||
|     CGRect f = self.frame; | ||||
|     return [NSString stringWithFormat: | ||||
|         @"%@ (%.1f, %.1f, %.1f, %.1f)", | ||||
|         NSStringFromClass([self.view class]), | ||||
|         f.origin.x, f.origin.y, f.size.width, f.size.height | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| - (NSString *)description{ | ||||
|     return self.view.description; | ||||
| } | ||||
|  | ||||
| - (id)ifImportant:(id)importantAttr ifNormal:(id)normalAttr { | ||||
|     return self.important ? importantAttr : normalAttr; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FHSView (Snapshotting) | ||||
|  | ||||
| + (UIImage *)drawView:(UIView *)view { | ||||
|     UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0); | ||||
|     [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES]; | ||||
|     UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); | ||||
|     UIGraphicsEndImageContext(); | ||||
|     return image; | ||||
| } | ||||
|  | ||||
| /// Recursively hides all views that may be obscuring the given view and collects them | ||||
| /// in the given array. You should unhide them all when you are done. | ||||
| + (BOOL)_hideViewsCoveringView:(UIView *)view | ||||
|                           root:(UIView *)rootView | ||||
|                    hiddenViews:(NSMutableArray<UIView *> *)hiddenViews { | ||||
|     // Stop when we reach this view | ||||
|     if (view == rootView) { | ||||
|         return YES; | ||||
|     } | ||||
|  | ||||
|     for (UIView *subview in rootView.subviews.reverseObjectEnumerator.allObjects) { | ||||
|         if ([self _hideViewsCoveringView:view root:subview hiddenViews:hiddenViews]) { | ||||
|             return YES; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (!rootView.isHidden) { | ||||
|         rootView.hidden = YES; | ||||
|         [hiddenViews addObject:rootView]; | ||||
|     } | ||||
|  | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Recursively hides all views that may be obscuring the given view and collects them | ||||
| /// in the given array. You should unhide them all when you are done. | ||||
| + (void)hideViewsCoveringView:(UIView *)view doWhileHidden:(void(^)(void))block { | ||||
|     NSMutableArray *viewsToUnhide = [NSMutableArray new]; | ||||
|     if ([self _hideViewsCoveringView:view root:view.window hiddenViews:viewsToUnhide]) { | ||||
|         block(); | ||||
|     } | ||||
|  | ||||
|     for (UIView *v in viewsToUnhide) { | ||||
|         v.hidden = NO; | ||||
|     } | ||||
| } | ||||
|  | ||||
| + (UIImage *)_snapshotVisualEffectBackdropView:(UIView *)view { | ||||
|     NSParameterAssert(view.window); | ||||
|  | ||||
|     // UIVisualEffectView is a special case that cannot be snapshotted | ||||
|     // the same way as any other view. From Apple docs: | ||||
|     // | ||||
|     //   Many effects require support from the window that hosts the | ||||
|     //   UIVisualEffectView. Attempting to take a snapshot of only the | ||||
|     //   UIVisualEffectView will result in a snapshot that does not | ||||
|     //   contain the effect. To take a snapshot of a view hierarchy | ||||
|     //   that contains a UIVisualEffectView, you must take a snapshot | ||||
|     //   of the entire UIWindow or UIScreen that contains it. | ||||
|     // | ||||
|     // To snapshot this view, we traverse the view hierarchy starting | ||||
|     // from the window and hide any views that are on top of the | ||||
|     // _UIVisualEffectBackdropView so that it is visible in a snapshot | ||||
|     // of the window. We then take a snapshot of the window and crop | ||||
|     // it to the part that contains the backdrop view. This appears to | ||||
|     // be the same technique that Xcode's own view debugger uses to | ||||
|     // snapshot visual effect views. | ||||
|     __block UIImage *image = nil; | ||||
|     [self hideViewsCoveringView:view doWhileHidden:^{ | ||||
|         image = [self drawView:view]; | ||||
|         CGRect cropRect = [view.window convertRect:view.bounds fromView:view]; | ||||
|         image = [UIImage imageWithCGImage:CGImageCreateWithImageInRect(image.CGImage, cropRect)]; | ||||
|     }]; | ||||
|  | ||||
|     return image; | ||||
| } | ||||
|  | ||||
| + (UIImage *)_snapshotView:(UIView *)view { | ||||
|     UIView *superview = view.superview; | ||||
|     // Is this view inside a UIVisualEffectView? | ||||
|     if ([superview isKindOfClass:[UIVisualEffectView class]]) { | ||||
|         // Is it (probably) the "backdrop" view of this UIVisualEffectView? | ||||
|         if (superview.subviews.firstObject == view) { | ||||
|             return [self _snapshotVisualEffectBackdropView:view]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Hide the view's subviews before we snapshot it | ||||
|     NSMutableIndexSet *toUnhide = [NSMutableIndexSet new]; | ||||
|     [view.subviews flex_forEach:^(UIView *v, NSUInteger idx) { | ||||
|         if (!v.isHidden) { | ||||
|             v.hidden = YES; | ||||
|             [toUnhide addIndex:idx]; | ||||
|         } | ||||
|     }]; | ||||
|  | ||||
|     // Snapshot the view, then unhide the previously-unhidden views | ||||
|     UIImage *snapshot = [self drawView:view]; | ||||
|     for (UIView *v in [view.subviews objectsAtIndexes:toUnhide]) { | ||||
|         v.hidden = NO; | ||||
|     } | ||||
|  | ||||
|     return snapshot; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,29 @@ | ||||
| // | ||||
| //  FHSViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| /// The view controller | ||||
| /// "FHS" stands for "FLEX (view) hierarchy snapshot" | ||||
| @interface FHSViewController : UIViewController | ||||
|  | ||||
| /// Use this when you want to snapshot a set of windows. | ||||
| + (instancetype)snapshotWindows:(NSArray<UIWindow *> *)windows; | ||||
| /// Use this when you want to snapshot a specific slice of the view hierarchy. | ||||
| + (instancetype)snapshotView:(UIView *)view; | ||||
| /// Use this when you want to emphasize specific views on the screen. | ||||
| /// These views must all be in the same window as the selected view. | ||||
| + (instancetype)snapshotViewsAtTap:(NSArray<UIView *> *)viewsAtTap selectedView:(UIView *)view; | ||||
|  | ||||
| @property (nonatomic, nullable) UIView *selectedView; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										270
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,270 @@ | ||||
| // | ||||
| //  FHSViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/6/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FHSViewController.h" | ||||
| #import "FHSSnapshotView.h" | ||||
| #import "FLEXHierarchyViewController.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "FLEXAlert.h" | ||||
| #import "FLEXWindow.h" | ||||
| #import "FLEXResources.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
|  | ||||
| BOOL const kFHSViewControllerExcludeFLEXWindows = YES; | ||||
|  | ||||
| @interface FHSViewController () <FHSSnapshotViewDelegate> | ||||
| /// An array of only the target views whose hierarchies | ||||
| /// we wish to snapshot, not every view in the snapshot. | ||||
| @property (nonatomic, readonly) NSArray<UIView *> *targetViews; | ||||
| @property (nonatomic, readonly) NSArray<FHSView *> *views; | ||||
| @property (nonatomic          ) NSArray<FHSViewSnapshot *> *snapshots; | ||||
| @property (nonatomic,         ) FHSSnapshotView *snapshotView; | ||||
|  | ||||
| @property (nonatomic, readonly) UIView *containerView; | ||||
| @property (nonatomic, readonly) NSArray<UIView *> *viewsAtTap; | ||||
| @property (nonatomic, readonly) NSMutableSet<Class> *forceHideHeaders; | ||||
| @end | ||||
|  | ||||
| @implementation FHSViewController | ||||
| @synthesize views = _views; | ||||
| @synthesize snapshotView = _snapshotView; | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (instancetype)snapshotWindows:(NSArray<UIWindow *> *)windows { | ||||
|     return [[self alloc] initWithViews:windows viewsAtTap:nil selectedView:nil]; | ||||
| } | ||||
|  | ||||
| + (instancetype)snapshotView:(UIView *)view { | ||||
|     return [[self alloc] initWithViews:@[view] viewsAtTap:nil selectedView:nil]; | ||||
| } | ||||
|  | ||||
| + (instancetype)snapshotViewsAtTap:(NSArray<UIView *> *)viewsAtTap selectedView:(UIView *)view { | ||||
|     NSParameterAssert(viewsAtTap.count); | ||||
|     NSParameterAssert(view.window); | ||||
|     return [[self alloc] initWithViews:@[view.window] viewsAtTap:viewsAtTap selectedView:view]; | ||||
| } | ||||
|  | ||||
| - (id)initWithViews:(NSArray<UIView *> *)views | ||||
|          viewsAtTap:(NSArray<UIView *> *)viewsAtTap | ||||
|        selectedView:(UIView *)view { | ||||
|     NSParameterAssert(views.count); | ||||
|  | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _forceHideHeaders = [NSMutableSet setWithObject:NSClassFromString(@"_UITableViewCellSeparatorView")]; | ||||
|         _selectedView = view; | ||||
|         _viewsAtTap = viewsAtTap; | ||||
|  | ||||
|         if (!viewsAtTap && kFHSViewControllerExcludeFLEXWindows) { | ||||
|             Class flexwindow = [FLEXWindow class]; | ||||
|             views = [views flex_filtered:^BOOL(UIView *view, NSUInteger idx) { | ||||
|                 return [view class] != flexwindow; | ||||
|             }]; | ||||
|         } | ||||
|  | ||||
|         _targetViews = views; | ||||
|         _views = [views flex_mapped:^id(UIView *view, NSUInteger idx) { | ||||
|             BOOL isScrollView = [view.superview isKindOfClass:[UIScrollView class]]; | ||||
|             return [FHSView forView:view isInScrollView:isScrollView]; | ||||
|         }]; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)refreshSnapshotView { | ||||
|     // Alert view to block interaction while we load everything | ||||
|     UIAlertController *loading = [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Please Wait").message(@"Generating snapshot…"); | ||||
|     }]; | ||||
|     [self presentViewController:loading animated:YES completion:^{ | ||||
|         self.snapshots = [self.views flex_mapped:^id(FHSView *view, NSUInteger idx) { | ||||
|             return [FHSViewSnapshot snapshotWithView:view]; | ||||
|         }]; | ||||
|         FHSSnapshotView *newSnapshotView = [FHSSnapshotView delegate:self]; | ||||
|  | ||||
|         // This work is highly intensive so we do it on a background thread first | ||||
|         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ | ||||
|             // Setting the snapshots computes lots of SCNNodes, takes several seconds | ||||
|             newSnapshotView.snapshots = self.snapshots; | ||||
|  | ||||
|             // After we finish generating all the model objects and scene nodes, display the view | ||||
|             dispatch_async(dispatch_get_main_queue(), ^{ | ||||
|                 // Dismiss alert | ||||
|                 [loading dismissViewControllerAnimated:YES completion:nil]; | ||||
|  | ||||
|                 self.snapshotView = newSnapshotView; | ||||
|             }); | ||||
|         }); | ||||
|     }]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - View Controller Lifecycle | ||||
|  | ||||
| - (void)loadView { | ||||
|     [super loadView]; | ||||
|     self.view.backgroundColor = FLEXColor.primaryBackgroundColor; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|  | ||||
|     // Initialize back bar button item for 3D view to look like a button | ||||
|     self.navigationItem.hidesBackButton = YES; | ||||
|     self.navigationItem.leftBarButtonItem = [UIBarButtonItem | ||||
|         flex_itemWithImage:FLEXResources.toggle2DIcon | ||||
|         target:self.navigationController | ||||
|         action:@selector(toggleHierarchyMode) | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidAppear:(BOOL)animated { | ||||
|     [super viewDidAppear:animated]; | ||||
|  | ||||
|     if (!_snapshotView) { | ||||
|         [self refreshSnapshotView]; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (void)setSelectedView:(UIView *)view { | ||||
|     _selectedView = view; | ||||
|     self.snapshotView.selectedView = view ? [self snapshotForView:view] : nil; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| #pragma mark Properties | ||||
|  | ||||
| - (FHSSnapshotView *)snapshotView { | ||||
|     return self.isViewLoaded ? _snapshotView : nil; | ||||
| } | ||||
|  | ||||
| - (void)setSnapshotView:(FHSSnapshotView *)snapshotView { | ||||
|     NSParameterAssert(snapshotView); | ||||
|  | ||||
|     _snapshotView = snapshotView; | ||||
|  | ||||
|     // Initialize our toolbar items | ||||
|     self.toolbarItems = @[ | ||||
|         [UIBarButtonItem flex_itemWithCustomView:snapshotView.spacingSlider], | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         [UIBarButtonItem | ||||
|             flex_itemWithImage:FLEXResources.moreIcon | ||||
|             target:self action:@selector(didPressOptionsButton:) | ||||
|         ], | ||||
|         UIBarButtonItem.flex_flexibleSpace, | ||||
|         [UIBarButtonItem flex_itemWithCustomView:snapshotView.depthSlider] | ||||
|     ]; | ||||
|     [self resizeToolbarItems:self.view.frame.size]; | ||||
|  | ||||
|     // If we have views-at-tap, dim the other views | ||||
|     [snapshotView emphasizeViews:self.viewsAtTap]; | ||||
|     // Set the selected view, if any | ||||
|     snapshotView.selectedView = [self snapshotForView:self.selectedView]; | ||||
|     snapshotView.headerExclusions = self.forceHideHeaders.allObjects; | ||||
|     [snapshotView setNeedsLayout]; | ||||
|  | ||||
|     // Remove old snapshot, if any, and add the new one | ||||
|     [_snapshotView removeFromSuperview]; | ||||
|     snapshotView.frame = self.containerView.bounds; | ||||
|     [self.containerView addSubview:snapshotView]; | ||||
| } | ||||
|  | ||||
| - (UIView *)containerView { | ||||
|     return self.view; | ||||
| } | ||||
|  | ||||
| #pragma mark Helper | ||||
|  | ||||
| - (FHSViewSnapshot *)snapshotForView:(UIView *)view { | ||||
|     if (!view || !self.snapshots.count) return nil; | ||||
|  | ||||
|     for (FHSViewSnapshot *snapshot in self.snapshots) { | ||||
|         FHSViewSnapshot *found = [snapshot snapshotForView:view]; | ||||
|         if (found) { | ||||
|             return found; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Error: we have snapshots but the view we requested is not in one | ||||
|     @throw NSInternalInconsistencyException; | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| #pragma mark Events | ||||
|  | ||||
| - (void)didPressOptionsButton:(UIBarButtonItem *)sender { | ||||
|     [FLEXAlert makeSheet:^(FLEXAlert *make) { | ||||
|         if (self.selectedView) { | ||||
|             make.button(@"Hide selected view").handler(^(NSArray<NSString *> *strings) { | ||||
|                 [self.snapshotView hideView:[self snapshotForView:self.selectedView]]; | ||||
|             }); | ||||
|             make.button(@"Hide headers for views like this").handler(^(NSArray<NSString *> *strings) { | ||||
|                 Class cls = [self.selectedView class]; | ||||
|                 if (![self.forceHideHeaders containsObject:cls]) { | ||||
|                     [self.forceHideHeaders addObject:[self.selectedView class]]; | ||||
|                     self.snapshotView.headerExclusions = self.forceHideHeaders.allObjects; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|         make.title(@"Options"); | ||||
|         make.button(@"Toggle headers").handler(^(NSArray<NSString *> *strings) { | ||||
|             [self.snapshotView toggleShowHeaders]; | ||||
|         }); | ||||
|         make.button(@"Toggle outlines").handler(^(NSArray<NSString *> *strings) { | ||||
|             [self.snapshotView toggleShowBorders]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self source:sender]; | ||||
| } | ||||
|  | ||||
| - (void)resizeToolbarItems:(CGSize)viewSize { | ||||
|     CGFloat sliderHeights = self.snapshotView.spacingSlider.bounds.size.height; | ||||
|     CGFloat sliderWidths = viewSize.width / 3.f; | ||||
|     CGRect frame = CGRectMake(0, 0, sliderWidths, sliderHeights); | ||||
|     self.snapshotView.spacingSlider.frame = frame; | ||||
|     self.snapshotView.depthSlider.frame = frame; | ||||
|  | ||||
|     [self.navigationController.toolbar setNeedsLayout]; | ||||
| } | ||||
|  | ||||
| - (void)viewWillTransitionToSize:(CGSize)size | ||||
|        withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator { | ||||
|     [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; | ||||
|  | ||||
|     [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) { | ||||
|         [self resizeToolbarItems:self.view.frame.size]; | ||||
|     } completion:nil]; | ||||
| } | ||||
|  | ||||
| #pragma mark FHSSnapshotViewDelegate | ||||
|  | ||||
| - (void)didDeselectView:(FHSViewSnapshot *)snapshot { | ||||
|     // Our setter would also call the setter for the snapshot view, | ||||
|     // which we don't need to do here since it is already selected | ||||
|     _selectedView = nil; | ||||
| } | ||||
|  | ||||
| - (void)didLongPressView:(FHSViewSnapshot *)snapshot { | ||||
|  | ||||
| } | ||||
|  | ||||
| - (void)didSelectView:(FHSViewSnapshot *)snapshot { | ||||
|     // Our setter would also call the setter for the snapshot view, | ||||
|     // which we don't need to do here since it is already selected | ||||
|     _selectedView = snapshot.view.view; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										37
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSViewSnapshot.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSViewSnapshot.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| // | ||||
| //  FHSViewSnapshot.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/9/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FHSView.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FHSViewSnapshot : NSObject | ||||
|  | ||||
| + (instancetype)snapshotWithView:(FHSView *)view; | ||||
|  | ||||
| @property (nonatomic, readonly) FHSView *view; | ||||
|  | ||||
| @property (nonatomic, readonly) NSString *title; | ||||
| /// Whether or not this view item should be visually distinguished | ||||
| @property (nonatomic, readwrite) BOOL important; | ||||
|  | ||||
| @property (nonatomic, readonly) CGRect frame; | ||||
| @property (nonatomic, readonly) BOOL hidden; | ||||
| @property (nonatomic, readonly) UIImage *snapshotImage; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<FHSViewSnapshot *> *children; | ||||
| @property (nonatomic, readonly) NSString *summary; | ||||
|  | ||||
| /// Returns a different color based on whether or not the view is important | ||||
| @property (nonatomic, readonly) UIColor *headerColor; | ||||
|  | ||||
| - (FHSViewSnapshot *)snapshotForView:(UIView *)view; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										62
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSViewSnapshot.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								Tweaks/FLEX/ViewHierarchy/SnapshotExplorer/FHSViewSnapshot.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| // | ||||
| //  FHSViewSnapshot.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/9/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FHSViewSnapshot.h" | ||||
| #import "NSArray+FLEX.h" | ||||
|  | ||||
| @implementation FHSViewSnapshot | ||||
|  | ||||
| + (instancetype)snapshotWithView:(FHSView *)view { | ||||
|     NSArray *children = [view.children flex_mapped:^id(FHSView *v, NSUInteger idx) { | ||||
|         return [self snapshotWithView:v]; | ||||
|     }]; | ||||
|     return [[self alloc] initWithView:view children:children]; | ||||
| } | ||||
|  | ||||
| - (id)initWithView:(FHSView *)view children:(NSArray<FHSViewSnapshot *> *)children { | ||||
|     NSParameterAssert(view); NSParameterAssert(children); | ||||
|  | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _view = view; | ||||
|         _title = view.title; | ||||
|         _important = view.important; | ||||
|         _frame = view.frame; | ||||
|         _hidden = view.hidden; | ||||
|         _snapshotImage = view.snapshotImage; | ||||
|         _children = children; | ||||
|         _summary = view.summary; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (UIColor *)headerColor { | ||||
|     if (self.important) { | ||||
|         return [UIColor colorWithRed: 0.000 green: 0.533 blue: 1.000 alpha: 0.900]; | ||||
|     } else { | ||||
|         return [UIColor colorWithRed:0.961 green: 0.651 blue: 0.137 alpha: 0.900]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (FHSViewSnapshot *)snapshotForView:(UIView *)view { | ||||
|     if (view == self.view.view) { | ||||
|         return self; | ||||
|     } | ||||
|  | ||||
|     for (FHSViewSnapshot *child in self.children) { | ||||
|         FHSViewSnapshot *snapshot = [child snapshotForView:view]; | ||||
|         if (snapshot) { | ||||
|             return snapshot; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,37 @@ | ||||
| // | ||||
| //  FHSSnapshotNodes.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/7/20. | ||||
| // | ||||
|  | ||||
| #import "FHSViewSnapshot.h" | ||||
| #import <SceneKit/SceneKit.h> | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| /// Container that holds references to the SceneKit nodes associated with a snapshot. | ||||
| @interface FHSSnapshotNodes : NSObject | ||||
|  | ||||
| + (instancetype)snapshot:(FHSViewSnapshot *)snapshot depth:(NSInteger)depth; | ||||
|  | ||||
| @property (nonatomic, readonly) FHSViewSnapshot *snapshotItem; | ||||
| @property (nonatomic, readonly) NSInteger depth; | ||||
|  | ||||
| /// The view image itself | ||||
| @property (nonatomic, nullable) SCNNode *snapshot; | ||||
| /// Goes on top of the snapshot, has rounded top corners | ||||
| @property (nonatomic, nullable) SCNNode *header; | ||||
| /// The bounding box drawn around the snapshot | ||||
| @property (nonatomic, nullable) SCNNode *border; | ||||
|  | ||||
| /// Used to indicate when a view is selected | ||||
| @property (nonatomic, getter=isHighlighted) BOOL highlighted; | ||||
| /// Used to indicate when a view is de-emphasized | ||||
| @property (nonatomic, getter=isDimmed) BOOL dimmed; | ||||
|  | ||||
| @property (nonatomic) BOOL forceHideHeader; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,90 @@ | ||||
| // | ||||
| //  FHSSnapshotNodes.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/7/20. | ||||
| // | ||||
|  | ||||
| #import "FHSSnapshotNodes.h" | ||||
| #import "SceneKit+Snapshot.h" | ||||
|  | ||||
| @interface FHSSnapshotNodes () | ||||
| @property (nonatomic, nullable) SCNNode *highlight; | ||||
| @property (nonatomic, nullable) SCNNode *dimming; | ||||
| @end | ||||
| @implementation FHSSnapshotNodes | ||||
|  | ||||
| + (instancetype)snapshot:(FHSViewSnapshot *)snapshot depth:(NSInteger)depth { | ||||
|     FHSSnapshotNodes *nodes = [self new]; | ||||
|     nodes->_snapshotItem = snapshot; | ||||
|     nodes->_depth = depth; | ||||
|     return nodes; | ||||
| } | ||||
|  | ||||
| - (void)setHighlighted:(BOOL)highlighted { | ||||
|     if (_highlighted != highlighted) { | ||||
|         _highlighted = highlighted; | ||||
|  | ||||
|         if (highlighted) { | ||||
|             if (!self.highlight) { | ||||
|                 // Create highlight node | ||||
|                 self.highlight = [SCNNode | ||||
|                     highlight:self.snapshotItem | ||||
|                     color:[UIColor.blueColor colorWithAlphaComponent:0.5] | ||||
|                 ]; | ||||
|             } | ||||
|             // Add add highlight node, remove dimming node if dimmed | ||||
|             [self.snapshot addChildNode:self.highlight]; | ||||
|             if (self.isDimmed) { | ||||
|                 [self.dimming removeFromParentNode]; | ||||
|             } | ||||
|         } else { | ||||
|             // Remove highlight node, add back dimming node if dimmed | ||||
|             [self.highlight removeFromParentNode]; | ||||
|             if (self.isDimmed) { | ||||
|                 [self.snapshot addChildNode:self.dimming]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setDimmed:(BOOL)dimmed { | ||||
|     if (_dimmed != dimmed) { | ||||
|         _dimmed = dimmed; | ||||
|  | ||||
|         if (dimmed) { | ||||
|             if (!self.dimming) { | ||||
|                 // Create dimming node | ||||
|                 self.dimming = [SCNNode | ||||
|                     highlight:self.snapshotItem | ||||
|                     color:[UIColor.blackColor colorWithAlphaComponent:0.5] | ||||
|                 ]; | ||||
|             } | ||||
|             // Add add dimming node if not highlighted | ||||
|             if (!self.isHighlighted) { | ||||
|                 [self.snapshot addChildNode:self.dimming]; | ||||
|             } | ||||
|         } else { | ||||
|             // Remove dimming node (if not already highlighted) | ||||
|             if (!self.isHighlighted) { | ||||
|                 [self.dimming removeFromParentNode]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setForceHideHeader:(BOOL)forceHideHeader { | ||||
|     if (_forceHideHeader != forceHideHeader) { | ||||
|         _forceHideHeader = forceHideHeader; | ||||
|  | ||||
|         if (self.header.parentNode) { | ||||
|             self.header.hidden = YES; | ||||
|             [self.header removeFromParentNode]; | ||||
|         } else { | ||||
|             self.header.hidden = NO; | ||||
|             [self.snapshot addChildNode:self.header]; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,62 @@ | ||||
| // | ||||
| //  SceneKit+Snapshot.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/8/20. | ||||
| // | ||||
|  | ||||
| #import <SceneKit/SceneKit.h> | ||||
| #import "FHSViewSnapshot.h" | ||||
| @class FHSSnapshotNodes; | ||||
|  | ||||
| extern CGFloat const kFHSSmallZOffset; | ||||
|  | ||||
| #pragma mark SCNNode | ||||
| @interface SCNNode (Snapshot) | ||||
|  | ||||
| /// @return the nearest ancestor snapshot node starting at this node | ||||
| @property (nonatomic, readonly) SCNNode *nearestAncestorSnapshot; | ||||
|  | ||||
| /// @return a node that renders a highlight overlay over a specified snapshot | ||||
| + (instancetype)highlight:(FHSViewSnapshot *)view color:(UIColor *)color; | ||||
| /// @return a node that renders a snapshot image | ||||
| + (instancetype)snapshot:(FHSViewSnapshot *)view; | ||||
| /// @return a node that draws a line between two vertices | ||||
| + (instancetype)lineFrom:(SCNVector3)v1 to:(SCNVector3)v2 color:(UIColor *)lineColor; | ||||
|  | ||||
| /// @return a node that can be used to render a colored border around the specified node | ||||
| - (instancetype)borderWithColor:(UIColor *)color; | ||||
| /// @return a node that renders a header above a snapshot node | ||||
| ///         using the title text from the view, if specified | ||||
| + (instancetype)header:(FHSViewSnapshot *)view; | ||||
|  | ||||
| /// @return a SceneKit node that recursively renders a hierarchy | ||||
| ///         of UI elements starting at the specified snapshot | ||||
| + (instancetype)snapshot:(FHSViewSnapshot *)view | ||||
|                   parent:(FHSViewSnapshot *)parentView | ||||
|               parentNode:(SCNNode *)parentNode | ||||
|                     root:(SCNNode *)rootNode | ||||
|                    depth:(NSInteger *)depthOut | ||||
|                 nodesMap:(NSMutableDictionary<NSString *, FHSSnapshotNodes *> *)nodesMap | ||||
|              hideHeaders:(BOOL)hideHeaders; | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark SCNShape | ||||
| @interface SCNShape (Snapshot) | ||||
| /// @return a shape with the given path, 0 extrusion depth, and a double-sided | ||||
| ///         material with the given diffuse contents inserted at index 0 | ||||
| + (instancetype)shapeWithPath:(UIBezierPath *)path materialDiffuse:(id)contents; | ||||
| /// @return a shape that is used to render the background of the snapshot header | ||||
| + (instancetype)nameHeader:(UIColor *)color frame:(CGRect)frame corners:(CGFloat)cornerRadius; | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark SCNText | ||||
| @interface SCNText (Snapshot) | ||||
| /// @return text geometry used to render text inside the snapshot header | ||||
| + (instancetype)labelGeometry:(NSString *)text font:(UIFont *)font; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,278 @@ | ||||
| // | ||||
| //  SceneKit+Snapshot.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/8/20. | ||||
| // | ||||
|  | ||||
| #import "SceneKit+Snapshot.h" | ||||
| #import "FHSSnapshotNodes.h" | ||||
|  | ||||
| /// This value is chosen such that this offset can be applied to avoid | ||||
| /// z-fighting amongst nodes at the same z-position, but small enough | ||||
| /// that they appear to visually be on the same plane. | ||||
| CGFloat const kFHSSmallZOffset = 0.05; | ||||
| CGFloat const kHeaderVerticalInset = 8.0; | ||||
|  | ||||
| #pragma mark SCNGeometry | ||||
| @interface SCNGeometry (SnapshotPrivate) | ||||
| @end | ||||
| @implementation SCNGeometry (SnapshotPrivate) | ||||
|  | ||||
| - (void)addDoubleSidedMaterialWithDiffuseContents:(id)contents { | ||||
|     SCNMaterial *material = [SCNMaterial new]; | ||||
|     material.doubleSided = YES; | ||||
|     material.diffuse.contents = contents; | ||||
|     [self insertMaterial:material atIndex:0]; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| #pragma mark SCNNode | ||||
| @implementation SCNNode (Snapshot) | ||||
|  | ||||
| - (SCNNode *)nearestAncestorSnapshot { | ||||
|     SCNNode *node = self; | ||||
|  | ||||
|     while (!node.name && node) { | ||||
|         node = node.parentNode; | ||||
|     } | ||||
|  | ||||
|     return node; | ||||
| } | ||||
|  | ||||
| + (instancetype)shapeNodeWithSize:(CGSize)size materialDiffuse:(id)contents offsetZ:(BOOL)offsetZ { | ||||
|     UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake( | ||||
|         0, 0, size.width, size.height | ||||
|     )]; | ||||
|     SCNShape *shape = [SCNShape shapeWithPath:path materialDiffuse:contents]; | ||||
|     SCNNode *node = [SCNNode nodeWithGeometry:shape]; | ||||
|      | ||||
|     if (offsetZ) { | ||||
|         node.position = SCNVector3Make(0, 0, kFHSSmallZOffset); | ||||
|     } | ||||
|     return node; | ||||
| } | ||||
|  | ||||
| + (instancetype)highlight:(FHSViewSnapshot *)view color:(UIColor *)color { | ||||
|     return [self shapeNodeWithSize:view.frame.size materialDiffuse:color offsetZ:YES]; | ||||
| } | ||||
|  | ||||
| + (instancetype)snapshot:(FHSViewSnapshot *)view { | ||||
|     id image = view.snapshotImage; | ||||
|     return [self shapeNodeWithSize:view.frame.size materialDiffuse:image offsetZ:NO]; | ||||
| } | ||||
|  | ||||
| + (instancetype)lineFrom:(SCNVector3)v1 to:(SCNVector3)v2 color:(UIColor *)lineColor { | ||||
|     SCNVector3 vertices[2] = { v1, v2 }; | ||||
|     int32_t _indices[2] = { 0, 1 }; | ||||
|     NSData *indices = [NSData dataWithBytes:_indices length:sizeof(_indices)]; | ||||
|      | ||||
|     SCNGeometrySource *source = [SCNGeometrySource geometrySourceWithVertices:vertices count:2]; | ||||
|     SCNGeometryElement *element = [SCNGeometryElement | ||||
|         geometryElementWithData:indices | ||||
|         primitiveType:SCNGeometryPrimitiveTypeLine | ||||
|         primitiveCount:2 | ||||
|         bytesPerIndex:sizeof(int32_t) | ||||
|     ]; | ||||
|  | ||||
|     SCNGeometry *geometry = [SCNGeometry geometryWithSources:@[source] elements:@[element]]; | ||||
|     [geometry addDoubleSidedMaterialWithDiffuseContents:lineColor]; | ||||
|     return [SCNNode nodeWithGeometry:geometry]; | ||||
| } | ||||
|  | ||||
| - (instancetype)borderWithColor:(UIColor *)color { | ||||
|     struct { SCNVector3 min, max; } bb; | ||||
|     [self getBoundingBoxMin:&bb.min max:&bb.max]; | ||||
|  | ||||
|     SCNVector3 topLeft = SCNVector3Make(bb.min.x, bb.max.y, kFHSSmallZOffset); | ||||
|     SCNVector3 bottomLeft = SCNVector3Make(bb.min.x, bb.min.y, kFHSSmallZOffset); | ||||
|     SCNVector3 topRight = SCNVector3Make(bb.max.x, bb.max.y, kFHSSmallZOffset); | ||||
|     SCNVector3 bottomRight = SCNVector3Make(bb.max.x, bb.min.y, kFHSSmallZOffset); | ||||
|  | ||||
|     SCNNode *top = [SCNNode lineFrom:topLeft to:topRight color:color]; | ||||
|     SCNNode *left = [SCNNode lineFrom:bottomLeft to:topLeft color:color]; | ||||
|     SCNNode *bottom = [SCNNode lineFrom:bottomLeft to:bottomRight color:color]; | ||||
|     SCNNode *right = [SCNNode lineFrom:bottomRight to:topRight color:color]; | ||||
|  | ||||
|     SCNNode *border = [SCNNode new]; | ||||
|     [border addChildNode:top]; | ||||
|     [border addChildNode:left]; | ||||
|     [border addChildNode:bottom]; | ||||
|     [border addChildNode:right]; | ||||
|  | ||||
|     return border; | ||||
| } | ||||
|  | ||||
| + (instancetype)header:(FHSViewSnapshot *)view { | ||||
|     SCNText *text = [SCNText labelGeometry:view.title font:[UIFont boldSystemFontOfSize:13.0]]; | ||||
|     SCNNode *textNode = [SCNNode nodeWithGeometry:text]; | ||||
|  | ||||
|     struct { SCNVector3 min, max; } bb; | ||||
|     [textNode getBoundingBoxMin:&bb.min max:&bb.max]; | ||||
|     CGFloat textWidth = bb.max.x - bb.min.x; | ||||
|     CGFloat textHeight = bb.max.y - bb.min.y; | ||||
|  | ||||
|     CGFloat snapshotWidth = view.frame.size.width; | ||||
|     CGFloat headerWidth = MAX(snapshotWidth, textWidth); | ||||
|     CGRect frame = CGRectMake(0, 0, headerWidth, textHeight + (kHeaderVerticalInset * 2)); | ||||
|     SCNNode *headerNode = [SCNNode nodeWithGeometry:[SCNShape | ||||
|         nameHeader:view.headerColor frame:frame corners:8 | ||||
|     ]]; | ||||
|     [headerNode addChildNode:textNode]; | ||||
|  | ||||
|     textNode.position = SCNVector3Make( | ||||
|         (frame.size.width / 2.f) - (textWidth / 2.f), | ||||
|         (frame.size.height / 2.f) - (textHeight / 2.f), | ||||
|         kFHSSmallZOffset | ||||
|     ); | ||||
|     headerNode.position = SCNVector3Make( | ||||
|        (snapshotWidth / 2.f) - (headerWidth / 2.f), | ||||
|        view.frame.size.height, | ||||
|        kFHSSmallZOffset | ||||
|     ); | ||||
|  | ||||
|     return headerNode; | ||||
| } | ||||
|  | ||||
| + (instancetype)snapshot:(FHSViewSnapshot *)view | ||||
|                   parent:(FHSViewSnapshot *)parent | ||||
|               parentNode:(SCNNode *)parentNode | ||||
|                     root:(SCNNode *)rootNode | ||||
|                    depth:(NSInteger *)depthOut | ||||
|                 nodesMap:(NSMutableDictionary<NSString *, FHSSnapshotNodes *> *)nodesMap | ||||
|              hideHeaders:(BOOL)hideHeaders { | ||||
|     NSInteger const depth = *depthOut; | ||||
|  | ||||
|     // Ignore elements that are not visible. | ||||
|     // These should appear in the list, but not in the 3D view. | ||||
|     if (view.hidden || CGSizeEqualToSize(view.frame.size, CGSizeZero)) { | ||||
|         return nil; | ||||
|     } | ||||
|  | ||||
|     // Create a node whose contents are the snapshot of the element | ||||
|     SCNNode *node = [self snapshot:view]; | ||||
|     node.name = view.view.identifier; | ||||
|  | ||||
|     // Begin building node tree | ||||
|     FHSSnapshotNodes *nodes = [FHSSnapshotNodes snapshot:view depth:depth]; | ||||
|     nodes.snapshot = node; | ||||
|  | ||||
|     // The node must be added to the root node | ||||
|     // for the coordinate space calculations below to work | ||||
|     [rootNode addChildNode:node]; | ||||
|     node.position = ({ | ||||
|         // Flip the y-coordinate since SceneKit has a | ||||
|         // flipped version of the UIKit coordinate system | ||||
|         CGRect pframe = parent ? parent.frame : CGRectZero; | ||||
|         CGFloat y = parent ? pframe.size.height - CGRectGetMaxY(view.frame) : 0; | ||||
|  | ||||
|         // To simplify calculating the z-axis spacing between the layers, we make | ||||
|         // each snapshot node a direct child of the root rather than embedding | ||||
|         // the nodes in their parent nodes in the same structure as the UI elements | ||||
|         // themselves. With this flattened hierarchy, the z-position can be | ||||
|         // calculated for every node simply by multiplying the spacing by the depth. | ||||
|         // | ||||
|         // `parentSnapshotNode` as referenced here is NOT the actual parent node | ||||
|         // of `node`, it is the node corresponding to the parent of the UI element. | ||||
|         // It is used to convert from frame coordinates, which are relative to | ||||
|         // the bounds of the parent, to coordinates relative to the root node. | ||||
|         SCNVector3 positionRelativeToParent = SCNVector3Make(view.frame.origin.x, y, 0); | ||||
|         SCNVector3 positionRelativeToRoot; | ||||
|         if (parent) { | ||||
|             positionRelativeToRoot = [rootNode convertPosition:positionRelativeToParent fromNode:parentNode]; | ||||
|         } else { | ||||
|             positionRelativeToRoot = positionRelativeToParent; | ||||
|         } | ||||
|         positionRelativeToRoot.z = 50 * depth; | ||||
|         positionRelativeToRoot; | ||||
|     }); | ||||
|  | ||||
|     // Make border node | ||||
|     nodes.border = [node borderWithColor:view.headerColor]; | ||||
|     [node addChildNode:nodes.border]; | ||||
|  | ||||
|     // Make header node | ||||
|     nodes.header = [SCNNode header:view]; | ||||
|     [node addChildNode:nodes.header]; | ||||
|     if (hideHeaders) { | ||||
|         nodes.header.hidden = YES; | ||||
|     } | ||||
|  | ||||
|     nodesMap[view.view.identifier] = nodes; | ||||
|  | ||||
|     NSMutableArray<FHSViewSnapshot *> *checkForIntersect = [NSMutableArray new]; | ||||
|     NSInteger maxChildDepth = depth; | ||||
|  | ||||
|     // Recurse to child nodes; overlapping children have higher depths | ||||
|     for (FHSViewSnapshot *child in view.children) { | ||||
|         NSInteger childDepth = depth + 1; | ||||
|  | ||||
|         // Children that intersect a sibling are rendered | ||||
|         // in a separate layer above the previous siblings | ||||
|         for (FHSViewSnapshot *sibling in checkForIntersect) { | ||||
|             if (CGRectIntersectsRect(sibling.frame, child.frame)) { | ||||
|                 childDepth = maxChildDepth + 1; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         id didMakeNode = [SCNNode | ||||
|             snapshot:child | ||||
|             parent:view | ||||
|             parentNode:node | ||||
|             root:rootNode | ||||
|             depth:&childDepth | ||||
|             nodesMap:nodesMap | ||||
|             hideHeaders:hideHeaders | ||||
|         ]; | ||||
|         if (didMakeNode) { | ||||
|             maxChildDepth = MAX(childDepth, maxChildDepth); | ||||
|             [checkForIntersect addObject:child]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     *depthOut = maxChildDepth; | ||||
|     return node; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark SCNShape | ||||
| @implementation SCNShape (Snapshot) | ||||
|  | ||||
| + (instancetype)shapeWithPath:(UIBezierPath *)path materialDiffuse:(id)contents { | ||||
|     SCNShape *shape = [SCNShape shapeWithPath:path extrusionDepth:0]; | ||||
|     [shape addDoubleSidedMaterialWithDiffuseContents:contents]; | ||||
|     return shape; | ||||
| } | ||||
|  | ||||
| + (instancetype)nameHeader:(UIColor *)color frame:(CGRect)frame corners:(CGFloat)radius { | ||||
|     UIBezierPath *path = [UIBezierPath | ||||
|         bezierPathWithRoundedRect:frame | ||||
|         byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight | ||||
|         cornerRadii:CGSizeMake(radius, radius) | ||||
|     ]; | ||||
|     return [SCNShape shapeWithPath:path materialDiffuse:color]; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark SCNText | ||||
| @implementation SCNText (Snapshot) | ||||
|  | ||||
| + (instancetype)labelGeometry:(NSString *)text font:(UIFont *)font { | ||||
|     NSParameterAssert(text); | ||||
|  | ||||
|     SCNText *label = [self new]; | ||||
|     label.string = text; | ||||
|     label.font = font; | ||||
|     label.alignmentMode = kCAAlignmentCenter; | ||||
|     label.truncationMode = kCATruncationEnd; | ||||
|  | ||||
|     return label; | ||||
| } | ||||
|  | ||||
| @end | ||||
		Reference in New Issue
	
	Block a user
	 Balackburn
					Balackburn