
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)
전체코드
<hide/>
import SwiftUI
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
}
}
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)
}
}
var body: some View {
GeometryReader { geo in
ZStack(alignment: .bottom){
self.showMyView(tabIndex: self.tabIndex)
Circle()
.foregroundColor(.white)
.frame(width:70, height: 70)
.offset(x: self.CalculatePosition(tabIndex: self.tabIndex, geo: geo), y: UIApplication.shared.windows.first?.safeAreaInsets.bottom == 0 ? 0 : -20)
VStack(spacing: 0) {
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)
.offset(y: self.tabIndex == .home ? -12 : 0 )
}
.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)
.offset(y: self.tabIndex == .photo ? -12 : 0)
}
.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)
.offset(y: self.tabIndex == .profile ? -12 : 0)
}
.background(Color.white)
}//HStack
Rectangle()
.foregroundColor(.white)
.frame(height: UIApplication.shared.windows.first?.safeAreaInsets.bottom == 0 ? 0 : 20)
}
}
.edgesIgnoringSafeArea(.all)
}
}
}
struct MyCustomView_Previews: PreviewProvider {
static var previews: some View {
MyCustomView(tabIndex: .home)
}
}
읽어주셔서 감사합니다🤟
본 게시글의 전체코드 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 |





