์•„๋ฌด๋ฆฌ ๋ด๋„ ํƒญ ๋ฐ”๊ฐ€ ๋งž๋Š”๊ฑฐ ๊ฐ™์€๋ฐ ๋””์ž์ธํŒ€์ด ํ† ๊ธ€์ด๋ผ ํ•˜๋‹ˆ ์šฐ์„  ํ† ๊ธ€์ธ๊ฑธ๋กœ..!! ๋ญ 2๊ฐœ๋ฉด ํ† ๊ธ€ ๋งž์ง€!

 

์„œ๋ฒ„๋กœ ๋ถ€ํ„ฐ ๋ช‡ ๊ฐœ๊ฐ€ ์˜ฌ์ง€ ๋ชจ๋ฅด๊ณ , ํ•ด๋‹น ์˜์—ญ์„ ํƒญํ–ˆ์„ ๋•Œ ํ† ๊ธ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์‹คํ–‰๋˜์•ผ ํ•œ๋‹ค. Namespace๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž์—ฐ์Šค๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฌผ๋ก  animation ์„ ์‚ฌ์šฉํ•ด๋„ ๋œ๋‹ค! ์ฒ˜์Œ์— GeometryReader๋กœ ์œ„์น˜๋ฅผ ๊ณ„์‚ฐ ํ•œ ๋’ค ํ•˜๋‚˜์˜ View์˜ ์œ„์น˜๋ฅผ ๋ณ€๊ฒฝํ•ด ์ฃผ์—ˆ๋”๋‹ˆ offset๊ณผ padding ์œผ๋กœ ๋ทฐ ๊ณ„์ธต์„ ๋ฐ”๊ฟจ์„ ๋•Œ ์ด์Šˆ๊ฐ€ ์žˆ์—ˆ๋‹ค. GeometryReader๋กœ ๊ณ„์‚ฐ ํ•œ ์œ„์น˜๊ฐ€ ๋‹ฌ๋ผ์ ธ์„œ ์—‰๋šฑํ•œ ๊ณณ์—์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๊ฐ€ ์‹œ์ž‘๋˜๊ณ  ์›€์ง์˜€๋‹ค. 

 

struct CustomToggle: View {
    let priceList = ["on", "off", "test"]
    /// ํ† ๊ธ€ ์• ๋‹ˆ๋ฉ”์ด์…˜
    @Namespace private var toggleAnimation
    /// ํ˜„์žฌ ์„ ํƒ๋œ ์œ„์น˜
    @State private var selectedIndex = 0
    /// on/off ์—ฌ๋ถ€
    @Binding var isOn: Bool
    
    var body: some View {
        ZStack {
            HStack(spacing: 0) {
                ForEach(priceList.indices, id: \.self) { index in
                    ToggleAnimationView(selectedIndex == index, content: Text(priceList[index])
                        .font(.system(size: 14))
                        .fontWeight(.bold)
                        .frame(height: 42)
                        .padding(.horizontal, 18)
                        .foregroundColor(selectedIndex == index ? Color.white : .black)
                        .onTapGesture {
                            withAnimation(.easeInOut) {
                                selectedIndex = index
                            }
                            
                        }
                    ) //: ToggleAnimationView
                } //: ForEach
            } //: HStack
        } //: ZStack
        .padding(2)
        .frame(height: 46)
        .background(
            RoundedRectangle(cornerRadius: 23)
                .stroke(.white, lineWidth: 1)
                .shadow(color: Color(white: 0, opacity: 0.5), radius: 3, x: 3, y: 3)
                .background(.gray)
                .clipShape(RoundedRectangle(cornerRadius: 23))
        )
        .frame(maxWidth: .infinity)
    }
    
    @ViewBuilder func ToggleAnimationView<Content: View>(_ isActive: Bool, content: Content) -> some View {
        if isActive {
            content
                .background(
                    RoundedRectangle(cornerRadius: 50)
                        .foregroundColor(.orange)
                        .padding(2)
                        .matchedGeometryEffect(id: "highlightitem", in: toggleAnimation)
                )
        } else {
            content
        }
    }
}

#Preview {
    CustomToggle(isOn: .constant(true))
}

 

ํ•ต์‹ฌ์€ ์„ ํƒ๋œ ์•„์ดํ…œ์˜ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ƒ‰์ƒ์„ ์ง€์ •ํ•˜๊ฒŒ ๋˜๋Š”๋ฐ, ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์— matchedGeometryEffect์™€ Namespace property wrapper์„ ์‚ฌ์šฉํ•ด์„œ effect๋ฅผ ์ง€์ •ํ•ด ์ฃผ๋ฉด ๋œ๋‹ค

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋‹ค์Œ์— ์„ ํƒ๋˜๋Š” ์•„์ดํ…œ์˜ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์™€ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์—ฐ๊ฒฐ๋œ๋‹ค

 

ForEach๋Š” SwiftUI์˜ system ์— ์˜ํ•ด parent View ์˜ transition์„ ๋”ฐ๋ฅด์ง€ ์•Š์œผ๋‹ˆ ์ฃผ์˜ํ•ด์•ผ ํ•œ๋‹ค. parentView์˜ transition์ด ๋“ค์–ด๊ฐ€๋Š” ๊ฒฝ์šฐ ๊ฐ row์— transition์ด๋‚˜ animation์„ ๋„ฃ์–ด์ค˜์•ผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋œ๋‹ค.

 

์ฐธ๊ณ  ์‚ฌ์ดํŠธ 

https://medium.com/appcoda-tutorials/how-to-build-an-animated-tab-bar-in-swiftui-26e4446f90ef