June 21, 2024

72. Отслеживаем видимость вьюхи

Иногда нужно знать наверняка, видна ли вьюха на экране. При этом обычные модификаторы onAppear + onDisappear не решают задачу. В этой статье покажу вариант решения при помощи GeometryReader, в котором будет лежать ScrollView. Бонусом будет камень в огород SwiftUI Preview 👩‍🚀

Демо

Обновляем текст по центру при появлении/скрытии самого нижнего элемента

Стартовый код

struct OnVisibilityChangeExample: View {
  @State private var isBottomElementVisible = false
   
  var body: some View {
    ScrollView {
      LazyVStack(spacing: 24) {
        ForEach(0..<20) { number in
          Capsule()
            .fill(.green.opacity(0.5))
            .frame(width: 150, height: 70)
            .overlay {
              Text("\(number)")
            }
        }
        Text("самый нижний элемент") // <- будем отслеживать его
      }
    }
    .overlay {
      Text("isBottomElementVisible: \(isBottomElementVisible)")
        .bold()
        .padding()
        .background {
          Rectangle()
            .fill(.yellow.opacity(0.5))
        }
    }
  }
}

Ничего хитрого, едем дальше.

Пробуем onAppear + onDisappear

Добавим два модификатора, остальной код прежний:

Text("самый нижний элемент")
  .onAppear { isBottomElementVisible = true }
  .onDisappear { isBottomElementVisible = false }
Все работает, ура! Или не совсем?

В таком сценарии, когда мы используем простые вьюхи и отслеживаем появление/исчезновение текста на экране, стандартные модификаторы onAppear/onDisappear вполне подходят.

Сравнение с GeometryReader

Теперь добавим еще одно стейт-свойство, поменяем текст на мелкий квадрат и посмотрим на разницу.

За решение поблагодарим автора этого комментария, наш код теперь выглядит так:

struct OnVisibilityChangeExample: View {
  @State private var isBottomElementVisible = false
  @State private var isBottomElementVisible2 = false // <- новое свойство
   
  var body: some View {
    GeometryReader { proxy in
      ScrollView {
        LazyVStack(spacing: 24) {
          ForEach(0..<20) { number in
            Capsule()
              .fill(.green.opacity(0.5))
              .frame(width: 150, height: 70)
              .overlay {
                Text("\(number)")
              }
          }
          Rectangle() // <- поставили вместо текста
            .frame(width: 5, height: 5)
            .onAppear { isBottomElementVisible = true }
            .onDisappear { isBottomElementVisible = false }
            .onVisibilityChange(proxy: proxy) { isVisible in // <- новый модификатор
              isBottomElementVisible2 = isVisible
            }
        }
      }
    }
    .overlay {
      VStack {
        Text("isBottomElementVisible: \(isBottomElementVisible)")
        Text("isBottomElementVisible2: \(isBottomElementVisible2)")
      }
      .bold()
      .padding()
      .background {
        Rectangle()
          .fill(.yellow.opacity(0.5))
      }
    }
  }
}
При сравнении видно, что два способа приводят к разным результатам

Конкретно в моем случае более точный результат дает именно GeometryReader (на гифке это вариант №2), а onDisappear (вариант №1) вызывается слишком поздно, когда вьюху уже давно не видно на экране - и это проблема для моего кейса.

Заключение

Задача может казаться тривиальной, потому что есть стандартные модификаторы для её решения, но не всегда стандартные инструменты приводят к нужному результату, и в таких ситуациях опыт + желание разобраться приводят к успеху 😁

Кстати, onAppear/onDisappear очень плохо работают в превью, поэтому при использовании этих модификаторов желательно проверять UI на стимуляторе (видео для статьи записывал на симуляторе).

Код для этой статьи можно посмотреть тут, а другие статьи - тут.