60. Убираем лишние вычисления body
Oleg991Покажу как избавиться от лишних вычислений body
и избежать ненужных перерисовок (и потенциальной потери данных) в SwiftUI с использованием модификатора equatable
и протокола Equatable
.
Проблема
Если вьюха принимает на вход много разных данных или лишние данные, то она может быть перерисована лишний раз, когда это не нужно - это может привести к сбросу данных в @State
-свойстве вьюхи, которая перерисовалась, что сломает нам UI, или просто создаст лишнюю нагрузку на девайс без визуальных изменений.
Важный нюанс
Применение модификатора equatable
не является обязательным условием для исправления вычисления body
- модификатор только подсветит ошибку в коде, если вьюха не конформит протоколу Equatable
:
Для исправления вычисления body
нам нужно конформить протокол Equatable
и передавать во вьюху только нужные данные для вычисления body
, далее будет больше деталей.
Определяем нечётность
Будем использовать простой экстеншен для определения нечётности числа:
extension Int { /// Является ли число нечётным var isOdd: Bool { self % 2 != 0 } }
Тестовая вьюха
Будем генерировать случайное число и показывать в рамке, является ли оно чётным или нечётным, ну и менять цвет вьюхи:
План в том, чтобы выполнять вычисление 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 ) } }
Видим, что значение свойства 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 } }
Нерабочий пример 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
.
Код для этой статьи можно посмотреть тут, а другие статьи - тут.