Как реализовать пользовательский 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, включая производительность, память и сопровождение.