How to check if a view is displayed on the screen? (Swift 5 and SwiftUI)
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 will only call the action when the view intersects the screen's visible bounds.
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 else { return }
action?()
action = nil
}
}
}
}
struct VisibleKey: PreferenceKey {
static var defaultValue: Bool = false
static func reduce(value: inout Bool, nextValue: () -> Bool) { }
}
}
Discussion
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.
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.
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
}