FHSView.m 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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. UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0);
  75. [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES];
  76. UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
  77. UIGraphicsEndImageContext();
  78. return image;
  79. }
  80. /// Recursively hides all views that may be obscuring the given view and collects them
  81. /// in the given array. You should unhide them all when you are done.
  82. + (BOOL)_hideViewsCoveringView:(UIView *)view
  83. root:(UIView *)rootView
  84. hiddenViews:(NSMutableArray<UIView *> *)hiddenViews {
  85. // Stop when we reach this view
  86. if (view == rootView) {
  87. return YES;
  88. }
  89. for (UIView *subview in rootView.subviews.reverseObjectEnumerator.allObjects) {
  90. if ([self _hideViewsCoveringView:view root:subview hiddenViews:hiddenViews]) {
  91. return YES;
  92. }
  93. }
  94. if (!rootView.isHidden) {
  95. rootView.hidden = YES;
  96. [hiddenViews addObject:rootView];
  97. }
  98. return NO;
  99. }
  100. /// Recursively hides all views that may be obscuring the given view and collects them
  101. /// in the given array. You should unhide them all when you are done.
  102. + (void)hideViewsCoveringView:(UIView *)view doWhileHidden:(void(^)())block {
  103. NSMutableArray *viewsToUnhide = [NSMutableArray new];
  104. if ([self _hideViewsCoveringView:view root:view.window hiddenViews:viewsToUnhide]) {
  105. block();
  106. }
  107. for (UIView *v in viewsToUnhide) {
  108. v.hidden = NO;
  109. }
  110. }
  111. + (UIImage *)_snapshotVisualEffectBackdropView:(UIView *)view {
  112. NSParameterAssert(view.window);
  113. // UIVisualEffectView is a special case that cannot be snapshotted
  114. // the same way as any other view. From Apple docs:
  115. //
  116. // Many effects require support from the window that hosts the
  117. // UIVisualEffectView. Attempting to take a snapshot of only the
  118. // UIVisualEffectView will result in a snapshot that does not
  119. // contain the effect. To take a snapshot of a view hierarchy
  120. // that contains a UIVisualEffectView, you must take a snapshot
  121. // of the entire UIWindow or UIScreen that contains it.
  122. //
  123. // To snapshot this view, we traverse the view hierarchy starting
  124. // from the window and hide any views that are on top of the
  125. // _UIVisualEffectBackdropView so that it is visible in a snapshot
  126. // of the window. We then take a snapshot of the window and crop
  127. // it to the part that contains the backdrop view. This appears to
  128. // be the same technique that Xcode's own view debugger uses to
  129. // snapshot visual effect views.
  130. __block UIImage *image = nil;
  131. [self hideViewsCoveringView:view doWhileHidden:^{
  132. image = [self drawView:view];
  133. CGRect cropRect = [view.window convertRect:view.bounds fromView:view];
  134. image = [UIImage imageWithCGImage:CGImageCreateWithImageInRect(image.CGImage, cropRect)];
  135. }];
  136. return image;
  137. }
  138. + (UIImage *)_snapshotView:(UIView *)view {
  139. UIView *superview = view.superview;
  140. // Is this view inside a UIVisualEffectView?
  141. if ([superview isKindOfClass:[UIVisualEffectView class]]) {
  142. // Is it (probably) the "backdrop" view of this UIVisualEffectView?
  143. if (superview.subviews.firstObject == view) {
  144. return [self _snapshotVisualEffectBackdropView:view];
  145. }
  146. }
  147. // Hide the view's subviews before we snapshot it
  148. NSMutableIndexSet *toUnhide = [NSMutableIndexSet new];
  149. [view.subviews flex_forEach:^(UIView *v, NSUInteger idx) {
  150. if (!v.isHidden) {
  151. v.hidden = YES;
  152. [toUnhide addIndex:idx];
  153. }
  154. }];
  155. // Snapshot the view, then unhide the previously-unhidden views
  156. UIImage *snapshot = [self drawView:view];
  157. for (UIView *v in [view.subviews objectsAtIndexes:toUnhide]) {
  158. v.hidden = NO;
  159. }
  160. return snapshot;
  161. }
  162. @end