mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-30 20:34:03 -04:00 
			
		
		
		
	added files via upload
This commit is contained in:
		
							
								
								
									
										26
									
								
								Tweaks/FLEX/ViewHierarchy/FLEXHierarchyViewController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Tweaks/FLEX/ViewHierarchy/FLEXHierarchyViewController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| // | ||||
| //  FLEXHierarchyViewController.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/9/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXNavigationController.h" | ||||
|  | ||||
| @protocol FLEXHierarchyDelegate <NSObject> | ||||
| - (void)viewHierarchyDidDismiss:(UIView *)selectedView; | ||||
| @end | ||||
|  | ||||
| /// A navigation controller which manages two child view controllers: | ||||
| /// a 3D Reveal-like hierarchy explorer, and a 2D tree-list hierarchy explorer. | ||||
| @interface FLEXHierarchyViewController : FLEXNavigationController | ||||
|  | ||||
| + (instancetype)delegate:(id<FLEXHierarchyDelegate>)delegate; | ||||
| + (instancetype)delegate:(id<FLEXHierarchyDelegate>)delegate | ||||
|               viewsAtTap:(NSArray<UIView *> *)viewsAtTap | ||||
|             selectedView:(UIView *)selectedView; | ||||
|  | ||||
| - (void)toggleHierarchyMode; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										154
									
								
								Tweaks/FLEX/ViewHierarchy/FLEXHierarchyViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								Tweaks/FLEX/ViewHierarchy/FLEXHierarchyViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,154 @@ | ||||
