确定UIView是否对用户可见?


78

是否可以确定用户是否UIView可以看到我?

我的视图已subview多次添加到Tab Bar Controller

该视图的每个实例都有一个NSTimer更新视图的。

但是我不想更新用户不可见的视图。

这可能吗?

谢谢


如果可以,您是否会考虑将您选择的答案更新为投票最多.window 的答案.superview(即,walkingbrad检查的答案),因为检查的答案(由mahboudz检查)在技术上不正确,并且对我造成了错误。
艾伯特·伦肖

Answers:


77

您可以检查是否:

  • 通过检查view.hidden将其隐藏
  • 通过检查,它在视图层次结构中 view.superview != nil
  • 您可以检查视图的边界以查看它是否在屏幕上

我唯一想到的另一件事是,您的视图是否隐藏在其他视图的后面,并且由于这个原因而无法显示。您可能必须仔细阅读后面的所有视图,以查看它们是否模糊了您的视图。


我这样做,但是忘了在这里发布解决方案:)谢谢(+1)
jantimon

您是如何完成模糊性检查的?
格雷格·科布斯

1
不容易。检查所有不透明子视图的边界,并跟踪视图中未被遮挡的部分,以检查下一个子视图。为了简化操作,您可能需要定义一些点,如果这些点可见,则假定您的视图是。您可以选择是否所有可见点都表明您的视图可见,或者任何可见点都可以满足您的要求。
mahboudz 2011年

即使UIView在滚动视图中的另一个UIView中,第三个也可以工作吗?
Radu Simionescu 2014年

4
我会添加检查Alpha的列表
朱利安·克罗尔(JulianKról)2014年

120

对于到此为止的其他任何人:

要确定某个UIView是否在屏幕上某个地方,而不是进行检查superview != nil,最好检查是否window != nil。在前一种情况下,视图可能具有超级视图,但该超级视图不在屏幕上:

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

当然,您还应该检查它是否为hidden或是否具有alpha > 0

关于不希望NSTimer在视图不可见时运行,应该尽可能手动隐藏这些视图,并在隐藏视图时停止计时器。但是,我完全不确定您在做什么。


嗨,卡兰巴!我需要杀死循环的动画
Anton Tropashko

2
抱歉,我对此投票,它不起作用。当我这样做时,po [self.view recursiveDescription]子视图将显示在视图层次结构中,但它们view.window始终为零。
Logicsaurus Rex

@LogicsaurusRex您可以具有当前不在窗口中显示的视图层次结构(因此,我最初重新回答该问题的原因)。如果您在此处概述了该情况,那么我想它self.viewwindow为零。是这样吗 (对不起,我答复延迟)
Walkingbrad

我想给你买一瓶酒;)
BartłomiejSemańczyk18年

22

这将确定视图的框架是否在其所有父视图的边界之内(直到根视图)。一个实际的用例是确定子视图在滚动视图中是否(至少部分)可见。

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)
}

较早的Swift版本

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)
}

潜在的改进:

  • 尊重alpha和尊重hidden
  • 尊重clipsToBounds,因为如果为假,视图可能会超出其超级视图的范围。

19

对我有用的解决方案是,首先检查视图是否具有窗口,然后遍历超级视图并检查是否:

  1. 该视图未隐藏。
  2. 该视图在其超级视图范围内。

到目前为止似乎工作良好。

斯威夫特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
}

很棒的答案。据我所知,这在UITableView和UIScrollView中可能会失败。
里德

3

我确实想知道用户是否可以看到视图,因此您必须考虑以下因素:

  • 视图的窗口是否不为nil并且等于最顶部的窗口
  • 是视图,它的所有父视图都是alpha> = 0.01(UIKit也使用阈值来确定它是否应该处理触摸)并且不是隐藏的
  • 视图的z-index(堆栈值)是否高于相同层次结构中的其他视图。
  • 即使z索引较低,如果顶部的其他视图具有透明的背景色,alpha 0或隐藏,也可以看到它。

特别是前面视图的透明背景色可能会带来程序检查的问题。真正确定的唯一方法是制作视图的程序化快照,以将其与整个屏幕快照一起在其框架内进行检查和区分。但是,对于视图不够鲜明(例如全白)的视图,这将不起作用。

为了获得灵感,请参阅iOS Calabash服务器项目中的isViewVisible方法


3

经过测试的解决方案。

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
}

2

在viewWillAppear中将值“ isVisible”设置为true,在viewWillDisappear中将其设置为false。了解UITabBarController子视图的最佳方法,也适用于导航控制器。


是的,这似乎是唯一可靠的方法
Anton Tropashko

2

我对@Audrey M.和@John Gibb的解决方案进行了基准测试。
@Audrey M.的方式表现更好(10次)。
所以我用那个使它可观察。

我制作了一个RxSwift Observable,以便在UIView可见时得到通知。
如果您要触发横幅“查看”事件,这可能会很有用

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)
        .flatMap { [base] _ in
            return Observable.just(base.isVisibleToUser)
        }.distinctUntilChanged()
    }
}

像这样使用它:

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)
}

1

这可以帮助您确定UIView是否是最顶层的视图。会有所帮助:

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
}

0

试试这个:

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
}

0

另一个有用的方法是didMoveToWindow() Example:当推视图控制器时,先前视图控制器的视图将调用此方法。self.window != nil在内部检查didMoveToWindow()有助于了解您的视图是从屏幕上出现还是从屏幕上消失。


-3

如果您使用view的隐藏属性,则:

view.hidden(目标C)或view.isHidden(swift)是读/写属性。这样您就可以轻松地读写

快速3.0

if(view.isHidden){
   print("Hidden")
}else{
   print("visible")
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.