← 아카이브로 돌아가기

Development · SwiftUI

matchedGeometryEffect로 뷰 전환 연결하기

Namespace와 동일한 ID를 이용해 서로 다른 뷰를 하나의 자연스러운 전환으로 연결하는 방법을 살펴봅니다.

Velog 원문 보기 ↗

Development 아카이브 표지

조건에 따라 뷰가 교체되면 기본 애니메이션만으로는 두 상태가 서로 이어진다는 느낌을 만들기 어렵다. matchedGeometryEffect는 사라지는 뷰와 나타나는 뷰를 같은 대상으로 인식시킨다.

MatchedGeometryEffect

일반적인 애니메이션 효과는 단일 뷰의 변화를 감지하여 그 변화에 애니메이션을 적용한다. matchedGeometryEffect를 이용하여 두 뷰 사이의 변화에 애니메이션 효과를 적용할 수 있다. matchedGeometryEffect를 적용하기 위해선 같은 뷰로 인식할 고유한 id값과 이 정보를 기억할 @Namespace라는 프로퍼티 래퍼가 필요하다.

struct MatchedGeometryEffectStudy: View {
    
    @State private var isClicked: Bool = false
    @Namespace private var namespace
    
    var body: some View {
        VStack {
            if !isClicked {
                RoundedRectangle(cornerRadius: 12)
                	// 애니메이션을 적용할 뷰 지정
                    .matchedGeometryEffect(id: "rectangle", in: namespace)
                    .frame(width: 100, height: 100)
            }
            Spacer()
            if isClicked {
                RoundedRectangle(cornerRadius: 12)
					// 애니메이션을 적용할 뷰 지정
                    .matchedGeometryEffect(id: "rectangle", in: namespace)
                    .frame(width: 200, height: 200)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.red)
        .onTapGesture {
            withAnimation(.easeInOut) {
                isClicked.toggle()
            }
        }
    }
}

이를 활용하여 SegmentedControl과 같은 효과를 구현할 수 있다.

struct MatchedGeometryEffectStudy2: View {
    
    let categories: [String] = ["Home", "Popular", "Saved"]
    @State private var selected: String = "Home"
    @Namespace private var namespace2
    
    var body: some View {
        HStack {
            ForEach(categories, id: \.self) { category in
                ZStack {
                    if selected == category {
                        RoundedRectangle(cornerRadius: 12)
                            .fill(Color.blue).opacity(0.3)
							// 애니메이션을 적용할 뷰 지정
                            .matchedGeometryEffect(id: "category", in: namespace2)
                    }
                    Text(category)
                }
                .frame(maxWidth: .infinity)
                .frame(height: 56)
                .onTapGesture {
                    withAnimation(.spring()) {
                        selected = category
                    }
                }
            }
        }
    }
}

← 아카이브로 돌아가기