mirror of
https://github.com/SoPat712/YTLitePlus.git
synced 2025-08-24 11:28:52 -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