본문 바로가기

IOS

SwiftUI Curved Custom Tab Bar - Kavsoft (iOS 인터렉티브 애니메이션)

728x90
반응형

결과물🤟🏻: 

메뉴 클릭시 해당 메뉴로 이동하는 애니메이션

result.gif

이번 애니메이션을 학습하게된 계기 🔫:

Custom shape와 path를 활용하여 메뉴 아이콘 선택 전환에 따른 path 변화를 애니메이션으로 표현

Reference ✂️ : 

https://youtu.be/XZuc8WnZIS4

 

과정 🎩:

Step 1. Tab bar 메뉴로 사용할 아이콘들과 명칭을 View 하단에 배치

Step 1.png

import SwiftUI


enum Tab: String, CaseIterable {
    case home = "Home"
    case services = "Services"
    case partners = "Partners"
    case activity = "Activity"
    
    // SF Symbol Image
    var systemImage: String {
        switch self {
        case .home:
            return "house"
        case .services:
            return "envelope.open.badge.clock"
        case .partners:
            return "hand.raised"
        case .activity:
            return "bell"
        }
    }
    
    // Return Current Tab Index
    var index: Int {
        return Tab.allCases.firstIndex(of: self) ?? 0
    }
}
@ViewBuilder
func CustomTabBar(_ tint: Color = Color("Blue"), _ inactiveTint: Color = .blue) -> some View {
    HStack(alignment: .bottom, spacing: 0) {
        ForEach(Tab.allCases, id: \.rawValue){
            TabItem(
                tint: tint,
                inactiveTint: inactiveTint,
                tab: $0,
                activeTab: $activeTab,
            )
        }
    }
    .padding(.horizontal, 15)
    .padding(.vertical, 10)
}


struct TabItem: Veiw {
	var tint: Color
    var inactiveTint: Color
    var tab: Tab
    
	var body: some View {
        VStack(spacing: 5){
            Image(systemName: tab.systemImage)
                .font(.title2)
                .foregroundColor(.white)

            Text(tab.rawValue)
                .font(.caption)
                .foregroundColor( .gray)
        }
        .frame(maxWidth: .infinity)
	}
}

 

Step 2. Tab bar 메뉴에서 특정 메뉴 선택시 선택됨을 표현할 수 있는 표현 설정

Step 2.gif

@State private var activeTab: Tab = .home

VStack(spacing: 5){
    Image(systemName: tab.systemImage)
        .font(.title2)
        .foregroundColor(activeTab == tab ? .white : inactiveTint)
        .frame(width: 35, height: 35)
        .background {
            if activeTab == tab {
                Circle()
                    .fill(tint.gradient)
                    .matchedGeometryEffect(id: "ACTIVETAB", in: animation)
            }
        }

    Text(tab.rawValue)
        .font(.caption)
        .foregroundColor(activeTab == tab ? tint : .gray)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.onTapGesture{
	activeTab = tab
}

Step 3. 선택된 메뉴를 더욱 강조 하기 위해 메뉴 전체를 하단 아래로 위치하게 하고 선택된 메뉴는 커지는 설정

Step 3.gif

.frame(width: activeTab == tab ? 58 : 35, height: activeTab == tab ? 58 : 35)
.background {
    if activeTab == tab {
        Circle()
            .fill(tint.gradient)
    }
}

Step 4. 메뉴 선택시 떠오르는 애니메이션을 설정

Step 4.gif

struct Home: View {
	@Namespace private var animation
    ...
}

struct TabItem: View {
	var animation: Namespace.ID
	
	...
    .background {
        if activeTab == tab {
            Circle()
                .fill(tint.gradient)
                .matchedGeometryEffect(id: "ACTIVETAB", in: animation)
        }
     ...
}

Step 5. 메뉴 선택시 선택된 아이콘을 표현할 원이 이동하는 애니메이션 설정

Step 5.gif

.animation(.interactiveSpring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7), value: activeTab)

Step 6. Tab bar와 main View를 구분해줄 수 있는 구분 선을 설정

Step 6.gif

 

.background(content: {
    TabShape(midPoint: .zero)
        .fill(.white)
        .ignoresSafeArea()
        .shadow(color: tint.opacity(0.2), radius: 5, x: 0, y: -5)
        .blur(radius: 2)
        .padding(.top, 25)
})


struct TabShape: Shape {
    var midPoint: CGFloat
    
    var animatableData: CGFloat {
        get { midPoint }
        set {
            midPoint = newValue
        }
    }
    
    func path(in rect: CGRect) -> Path {
        return Path { path in
            path.addPath(Rectangle().path(in: rect))
            
            path.move(to: .init(x: midPoint - 60, y: 0))
            
            let to = CGPoint(x: midPoint, y: -25)
            let control1 = CGPoint(x: midPoint - 25, y: 0)
            let control2 = CGPoint(x: midPoint - 25, y: -25)
            
            path.addCurve(to: to, control1: control1, control2: control2)
            
            // Since we have moved our point +/- 60, you can also use 30 instead of value 25, but I'm fine with 25. If you choose to change, then change all the instances of +/- 25 to +/- 30 in the X axis only.
            let to1 = CGPoint(x: midPoint + 60, y: 0)
            let control3 = CGPoint(x: midPoint + 25, y: -25)
            let control4 = CGPoint(x: midPoint + 25, y: 0)
            
            path.addCurve(to: to1, control1: control3, control2: control4)
            
        }
    }
}

Step 7. 메뉴 선택시 구분 선이 선택된 메뉴를 강조하도록 메뉴 위치에 따른 구분선 모양 변경 설정

Step 7.gif

TabShape(midPoint: tabShapePosition.x)
...

Step 8. 구분선이 부드럽게 이동하도록 애니메이션 설정

Step 8.gif

.animation(.interactiveSpring(response: 0.6, dampingFraction: 0.7, blendDuration: 0.7), value: activeTab)

Step 9. 메뉴 전환시 View화면이 동일하게 전환되지 않는 iOS 버그를 수정

Step 9.gif

init() {
    UITabBar.appearance().isHidden = true
}

학습하며 느낀점 🧑🏻‍🏫:

CGPoint와 path를 활용하여 onTapGesture에서 클릭한 위치에 따른 애니메이션 변화를 설정할 수 있다는 것을 배웠다.

도형과 도형의 관계가 아닌 View의 위치와 Gesture에 따른 상호작용을 활용하면 코드에서는 각각의 독립적인 객체로 보일 수 있으나 View에서는 함께 동작하는 것처럼 보일 수 있다.

반응형