// // 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 () /// An array of only the target views whose hierarchies /// we wish to snapshot, not every view in the snapshot. @property (nonatomic, readonly) NSArray *targetViews; @property (nonatomic, readonly) NSArray *views; @property (nonatomic ) NSArray *snapshots; @property (nonatomic, ) FHSSnapshotView *snapshotView; @property (nonatomic, readonly) UIView *containerView; @property (nonatomic, readonly) NSArray *viewsAtTap; @property (nonatomic, readonly) NSMutableSet *forceHideHeaders; @end @implementation FHSViewController @synthesize views = _views; @synthesize snapshotView = _snapshotView; #pragma mark - Initialization + (instancetype)snapshotWindows:(NSArray *)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 *)viewsAtTap selectedView:(UIView *)view { NSParameterAssert(viewsAtTap.count); NSParameterAssert(view.window); return [[self alloc] initWithViews:@[view.window] viewsAtTap:viewsAtTap selectedView:view]; } - (id)initWithViews:(NSArray *)views viewsAtTap:(NSArray *)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 #if !TARGET_OS_TV self.navigationItem.hidesBackButton = YES; #endif 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; #if !TARGET_OS_TV // 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]; #endif // 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 *strings) { [self.snapshotView hideView:[self snapshotForView:self.selectedView]]; }); make.button(@"Hide headers for views like this").handler(^(NSArray *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 *strings) { [self.snapshotView toggleShowHeaders]; }); make.button(@"Toggle outlines").handler(^(NSArray *strings) { [self.snapshotView toggleShowBorders]; }); make.button(@"Cancel").cancelStyle(); } showFrom:self source:sender]; } - (void)resizeToolbarItems:(CGSize)viewSize { #if !TARGET_OS_TV 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]; #endif } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; [coordinator animateAlongsideTransition:^(id _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