iyOmSd/Title: SwiftUI

[SwiftUI] Namespace + matchedGeometryEffect(feat. 상단탭바 UI)

냄수 2022. 7. 14. 18:59
반응형

SwiftUI로 위와같은 UI기능을 만드려고 하면서 알게 된 

Namespace에 대해 간단하게 정리해보려고해요

먼저 개념을 둘러보면

 

프로퍼티를 포함하는 객체(뷰)의 영구 id로 정의된 네임스페이스에 접근할 수 있는 동적 속성 타입

객체의 정보를 id로 다른 뷰와 공유할 수 있는거에요

이 속성과 같이 사용하는게 

View.matchedGeometryEffect(id: in:) 함수구요

 

동일한 키를 가진 다른 뷰가 새로운 뷰를 삽입하는 경우

시스템은 이전 위치에서 새로운 위치로 이동하는 하나의 뷰처럼 보이도록

window공간에 frame 사각형을 합칩니다.

일반적인 전환 메커니즘은 전환중 두 뷰가 각각 렌더링 되는 방식을 정의하며, 

뷰의 렌더링이아닌 연결될 geometry에만 정렬합니다.

 

 

사용은 간단해요

상단탭바같이 사용하기위해 구현하려다보니 스유로는 잘 생각이안나더라구요

어느 하이라키에 저 언더바를 넣어야할지 부터 어떻게 위치를 잡아야할지...

막막하더라구요

먼저 각 버튼을 HStack에 담아서 탭바버튼을 만들거에요

그리고 각각 버튼안에 언더바(검정선)을 넣어줄거에요

struct TabBarView: View {
    @Binding var currentTab: Int
    var tabBarOptions: [String] = ["hello", "world", "this", "is", "something"]
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 20) {
                ForEach(tabBarOptions.indices, id: \.self) { index in
                    let title = tabBarOptions[index]
                    TabBarItem(currentTab: $currentTab,
                               title: title,
                               tab: index)
                }
            }
            .padding(.horizontal)
        }
        .background(Color.white)
        .frame(height: 40)
    }
}

struct TabBarItem: View {
    @Binding var currentTab: Int
    
    var title: String
    var tab: Int
    
    var body: some View {
        Button {
            currentTab = tab
        } label: {
            VStack {
                Spacer()
                Text(title)
                if currentTab == tab {
                    Color.black
                        .frame(height: 2)
                } else {
                    Color.clear.frame(height: 2)
                }
            }
            .animation(.spring(), value: currentTab)
        }
        .buttonStyle(.plain)
    }
}

현 상태로는 버튼을 클릭하면 이렇게 버튼안에있는 버튼이 켜졋다 꺼졋다 하죠

이것을 이제

@Namespace를 사용하면

스르르륵 움직이게 할 수 있어요!!

 

위의 코드를 조금 손봐야해요

우선 모든 버튼의 Namespace를 같게해줘야해요


struct TabBarView: View {
    @Binding var currentTab: Int
    @Namespace var namespace    << 추가된 부분
    var tabBarOptions: [String] = ["hello", "world", "this", "is", "something"]
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 20) {
                ForEach(tabBarOptions.indices, id: \.self) { index in
                    let title = tabBarOptions[index]
                    TabBarItem(currentTab: $currentTab,
                               namespace: namespace,     << 추가된 부분
                               title: title,
                               tab: index)
                }
            }
            .padding(.horizontal)
        }
        .background(Color.white)
        .frame(height: 40)
    }
}


struct TabBarItem: View {
    @Binding var currentTab: Int
    let namespace: Namespace.ID    << 추가된 부분
    
    var title: String
    var tab: Int
    
    var body: some View {
        Button {
            currentTab = tab
        } label: {
            VStack {
                Spacer()
                Text(title)
                if currentTab == tab {
                    Color.black
                        .frame(height: 2)
                        .matchedGeometryEffect(id: "underline",    << 추가된 부분
                                               in: namespace.self)
                } else {
                    Color.clear.frame(height: 2)
                }
            }
            .animation(.spring(), value: currentTab)
        }
        .buttonStyle(.plain)
    }
}

탭바에서 Namespace를 만들어서

각 버튼을 그 Namespace.ID를 받아생성해요

그런 뒤 

View.matchedGeometryEffect

를 이용해서 애니메이션을 처리해주면

서로 다른 뷰이지만 뷰에 대한 정보가 공유되서 같은뷰처럼 취급되고

애니메이션이 어디로 이동할지 결과를 알고있어요

따라서

눌려진 버튼에있는 언더바가 히든되고 누른 버튼에 있는 언더바가 보인다

->

눌려진 버튼의 언더바가 누른버튼 언더바로 이동한다

처럼

언더바가 깜박이는게아니라

누른 버튼으로 슬라이드되서 원하는 동작을 만들 수 있죠

 

 

 

 

 

 

 

반응형