Determine whether UIView is visible to the user?
Asked Answered
S

13

97

Is it possible to determine whether my UIView is visible to the user or not?

My View is added as a subview several times into a UITabBarController.

Each instance of this view has an NSTimer that updates the view.

However, I don't want to update a view which is not visible to the user.

Is this possible?

Sam answered 8/10, 2009 at 10:24 Comment(1)
If you can, would you consider updating your selected answer to the one with the most upvotes—that is, the one that checks .window (by walkingbrad), as the answer that checks .superview (by mahboudz) is not technically correct and has caused bugs for me.Haematoxylon
D
82

You can check if:

  • it is hidden, by checking view.hidden
  • it is in the view hierarchy, by checking view.superview != nil
  • you can check the bounds of a view to see if it is on screen

The only other thing I can think of is if your view is buried behind others and can't be seen for that reason. You may have to go through all the views that come after to see if they obscure your view.

Deputation answered 11/10, 2009 at 7:19 Comment(7)
I did that this way but forgot to post the solution here :) Thanks (+1)Sam
How did you accomplish your obscurity check?Cheatham
Not easily. Check the bounds of all opaque child views, and keep track of portions of your view that aren't obscured to check against the next child subview. To make it easier, you might want to define a few points and if those points are visible, then assume your view is. You can choose whether all points being visible is an indication that your view is visible, or whether any single point visible will satisfy your requirement.Deputation
the third one works even if UIView is inside another UIView which is inside a scroll view?Amphibious
What will still fall through is if one of the superviews is hidden.Daggna
I would add to the list checking an alphaHarpist
It seems, unfortunately, this is not a comprehensive answer. There are many possible reasons a view might be visible, or not, making it not easily possible to determine.Ministerial
W
135

For anyone else that ends up here:

To determine if a UIView is onscreen somewhere, rather than checking superview != nil, it is better to check if window != nil. In the former case, it is possible that the view has a superview but that the superview is not on screen:

if (view.window != nil) {
    // do stuff
}

Of course you should also check if it is hidden or if it has an alpha > 0.

Regarding not wanting your NSTimer running while the view is not visible, you should hide these views manually if possible and have the timer stop when the view is hidden. However, I'm not at all sure of what you're doing.

Wilscam answered 12/11, 2013 at 21:26 Comment(4)
ay caramba! just what I needed to kill the cycled animationsThorathoracic
Sorry I voted this up, it doesnt work. When I do po [self.view recursiveDescription] my subviews are shown in the view hierarchy, yet their view.window is always nil.Geotaxis
@LogicsaurusRex You can have a view hierarchy that is not currently being shown in the window (hence why I originally re-answered the question). In the case you've outlined here, I imagine that self.view also has a window of nil. Is that the case? (sorry for late reply)Wilscam
I would buy you a bottle of wine;)Inpatient
D
82

You can check if:

  • it is hidden, by checking view.hidden
  • it is in the view hierarchy, by checking view.superview != nil
  • you can check the bounds of a view to see if it is on screen

The only other thing I can think of is if your view is buried behind others and can't be seen for that reason. You may have to go through all the views that come after to see if they obscure your view.

Deputation answered 11/10, 2009 at 7:19 Comment(7)
I did that this way but forgot to post the solution here :) Thanks (+1)Sam
How did you accomplish your obscurity check?Cheatham
Not easily. Check the bounds of all opaque child views, and keep track of portions of your view that aren't obscured to check against the next child subview. To make it easier, you might want to define a few points and if those points are visible, then assume your view is. You can choose whether all points being visible is an indication that your view is visible, or whether any single point visible will satisfy your requirement.Deputation
the third one works even if UIView is inside another UIView which is inside a scroll view?Amphibious
What will still fall through is if one of the superviews is hidden.Daggna
I would add to the list checking an alphaHarpist
It seems, unfortunately, this is not a comprehensive answer. There are many possible reasons a view might be visible, or not, making it not easily possible to determine.Ministerial
A
41

This will determine if a view's frame is within the bounds of all of its superviews (up to the root view). One practical use case is determining if a child view is (at least partially) visible within a scrollview.

Swift 5.x:

