Search code examples
iosobjective-cuiviewintersectionoverlap

Detecting visibility percentage of a UIView in iOS views


I need to find out whether a target UIView object is visible on the screen. From my research, it looks like the consensus is to check if the window property of UIView object is the keyWindow. But this only partially solves my problem, because a view may be partially blocked by another view object on top of it. For example:

Half of the square in gray color is blocked by a rectangle in blue color, so the visibility of the gray square is 50% to the user.

gray view is blocked partially by the blue view

The solution I am working on is essentially following, but it does not work quite right:

  1. Get the UIWindow object on the top:
    UIWindow *topWindow = [[[UIApplication sharedApplication].windows sortedArrayUsingComparator:^NSComparisonResult (UIWindow *win1, UIWindow *win2) {
    if (win1.windowLevel < win2.windowLevel) {
        return NSOrderedAscending;
    } else if (win1.windowLevel > win2.windowLevel) {
        return NSOrderedDescending;
    }
        return NSOrderedSame;
    }] firstObject];

  1. Then traverse all the subviews of the topWindow and find all the subviews after my target view object:

  2. Convert the frame of targetView and all the subviews after it to the coordinates of topWindow, for detecting if there's intersection.

NSMutableArray *stack = [[NSMutableArray alloc] initWithArray:topWindow.subviews];
UIView *targetView = nil;
NSInteger targetViewTag = 10;
NSMutableArray *postViews = [NSMutableArray array]; // views detected after targetView, used for detection overlap with targetView

while ([stack count] > 0) {
            UIView *curr = [stack lastObject];
            [stack removeObject:curr];

            if (curr.isHidden || curr.window == nil) {
                continue;
            }

            if (curr.tag == targetViewTag) {
                targetView = curr;
            } else if (targetView) {
                [postViews addObject:curr];
            }

            for (UIView *sub in [curr subviews]) {
              [stack addObject:sub];
            }
        }

    // if targetView is found:
    if (targetView) {
      float visiblePercentage = 100;

      for (UIView *pw in postViews) {
                    // convert frames to topWindow for detecting intersection
                    CGRect swFrame = [targetView convertRect:targetView.frame toView:topWindow];
                    CGRect pwFrame = [pw convertRect:pw.frame toView:topWindow];
                    
                    if (CGRectIntersectsRect(swFrame, pwFrame)) {
                        CGRect intersection = CGRectIntersection(swFrame, pwFrame);

                        CGFloat intersectArea = intersection.size.width * intersection.size.height;
                        CGFloat swArea = swFrame.size.width * swFrame.size.height;
                        // update visiblePercentage
                        visiblePercentage = visiblePercentage - (100 * intersectArea / swArea);
                    }
                }

   }

I am not sure if it's a good idea to convert the frames for views in different subviews to the topWindow coordinate, but the visiblePercentage does not look right, as it gives me negative number. Anyone has better way of calculating the visible percentage for partially/entirely blocked views? Thanks!


Solution

  • Having spent more time on my algorithm, I finally realized why it did not work for me, because the way for traversing the view hierarchy with the stack data structure would make the blocking views pop out of the stack sooner than targetView. So after making the change in the following line, the calculation for visibility percentage works for me:

    
    while ([stack count] > 0) {
                UIView *curr = [stack lastObject];
                [stack removeObject:curr];
    
                if (curr.isHidden || curr.window == nil) {
                    continue;
                }
    
                if (curr.tag == targetViewTag) {
                    targetView = curr;
                } else if (targetView == nil) { // NOTE: correct this line from else if (targetView)
                    [postViews addObject:curr];
                }
    
                for (UIView *sub in [curr subviews]) {
                  [stack addObject:sub];
                }
            }
    
    

    The stack data structure pops out those views on top of my targetView first, which is the trick for fixing my problem. Thanks for looking anyway :)