65. Чем опасен if-модификатор
В SwiftUI-комьюнити популярен модификатор if или applyIf, который применяет к вьюхе какие-то эффекты при определенном условии. В этой статье расскажу и покажу, почему лучше не использовать такие модификаторы.
Код для модификатора
extension View {
@ViewBuilder
func applyIf<M: View>(condition: Bool, transform: (Self) -> M) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
На первый взгляд ничего опасного в коде нет.
Проблема 1
Сделаем вьюху с тогглом и прямоугольником, где ширина прямоугольника будет меняться в зависимости от состояния тоггла, и применим модификатор appliIf:
struct ConditionalModifierExample: View {
@State private var isOn = false
var body: some View {
VStack {
Toggle("Toggle", isOn: $isOn)
Rectangle()
.applyIf(
condition: isOn,
transform: { $0.frame(width: 100) }
)
}
.animation(.linear(duration: 1), value: isOn)
.padding(.horizontal)
.frame(height: 200)
}
}
Теперь поправим проблему, заменив applyIf тернарным оператором:
struct ConditionalModifierExample2: View {
@State private var isOn = false
var body: some View {
VStack {
Toggle("Toggle", isOn: $isOn)
Rectangle()
.frame(width: isOn ? 100 : nil) // <- тут тернарный оператор
}
.animation(.linear(duration: 1), value: isOn)
.padding(.horizontal)
.frame(height: 200)
}
}
Пояснение
Чтобы SwiftUI анимировать изменения, он должен сравнить значение вьюхи до начала анимации и после окончания анимации. Затем SwiftUI выполняет переход между двумя значениями.
Вот тип нашего Rectangle().applyIf(…):
_ConditionalContent<ModifiedContent<Rectangle, _FrameLayout>, Rectangle>
Самый верхний тип _ConditionalContent - это enum, который содержит или значение из ветки if, или значение из ветки else.
Когда условие меняется, SwiftUI не может выполнить переход от старого значения к новому, потому что у них разные типы. В этой ситуации при смене условия происходит замена: одна вьюшка убирается, другая добавляется. Для такой трансформации есть дефолтный переход - именно его мы видим в первом видосе, хотя наша цель - линейная анимация.
Теперь посмотрим на тип Rectangle().frame(…):
ModifiedContent<Rectangle, _FrameLayout>
Когда мы анимируем изменение свойств фрейма, SwiftUI может сразу сделать переход от старого значения к новому, т.к. тип один и тот же, поэтому анимация выглядит как нужно.
Во втором примере мы использовали тернарный оператор - такой подход поддерживается большинством вьюх в SwiftUI. Например, таким же образом можно анимировать цвет или паддинг (с одной стороны тернарника явное значение, с другой - nil).
Важный нюанс: applyIf ломает анимацию даже если мы делаем все правильно перед ним
struct ConditionalModifierExample3: View {
@State private var isOn = false
var body: some View {
VStack {
Toggle("Toggle", isOn: $isOn)
Rectangle()
.frame(width: isOn ? 100 : nil) // <- тернарный оператор
.applyIf(condition: isOn) { $0.border(Color.red) } // <- ломает анимацию
}
.animation(.linear(duration: 1), value: isOn)
.padding(.horizontal)
.frame(height: 200)
}
}
В этом примере анимация не происходит ни для фрейма, ни для бордюра, потому что SwiftUI воспринимает ветки if/else как разные вьюхи.
Проблема 2
Допустим, даже поломанная анимация нас устраивает (пользователь может не заметить, да?).
Есть еще одна проблема - потеря состояния. Когда мы применяем applyIf для вьюхи, которая содержит State-свойства, значения этих свойств будут потеряны при изменении условия. SwiftUI помнит о значении State-свойств в связке с позицией вьюхи в иерархии вьюх.
Рассмотрим для примера такую ситуацию:
/// Вьюха, имеющая свое @State-свойство
struct StatefulView: View {
@State private var text = ""
var body: some View {
TextField("My text", text: $text)
}
}
/// Контейнер для `StatefulView`
struct StatefulViewExample: View {
@State private var isRed = false
var body: some View {
VStack {
StatefulView().applyIf(condition: isRed) {
$0.background(Color.red)
}
Button("Change color") {
isRed.toggle()
}
}
}
}
Пояснение
Когда мы меняем значение в поле isRed, вьюха меняется, как и положение StatefulView() - она перемещается в другую ветку if/else в _ConditionalContent. Это приводит к сбросу значения в State-свойстве, т.к. по мнению SwiftUI старая вьюха была целиком заменена на новую, поэтому текстфилд останется без текста.
Такая же проблема будет и в случае со StateObject.
Заключение
Надеюсь, эта статья поможет тебе лучше разобраться в теме.
Перед использованием красивых модификаторов из интернетов нужно тщательно проверять как они работают 🙂
Код можно посмотреть тут.
Понравилась статья? Подпишись на мой блог!