func isVisible(view: UIView) -> Bool {
    func isVisible(view: UIView, inView: UIView?) -> Bool {
        guard let inView = inView else { return true }
        let viewFrame = inView.convert(view.bounds, from: view)
        if viewFrame.intersects(inView.bounds) {
            return isVisible(view: view, inView: inView.superview)
        }
        return false
    }
    return isVisible(view: view, inView: view.superview)
}

Older swift versions

func isVisible(view: UIView) -> Bool {
    func isVisible(view: UIView, inView: UIView?) -> Bool {
        guard let inView = inView else { return true }
        let viewFrame = inView.convertRect(view.bounds, fromView: view)
        if CGRectIntersectsRect(viewFrame, inView.bounds) {
            return isVisible(view, inView: inView.superview)
        }
        return false
    }
    return isVisible(view, inView: view.superview)
}

Potential improvements:

  • Respect alpha and hidden.
  • Respect clipsToBounds, as a view may exceed the bounds of its superview if false.
Alissaalistair answered 6/1, 2016 at 20:15 Comment(2)
Perfect. I have a stack view as the content of a scroll view and determining which of the arrangedSubviews are (at least partially) visible is exactly what I was looking for.Rabelais
Thanks for posting this @johngibb. My use case was needing to check if it was completely visible and I found I could achieve this be replacing intersects with contains(_:)Dd
H
21

The solution that worked for me was to first check if the view has a window, then to iterate over superviews and check if:

  1. the view is not hidden.
  2. the view is within its superviews bounds.

Seems to work well so far.

Swift 3.0

public func isVisible(view: UIView) -> Bool {

  if view.window == nil {
    return false
  }

  var currentView: UIView = view
  while let superview = currentView.superview {

    if (superview.bounds).intersects(currentView.frame) == false {
      return false;
    }

    if currentView.isHidden {
      return false
    }

    currentView = superview
  }

  return true
}
Hallow answered 14/7, 2017 at 13:6 Comment(2)
Awesome answer. This can fail in UITableView and UIScrollView from what I can tell though.Lawanda
This worked perfectly for me inside UITableView and UIScrollView.Dd
I
8

I benchmarked both @Audrey M. and @John Gibb their solutions.
And @Audrey M. his way performed better (times 10).
So I used that one to make it observable.

I made a RxSwift Observable, to get notified when the UIView became visible.
This could be useful if you want to trigger a banner 'view' event

import Foundation
import UIKit
import RxSwift

extension UIView {
    var isVisibleToUser: Bool {

        if isHidden || alpha == 0 || superview == nil {
            return false
        }

        guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
            return false
        }

        let viewFrame = convert(bounds, to: rootViewController.view)

        let topSafeArea: CGFloat
        let bottomSafeArea: CGFloat

        if #available(iOS 11.0, *) {
            topSafeArea = rootViewController.view.safeAreaInsets.top
            bottomSafeArea = rootViewController.view.safeAreaInsets.bottom
        } else {
            topSafeArea = rootViewController.topLayoutGuide.length
            bottomSafeArea = rootViewController.bottomLayoutGuide.length
        }

        return viewFrame.minX >= 0 &&
            viewFrame.maxX <= rootViewController.view.bounds.width &&
            viewFrame.minY >= topSafeArea &&
            viewFrame.maxY <= rootViewController.view.bounds.height - bottomSafeArea

    }
}

extension Reactive where Base: UIView {
    var isVisibleToUser: Observable<Bool> {
        // Every second this will check `isVisibleToUser`
        return Observable<Int>.interval(.milliseconds(1000),
                                        scheduler: MainScheduler.instance)
        .map { [base] _ in
            return base.isVisibleToUser
        }.distinctUntilChanged()
    }
}

Use it as like this:

import RxSwift
import UIKit
import Foundation

private let disposeBag = DisposeBag()

private func _checkBannerVisibility() {

    bannerView.rx.isVisibleToUser
        .filter { $0 }
        .take(1) // Only trigger it once
        .subscribe(onNext: { [weak self] _ in
            // ... Do something
        }).disposed(by: disposeBag)
}
Imperforate answered 10/10, 2019 at 7:57 Comment(1)
@Audrey M answer only checks to see if it's within the screen bounds and not if it's actually visible. John Gibb's answer is the one that actually checks if a view can be seen.Dd
K
5

Tested solution.

