FHSView.m 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. //
  2. // FHSView.m
  3. // FLEX
  4. //
  5. // Created by Tanner Bennett on 1/6/20.
  6. //
  7. #import "FHSView.h"
  8. #import "FLEXUtility.h"
  9. #import "NSArray+FLEX.h"
  10. @interface FHSView (Snapshotting)
  11. + (UIImage *)_snapshotView:(UIView *)view;
  12. @end
  13. @implementation FHSView
  14. + (instancetype)forView:(UIView *)view isInScrollView:(BOOL)inScrollView {
  15. return [[self alloc] initWithView:view isInScrollView:inScrollView];
  16. }
  17. - (id)initWithView:(UIView *)view isInScrollView:(BOOL)inScrollView {
  18. self = [super init];
  19. if (self) {
  20. _view = view;
  21. _inScrollView = inScrollView;
  22. _identifier = NSUUID.UUID.UUIDString;
  23. UIViewController *controller = [FLEXUtility viewControllerForView:view];
  24. if (controller) {
  25. _important = YES;
  26. _title = [NSString stringWithFormat:
  27. @"%@ (for %@)",
  28. NSStringFromClass([controller class]),
  29. NSStringFromClass([view class])
  30. ];
  31. } else {
  32. _title = NSStringFromClass([view class]);
  33. }
  34. }
  35. return self;
  36. }
  37. - (CGRect)frame {
  38. if (_inScrollView) {
  39. CGPoint offset = [(UIScrollView *)self.view.superview contentOffset];
  40. return CGRectOffset(self.view.frame, -offset.x, -offset.y);
  41. } else {
  42. return self.view.frame;
  43. }
  44. }
  45. - (BOOL)hidden {
  46. return self.view.isHidden;
  47. }
  48. - (UIImage *)snapshotImage {
  49. return [FHSView _snapshotView:self.view];
  50. }
  51. - (NSArray<FHSView *> *)children {
  52. BOOL isScrollView = [self.view isKindOfClass:[UIScrollView class]];
  53. return [self.view.subviews flex_mapped:^id(UIView *subview, NSUInteger idx) {
  54. return [FHSView forView:subview isInScrollView:isScrollView];
  55. }];
  56. }
  57. - (NSString *)summary {
  58. CGRect f = self.frame;
  59. return [NSString stringWithFormat:
  60. @"%@ (%.1f, %.1f, %.1f, %.1f)",
  61. NSStringFromClass([self.view class]),
  62. f.origin.x, f.origin.y, f.size.width, f.size.height
  63. ];
  64. }
  65. - (NSString *)description{
  66. return self.view.description;
  67. }
  68. - (id)ifImportant:(id)importantAttr ifNormal:(id)normalAttr {
  69. return self.important ? importantAttr : normalAttr;
  70. }
  71. @end
  72. @implementation FHSView (Snapshotting)
  73. + (UIImage *)drawView:(UIView *)view {
  74. #if TARGET_OS_TV
  75. UIGraphicsBeginImageContextWithOptions(view.layer.bounds.size, NO, 0.0);
  76. CGContextRef imageContext = UIGraphicsGetCurrentContext();
  77. [view.layer renderInContext:imageContext];
  78. #else
  79. UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0);
  80. [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES];
  81. #endif
  82. UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  83. UIGraphicsEndImageContext();
  84. return image;
  85. }
  86. /// Recursively hides all views that may be obscuring the given view and collects them
  87. /// in the given array. You should unhide them all when you are done.
  88. + (BOOL)_hideViewsCoveringView:(UIView *)view
  89. root:(UIView *)rootView
  90. hiddenViews:(NSMutableArray<UIView *> *)hiddenViews {
  91. // Stop when we reach this view
  92. if (view == rootView) {
  93. return YES;
  94. }
  95. for (UIView *subview in rootView.subviews.reverseObjectEnumerator.allObjects) {
  96. if ([self _hideViewsCoveringView:view root:subview hiddenViews:hiddenViews]) {
  97. return YES;
  98. }
  99. }
  100. if (!rootView.isHidden) {
  101. rootView.hidden = YES;
  102. [hiddenViews addObject:rootView];
  103. }
  104. return NO;
  105. }
  106. /// Recursively hides all views that may be obscuring the given view and collects them
  107. /// in the given array. You should unhide them all when you are done.
  108. + (void)hideViewsCoveringView:(UIView *)view doWhileHidden:(void(^)())block {
  109. NSMutableArray *viewsToUnhide = [NSMutableArray new];
  110. if ([self _hideViewsCoveringView:view root:view.window hiddenViews:viewsToUnhide]) {
  111. block();
  112. }
  113. for (UIView *v in viewsToUnhide) {
  114. v.hidden = NO;
  115. }
  116. }
  117. + (UIImage *)_snapshotVisualEffectBackdropView:(UIView *)view {
  118. NSParameterAssert(view.window);
  119. // UIVisualEffectView is a special case that cannot be snapshotted
  120. // the same way as any other view. From Apple docs:
  121. //
  122. // Many effects require support from the window that hosts the
  123. // UIVisualEffectView. Attempting to take a snapshot of only the
  124. // UIVisualEffectView will result in a snapshot that does not
  125. // contain the effect. To take a snapshot of a view hierarchy
  126. // that contains a UIVisualEffectView, you must take a snapshot
  127. // of the entire UIWindow or UIScreen that contains it.
  128. //
  129. // To snapshot this view, we traverse the view hierarchy starting
  130. // from the window and hide any views that are on top of the
  131. // _UIVisualEffectBackdropView so that it is visible in a snapshot
  132. // of the window. We then take a snapshot of the window and crop
  133. // it to the part that contains the backdrop view. This appears to
  134. // be the same technique that Xcode's own view debugger uses to
  135. // snapshot visual effect views.
  136. __block UIImage *image = nil;
  137. [self hideViewsCoveringView:view doWhileHidden:^{
  138. image = [self drawView:view];
  139. CGRect cropRect = [view.window convertRect:view.bounds fromView:view];
  140. image = [UIImage imageWithCGImage:CGImageCreateWithImageInRect(image.CGImage, cropRect)];
  141. }];
  142. return image;
  143. }
  144. + (UIImage *)_snapshotView:(UIView *)view {
  145. UIView *superview = view.superview;
  146. // Is this view inside a UIVisualEffectView?
  147. if ([superview isKindOfClass:[UIVisualEffectView class]]) {
  148. // Is it (probably) the "backdrop" view of this UIVisualEffectView?
  149. if (superview.subviews.firstObject == view) {
  150. return [self _snapshotVisualEffectBackdropView:view];
  151. }
  152. }
  153. // Hide the view's subviews before we snapshot it
  154. NSMutableIndexSet *toUnhide = [NSMutableIndexSet new];
  155. [view.subviews flex_forEach:^(UIView *v, NSUInteger idx) {
  156. if (!v.isHidden) {
  157. v.hidden = YES;
  158. [toUnhide addIndex:idx];
  159. }
  160. }];
  161. // Snapshot the view, then unhide the previously-unhidden views
  162. UIImage *snapshot = [self drawView:view];
  163. for (UIView *v in [view.subviews objectsAtIndexes:toUnhide]) {
  164. v.hidden = NO;
  165. }
  166. return snapshot;
  167. }
  168. @end