I have a view like below. I want to find out if it is the view which is displayed on the screen. Is there a function to achieve this?
struct TestView: View {
var body: some View {
Text("Test View")
}
}
I have a view like below. I want to find out if it is the view which is displayed on the screen. Is there a function to achieve this?
struct TestView: View {
var body: some View {
Text("Test View")
}
}
You could use onAppear on any kind of view that conforms to View protocol.
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.")
}
}
}
PS: Although this solution covers most cases, it has many edge cases that has not been covered. I'll be updating this answer when Apple releases a better solution for this requirement.
private func () { if (TestView.isDisplayed()) {do something}}
? –
Nasser .onAppear()
–
Nelda As mentioned by Oleg, depending on your use case, a possible issue with onAppear
is its action
will be performed as soon as the View
is in a view hierarchy, regardless of whether the view is potentially visible to the user.
My use case is wanting to lazy load content when a view actually becomes visible. I didn't want to rely on the view being encapsulated in a LazyHStack
or similar.
To achieve this I've added an extension onBecomingVisible
to View
that has the same kind of API as onAppear
, but only calls the action when (and only if) the view first intersects the screen's visible bounds. The action is never subsequently called.
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) { }
}
}
I'm not thrilled by using UIScreen.main.bounds
in the code! Perhaps a geometry proxy could be used for this instead, or some @Environment
value – I've not thought about this yet though.
in:
parameter to onBecomingVisible(perform:)
could work? That would then let a caller specify it from what they know (eg, an environment value as you suggest, but also other approaches). … I guess it's possible it would need to be a reference to state instead of a fixed value though, given windows can themself be changing shape? –
Tree proxy
and coordinateSpace
of the ScrollView into onBecomingVisible
. I used the frame of the View's proxy according to the named coordinateSpace
like this itemProxy.frame(in: .named(coordinateSpace))
. –
Pamalapamela UIScreen.main.bounds.intersects(proxy.frame(in: .global))
. You'd need to have a function to replace intersects
that would instead reflect fullyContains
. Be careful if you take this approach that the main screen can contain the displayed view. If the view is larger than the screen then this will never happen! –
Tree You can check the position of view in global scope using GeometryReader and 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
}
You could use onAppear on any kind of view that conforms to View protocol.
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.")
}
}
}
PS: Although this solution covers most cases, it has many edge cases that has not been covered. I'll be updating this answer when Apple releases a better solution for this requirement.
private func () { if (TestView.isDisplayed()) {do something}}
? –
Nasser .onAppear()
–
Nelda I think @Benjohn's view extension is the way to go but ran into the concern he noted with using UIScreen to determine view visibility. I was trying to determine when a view became visible or not visible in a ScrollView somewhere in the middle of the screen. Specifically, the view I was trying to control was a MapKit view that consumes a staggering amount of memory when created. I know MapKit is complex, but I'd sure like to know how it's possible to consume so much memory even for displaying an empty map. In any case, I modified Ben's code to use parent/child GeometryProxies to determine visibility. The usage is straightforward. You need a GeometryReader for the parent view, and you add this to the child view:
.onVisibilityChange(proxy: self.parentProxy) {isVisible in
debugPrint("View visibility changed to \(isVisible)")
}
The revised code is:
public extension View {
func onVisibilityChange(proxy: GeometryProxy, perform action: @escaping (Bool)->Void)-> some View {
modifier(BecomingVisible(parentProxy: proxy, action: action))
}
}
private struct BecomingVisible: ViewModifier {
var parentProxy: GeometryProxy
var action: ((Bool)->Void)
@State var isVisible: Bool = false
func checkVisible(proxy: GeometryProxy) {
let parentFrame = self.parentProxy.frame(in: .global)
let childFrame = proxy.frame(in: .global)
let isVisibleNow = parentFrame.intersects(childFrame)
if (self.isVisible != isVisibleNow) {
self.isVisible = isVisibleNow
self.action(isVisibleNow)
}
}
func body(content: Content) -> some View {
content.overlay {
GeometryReader { proxy in
Color.clear
.onAppear() {
self.checkVisible(proxy: proxy)
}
.onChange(of: proxy.frame(in: .global)) {
self.checkVisible(proxy: proxy)
}
}
}
}
}
If you're using UIKit and SceneDelegate along with SwiftUI, you can solve this with a combination of UIHostingViewController and a "visibleViewController" property like the one below. This solution worked the best for my use case.
Basically, just check if SceneDelegate's topmost view controller is the same as the SwiftUI View's hosting controller.
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
}
Then in your SwiftUI View, you can add this Boolean:
var isViewDisplayed: Bool {
if let visibleVc = SceneDelegate.visibleViewController {
return visibleVc.isKind(of: CustomHostingViewController.self)
} else {
return false
}
}
© 2022 - 2024 — McMap. All rights reserved.