| // | ||||
| //  FLEXHierarchyViewController.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 1/9/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXHierarchyViewController.h" | ||||
| #import "FLEXHierarchyTableViewController.h" | ||||
| #import "FHSViewController.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXTabList.h" | ||||
| #import "FLEXResources.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
|  | ||||
| typedef NS_ENUM(NSUInteger, FLEXHierarchyViewMode) { | ||||
|     FLEXHierarchyViewModeTree = 1, | ||||
|     FLEXHierarchyViewMode3DSnapshot | ||||
| }; | ||||
|  | ||||
| @interface FLEXHierarchyViewController () | ||||
| @property (nonatomic, readonly, weak) id<FLEXHierarchyDelegate> hierarchyDelegate; | ||||
| @property (nonatomic, readonly) FHSViewController *snapshotViewController; | ||||
| @property (nonatomic, readonly) FLEXHierarchyTableViewController *treeViewController; | ||||
|  | ||||
| @property (nonatomic) FLEXHierarchyViewMode mode; | ||||
|  | ||||
| @property (nonatomic, readonly) UIView *selectedView; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXHierarchyViewController | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (instancetype)delegate:(id<FLEXHierarchyDelegate>)delegate { | ||||
|     return [self delegate:delegate viewsAtTap:nil selectedView:nil]; | ||||
| } | ||||
|  | ||||
| + (instancetype)delegate:(id<FLEXHierarchyDelegate>)delegate | ||||
|               viewsAtTap:(NSArray<UIView *> *)viewsAtTap | ||||
|             selectedView:(UIView *)selectedView { | ||||
|     return [[self alloc] initWithDelegate:delegate viewsAtTap:viewsAtTap selectedView:selectedView]; | ||||
| } | ||||
|  | ||||
| - (id)initWithDelegate:(id)delegate viewsAtTap:(NSArray<UIView *> *)viewsAtTap selectedView:(UIView *)view { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         NSArray<UIWindow *> *allWindows = FLEXUtility.allWindows; | ||||
|         _hierarchyDelegate = delegate; | ||||
|         _treeViewController = [FLEXHierarchyTableViewController | ||||
|             windows:allWindows viewsAtTap:viewsAtTap selectedView:view | ||||
|         ]; | ||||
|  | ||||
|         if (viewsAtTap) { | ||||
|             _snapshotViewController = [FHSViewController snapshotViewsAtTap:viewsAtTap selectedView:view]; | ||||
|         } else { | ||||
|             _snapshotViewController = [FHSViewController snapshotWindows:allWindows]; | ||||
|         } | ||||
|  | ||||
|         self.modalPresentationStyle = UIModalPresentationFullScreen; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Lifecycle | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|  | ||||
|     // 3D toggle button | ||||
|     self.treeViewController.navigationItem.leftBarButtonItem = [UIBarButtonItem | ||||
|         flex_itemWithImage:FLEXResources.toggle3DIcon target:self action:@selector(toggleHierarchyMode) | ||||
|     ]; | ||||
|  | ||||
|     // Dismiss when tree view row is selected | ||||
|     __weak id<FLEXHierarchyDelegate> delegate = self.hierarchyDelegate; | ||||
|     self.treeViewController.didSelectRowAction = ^(UIView *selectedView) { | ||||
|         [delegate viewHierarchyDidDismiss:selectedView]; | ||||
|     }; | ||||
|  | ||||
|     // Start of in tree view | ||||
|     _mode = FLEXHierarchyViewModeTree; | ||||
|     [self pushViewController:self.treeViewController animated:NO]; | ||||
| } | ||||
|  | ||||
| - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated { | ||||
|     // Done button: manually added here because the hierarhcy screens need to actually pass | ||||
|     // data back to the explorer view controller so that it can highlight selected views | ||||
|     viewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] | ||||
|         initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(donePressed) | ||||
|     ]; | ||||
|  | ||||
|     [super pushViewController:viewController animated:animated]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (void)donePressed { | ||||
|     // We need to manually close ourselves here because | ||||
|     // FLEXNavigationController doesn't ever close tabs itself  | ||||
|     [FLEXTabList.sharedList closeTab:self]; | ||||
|     [self.hierarchyDelegate viewHierarchyDidDismiss:self.selectedView]; | ||||
| } | ||||
|  | ||||
| - (void)toggleHierarchyMode { | ||||
|     switch (self.mode) { | ||||
|         case FLEXHierarchyViewModeTree: | ||||
|             self.mode = FLEXHierarchyViewMode3DSnapshot; | ||||
|             break; | ||||
|         case FLEXHierarchyViewMode3DSnapshot: | ||||
|             self.mode = FLEXHierarchyViewModeTree; | ||||
|             break; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setMode:(FLEXHierarchyViewMode)mode { | ||||
|     if (mode != _mode) { | ||||
|         // The tree view controller is our top stack view controller, and | ||||
|         // changing the mode simply pushes the snapshot view. In the future, | ||||
|         // I would like to have the 3D toggle button transparently switch | ||||
|         // between two views instead of pushing a new view controller. | ||||
|         // This way the views should share the search controller somehow. | ||||
|         switch (mode) { | ||||
|             case FLEXHierarchyViewModeTree: | ||||
|                 [self popViewControllerAnimated:NO]; | ||||
|                 self.toolbarHidden = YES; | ||||
|                 self.treeViewController.selectedView = self.selectedView; | ||||
|                 break; | ||||
|             case FLEXHierarchyViewMode3DSnapshot: | ||||
|                 [self pushViewController:self.snapshotViewController animated:NO]; | ||||
|                 self.toolbarHidden = NO; | ||||
|                 self.snapshotViewController.selectedView = self.selectedView; | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         // Change this last so that self.selectedView works right above | ||||
|         _mode = mode; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (UIView *)selectedView { | ||||
|     switch (self.mode) { | ||||
|         case FLEXHierarchyViewModeTree: | ||||
|             return self.treeViewController.selectedView; | ||||
|         case FLEXHierarchyViewMode3DSnapshot: | ||||
|             return self.snapshotViewController.selectedView; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										17
									
								
								Tweaks/FLEX/ViewHierarchy/FLEXImagePreviewViewController.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Tweaks/FLEX/ViewHierarchy/FLEXImagePreviewViewController.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  FLEXImagePreviewViewController.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/12/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @interface FLEXImagePreviewViewController : UIViewController | ||||
|  | ||||
| + (instancetype)previewForView:(UIView *)view; | ||||
| + (instancetype)previewForLayer:(CALayer *)layer; | ||||
| + (instancetype)forImage:(UIImage *)image; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										147
									
								
								Tweaks/FLEX/ViewHierarchy/FLEXImagePreviewViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								Tweaks/FLEX/ViewHierarchy/FLEXImagePreviewViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| // | ||||
| //  FLEXImagePreviewViewController.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 6/12/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXImagePreviewViewController.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXColor.h" | ||||
| #import "FLEXResources.h" | ||||
|  | ||||
| @interface FLEXImagePreviewViewController () <UIScrollViewDelegate> | ||||
| @property (nonatomic) UIImage *image; | ||||
| @property (nonatomic) UIScrollView *scrollView; | ||||
| @property (nonatomic) UIImageView *imageView; | ||||
| @property (nonatomic) UITapGestureRecognizer *bgColorTapGesture; | ||||
| @property (nonatomic) NSInteger backgroundColorIndex; | ||||
| @property (nonatomic, readonly) NSArray<UIColor *> *backgroundColors; | ||||
| @end | ||||
|  | ||||
| #pragma mark - | ||||
| @implementation FLEXImagePreviewViewController | ||||
|  | ||||
| #pragma mark Initialization | ||||
|  | ||||
| + (instancetype)previewForView:(UIView *)view { | ||||
|     return [self forImage:[FLEXUtility previewImageForView:view]]; | ||||
| } | ||||
|  | ||||
| + (instancetype)previewForLayer:(CALayer *)layer { | ||||
|     return [self forImage:[FLEXUtility previewImageForLayer:layer]]; | ||||
| } | ||||
|  | ||||
| + (instancetype)forImage:(UIImage *)image { | ||||
|     return [[self alloc] initWithImage:image]; | ||||
| } | ||||
|  | ||||
| - (id)initWithImage:(UIImage *)image { | ||||
|     NSParameterAssert(image); | ||||
|      | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         self.title = @"Preview"; | ||||
|         self.image = image; | ||||
|         _backgroundColors = @[FLEXResources.checkerPatternColor, UIColor.whiteColor, UIColor.blackColor]; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Lifecycle | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|      | ||||
|     self.imageView = [[UIImageView alloc] initWithImage:self.image]; | ||||
|     self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds]; | ||||
|     self.scrollView.delegate = self; | ||||
|     self.scrollView.backgroundColor = self.backgroundColors.firstObject; | ||||
|     self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; | ||||
|     [self.scrollView addSubview:self.imageView]; | ||||
|     self.scrollView.contentSize = self.imageView.frame.size; | ||||
|     self.scrollView.minimumZoomScale = 1.0; | ||||
|     self.scrollView.maximumZoomScale = 2.0; | ||||
|     [self.view addSubview:self.scrollView]; | ||||
|      | ||||
|     self.bgColorTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(changeBackground)]; | ||||
|     [self.scrollView addGestureRecognizer:self.bgColorTapGesture]; | ||||
|      | ||||
|     self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] | ||||
|         initWithBarButtonSystemItem:UIBarButtonSystemItemAction | ||||
|         target:self | ||||
|         action:@selector(actionButtonPressed:) | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLayoutSubviews { | ||||
|     [self centerContentInScrollViewIfNeeded]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark UIScrollViewDelegate | ||||
|  | ||||
| - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { | ||||
|     return self.imageView; | ||||
| } | ||||
|  | ||||
| - (void)scrollViewDidZoom:(UIScrollView *)scrollView { | ||||
|     [self centerContentInScrollViewIfNeeded]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Private | ||||
|  | ||||
| - (void)centerContentInScrollViewIfNeeded { | ||||
|     CGFloat horizontalInset = 0.0; | ||||
|     CGFloat verticalInset = 0.0; | ||||
|     if (self.scrollView.contentSize.width < self.scrollView.bounds.size.width) { | ||||
|         horizontalInset = (self.scrollView.bounds.size.width - self.scrollView.contentSize.width) / 2.0; | ||||
|     } | ||||
|     if (self.scrollView.contentSize.height < self.scrollView.bounds.size.height) { | ||||
|         verticalInset = (self.scrollView.bounds.size.height - self.scrollView.contentSize.height) / 2.0; | ||||
|     } | ||||
|     self.scrollView.contentInset = UIEdgeInsetsMake(verticalInset, horizontalInset, verticalInset, horizontalInset); | ||||
| } | ||||
|  | ||||
| - (void)changeBackground { | ||||
|     self.backgroundColorIndex++; | ||||
|     self.backgroundColorIndex %= self.backgroundColors.count; | ||||
|     self.scrollView.backgroundColor = self.backgroundColors[self.backgroundColorIndex]; | ||||
| } | ||||
|  | ||||
| - (void)actionButtonPressed:(id)sender { | ||||
|     static BOOL canSaveToCameraRoll = NO, didShowWarning = NO; | ||||
|     static dispatch_once_t onceToken; | ||||
|     dispatch_once(&onceToken, ^{ | ||||
|         if (UIDevice.currentDevice.systemVersion.floatValue < 10) { | ||||
|             canSaveToCameraRoll = YES; | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         NSBundle *mainBundle = NSBundle.mainBundle; | ||||
|         if ([mainBundle.infoDictionary.allKeys containsObject:@"NSPhotoLibraryUsageDescription"]) { | ||||
|             canSaveToCameraRoll = YES; | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:@[self.image] applicationActivities:@[]]; | ||||
|      | ||||
|     if (!canSaveToCameraRoll && !didShowWarning) { | ||||
|         didShowWarning = YES; | ||||
|         NSString *msg = @"Add 'NSPhotoLibraryUsageDescription' to this app's Info.plist to save images."; | ||||
|         [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|             make.title(@"Reminder").message(msg); | ||||
|             make.button(@"OK").handler(^(NSArray<NSString *> *strings) { | ||||
|                 [self presentViewController:activityVC animated:YES completion:nil]; | ||||
|             }); | ||||
|         } showFrom:self]; | ||||
|     } else { | ||||
|         [self presentViewController:activityVC animated:YES completion:nil]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										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 | ||||
| @@ -0,0 +1,19 @@ | ||||
| // | ||||
| //  FLEXHierarchyTableViewCell.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 2014-05-02. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <UIKit/UIKit.h> | ||||
|  | ||||
| @interface FLEXHierarchyTableViewCell : UITableViewCell | ||||
|  | ||||
| - (id)initWithReuseIdentifier:(NSString *)reuseIdentifier; | ||||
|  | ||||
| @property (nonatomic) NSInteger viewDepth; | ||||
| @property (nonatomic) UIColor *randomColorTag; | ||||
| @property (nonatomic) UIColor *indicatedViewColor; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,169 @@ | ||||
| // | ||||
| //  FLEXHierarchyTableViewCell.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 2014-05-02. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXHierarchyTableViewCell.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXResources.h" | ||||
| #import "FLEXColor.h" | ||||
|  | ||||
| @interface FLEXHierarchyTableViewCell () | ||||
|  | ||||
| /// Indicates how deep the view is in the hierarchy | ||||
| @property (nonatomic) UIView *depthIndicatorView; | ||||
| /// Holds the color that visually distinguishes views from one another | ||||
| @property (nonatomic) UIImageView *colorCircleImageView; | ||||
| /// A checker-patterned view, used to help show the color of a view, like a photoshop canvas | ||||
| @property (nonatomic) UIView *backgroundColorCheckerPatternView; | ||||
| /// The subview of the checker pattern view which holds the actual color of the view | ||||
| @property (nonatomic) UIView *viewBackgroundColorView; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXHierarchyTableViewCell | ||||
|  | ||||
| - (id)initWithReuseIdentifier:(NSString *)reuseIdentifier { | ||||
|     return [self initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier]; | ||||
| } | ||||
|  | ||||
| - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { | ||||
|     self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; | ||||
|     if (self) { | ||||
|         self.depthIndicatorView = [UIView new]; | ||||
|         self.depthIndicatorView.backgroundColor = FLEXUtility.hierarchyIndentPatternColor; | ||||
|         [self.contentView addSubview:self.depthIndicatorView]; | ||||
|          | ||||
|         UIImage *defaultCircleImage = [FLEXUtility circularImageWithColor:UIColor.blackColor radius:5]; | ||||
|         self.colorCircleImageView = [[UIImageView alloc] initWithImage:defaultCircleImage]; | ||||
|         [self.contentView addSubview:self.colorCircleImageView]; | ||||
|          | ||||
|         self.textLabel.font = UIFont.flex_defaultTableCellFont; | ||||
|         self.detailTextLabel.font = UIFont.flex_defaultTableCellFont; | ||||
|         self.accessoryType = UITableViewCellAccessoryDetailButton; | ||||
|          | ||||
|         // Use a pattern-based color to simplify application of the checker pattern | ||||
|         static UIColor *checkerPatternColor = nil; | ||||
|         static dispatch_once_t once; | ||||
|         dispatch_once(&once, ^{ | ||||
|             checkerPatternColor = [UIColor colorWithPatternImage:FLEXResources.checkerPattern]; | ||||
|         }); | ||||
|          | ||||
|         self.backgroundColorCheckerPatternView = [UIView new]; | ||||
|         self.backgroundColorCheckerPatternView.clipsToBounds = YES; | ||||
|         self.backgroundColorCheckerPatternView.layer.borderColor = FLEXColor.tertiaryBackgroundColor.CGColor; | ||||
|         self.backgroundColorCheckerPatternView.layer.borderWidth = 2.f / UIScreen.mainScreen.scale; | ||||
|         self.backgroundColorCheckerPatternView.backgroundColor = checkerPatternColor; | ||||
|         [self.contentView addSubview:self.backgroundColorCheckerPatternView]; | ||||
|         self.viewBackgroundColorView = [UIView new]; | ||||
|         [self.backgroundColorCheckerPatternView addSubview:self.viewBackgroundColorView]; | ||||
|     } | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated { | ||||
|     UIColor *originalColour = self.viewBackgroundColorView.backgroundColor; | ||||
|     [super setHighlighted:highlighted animated:animated]; | ||||
|      | ||||
|     // UITableViewCell changes all subviews in the contentView to backgroundColor = clearColor. | ||||
|     // We want to preserve the hierarchy background color when highlighted. | ||||
|     self.depthIndicatorView.backgroundColor = FLEXUtility.hierarchyIndentPatternColor; | ||||
|      | ||||
|     self.viewBackgroundColorView.backgroundColor = originalColour; | ||||
| } | ||||
|  | ||||
| - (void)setSelected:(BOOL)selected animated:(BOOL)animated { | ||||
|     UIColor *originalColour = self.viewBackgroundColorView.backgroundColor; | ||||
|     [super setSelected:selected animated:animated]; | ||||
|      | ||||
|     // See setHighlighted above. | ||||
|     self.depthIndicatorView.backgroundColor = FLEXUtility.hierarchyIndentPatternColor; | ||||
|      | ||||
|     self.viewBackgroundColorView.backgroundColor = originalColour; | ||||
| } | ||||
|  | ||||
| - (void)layoutSubviews { | ||||
|     [super layoutSubviews]; | ||||
|      | ||||
|     const CGFloat kContentPadding = 6; | ||||
|     const CGFloat kDepthIndicatorWidthMultiplier = 4; | ||||
|     const CGFloat kViewColorIndicatorSize = 22; | ||||
|      | ||||
|     const CGRect bounds = self.contentView.bounds; | ||||
|     const CGFloat centerY = CGRectGetMidY(bounds); | ||||
|     const CGFloat textLabelCenterY = CGRectGetMidY(self.textLabel.frame); | ||||
|      | ||||
|     BOOL hideCheckerView = self.backgroundColorCheckerPatternView.hidden; | ||||
|     CGFloat maxWidth = CGRectGetMaxX(bounds); | ||||
|     maxWidth -= (hideCheckerView ? kContentPadding : (kViewColorIndicatorSize + kContentPadding * 2)); | ||||
|      | ||||
|     CGRect depthIndicatorFrame = self.depthIndicatorView.frame = CGRectMake( | ||||
|         kContentPadding, 0, self.viewDepth * kDepthIndicatorWidthMultiplier, CGRectGetHeight(bounds) | ||||
|     ); | ||||
|      | ||||
|     // Circle goes after depth, and its center Y = textLabel's center Y | ||||
|     CGRect circleFrame = self.colorCircleImageView.frame; | ||||
|     circleFrame.origin.x = CGRectGetMaxX(depthIndicatorFrame) + kContentPadding; | ||||
|     circleFrame.origin.y = FLEXFloor(textLabelCenterY - CGRectGetHeight(circleFrame) / 2.f); | ||||
|     self.colorCircleImageView.frame = circleFrame; | ||||
|      | ||||
|     // Text label goes after random color circle, width extends to the edge | ||||
|     // of the contentView or to the padding before the color indicator view | ||||
|     CGRect textLabelFrame = self.textLabel.frame; | ||||
|     CGFloat textOriginX = CGRectGetMaxX(circleFrame) + kContentPadding; | ||||
|     textLabelFrame.origin.x = textOriginX; | ||||
|     textLabelFrame.size.width = maxWidth - textOriginX; | ||||
|     self.textLabel.frame = textLabelFrame; | ||||
|      | ||||
|     // detailTextLabel leading edge lines up with the circle, and the | ||||
|     // width extends to the same max X as the same max X as the textLabel | ||||
|     CGRect detailTextLabelFrame = self.detailTextLabel.frame; | ||||
|     CGFloat detailOriginX = circleFrame.origin.x; | ||||
|     detailTextLabelFrame.origin.x = detailOriginX; | ||||
|     detailTextLabelFrame.size.width = maxWidth - detailOriginX; | ||||
|     self.detailTextLabel.frame = detailTextLabelFrame; | ||||
|      | ||||
|     // Checker pattern view starts after the padding after the max X of textLabel, | ||||
|     // and is centered vertically within the entire contentView | ||||
|     self.backgroundColorCheckerPatternView.frame = CGRectMake( | ||||
|         CGRectGetMaxX(self.textLabel.frame) + kContentPadding, | ||||
|         centerY - kViewColorIndicatorSize / 2.f, | ||||
|         kViewColorIndicatorSize, | ||||
|         kViewColorIndicatorSize | ||||
|     ); | ||||
|      | ||||
|     // Background color view fills it's superview | ||||
|     self.viewBackgroundColorView.frame = self.backgroundColorCheckerPatternView.bounds; | ||||
|     self.backgroundColorCheckerPatternView.layer.cornerRadius = kViewColorIndicatorSize / 2.f; | ||||
| } | ||||
|  | ||||
| - (void)setRandomColorTag:(UIColor *)randomColorTag { | ||||
|     if (![_randomColorTag isEqual:randomColorTag]) { | ||||
|         _randomColorTag = randomColorTag; | ||||
|         self.colorCircleImageView.image = [FLEXUtility circularImageWithColor:randomColorTag radius:6]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)setViewDepth:(NSInteger)viewDepth { | ||||
|     if (_viewDepth != viewDepth) { | ||||
|         _viewDepth = viewDepth; | ||||
|         [self setNeedsLayout]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (UIColor *)indicatedViewColor { | ||||
|     return self.viewBackgroundColorView.backgroundColor; | ||||
| } | ||||
|  | ||||
| - (void)setIndicatedViewColor:(UIColor *)color { | ||||
|     self.viewBackgroundColorView.backgroundColor = color; | ||||
|      | ||||
|     // Hide the checker pattern view if there is no background color | ||||
|     self.backgroundColorCheckerPatternView.hidden = color == nil; | ||||
|     [self setNeedsLayout]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,20 @@ | ||||
| // | ||||
| //  FLEXHierarchyTableViewController.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 2014-05-01. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewController.h" | ||||
|  | ||||
| @interface FLEXHierarchyTableViewController : FLEXTableViewController | ||||
|  | ||||
| + (instancetype)windows:(NSArray<UIWindow *> *)allWindows | ||||
|              viewsAtTap:(NSArray<UIView *> *)viewsAtTap | ||||
|            selectedView:(UIView *)selectedView; | ||||
|  | ||||
| @property (nonatomic) UIView *selectedView; | ||||
| @property (nonatomic) void(^didSelectRowAction)(UIView *selectedView); | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,253 @@ | ||||
| // | ||||
| //  FLEXHierarchyTableViewController.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 2014-05-01. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXColor.h" | ||||
| #import "FLEXHierarchyTableViewController.h" | ||||
| #import "NSMapTable+FLEX_Subscripting.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXHierarchyTableViewCell.h" | ||||
| #import "FLEXObjectExplorerViewController.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXResources.h" | ||||
| #import "FLEXWindow.h" | ||||
|  | ||||
| typedef NS_ENUM(NSUInteger, FLEXHierarchyScope) { | ||||
|     FLEXHierarchyScopeFullHierarchy, | ||||
|     FLEXHierarchyScopeViewsAtTap | ||||
| }; | ||||
|  | ||||
| @interface FLEXHierarchyTableViewController () | ||||
|  | ||||
| @property (nonatomic) NSArray<UIView *> *allViews; | ||||
| @property (nonatomic) NSMapTable<UIView *, NSNumber *> *depthsForViews; | ||||
| @property (nonatomic) NSArray<UIView *> *viewsAtTap; | ||||
| @property (nonatomic) NSArray<UIView *> *displayedViews; | ||||
| @property (nonatomic, readonly) BOOL showScopeBar; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXHierarchyTableViewController | ||||
|  | ||||
| + (instancetype)windows:(NSArray<UIWindow *> *)allWindows | ||||
|              viewsAtTap:(NSArray<UIView *> *)viewsAtTap | ||||
|            selectedView:(UIView *)selected { | ||||
|     NSParameterAssert(allWindows.count); | ||||
|  | ||||
|     NSArray *allViews = [self allViewsInHierarchy:allWindows]; | ||||
|     NSMapTable *depths = [self hierarchyDepthsForViews:allViews]; | ||||
|     return [[self alloc] initWithViews:allViews viewsAtTap:viewsAtTap selectedView:selected depths:depths]; | ||||
| } | ||||
|  | ||||
| - (instancetype)initWithViews:(NSArray<UIView *> *)allViews | ||||
|                    viewsAtTap:(NSArray<UIView *> *)viewsAtTap | ||||
|                  selectedView:(UIView *)selectedView | ||||
|                        depths:(NSMapTable<UIView *, NSNumber *> *)depthsForViews { | ||||
|     NSParameterAssert(allViews); | ||||
|     NSParameterAssert(depthsForViews.count == allViews.count); | ||||
|  | ||||
|     self = [super initWithStyle:UITableViewStylePlain]; | ||||
|     if (self) { | ||||
|         self.allViews = allViews; | ||||
|         self.depthsForViews = depthsForViews; | ||||
|         self.viewsAtTap = viewsAtTap; | ||||
|         self.selectedView = selectedView; | ||||
|          | ||||
|         self.title = @"View Hierarchy Tree"; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|  | ||||
|     // Preserve selection between presentations | ||||
|     self.clearsSelectionOnViewWillAppear = NO; | ||||
|      | ||||
|     // A little more breathing room | ||||
|     self.tableView.rowHeight = 50.0; | ||||
|     self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; | ||||
|     // Separator inset clashes with persistent cell selection | ||||
|     [self.tableView setSeparatorInset:UIEdgeInsetsZero]; | ||||
|      | ||||
|     self.showsSearchBar = YES; | ||||
|     self.showSearchBarInitially = YES; | ||||
|     // Using pinSearchBar on this screen causes a weird visual | ||||
|     // thing on the next view controller that gets pushed. | ||||
|     // | ||||
|     // self.pinSearchBar = YES; | ||||
|     self.searchBarDebounceInterval = kFLEXDebounceInstant; | ||||
|     self.automaticallyShowsSearchBarCancelButton = NO; | ||||
|     if (self.showScopeBar) { | ||||
|         self.searchController.searchBar.showsScopeBar = YES; | ||||
|         self.searchController.searchBar.scopeButtonTitles = @[@"Full Hierarchy", @"Views at Tap"]; | ||||
|         self.selectedScope = FLEXHierarchyScopeViewsAtTap; | ||||
|     } | ||||
|      | ||||
|     [self updateDisplayedViews]; | ||||
| } | ||||
|  | ||||
| - (void)viewWillAppear:(BOOL)animated { | ||||
|     [super viewWillAppear:animated]; | ||||
|      | ||||
|     [self disableToolbar]; | ||||
| } | ||||
|  | ||||
| - (void)viewDidAppear:(BOOL)animated { | ||||
|     [super viewDidAppear:animated]; | ||||
|      | ||||
|     [self trySelectCellForSelectedView]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Hierarchy helpers | ||||
|  | ||||
| + (NSArray<UIView *> *)allViewsInHierarchy:(NSArray<UIWindow *> *)windows { | ||||
|     return [windows flex_flatmapped:^id(UIWindow *window, NSUInteger idx) { | ||||
|         if (![window isKindOfClass:[FLEXWindow class]]) { | ||||
|             return [self viewWithRecursiveSubviews:window]; | ||||
|         } | ||||
|  | ||||
|         return nil; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| + (NSArray<UIView *> *)viewWithRecursiveSubviews:(UIView *)view { | ||||
|     NSMutableArray<UIView *> *subviews = [NSMutableArray arrayWithObject:view]; | ||||
|     for (UIView *subview in view.subviews) { | ||||
|         [subviews addObjectsFromArray:[self viewWithRecursiveSubviews:subview]]; | ||||
|     } | ||||
|  | ||||
|     return subviews; | ||||
| } | ||||
|  | ||||
| + (NSMapTable<UIView *, NSNumber *> *)hierarchyDepthsForViews:(NSArray<UIView *> *)views { | ||||
|     NSMapTable<UIView *, NSNumber *> *depths = [NSMapTable strongToStrongObjectsMapTable]; | ||||
|     for (UIView *view in views) { | ||||
|         NSInteger depth = 0; | ||||
|         UIView *tryView = view; | ||||
|         while (tryView.superview) { | ||||
|             tryView = tryView.superview; | ||||
|             depth++; | ||||
|         } | ||||
|         depths[(id)view] = @(depth); | ||||
|     } | ||||
|  | ||||
|     return depths; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark Selection and Filtering Helpers | ||||
|  | ||||
| - (void)trySelectCellForSelectedView { | ||||
|     NSUInteger selectedViewIndex = [self.displayedViews indexOfObject:self.selectedView]; | ||||
|     if (selectedViewIndex != NSNotFound) { | ||||
|         UITableViewScrollPosition scrollPosition = UITableViewScrollPositionMiddle; | ||||
|         NSIndexPath *selectedViewIndexPath = [NSIndexPath indexPathForRow:selectedViewIndex inSection:0]; | ||||
|         [self.tableView selectRowAtIndexPath:selectedViewIndexPath animated:YES scrollPosition:scrollPosition]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)updateDisplayedViews { | ||||
|     NSArray<UIView *> *candidateViews = nil; | ||||
|     if (self.showScopeBar) { | ||||
|         if (self.selectedScope == FLEXHierarchyScopeViewsAtTap) { | ||||
|             candidateViews = self.viewsAtTap; | ||||
|         } else if (self.selectedScope == FLEXHierarchyScopeFullHierarchy) { | ||||
|             candidateViews = self.allViews; | ||||
|         } | ||||
|     } else { | ||||
|         candidateViews = self.allViews; | ||||
|     } | ||||
|      | ||||
|     if (self.searchText.length) { | ||||
|         self.displayedViews = [candidateViews filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UIView *candidateView, NSDictionary<NSString *, id> *bindings) { | ||||
|             NSString *title = [FLEXUtility descriptionForView:candidateView includingFrame:NO]; | ||||
|             NSString *candidateViewPointerAddress = [NSString stringWithFormat:@"%p", candidateView]; | ||||
|             BOOL matchedViewPointerAddress = [candidateViewPointerAddress rangeOfString:self.searchText options:NSCaseInsensitiveSearch].location != NSNotFound; | ||||
|             BOOL matchedViewTitle = [title rangeOfString:self.searchText options:NSCaseInsensitiveSearch].location != NSNotFound; | ||||
|             return matchedViewPointerAddress || matchedViewTitle; | ||||
|         }]]; | ||||
|     } else { | ||||
|         self.displayedViews = candidateViews; | ||||
|     } | ||||
|      | ||||
|     [self.tableView reloadData]; | ||||
| } | ||||
|  | ||||
| - (void)setSelectedView:(UIView *)selectedView { | ||||
|     _selectedView = selectedView; | ||||
|     if (self.isViewLoaded) { | ||||
|         [self trySelectCellForSelectedView]; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Search Bar / Scope Bar | ||||
|  | ||||
| - (BOOL)showScopeBar { | ||||
|     return self.viewsAtTap.count > 0; | ||||
| } | ||||
|  | ||||
| - (void)updateSearchResults:(NSString *)newText { | ||||
|     [self updateDisplayedViews]; | ||||
|      | ||||
|     // If the search bar text field is active, don't scroll on selection because we may want | ||||
|     // to continue typing. Otherwise, scroll so that the selected cell is visible. | ||||
|     if (!self.searchController.searchBar.isFirstResponder) { | ||||
|         [self trySelectCellForSelectedView]; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Table View Data Source | ||||
|  | ||||
| - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { | ||||
|     return self.displayedViews.count; | ||||
| } | ||||
|  | ||||
| - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     static NSString *CellIdentifier = @"Cell"; | ||||
|     FLEXHierarchyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; | ||||
|     if (!cell) { | ||||
|         cell = [[FLEXHierarchyTableViewCell alloc] initWithReuseIdentifier:CellIdentifier]; | ||||
|     } | ||||
|      | ||||
|     UIView *view = self.displayedViews[indexPath.row]; | ||||
|  | ||||
|     cell.textLabel.text = [FLEXUtility descriptionForView:view includingFrame:NO]; | ||||
|     cell.detailTextLabel.text = [FLEXUtility detailDescriptionForView:view]; | ||||
|     cell.randomColorTag = [FLEXUtility consistentRandomColorForObject:view]; | ||||
|     cell.viewDepth = self.depthsForViews[view].integerValue; | ||||
|     cell.indicatedViewColor = view.backgroundColor; | ||||
|  | ||||
|     if (view.isHidden || view.alpha < 0.01) { | ||||
|         cell.textLabel.textColor = FLEXColor.deemphasizedTextColor; | ||||
|         cell.detailTextLabel.textColor = FLEXColor.deemphasizedTextColor; | ||||
|     } else { | ||||
|         cell.textLabel.textColor = FLEXColor.primaryTextColor; | ||||
|         cell.detailTextLabel.textColor = FLEXColor.primaryTextColor; | ||||
|     } | ||||
|      | ||||
|     return cell; | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     _selectedView = self.displayedViews[indexPath.row]; // Don't scroll, avoid setter | ||||
|     if (self.didSelectRowAction) { | ||||
|         self.didSelectRowAction(_selectedView); | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath { | ||||
|     UIView *drillInView = self.displayedViews[indexPath.row]; | ||||
|     FLEXObjectExplorerViewController *viewExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:drillInView]; | ||||
|     [self.navigationController pushViewController:viewExplorer animated:YES]; | ||||
| } | ||||
|  | ||||
| @end | ||||
		Reference in New Issue
	
	Block a user
	 Balackburn
					Balackburn