
CustomTabView
우선 MyView
라는 SwiftUI
파일을 생성해서 위에 보이는 메인 배경 뷰를 만들어 주겠습니다.
// MyView.swift import SwiftUI struct MyView: View { //타이틀과 배경색을 변수로 지정 var title: String var bgColor: Color var body: some View { ZStack { bgColor //safeArea 부분까지 채워줌 .edgesIgnoringSafeArea(.all) Text(title) .font(.largeTitle) .foregroundColor(.white) .fontWeight(.bold) } } } struct MyView_Previews: PreviewProvider { static var previews: some View { //미리보기를위해 임시로 아래와같이 기본값을 정해줘야함 MyView(title: "서근개발블로그", bgColor: Color.red) } }
TabView 구현
이제 tabView
를 직접 만들어서 구현하려고 합니다. enum
을 사용하여 tabBar
에 보여줄 텍스트를 작성합니다.
struct MyCustomView: View { enum TabIndex { case home, photo, profile }
이제 이 열거형을 매개변수
와 매개값
으로 사용해야 합니다.
열거형 아래에 @State var tabIndex: tabIndex
를 넣어줍니다.
MyView
를 switch
문으로 가져오려고 하는데 먼저 함수를 작성해야 합니다. 작성해줬던 TabIndex
를 매개변수로 가져오고 MyView
를 반환해주겠습니다. default
부분은 기본값이 되므로 홈에 있는 값을 넣어줍니다.
func showMyView(tabIndex: TabIndex) -> MyView { switch tabIndex { case .home: return MyView(title: "서근개발블로그", bgColor: Color.yellow) case .photo: return MyView(title: "사진첩", bgColor: Color.red) case .profile: return MyView(title: "사용자 계정", bgColor: Color.blue) default: return MyView(title: "서근개발블로그", bgColor: Color.yellow) } }
그리고 TabBar
를 꾸며줄 건데 GeometryReader
를 사용하여 동일한 width
를 만들어줘야 합니다. ZStack
아래에 우리가 만들어둔 MyView
도 호출해줍니다.
var body: some View { GeometryReader { geo in ZStack { self.showMyView(tabIndex: self.tabIndex) HStack { Button(action: { print("홈을 선택했습니다.") }) { Image(systemName: "house.fill") .frame(width: geo.size.width / 3, height: 50) .font(.title2) .foregroundColor(.blue) } .background(Color.white) } } }.edgesIgnoringSafeArea(.all)
저희는 한 화면에 3
개의 tabBar
를 만들어 주려고 하기 때문에 전체 가로에서 나누기 3
을 했습니다. 그리고 Xcode를 보면 Preview
쪽에 에러가 나는데 이것은 기본값이 없기 때문입니다.
기본값은 MyCustomView(tabIndex: .home)
이라고 넣어줍니다.
그런데 아이콘이 화면 위쪽에 배치되어 있죠? ZStack
에 alignment
를 bottom
으로 설정해줍니다. ZStack(alignment: .bottom)
HStack
에 있는 버튼을 복사해서 탭에 총 3
개가 되도록 붙여 넣기 합니다.

Stack 사용 시 주의사항SwiftUI
의 모든 Stack
에는 자동으로 spacing
이 적용되어있습니다.
이것을 해결하기 위해서는 HStack(spacing: 0)
으로 설정해줘야 합니다.

현재까지 코드
struct MyCustomView: View { enum TabIndex { case home, photo, profile } @State var tabIndex: TabIndex func showMyView(tabIndex: TabIndex) -> MyView { switch tabIndex { case .home: return MyView(title: "서근개발블로그", bgColor: Color.yellow) case .photo: return MyView(title: "사진첩", bgColor: Color.red) case .profile: return MyView(title: "사용자 계정", bgColor: Color.blue) default: return MyView(title: "서근개발블로그", bgColor: Color.yellow) } } var body: some View { GeometryReader { geo in ZStack(alignment: .bottom){ self.showMyView(tabIndex: self.tabIndex) HStack(spacing: 0) { Button(action: { print("홈을 선택했습니다.") }) { Image(systemName: "house.fill") .frame(width: geo.size.width / 3, height: 50) .font(.title2) .foregroundColor(.blue) } .background(Color.white) Button(action: { print("홈을 선택했습니다.") }) { Image(systemName: "photo.fill") .frame(width: geo.size.width / 3, height: 50) .font(.title2) .foregroundColor(.blue) } .background(Color.white) Button(action: { print("홈을 선택했습니다.") }) { Image(systemName: "person.circle.fill") .frame(width: geo.size.width / 3, height: 50) .font(.title2) .foregroundColor(.blue) } .background(Color.white) } } } .edgesIgnoringSafeArea(.all) } } struct MyCustomView_Previews: PreviewProvider { static var previews: some View { MyCustomView(tabIndex: .home) } }
버튼 클릭시 뷰 전환
버튼을 눌렀을 때 지정된 View
가 나오도록 Button
의 Action
쪽에 아래와 같이 코드를 작성합니다.
Button(action: { self.tabIndex = .home print("홈을 선택했습니다.") }) { }

활성화 된 버튼과 비활성화 버튼 색 효과
한 가지 재미난 기능을 추가해주려고 하는데 예를 들어 홈버튼을 눌렀을 때는 나머지 버튼들이 회색으로 변하고 원하는 버튼을 눌렀을 땐 지정된 색이 되도록 하려고 합니다. 이 기능을 사용하기 위해서는 함수를 작성해줘야 합니다.
func changeColor(tabIndex: TabIndex) -> Color { switch tabIndex { case .home: return Color.yellow case .photo: return Color.red case .profile: return Color.blue default: return Color.yellow } }
함수를 호출해보도록 하겠습니다. Button Image
의 foregroundColor
쪽을 수정합니다.
//tabIndex가 home이 선택되었다면, changeColor를 실행하고, 그게 아니라면 gray색을 부여해라 .foregroundColor(self.tabIndex == .home ? self.changeColor(tabIndex: tabIndex): Color.gray)
Button(action: { self.tabIndex = .home print("홈을 선택했습니다.") }) { Image(systemName: "house.fill") .frame(width: geo.size.width / 3, height: 50) .font(.title2) .foregroundColor(self.tabIndex == .home ? self.changeColor(tabIndex: tabIndex): Color.gray) } .background(Color.white)

버튼 클릭시 아이콘 확대 효과
버튼의 색에 변화를 주었으니 이제 배운 것을 활용하여 버튼을 누를 때마다 사이즈가 커지는 방법도 할 수 있습니다.
.scaleEffect
를 사용해야 합니다.
@State var ImageSize: CGFloat = 1.2
Image(systemName: "house.fill") //만약 tabIndex가 home이라면 ImageSize를 실행하고 그게 아니라면 사이즈는 1.0 이다. .scaleEffect(self.tabIndex == .home ? self.ImageSize : 1.0)

버튼 이미지 크기가 변할때 animation
효과를 주려면 withAnimation 을 적용시키면 됩니다.
Button(action: { withAnimation { self.tabIndex = .home print("홈을 선택했습니다.") } }) {

아이콘을 보면 자연스럽게 애니메이션 효과가 들어갔지만, 한 가지 거슬리는 점은 텍스트까지 애니메이션 효과가 들어갔다는 것입니다. View
의 애니메이션 효과를 주지 않기 위해서는 MyView
로 이동해서 아래와 같이 코드를 넣어줘야 합니다.
// MyView.swift struct MyView: View { ... var body: some View { ZStack { ... } .animation(.none) } }
현재까지 코드
struct MyCustomView: View { enum TabIndex { case home, photo, profile } @State var tabIndex: TabIndex @State var ImageSize: CGFloat = 1.2 func showMyView(tabIndex: TabIndex) -> MyView { switch tabIndex { case .home: return MyView(title: "서근개발블로그", bgColor: Color.yellow) case .photo: return MyView(title: "사진첩", bgColor: Color.red) case .profile: return MyView(title: "사용자 계정", bgColor: Color.blue) default: return MyView(title: "서근개발블로그", bgColor: Color.yellow) } } func changeColor(tabIndex: TabIndex) -> Color { switch tabIndex { case .home: return Color.yellow case .photo: return Color.red case .profile: return Color.blue default: return Color.yellow } } var body: some View { GeometryReader { geo in ZStack(alignment: .bottom){ self.showMyView(tabIndex: self.tabIndex) HStack(spacing: 0) { Button(action: { withAnimation { self.tabIndex = .home print("홈을 선택했습니다.") } }) { Image(systemName: "house.fill") .scaleEffect(self.tabIndex == .home ? self.ImageSize : 1.0) .frame(width: geo.size.width / 3, height: 50) .font(.title2) .foregroundColor(self.tabIndex == .home ? self.changeColor(tabIndex: tabIndex): Color.gray) } .background(Color.white) Button(action: { withAnimation { self.tabIndex = .photo print("홈을 선택했습니다.") } }) { Image(systemName: "photo.fill") .scaleEffect(self.tabIndex == .photo ? self.ImageSize : 1.0) .frame(width: geo.size.width / 3, height: 50) .font(.title2) .foregroundColor(self.tabIndex == .photo ? self.changeColor(tabIndex: tabIndex): Color.gray) } .background(Color.white) Button(action: { withAnimation { self.tabIndex = .profile print("홈을 선택했습니다.") } }) { Image(systemName: "person.circle.fill") .scaleEffect(self.tabIndex == .profile ? self.ImageSize : 1.0) .frame(width: geo.size.width / 3, height: 50) .font(.title2) .foregroundColor(self.tabIndex == .profile ? self.changeColor(tabIndex: tabIndex): Color.gray) } .background(Color.white) } } } .edgesIgnoringSafeArea(.all) } }
탭바 아이콘 클릭시 반원 효과주기
tabBar
의 버튼이 눌리면 뒤에 반원의 도형이 따라오도록 만들어줍니다. ZStack
안에 Circle
을 추가합니다.
ZStack(alignment: .bottom){ self.showMyView(tabIndex: self.tabIndex) Circle() .foregroundColor(.white) .frame(width: 70, height: 70)

지금은 도형이 가운데에 가있죠? 하지만 우리는 버튼이 누르는 곳으로 이 도형을 이동시켜야 합니다. offset
수정자가 필요하고 이것의 위치를 계산하는 함수도 만들어줘야 합니다. .offset(x: geo.size.width / 3, y: 0)

home
쪽으로 이동시키려면 .offset(x: -(geo.size.width / 3), y: 0)

자, 어느 정도 위치를 표현하는 법은 알게 되었습니다. 이제 함수를 작성해줄게요.
func CalculatePosition(tabIndex: TabIndex, geo: GeometryProxy) -> CGFloat { switch tabIndex { case .home: return -(geo.size.width / 3) case .photo: return 0 case .profile: return (geo.size.width / 3) default: return -(geo.size.width / 3) } }
Geometry
를 매개변수
로 가져올 때는 GeometryProxy
를 사용하고 CGFloat
를 반환
해야 합니다.
Circle() ... .offset(x: self.CalculatePosition(tabIndex: self.tabIndex, geo: geo),y: 0)

버튼 클릭시 위로 떠오르는 효과
다음으로는 특정 버튼을 눌렀을 때 아이콘이 위로 떠오르기 offset
효과를 주겠습니다.
Button(action: { withAnimation { ... } }) { Image(systemName: "photo.fill") ... //tabIndex가 photo라면 y축은 -12 그게아니라면 0 .offset(y: self.tabIndex == .photo ? -12 : 0)

시뮬레이터 메인화면 지정
우리는 MyCustomView
라는 파일을 새로 만들어서 그곳에 코드를 작성했지만 Xcode
의 기본 View
는 ContentView
로 되어있습니다. 이것을 MyCustomView
로 바꿔주겠습니다.
SwiftUI_TTTApp
으로 들어가서 ContentView()
부분을 MyCustomView()
로 수정합니다.
import SwiftUI @main struct SwiftUI_TTTApp: App { var body: some Scene { WindowGroup { MyCustomView() } } }

위처럼 오류가 나는 이유는 기본값이 설정되어있지 않기 때문입니다. Fix
를 눌러주세요. 기본값은 home
으로 해주겠습니다.
import SwiftUI @main struct SwiftUI_TTTApp: App { var body: some Scene { WindowGroup { MyCustomView(tabIndex: .home) } } }
그리고 ⌘
+ R
을 눌러 실행해보면 정상적으로 화면이 보여집니다.

베젤이 없는 기종과 있는 기기에 SafeArea 각각 적용
한 가지 문제가 있습니다. 위 이미지처럼 베젤이 없는 iPhone X
이후 시리즈에서는 컨트롤바 때문에 버튼이 가려집니다. tabBar
아래쪽에 Rectangel
을 추가하여 높이를 좀 추가해주도록 하겠습니다. 그러기 위해선 HStack
위에 VStack
으로 한번 더 감싸줘야 합니다.

Stack
에 spacing
이 자동으로 적용되어있어서 마찬가지로 VStack
에도 sapcing
을 0
으로 설정해줍니다.
var body: some View { GeometryReader { geo in ZStack(alignment: .bottom){ ... .offset(x: self.CalculatePosition(tabIndex: self.tabIndex, geo: geo),y: -20) VStack(spacing: 0) { HStack(spacing: 0) { ... } Rectangle() .foregroundColor(.white) .frame(height: 20) } } .edgesIgnoringSafeArea(.all) } } }

아까보다 훨씬 좋아졌어요 그렇죠? 하지만! 한 가지 더 문제점이 있죠.. 베젤이 있는 iPhone 8
으로 실행해보도록 하겠습니다.

굉장히 보기 안 좋습니다. 이럴 때는 frame
에 코드를 작성해 주면 되는데 자세한 코드 설명은 아래 링크를 클릭해주세요.
Rectangle() .foregroundColor(.white) .frame(height: UIApplication.shared.windows.first?.safeAreaInsets.bottom == 0 ? 0 : 20)
ZStack Circle
의 offset
부분도 수정해줘야겠죠?
.offset(x: self.CalculatePosition(tabIndex: self.tabIndex, geo: geo), y: UIApplication.shared.windows.first?.safeAreaInsets.bottom == 0 ? 0 : -20)
전체코드
읽어주셔서 감사합니다🤟
본 게시글의 전체코드 GitHub 👇🏻
Seogun95/SwiftUI_CustomTabView_TUT
SwiftUI_CustomTabView_TUT. Contribute to Seogun95/SwiftUI_CustomTabView_TUT development by creating an account on GitHub....
github.com
'PROJECT > Simple' 카테고리의 다른 글
SwiftUI Project10 : 영화 캐릭터 정보 앱 #2 - 모델생성 (0) | 2021.04.17 |
---|---|
SwiftUI Project10 : 영화 캐릭터 정보 앱 #1 (0) | 2021.04.16 |
SwiftUI Project8 : WebView and Image (0) | 2021.03.21 |
SwiftUI Project7 : Stack / ScrollView / Link (0) | 2021.03.11 |
SwiftUI Project6 : Use Views From Other Frameworks (0) | 2021.03.08 |