如何检查视图是否显示在屏幕上?(Swift 5和SwiftUI)

34

我有一个类似下面的视图。我想找出它是否是在屏幕上显示的视图。有没有一个函数可以实现这个功能?

struct TestView: View {
    var body: some View {
        Text("Test View")
    }
}

1
你想将 SwiftUI 的主动行为转化为反应性质。在 SwiftUI 的概念中,一些状态(或者在视图模型中或者在 @State 中)决定了视图是否可见。因此,有了这样的状态,你不需要询问视图,而是可以直接使用它。 - Asperi
2
@Asperi,这不是关于设置可见性,而是要检查视图当前是否在视口内以及用户是否可以看到它。 - Big_Chair
4个回答

17

正如Oleg提到的,根据你的用例,onAppear可能存在的一个问题是,无论视图是否可见,其action都会在View位于视图层次结构中时立即执行。

我的用例是想要在视图实际可见时延迟加载内容。我不想依赖于视图被封装在LazyHStack或类似视图中。

为了实现这一点,我添加了一个扩展onBecomingVisibleView,它具有与onAppear相同类型的API,但仅在(且仅当)视图首次相交于屏幕可见范围时调用操作。不会随后调用该操作。

public extension View {
    
    func onBecomingVisible(perform action: @escaping () -> Void) -> some View {
        modifier(BecomingVisible(action: action))
    }
}

private struct BecomingVisible: ViewModifier {
    
    @State var action: (() -> Void)?

    func body(content: Content) -> some View {
        content.overlay {
            GeometryReader { proxy in
                Color.clear
                    .preference(
                        key: VisibleKey.self,
                        // See discussion!
                        value: UIScreen.main.bounds.intersects(proxy.frame(in: .global))
                    )
                    .onPreferenceChange(VisibleKey.self) { isVisible in
                        guard isVisible, let action else { return }
                        action()
                        action = nil
                    }
            }
        }
    }

    struct VisibleKey: PreferenceKey {
        static var defaultValue: Bool = false
        static func reduce(value: inout Bool, nextValue: () -> Bool) { }
    }
}

讨论

我对在代码中使用UIScreen.main.bounds并不感到兴奋!或许可以使用几何代理或一些@Environment值代替 - 虽然我还没有考虑过这个。


1
我看过一些其他的SO答案,人们把场景窗口的引用放入环境中。这比使用UIScreen.main好,因为iPad应用程序可以非全屏显示,甚至可以有多个屏幕。 - orion
1
我看过一些其他的SO答案,人们把对场景窗口的引用放入环境中。这比UIScreen.main好,因为iPad应用程序可以是非全屏的,甚至可以有多个屏幕。 - undefined
谢谢 @orion。我觉得在 onBecomingVisible(perform:) 中添加一个可选的 in: 参数可能行得通?这样调用者就可以根据他们所知道的来指定它(比如,像你建议的环境值,还有其他方法)。...不过,考虑到窗口本身可能会改变形状,可能需要将其作为状态的引用而不是固定值。 - Benjohn
我使用这个解决方案来追踪从 ScrollView 上可见的视图。我将 ScrollView 包裹在 GeometryReader 中,并将 ScrollView 的 proxycoordinateSpace 传递给 onBecomingVisible。我根据指定的 coordinateSpace 使用视图的 proxy 的 frame,例如 itemProxy.frame(in: .named(coordinateSpace)) - joeshonm
我使用了这个解决方案来追踪一个视图是否在滚动视图上可见。我将滚动视图包裹在GeometryReader中,并将ScrollView的proxycoordinateSpace传递给onBecomingVisible。我使用了视图的代理的框架,根据指定的coordinateSpace,像这样itemProxy.frame(in: .named(coordinateSpace)) - undefined

15

您可以在符合View协议的任何类型的视图上使用onAppear。

struct TestView: View {
    @State var isViewDisplayed = false
    var body: some View {
        Text("Test View")
        .onAppear {
            self.isViewDisplayed = true
        }
        .onDisappear {
            self.isViewDisplayed = false
        }
    }

    func someFunction() {
        if isViewDisplayed {
            print("View is displayed.")
        } else {
            print("View is not displayed.")
        }
    }
}

提示:虽然这个解决方案涵盖了大多数情况,但还有许多未被覆盖的特殊情况。当苹果发布更好的解决方案以满足此要求时,我会更新此答案。


50
这并不是真正显示在屏幕上的内容。即使它不显示在屏幕上,它也会被调用。只要加载视图,它就会调用.onAppear() - Oleg G.
2
这不是一个很好的解决方案。如果全屏覆盖发生并且底层视图消失,onDisappear将不会触发。 - FontFamily
如果您将视图放在tabview/navigationview中,则此方法在iOS14上无法正常工作。存在一个错误,即在切换选项卡时会立即调用onDisappear后调用onAppear。 - Benzy
1
当视图变得可见时,不能保证调用OnAppear方法。 - Karlth
正如答案中所提到的,它有许多边缘情况,如果苹果发布了更好的“SwiftUI解决方案”,我一定会更新它。 - Frankenstein
显示剩余5条评论

14

你可以使用GeometryReader和GeometryProxy在全局范围内检查视图的位置。

        struct CustomButton: View {
            var body: some View {
                GeometryReader { geometry in
                    VStack {
                        Button(action: {
                        }) {
                            Text("Custom Button")
                                .font(.body)
                                .fontWeight(.bold)
                                .foregroundColor(Color.white)
                        }
                        .background(Color.blue)
                    }.navigationBarItems(trailing: self.isButtonHidden(geometry) ?
                            HStack {
                                Button(action: {
                                }) {
                                    Text("Custom Button")
                                } : nil)
                }
            }

            private func isButtonHidden(_ geometry: GeometryProxy) -> Bool {
    // Alternatively, you can also check for geometry.frame(in:.global).origin.y if you know the button height.
                if geometry.frame(in: .global).maxY <= 0 {
                    return true
                }
                return false
            }

1
这段代码不起作用,可能有一些部分丢失了... - Daniele Ceglia

0
如果您正在使用UIKit和SceneDelegate与SwiftUI一起使用,您可以通过UIHostingViewController和一个名为"visibleViewController"的属性的组合来解决这个问题,就像下面的示例一样。这个解决方案对我的用例效果最好。
基本上,只需检查SceneDelegate的最顶层视图控制器是否与SwiftUI视图的托管控制器相同即可。
    static var visibleViewController: UIViewController? {
        get {
            guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
            let delegate = windowScene.delegate as? SceneDelegate, let window = delegate.window else { return nil }
            guard let rootVC = window.rootViewController else { return nil }
            return getVisibleViewController(rootVC)
        }
    }
    
    static private func getVisibleViewController(_ rootViewController: UIViewController) -> UIViewController? {
        if let presentedViewController = rootViewController.presentedViewController {
            return getVisibleViewController(presentedViewController)
        }

        if let navigationController = rootViewController as? UINavigationController {
            return navigationController.visibleViewController
        }

        if let tabBarController = rootViewController as? UITabBarController {
            if let selectedTabVC = tabBarController.selectedViewController {
                return getVisibleViewController(selectedTabVC)
            }
            return tabBarController
        }

        return rootViewController
    }

然后在您的SwiftUI视图中,您可以添加此布尔值:
    var isViewDisplayed: Bool {
        if let visibleVc = SceneDelegate.visibleViewController {
            return visibleVc.isKind(of: CustomHostingViewController.self)
        } else {
            return false
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接