func isVisible(_ view: UIView) -> Bool {
    if view.isHidden || view.superview == nil {
        return false
    }

    if let rootViewController = UIApplication.shared.keyWindow?.rootViewController,
        let rootView = rootViewController.view {

        let viewFrame = view.convert(view.bounds, to: rootView)

        let topSafeArea: CGFloat
        let bottomSafeArea: CGFloat

        if #available(iOS 11.0, *) {
            topSafeArea = rootView.safeAreaInsets.top
            bottomSafeArea = rootView.safeAreaInsets.bottom
        } else {
            topSafeArea = rootViewController.topLayoutGuide.length
            bottomSafeArea = rootViewController.bottomLayoutGuide.length
        }

        return viewFrame.minX >= 0 &&
               viewFrame.maxX <= rootView.bounds.width &&
               viewFrame.minY >= topSafeArea &&
               viewFrame.maxY <= rootView.bounds.height - bottomSafeArea
    }

    return false
}
Kaluga answered 6/8, 2019 at 11:30 Comment(0)
E
4

I you truly want to know if a view is visible to the user you would have to take into account the following:

  • Is the view's window not nil and equal to the top most window
  • Is the view, and all of its superviews alpha >= 0.01 (threshold value also used by UIKit to determine whether it should handle touches) and not hidden
  • Is the z-index (stacking value) of the view higher than other views in the same hierarchy.
  • Even if the z-index is lower, it can be visible if other views on top have a transparent background color, alpha 0 or are hidden.

Especially the transparent background color of views in front may pose a problem to check programmatically. The only way to be truly sure is to make a programmatic snapshot of the view to check and diff it within its frame with the snapshot of the entire screen. This won't work however for views that are not distinctive enough (e.g. fully white).

For inspiration see the method isViewVisible in the iOS Calabash-server project

Edenedens answered 7/11, 2016 at 12:4 Comment(0)
N
3

The simplest Swift 5 solution I could come up with that worked in my situation (I was looking for a button embedded in my tableViewFooter).

John Gibbs solution also worked but in my cause I did not need all the recursion.

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let viewFrame = scrollView.convert(targetView.bounds, from: targetView)
        if viewFrame.intersects(scrollView.bounds) {
            // targetView is visible 
        }
        else {
            // targetView is not visible
        }
    }
Nathannathanael answered 10/2, 2021 at 15:22 Comment(0)
T
2

In viewWillAppear set a value "isVisible" to true, in viewWillDisappear set it to false. Best way to know for a UITabBarController subviews, also works for navigation controllers.

Tempura answered 26/1, 2016 at 18:34 Comment(1)
yeah, it seems to be the only reliable wayThorathoracic
P
2

Another useful method is didMoveToWindow() Example: When you push view controller, views of your previous view controller will call this method. Checking self.window != nil inside of didMoveToWindow() helps to know whether your view is appearing or disappearing from the screen.

Prestigious answered 16/5, 2020 at 19:11 Comment(0)
G
1

This can help you figure out if your UIView is the top-most view. Can be helpful:

let visibleBool = view.superview?.subviews.last?.isEqual(view)
//have to check first whether it's nil (bc it's an optional) 
//as well as the true/false 
if let visibleBool = visibleBool where visibleBool { value
  //can be seen on top
} else {
  //maybe can be seen but not the topmost view
}
Generic answered 26/4, 2016 at 20:53 Comment(0)
B
0

try this:

func isDisplayedInScreen() -> Bool
{
 if (self == nil) {
     return false
  }
    let screenRect = UIScreen.main.bounds 
    // 
    let rect = self.convert(self.frame, from: nil)
    if (rect.isEmpty || rect.isNull) {
        return false
    }
    // 若view 隐藏
    if (self.isHidden) {
        return false
    }

    // 
    if (self.superview == nil) {
        return false
    }
    // 
    if (rect.size.equalTo(CGSize.zero)) {
        return  false
    }
    //
    let intersectionRect = rect.intersection(screenRect)
    if (intersectionRect.isEmpty || intersectionRect.isNull) {
        return false
    }
    return true
}
Butternut answered 13/3, 2019 at 10:35 Comment(0)
A
-4

In case you are using hidden property of view then :

view.hidden (objective C) or view.isHidden(swift) is read/write property. So you can easily read or write

For swift 3.0

if(view.isHidden){
   print("Hidden")
}else{
   print("visible")
}
Armandinaarmando answered 8/5, 2017 at 12:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.