Что такое matchedGeometryEffect и как он используется?
matchedGeometryEffect синхронизирует позицию и размер двух View с одинаковым id в одном @Namespace, создавая плавный shared-element переход — карточка «летит» в детальный экран и обратно.
matchedGeometryEffect в SwiftUI
matchedGeometryEffect — модификатор, который синхронизирует геометрию (позицию и размер) двух View в разных частях иерархии. SwiftUI интерполирует фрейм одного View к фрейму другого, создавая плавный shared-element transition — переход, при котором элемент «летит» из одного места в другое.
Как это работает
Модификатор требует два параметра:
id— уникальный идентификатор пары View;in— пространство имён (@Namespace), которое связывает пару вместе.
Пространство имён создаётся как @Namespace private var animation и передаётся в оба View. SwiftUI находит View с одинаковыми id в одном пространстве имён и анимирует переход между ними.
Пример: карточка, раскрывающаяся в детальный экран
struct Item: Identifiable {
let id: Int
let title: String
let color: Color
}
struct HeroTransitionView: View {
@Namespace private var animation
@State private var selectedItem: Item? = nil
let items: [Item] = [
Item(id: 1, title: "Swift", color: .orange),
Item(id: 2, title: "Python", color: .blue),
Item(id: 3, title: "Kotlin", color: .purple)
]
var body: some View {
ZStack {
// Сетка карточек
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))]) {
ForEach(items) { item in
if selectedItem?.id != item.id {
CardView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
selectedItem = item
}
}
}
}
}
.padding()
}
// Детальный экран
if let item = selectedItem {
DetailView(item: item)
.matchedGeometryEffect(id: item.id, in: animation)
.ignoresSafeArea()
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
selectedItem = nil
}
}
}
}
}
}
struct CardView: View {
let item: Item
var body: some View {
item.color
.frame(height: 100)
.cornerRadius(12)
.overlay(Text(item.title).foregroundColor(.white).bold())
}
}
struct DetailView: View {
let item: Item
var body: some View {
item.color
.overlay(
VStack {
Text(item.title)
.font(.largeTitle.bold())
.foregroundColor(.white)
Text("Нажмите, чтобы закрыть")
.foregroundColor(.white.opacity(0.7))
}
)
}
}
Параметр isSource
По умолчанию оба View являются источником геометрии. Параметр isSource: false говорит SwiftUI, что данный View должен принять геометрию от парного View, а не отдавать свою:
// View-источник определяет итоговую позицию
Text("Hero")
.matchedGeometryEffect(id: "hero", in: ns, isSource: true)
// View-получатель адаптируется под позицию источника
Text("Hero")
.matchedGeometryEffect(id: "hero", in: ns, isSource: false)
Анимация табов
struct TabSelector: View {
@Namespace private var tabAnimation
@State private var activeTab = 0
let tabs = ["Работа", "Компании", "Профиль"]
var body: some View {
HStack {
ForEach(tabs.indices, id: \.self) { index in
Button(tabs[index]) {
withAnimation(.easeInOut(duration: 0.2)) {
activeTab = index
}
}
.padding(.vertical, 8)
.padding(.horizontal, 16)
.background {
if activeTab == index {
Capsule()
.fill(Color.accentColor)
.matchedGeometryEffect(id: "tab-indicator", in: tabAnimation)
}
}
.foregroundColor(activeTab == index ? .white : .primary)
}
}
.padding(4)
.background(Color(.systemGray6), in: Capsule())
}
}
Подводные камни
- Оба View должны существовать одновременно в иерархии — если оба View скрыты или только один из них рендерится, анимация не произойдёт; используйте
opacity(0)вместо условного исключения из иерархии. - @Namespace должен принадлежать общему родителю — нельзя передавать Namespace между независимыми экранами через NavigationStack; для этого нужно поднять состояние выше.
- Конфликт нескольких View с одинаковым id — два View с одинаковым
idв одном namespace вызывают предупреждение и непредсказуемое поведение анимации. - Не работает через NavigationStack без дополнительных усилий — стандартный push-transition перекрывает matchedGeometryEffect; нужен кастомный контейнер через ZStack.
- Производительность при большом числе пар — каждая пара с одинаковым namespace участвует в layout pass; при десятках пар это заметно нагружает рендер.
- withAnimation обязателен — без обёртки изменений в withAnimation переход произойдёт мгновенно без интерполяции геометрии.
- Clip-контейнеры обрезают анимацию — если родительский View имеет
.clipped()или.cornerRadius()без.clipShape, анимирующийся View будет обрезан по границе родителя.
Common mistakes
- Сводить «
matchedGeometryEffectи как он используется» к синтаксису и не объяснять environment. - Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swiftui-13.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «
matchedGeometryEffectи как он используется» и подтверждает ее корректным примером. - Умеет связать ответ с main actor, тестированием и отладкой на устройстве.
- Называет ограничения подхода swiftui-13, включая производительность, память и сопровождение.