60. Убираем лишние вычисления body

60. Убираем лишние вычисления body

Oleg991

Покажу как избавиться от лишних вычислений body и избежать ненужных перерисовок (и потенциальной потери данных) в SwiftUI с использованием модификатора equatable и протокола Equatable.

Проблема

Если вьюха принимает на вход много разных данных или лишние данные, то она может быть перерисована лишний раз, когда это не нужно - это может привести к сбросу данных в @State-свойстве вьюхи, которая перерисовалась, что сломает нам UI, или просто создаст лишнюю нагрузку на девайс без визуальных изменений.

Важный нюанс

Применение модификатора equatable не является обязательным условием для исправления вычисления body - модификатор только подсветит ошибку в коде, если вьюха не конформит протоколу Equatable:

Модификатор .equatable() показывает, что вьюха не конформит протоколу Equatable

Для исправления вычисления body нам нужно конформить протокол Equatable и передавать во вьюху только нужные данные для вычисления body, далее будет больше деталей.

Определяем нечётность

Будем использовать простой экстеншен для определения нечётности числа:

extension Int {
  /// Является ли число нечётным
  var isOdd: Bool { self % 2 != 0 }
}

Тестовая вьюха

Будем генерировать случайное число и показывать в рамке, является ли оно чётным или нечётным, ну и менять цвет вьюхи:

Демо-вьюха

План в том, чтобы выполнять вычисление body в цветной вьюхе только при изменении чётности сгенерированного числа, т.е. при нажатии на кнопку для генерации числа мы должны видеть в консоли только разные значения для isOdd (нечётный):

Ожидаемый (правильный) результат вычисления body - только при изменении свойства isOdd

Для цветной вьюхи будем использовать такой код:

struct ExampleTextView: View {
    let text: String
    let backgroundColor: Color
     
    var body: some View {
      Text(text)
        .foregroundStyle(.white)
        .padding(20)
        .background(
          RoundedRectangle(cornerRadius: 10)
            .fill(backgroundColor)
        )
    }
  }

Тут нет никаких подводных камней. Это обычная вьюха, принимающая на вход только текст и цвет фона.

Нерабочий пример 1

Передадим во вьюху свойство number:

struct BrokenView1: View {
  let number: Int
   
  var body: some View {
    print("Вычисляем body 1, isOdd = \(number.isOdd)")
    return ExampleTextView(
      text: number.isOdd ? "Нечётный" : "Чётный",
      backgroundColor: number.isOdd ? Color.red : Color.green
    )
  }
}
Демо лишних вычислений body

Видим, что значение свойства isOdd часто повторяется в консоли, т.е. вьюха делает лишнюю работу, хотя визуально ведет себя как нужно.

Попробуем исправить это поведение при помощи протокола Equatable и реализуем единственный метод ==, который требуется протоколом:

struct BrokenView1: View, Equatable {
  let number: Int
   
  var body: some View {
    print("Вычисляем body 1, isOdd = \(number.isOdd)")
    return ExampleTextView(
      text: number.isOdd ? "Нечётный" : "Чётный",
      backgroundColor: number.isOdd ? Color.red : Color.green
    )
  }
   
  static func == (lhs: BrokenView1, rhs: BrokenView1) -> Bool {
    lhs.number.isOdd == rhs.number.isOdd
  }
}
Результат остался прежним - body вычисляется некорректно

Нерабочий пример 2

В этом примере попробуем передать на вход два свойства:

  • number (то же, что и в предыдущем примере)
  • isOdd (является ли число нечётным) - на основании этого свойства будем менять вид вьюхи
struct BrokenView2: View, Equatable {
  let number: Int
  let isOdd: Bool
   
  var body: some View {
    print("Вычисляем body 2, isOdd = \(isOdd)")
    return ExampleTextView(
      text: isOdd ? "Нечётный" : "Чётный",
      backgroundColor: isOdd ? Color.red : Color.green
    )
  }
   
  static func == (lhs: BrokenView2, rhs: BrokenView2) -> Bool {
    lhs.number.isOdd == rhs.number.isOdd
  }
}

Поведение вьюхи не изменилось, поэтому без гифки.

Тут не важно, какую логику сравнения в методе == мы напишем - результат все равно останется одинаковым, будут лишние вычисления body.

Рабочий пример 1

Чтобы исправить ситуацию, достаточно добавить какое-то @State-свойство в целевую вьюху (и обязательно реализовать протокол Equatable):

struct HardView: View, Equatable {
  let number: Int
  let isOdd: Bool
  @State private var test = false // <- новое свойство
   
  var body: some View {
    print("Вычисляем body 3, isOdd = \(isOdd)")
    return ExampleTextView(
      text: isOdd ? "Нечётный" : "Чётный",
      backgroundColor: isOdd ? Color.red : Color.green
    )
  }
   
  static func == (lhs: HardView, rhs: HardView) -> Bool {
    lhs.number.isOdd == rhs.number.isOdd
//      lhs.isOdd == rhs.isOdd // <- тоже рабочий вариант
  }
}

Теперь в дело включился @State-механизм, и у нас все заработало как и нужно - body вычисляется только при реальном изменении свойства isOdd.

Рабочий пример 2

Самый простой способ избежать лишних вычислений body - передать во вьюху только нужное свойство isOdd - все заработает как нужно даже без протокола Equatable:

struct EasyFinalView: View {
  let isOdd: Bool
   
  var body: some View {
    print("Вычисляем body, isOdd = \(isOdd)")
    return ExampleTextView(
      text: isOdd ? "Нечётный" : "Чётный",
      backgroundColor: isOdd ? Color.red : Color.green
    )
  }
}

Все варианты вместе

Визуально все 4 варианта выглядят одинаково, но поведение body все же отличается. На демо ниже видно, что при втором нажатии сработали принты в нерабочих примерах (1 и 2), хотя визуально вьюхи не изменились:

Демо всех вариантов сразу

При изменении isOdd при корректном поведении body в консоли должны быть всегда принты по порядке от 1 до 4:

Вычисляем body 1, isOdd = false
Вычисляем body 2, isOdd = false
Вычисляем body 3, isOdd = false
Вычисляем body 4, isOdd = false

Заключение

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

Если нужна нестандартная логика сравнения вьюх, то можно вспомнить о протоколе Equatable.

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



Report Page