🦁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 |