Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
SwiftUI/SwiftUI(Advanced)

3. MatchedGeometryEffect / custom segmented Control 세그먼트컨트롤

by Toughie 2023. 6. 20.

🦁MatchedGeometryEffect / custom segmented Control🦁

정말 기똥찬 모디파이어다.. 미리 알았다면 챌린지에서 유용하게 사용했을텐데 ㅜㅜ

뷰 간 애니메이션 전환에 사용하는 modifier로, 두 개 이상의 뷰 사이에서 공통 식별자(identifier)를 기반으로 애니메이션 전환을 해준다.

동일한 식별자를 가진 뷰들 간의 속성이 자연스럽게 전환되는 애니메이션을 구현할 수 있다.


위치 이동 예시

만약 이런 애니메이션을 구현한다면? 방법은 다양하겠지만 가장 단순한 방식은 바로 offset을 조정하는 것이다.

struct MatchedGeometryEffectPrac: View {
    
    @State private var isTapped: Bool = false
    
    var body: some View {
        VStack {
                RoundedRectangle(cornerRadius: 25)
                    .frame(width: 100, height: 100)
                    .offset(y: isTapped ? UIScreen.main.bounds.height * 0.8 : 0)
            Spacer()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.blue.opacity(0.7))
        .onTapGesture {
            withAnimation(.easeInOut) {
                isTapped.toggle()
            }
        }
    }
}

VStack 영역에 tapGesture를 먹여서 애니메이션을 통해 isTapped를 토글하고, 삼항연산자를 통해 offset을 조정한다.

동작은 잘?한다. 하지만 offset을 얼마나 조정할 것인가 정하기도 애매하고 직접 x나 y offset을 조정하는 것은 그닥 좋은 방식은 아니라고 생각한다. 

 

matchedGeometryEffect를 사용한다면?

struct MatchedGeometryEffectPrac: View {
    
    @State private var isTapped: Bool = false
    @Namespace private var namespace
    
    var body: some View {
        VStack {
            if !isTapped {
                RoundedRectangle(cornerRadius: 25)
                    .matchedGeometryEffect(id: "rectangle", in: namespace)
                    .frame(width: 100, height: 100)
            }
            Spacer()
            
            if isTapped {
                RoundedRectangle(cornerRadius: 25)
                    .matchedGeometryEffect(id: "rectangle", in: namespace)
                    .frame(width: 100, height: 100)
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.blue.opacity(0.7))
        .onTapGesture {
            withAnimation(.easeInOut) {
                isTapped.toggle()
            }
        }
    }
}

@State Bool값에 따라 RoundedRectangle이 위, 아래에 그려진다. 

그런데 .matchedGeometryEffect를 통해서 두 RoundedRectangle이 같은 녀석으로 인식되는 것이다.

따라서 아주 자연스러운 애니메이션 구현이 이루어진다.

이것은 같은 id 즉 "rectangle"를 통해서 가능한 것이다. 

여기서 namespace는?

namespace는 애니메이션 전환 범위를 정의할 때 사용되는 개념이다.

위에서 언급한 id, 즉 식별자의 그룹이라고 이해하면 된다.

여기서 상단 RoundedRectangle을 소스뷰, 하단 RoundedRectangle을 타겟뷰라고도 할 수 있는데

.matchedGeometryEffect를 활용해서 애니메이션 전환을 구현할 때 소스뷰와 타겟뷰는 동일한 namespace를 공유해야한다.


Custom Segmented Control

matchedGeometryEffect를 활용해서 아래와 같은 커스텀 Segmented Control을 만들어보자.

struct CustomSegmentedControl: View {
    let categories: [String] = ["바다", "산", "집"]
    
    @State private var selected: String = "바다"
    
    @Namespace private var namespace2
    
    var body: some View {
        
        VStack {
            HStack {
                ForEach(categories, id: \.self) { category in
                    
                    ZStack(alignment: .bottom) {
                        //밑줄.선택 되었을 때만 보이도록
                        if selected == category {
                            RoundedRectangle(cornerRadius: 10)
                                .fill(Color.blue.opacity(0.5))
                                //자연스러운 애니메이션을 위한 matchedGeometryEffect
                                .matchedGeometryEffect(id: "categoryBack", in: namespace2)
                                .frame(width: 50, height: 3)
                                .offset(y:10)
                        }
                        
                        Text(category)
                            .foregroundColor(selected == category ? .blue : .gray)
                            .font(.headline)
                        
                    }
                    .frame(maxWidth: .infinity)
                    .frame(height: 55)
                    //선택
                    .onTapGesture {
                        withAnimation(.spring()) {
                            selected = category
                        }
                    }
                }
            }
            .padding()
            
            Spacer()
            
            Text(emojiForCategory(category:selected))
                .font(.largeTitle)
            
            Spacer()
        }
    }
    
    func emojiForCategory(category: String) -> String {
        switch category {
        case "바다":
            return "🌊"
        case "산":
            return "⛰️"
        case "집":
            return "🏠"
        default:
            return "🍀"
        }
    }
}

 

부드러운 애니메이션 구현이 필요한 경우 matchedGeometryEffect를 활용하면 좋을 것 같다.

Anytransition도!

'SwiftUI > SwiftUI(Advanced)' 카테고리의 다른 글

5. Generic 제네릭  (0) 2023.06.21
4. custom Shape / path  (0) 2023.06.20
2. AnyTransition Custom / 트랜지션 커스텀  (0) 2023.06.19
1. ButtonStyle Custom / 버튼 스타일  (0) 2023.06.19
0. ViewModifier/ 뷰 모디파이어  (0) 2023.06.18