October 25, 2024

90. Прячем вьюху со знанием дела

Кто регулярно верстает на SwiftUI, знает как скрыть вьюху при необходимости. В этой статье покажу несколько способов как это сделать, и возможные неочевидные последствия, о которых нужно знать.

План

  1. Сделаем класс, который будет символизировать сервис, делающий важную работу для экрана (можно назвать вьюмоделью). Для удобства дебага будем хранить в классе константу (Int)
  2. Для наглядности будем делать принты при создании (инициализация) и удалении (деинициализация) этого класса
  3. Сверстаем вьюху, которая будет хранить в себе этот класс-вьюмодель в качестве @StateObject, и выводить на экран текст с номером из этого класса
  4. Сверстаем родительскую вьюху, которая будет символизировать большой и сложный экран. На этом экране будет кнопка для скрытия вьюхи из шага 3
  5. Накидаем в родительскую вьюху несколько вьюх из шага 3 и будем скрывать их разными способами
  6. Сделаем выводы

Реализация

Класс-вьюмодель

final class TestViewModel: ObservableObject {
  let number: Int
   
  init(number: Int) {
    self.number = number
    print("создаем \(number)")
  }
   
  deinit {
    print("удаляем \(number)")
  }
}

Используем ObservableObject для образа - нужно помнить, что внутри там могут быть самые разные @Published-свойства, состояния которых нам важны.

Вьюха с вьюмоделью

struct DemoSubview123: View {
  @StateObject private var viewModel: TestViewModel
   
  init(number: Int) {
    self._viewModel = .init(wrappedValue: .init(number: number))
  }
   
  var body: some View {
    Text("Вьюха № \(viewModel.number)")
  }
}

Обычная вьюха с вьюмоделью, которая должна создаваться только 1 раз, после чего эту вьюмодель нельзя изменить без осложнений (о чем можно почитать в документации).

Демо экран

struct DemoLargeContentView: View {
  @State private var showSubview = true
   
  var body: some View {
    VStack(spacing: 20) {
      Button("\(showSubview ? "Скрыть" : "Показать")") {
        withAnimation {
          showSubview.toggle()
        }
      }
      VStack(spacing: 20) {
        if showSubview {
          DemoSubview123(number: 1)
            .background(.green.opacity(0.5))
        }
        DemoSubview123(number: 2)
          .opacity(showSubview ? 1 : 0)
        DemoSubview123(number: 3)
          .frame(
            width: showSubview ? nil : 0,
            height: showSubview ? nil : 0
          )
      }
    }
  }
}

В нашем примере есть 3 способа скрыть вьюху:

  1. Использовать if/else
  2. Менять прозрачность
  3. Менять фрейм

При желании, конечно, можно скрыть вьюху при помощи ZStack, но тогда эксперимент будет не таким интересным на мой взгляд.

Тестируем реализацию

Запускаем превью, видим в консоли принты:

создаем 1
создаем 2
создаем 3

Все как и задумано: три вьюхи создали по одной вьюмодели.

Жмем на кнопку "Скрыть", в консоли появляется одна строка:

удаляем 1

На этом моменте вспоминаем, что вьюмодель держит в себе важные @Published-свойства, каждое из которых теперь обнулилось. Там могли быть данные, которые долго загружались, или которые нигде не сохранены, но нужны пользователю.

Жмем на кнопку "Показать", в консоли появляется одна строка:

создаем 1

Если при инициализации вьюмодели или при "первом" появлении вьюхи у нас выполняется какая-то логика, то в этот момент она стартовала, и это важно понимать.

Бонус 1

Что если нас устраивает поведение вьюмоделей, и главное UI?

Добавим для каждой вьюхи разный цвет в бэкграунд, а на стек с вьюхами добавим бордюр, и нажмем "Скрыть", получим такой результат - дело во второй вьюхе, которая скрывается через прозрачность. Если убрать вторую вьюху, то весь VStack будет корректно скрыт.

Бонус 2

Что если нам очень хочется скрыть вьюху через if/else, но нужно сохранить все данные во вьюмодели?

Все очень просто: достаточно вынести источник истины за пределы этой вьюхи и if/else, главное чтобы источник истины (другая вьюмодель или стейт-свойства) стабильно существовали при изменении условия для скрытия вьюхи, например:

struct DemoParentView: View {
  @StateObject private var viewModel = TestViewModel(number: 1)
  @State private var showSubview = true
   
  var body: some View {
    VStack(spacing: 20) {
      Button("\(showSubview ? "Скрыть" : "Показать")") {
        withAnimation {
          showSubview.toggle()
        }
      }
      if showSubview {
        Text("Вьюха № \(viewModel.number)")
      }
    }
  }
}

Заключение

Важно помнить, что скрытие вьюхи при помощи if/else приводит к ее пересозданию с нуля, поэтому нужно либо быть готовыми к потере данных на ней, либо использовать ее для отображения данных из родительской вьюхи.

Если у кого-то чуть-чуть поедет верстка из-за скрытия при помощи прозрачности, то ничего страшного не случится. Зато будет повод пообщаться с дизайнерами/тестировщиками 😁

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