SwiftUIJuniorCoding

Как реализовать пользовательский ViewModifier в SwiftUI?

Создайте структуру, реализующую протокол ViewModifier с методом body(content:), возвращающим модифицированный Content. Применяйте через .modifier(MyModifier()) или через расширение View с удобным методом.

Протокол ViewModifier

ViewModifier — это протокол SwiftUI, позволяющий инкапсулировать и переиспользовать группу модификаторов. Вместо того чтобы копировать цепочку .padding().background().cornerRadius() в каждый view, вы описываете её один раз в структуре-модификаторе.

Протокол требует реализовать единственный метод:

func body(content: Content) -> some View

Параметр content — это placeholder, представляющий view, к которому применяется модификатор.

Базовый пример

import SwiftUI

// 1. Описываем модификатор
struct CardStyle: ViewModifier {
    var cornerRadius: CGFloat = 12
    var shadowRadius: CGFloat = 4

    func body(content: Content) -> some View {
        content
            .padding(16)
            .background(Color(.systemBackground))
            .cornerRadius(cornerRadius)
            .shadow(color: .black.opacity(0.1), radius: shadowRadius, x: 0, y: 2)
    }
}

// 2. Расширение View для удобного вызова
extension View {
    func cardStyle(
        cornerRadius: CGFloat = 12,
        shadowRadius: CGFloat = 4
    ) -> some View {
        modifier(CardStyle(
            cornerRadius: cornerRadius,
            shadowRadius: shadowRadius
        ))
    }
}

// 3. Использование
struct ProfileCard: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Иван Петров")
                .font(.headline)
            Text("iOS разработчик")
                .font(.subheadline)
                .foregroundColor(.secondary)
        }
        .cardStyle(cornerRadius: 16)
    }
}

Модификатор с @State (собственное состояние)

ViewModifier может хранить собственное состояние через @State — это делает его полноценным компонентом:

struct ShakeEffect: ViewModifier, Animatable {
    var shakes: CGFloat = 0

    var animatableData: CGFloat {
        get { shakes }
        set { shakes = newValue }
    }

    func body(content: Content) -> some View {
        content
            .offset(x: sin(shakes * .pi * 2) * 10)
    }
}

extension View {
    func shake(trigger: Bool) -> some View {
        modifier(ShakeEffect(shakes: trigger ? 3 : 0))
    }
}

// Использование с анимацией
struct LoginView: View {
    @State private var wrongPassword = false

    var body: some View {
        TextField("Пароль", text: .constant(""))
            .shake(trigger: wrongPassword)
            .onTapGesture {
                withAnimation(.easeInOut(duration: 0.4)) {
                    wrongPassword.toggle()
                }
            }
    }
}

Модификатор с Environment

struct AdaptiveText: ViewModifier {
    @Environment(\.colorScheme) private var colorScheme

    func body(content: Content) -> some View {
        content
            .foregroundColor(colorScheme == .dark ? .white : .black)
            .font(.system(size: 16, weight: .medium, design: .rounded))
    }
}

extension View {
    func adaptiveText() -> some View {
        modifier(AdaptiveText())
    }
}

Условное применение модификатора

extension View {
    @ViewBuilder
    func `if`<Transform: View>(
        _ condition: Bool,
        transform: (Self) -> Transform
    ) -> some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}

// Использование
Text("Важно!")
    .if(isHighlighted) { view in
        view.foregroundColor(.red).bold()
    }

Разница между ViewModifier и прямым расширением View

Прямое расширение View возвращает some View и не имеет собственного состояния. ViewModifier — полноценная структура, которая может:

  • хранить @State, @Environment, @Binding
  • реализовывать Animatable для кастомных анимаций
  • содержать @ViewBuilder-параметры для добавления дочерних view
  • быть передана как значение (modifier(someModifier))

Подводные камни

  • Порядок применения модификаторов важен: .padding().background() и .background().padding() дают разный результат — padding включается в фон или нет.
  • Нельзя использовать модификатор до инициализации view: ViewModifier применяется к уже существующему content, нельзя изменить тип или структуру исходного view.
  • @State в модификаторе сбрасывается при пересоздании родителя: если родительский view пересоздаётся с новой structural identity, модификатор тоже пересоздаётся и теряет состояние.
  • Производительность условных модификаторов: паттерн .if(_:transform:) с @ViewBuilder меняет тип view при изменении условия — SwiftUI трактует это как разные view, что нарушает анимации переходов.
  • Именование конфликтует с системными методами: если назвать расширение так же, как встроенный модификатор (например, .padding()), компилятор может не выбрать нужную перегрузку.
  • Нельзя возвращать несколько корневых View из body: метод body(content:) должен возвращать одно view — используйте Group или VStack при необходимости объединить несколько элементов.

Common mistakes

  • Сводить «реализовать пользовательский ViewModifier в SwiftUI» к синтаксису и не объяснять инвалидация body.
  • Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swiftui-16.
  • Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.

What the interviewer is testing

  • Формулирует точную модель для «реализовать пользовательский ViewModifier в SwiftUI» и подтверждает ее корректным примером.
  • Умеет связать ответ с navigation state, тестированием и отладкой на устройстве.
  • Называет ограничения подхода swiftui-16, включая производительность, память и сопровождение.

Sources

Related topics