Реализация Matched Geometry Effect в iOS-приложении (SwiftUI)

TRUETECH занимается разработкой, поддержкой и обслуживанием мобильных приложений iOS, Android, PWA. Имеем большой опыт и экспертизу для публикации мобильных приложений в популярные маркеты Google Play, App Store, Amazon, AppGallery и другие.

Разработка и поддержка любых видов мобильных приложений:

Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

Это лишь некоторые из типы мобильных приложений, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента.

Услуги, которые мы предлагаем
Показано 1 из 1Все 1735 услуг
Реализация Matched Geometry Effect в iOS-приложении (SwiftUI)
Средний
от 1 дня до 3 дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    792
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    671
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1097
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    969
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    914
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    495

Реализация Matched Geometry Effect в iOS-приложении (SwiftUI)

matchedGeometryEffect — SwiftUI-механизм для анимации геометрического перехода между двумя View с одним идентификатором. При переключении состояния (показать/скрыть, развернуть/свернуть) SwiftUI интерполирует position, size и anchor point между парными элементами — результат выглядит как плавное «перетекание» одного в другой.

Мощный инструмент. И регулярный источник неожиданных артефактов, если не понимать, как он работает внутри.

Базовый принцип и типичные кейсы

matchedGeometryEffect требует двух вещей: Namespace (разделяемое пространство идентификаторов) и одинаковый id у пары View. isSource: true — этот View является источником размера для расчёта геометрии.

struct ExpandableCard: View {
    @State private var isExpanded = false
    @Namespace private var cardNamespace

    var body: some View {
        if isExpanded {
            // Полноэкранный вид
            VStack {
                Image("product")
                    .resizable()
                    .matchedGeometryEffect(id: "product-image", in: cardNamespace)
                    .frame(maxWidth: .infinity)
                    .frame(height: 300)
                Text("Подробное описание...")
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .onTapGesture { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
                isExpanded = false
            }}
        } else {
            // Карточка в списке
            HStack {
                Image("product")
                    .resizable()
                    .matchedGeometryEffect(id: "product-image", in: cardNamespace)
                    .frame(width: 80, height: 80)
                    .cornerRadius(8)
                Text("Краткое название")
            }
            .onTapGesture { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
                isExpanded = true
            }}
        }
    }
}

Подводные камни и как их обходить

Проблема №1: оба View рендерятся одновременно. matchedGeometryEffect не скрывает элементы автоматически — если оба View присутствуют в иерархии одновременно, вы увидите оба. Паттерн выше с if/else — правильный: в каждый момент времени существует только один вариант View.

Для списка с множеством элементов (LazyVGrid + детальный оверлей) — правильная структура:

ZStack {
    LazyVGrid(columns: ...) {
        ForEach(products) { product in
            ProductCard(product: product, namespace: gridNamespace,
                        isSelected: selectedProduct?.id == product.id)
                .onTapGesture { withAnimation(.spring()) { selectedProduct = product } }
        }
    }

    if let selected = selectedProduct {
        ProductDetail(product: selected, namespace: gridNamespace)
            .onTapGesture { withAnimation(.spring()) { selectedProduct = nil } }
    }
}

В ProductCard: если isSelected == true, скрываем оригинал через .opacity(0) — позиция в сетке остаётся, но элемент не виден. matchedGeometryEffect продолжает использовать его геометрию как источник.

Image(product.imageName)
    .matchedGeometryEffect(id: "product-\(product.id)", in: gridNamespace,
                           isSource: !isSelected)
    .opacity(isSelected ? 0 : 1)

Проблема №2: namespace — только внутри одного View-дерева. @Namespace нельзя передать через NavigationLink на другой экран — они в разных иерархиях. matchedGeometryEffect работает только внутри одного body или через передачу Namespace.ID как параметра вниз по дереву. Для межэкранных переходов через NavigationStack — нужен iOS 18 NavigationTransition API или кастомный AnyTransition.

Проблема №3: Layout loop. Если в одном контейнере одновременно присутствуют два View с isSource: true и одним id — SwiftUI входит в layout loop. Консоль: "Bound preference ... tried to update multiple times per frame". Всегда только один источник.

Проблема №4: анимация обрезается. View внутри List или ScrollView clip-ируются по bounds контейнера. При расширении карточки анимация обрезается краем списка. Решение — выносить детальный вид из List в ZStack поверх него, как в паттерне выше.

Анимированный custom Tab Bar

Популярный кейс: активный индикатор tab bar плавно перемещается между табами:

struct AnimatedTabBar: View {
    @State private var selectedTab = 0
    @Namespace private var tabNamespace

    let tabs = ["house", "magnifyingglass", "heart", "person"]

    var body: some View {
        HStack {
            ForEach(tabs.indices, id: \.self) { index in
                ZStack {
                    if selectedTab == index {
                        RoundedRectangle(cornerRadius: 12)
                            .fill(Color.blue.opacity(0.15))
                            .matchedGeometryEffect(id: "tab-indicator", in: tabNamespace)
                            .frame(width: 48, height: 36)
                    }
                    Image(systemName: tabs[index])
                        .foregroundColor(selectedTab == index ? .blue : .gray)
                }
                .frame(maxWidth: .infinity)
                .onTapGesture {
                    withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                        selectedTab = index
                    }
                }
            }
        }
        .padding(8)
        .background(Color(.systemBackground))
    }
}

Индикатор — один View с matchedGeometryEffect, который «прыгает» между позициями табов через spring. Это работает потому, что matchedGeometryEffect с одним id в ForEach применяется к тому единственному элементу, где условие истинно.

Сроки

Expandable card с matchedGeometryEffect (одна карточка): 0.5–1 день. LazyGrid с детальным оверлеем и корректной обработкой видимости: 1–2 дня. Анимированный tab bar или custom navigation indicator: несколько часов. Стоимость рассчитывается индивидуально.