I'm trying to hide BottomRoundedRectangle()
when I see the 2nd LazyVStack {…}
on the screen, but it doesn't work.
value
in .onPreferenceChange(OffsetKey.self)
doesn't change as well.
struct OffsetKey: PreferenceKey {
typealias Value = CGFloat
static let defaultValue: Value = .zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue()
}
}
struct OffsetKeyBackground: ViewModifier {
func body(content: Content) -> some View {
GeometryReader { reader in
content
.preference(
key: OffsetKey.self,
value: -reader.frame(in: .named("secondLazyStack")).origin.y
)
}
}
}
struct ScrollQuestion: View {
@State
private var elementValue: CGFloat = .zero
var body: some View {
ScrollView(.vertical) {
// Dynamic long text there
LazyVStack {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(height: 920) // dynamic text, 920 for example, may be bigger
}
// 2nd LazyVStack
LazyVStack {
RoundedRectangle(cornerRadius: 12)
.frame(height: 820)
}
.coordinateSpace(name: "secondLazyStack")
}
.safeAreaInset(edge: .bottom) {
// How to hide this correctly when we see second LazyVStack ?
BottomRoundedRectangle()
// .opacity(elementValue < 50 ? 1 : 0)
}
.modifier(OffsetKeyBackground())
.onPreferenceChange(OffsetKey.self) { value in
debugPrint(value)
elementValue = value
}
}
}
struct BottomRoundedRectangle: View {
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.frame(height: 40)
.foregroundStyle(.indigo)
.padding(.horizontal)
Text("Visible only in first LazyVStack")
.foregroundStyle(.white)
}
}
}
#Preview {
ScrollQuestion()
}
What do I need to do to hide BottomRoundedRectangle()
correctly ?
You can detect this by wrapping the second LazyVStack
in a GeometryReader
.
If the scroll view's frame intersects the frame of the LazyVStack
, that means at least some part of the LazyVStack
is on screen.
struct IsOnScreenKey: PreferenceKey {
static let defaultValue: Bool? = nil
static func reduce(value: inout Value, nextValue: () -> Value) {
if let next = nextValue() {
value = next
}
}
}
struct DetectIsOnScreen: ViewModifier {
func body(content: Content) -> some View {
GeometryReader { reader in
content
.preference(
key: IsOnScreenKey.self,
// bounds(of: .scrollView) gives us the scroll view's frame
// frame(in: .local) gives us the LazyVStack's frame
// both are in the local coordinate space
value: reader.bounds(of: .scrollView)?.intersects(reader.frame(in: .local)) ?? false
)
}
}
}
Then you can do:
@State
private var isSecondVStackVisible = false
var body: some View {
ScrollView(.vertical) {
LazyVStack {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(height: 920)
}
LazyVStack {
RoundedRectangle(cornerRadius: 12)
.modifier(DetectIsOnScreen())
.frame(height: 820)
.onPreferenceChange(IsOnScreenKey.self) { value in
isSecondVStackVisible = value ?? false
}
}
}
.safeAreaInset(edge: .bottom) {
BottomRoundedRectangle()
.opacity(isSecondVStackVisible ? 0 : 1)
}
}
Note that in this particular case, it is also possible to just set the preference to
reader.bounds(of: .scrollView) != nil
This is because LazyVStack
s are lazy - if they are not on screen, it literally doesn't exist. The geometry reader can't convert the scroll view's frame to a coordinate space that doesn't exist.