MatchedGeometryEffect
Tạo shared element transitions giữa các views.
1. Cơ bản
Phần tiêu đề “1. Cơ bản”struct ContentView: View { @Namespace private var animation @State private var isExpanded = false
var body: some View { if isExpanded { RoundedRectangle(cornerRadius: 20) .fill(.blue) .matchedGeometryEffect(id: "shape", in: animation) .frame(width: 300, height: 200) } else { RoundedRectangle(cornerRadius: 10) .fill(.blue) .matchedGeometryEffect(id: "shape", in: animation) .frame(width: 50, height: 50) } }}2. Hero Animation
Phần tiêu đề “2. Hero Animation”struct PhotoGrid: View { @Namespace private var namespace @State private var selectedPhoto: Photo?
var body: some View { ZStack { // Grid LazyVGrid(columns: columns) { ForEach(photos) { photo in Image(photo.name) .matchedGeometryEffect(id: photo.id, in: namespace) .onTapGesture { withAnimation(.spring()) { selectedPhoto = photo } } } }
// Detail overlay if let photo = selectedPhoto { Image(photo.name) .matchedGeometryEffect(id: photo.id, in: namespace) .onTapGesture { withAnimation(.spring()) { selectedPhoto = nil } } } } }}3. Tab Indicator
Phần tiêu đề “3. Tab Indicator”struct AnimatedTabs: View { @Namespace private var namespace @State private var selectedTab = 0 let tabs = ["Home", "Search", "Profile"]
var body: some View { HStack { ForEach(tabs.indices, id: \.self) { index in VStack { Text(tabs[index])
if selectedTab == index { Rectangle() .fill(.blue) .frame(height: 2) .matchedGeometryEffect(id: "tab", in: namespace) } else { Rectangle() .fill(.clear) .frame(height: 2) } } .onTapGesture { withAnimation(.spring()) { selectedTab = index } } } } }}📝 Key Points
Phần tiêu đề “📝 Key Points”- Dùng
@Namespaceđể tạo animation namespace - ID phải unique trong namespace
- Cần
withAnimationđể trigger - Great cho hero animations, tab indicators