궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
PROJECT/Simple

SwiftUI Project9 : CustomTabView (geometryReader)

서근
QUOTE THE DAY

“ 바쁘다는 것만으로는 충분치 않다. 문제는 ‘무엇 때문에 바쁜가’이다. ”

- Henry David Thoreau (헨리 데이비드 소로)
Written by SeogunSEOGUN

SwiftUI Project9 : CustomTabView (geometryReader)


CustomTabView 

우선 MyView 라는 SwiftUI 파일을 생성해서 위에 보이는 메인 배경 뷰를 만들어 주겠습니다.

swift
UNFOLDED
// 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에 보여줄 텍스트를 작성합니다.

swift
UNFOLDED
struct MyCustomView: View {
enum TabIndex {
case home, photo, profile
}

이제 이 열거형을 매개변수매개값으로 사용해야 합니다.

 

열거형 아래에  @State var tabIndex: tabIndex 를 넣어줍니다.

 

MyViewswitch 문으로 가져오려고 하는데 먼저 함수를 작성해야 합니다.  작성해줬던 TabIndex를 매개변수로 가져오고 MyView를 반환해주겠습니다. default 부분은 기본값이 되므로 홈에 있는 값을 넣어줍니다.

swift
UNFOLDED
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도 호출해줍니다.

swift
UNFOLDED
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)이라고 넣어줍니다.

 

그런데 아이콘이 화면 위쪽에 배치되어 있죠? ZStackalignmentbottom으로 설정해줍니다. ZStack(alignment: .bottom)

 

HStack에 있는 버튼을 복사해서 탭에 총 3개가 되도록 붙여 넣기 합니다. 

 

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - TabView 구현

TIP
 
 

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

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - TabView 구현

 

현재까지 코드

더보기
swift
UNFOLDED
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가 나오도록 ButtonAction쪽에 아래와 같이 코드를 작성합니다.

swift
UNFOLDED
Button(action: {
self.tabIndex = .home
print("홈을 선택했습니다.")
}) { }

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 버튼 클릭시 뷰 전환

활성화 된 버튼과 비활성화 버튼 색 효과

한 가지 재미난 기능을 추가해주려고 하는데 예를 들어 홈버튼을 눌렀을 때는 나머지 버튼들이 회색으로 변하고 원하는 버튼을 눌렀을 땐 지정된 색이 되도록 하려고 합니다. 이 기능을 사용하기 위해서는 함수를 작성해줘야 합니다.

swift
UNFOLDED
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 ImageforegroundColor쪽을 수정합니다.

swift
UNFOLDED
//tabIndex가 home이 선택되었다면, changeColor를 실행하고, 그게 아니라면 gray색을 부여해라
.foregroundColor(self.tabIndex == .home ? self.changeColor(tabIndex: tabIndex): Color.gray)
swift
UNFOLDED
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)

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 활성화 된 버튼과 비활성화 버튼 색 효과

버튼 클릭시 아이콘 확대 효과

버튼의 색에 변화를 주었으니 이제 배운 것을 활용하여 버튼을 누를 때마다 사이즈가 커지는 방법도 할 수 있습니다.

.scaleEffect를 사용해야 합니다.

swift
UNFOLDED
 @State var ImageSize: CGFloat = 1.2
swift
UNFOLDED
Image(systemName: "house.fill")
//만약 tabIndex가 home이라면 ImageSize를 실행하고 그게 아니라면 사이즈는 1.0 이다.
.scaleEffect(self.tabIndex == .home ? self.ImageSize : 1.0)

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 버튼 클릭시 아이콘 확대 효과

버튼 이미지 크기가 변할때 animation효과를 주려면 withAnimation 을 적용시키면 됩니다.

swift
UNFOLDED
Button(action: {
withAnimation {
self.tabIndex = .home
print("홈을 선택했습니다.")
}
}) {

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 버튼 클릭시 아이콘 확대 효과

아이콘을 보면 자연스럽게 애니메이션 효과가 들어갔지만, 한 가지 거슬리는 점은 텍스트까지 애니메이션 효과가 들어갔다는 것입니다. View의 애니메이션 효과를 주지 않기 위해서는 MyView로 이동해서 아래와 같이 코드를 넣어줘야 합니다.

swift
UNFOLDED
// MyView.swift
struct MyView: View {
...
var body: some View {
ZStack {
...
}
.animation(.none)
}
}

현재까지 코드

더보기
swift
UNFOLDED
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을 추가합니다.

swift
UNFOLDED
ZStack(alignment: .bottom){
self.showMyView(tabIndex: self.tabIndex)
Circle()
.foregroundColor(.white)
.frame(width: 70, height: 70)

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 탭바 아이콘 클릭시 반원 효과주기

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

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 탭바 아이콘 클릭시 반원 효과주기

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

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 탭바 아이콘 클릭시 반원 효과주기

자, 어느 정도 위치를 표현하는 법은 알게 되었습니다. 이제 함수를 작성해줄게요.

swift
UNFOLDED
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반환해야 합니다.

swift
UNFOLDED
Circle()
...
.offset(x: self.CalculatePosition(tabIndex: self.tabIndex, geo: geo),y: 0)

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 탭바 아이콘 클릭시 반원 효과주기

버튼 클릭시 위로 떠오르는 효과

다음으로는 특정 버튼을 눌렀을 때 아이콘이 위로 떠오르기 offset 효과를 주겠습니다.

swift
UNFOLDED
Button(action: {
withAnimation {
...
}
}) {
Image(systemName: "photo.fill")
...
//tabIndex가 photo라면 y축은 -12 그게아니라면 0
.offset(y: self.tabIndex == .photo ? -12 : 0)

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 버튼 클릭시 위로 떠오르는 효과

시뮬레이터 메인화면 지정

우리는 MyCustomView라는 파일을 새로 만들어서 그곳에 코드를 작성했지만 Xcode의 기본 ViewContentView로 되어있습니다. 이것을 MyCustomView로 바꿔주겠습니다.

 

SwiftUI_TTTApp 으로 들어가서 ContentView()부분을 MyCustomView()로 수정합니다.

swift
UNFOLDED
import SwiftUI
@main
struct SwiftUI_TTTApp: App {
var body: some Scene {
WindowGroup {
MyCustomView()
}
}
}

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 시뮬레이터 메인화면 지정

위처럼 오류가 나는 이유는 기본값이 설정되어있지 않기 때문입니다. Fix를 눌러주세요. 기본값은 home으로 해주겠습니다.

swift
UNFOLDED
import SwiftUI
@main
struct SwiftUI_TTTApp: App {
var body: some Scene {
WindowGroup {
MyCustomView(tabIndex: .home)
}
}
}

그리고 + R 을 눌러 실행해보면 정상적으로 화면이 보여집니다.

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 시뮬레이터 메인화면 지정

베젤이 없는 기종과 있는 기기에 SafeArea 각각 적용

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

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 베젤이 없는 기종과 있는 기기에 SafeArea 각각 적용

Stackspacing이 자동으로 적용되어있어서 마찬가지로 VStack에도 sapcing0으로 설정해줍니다.

swift
UNFOLDED
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)
}
}
}

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 베젤이 없는 기종과 있는 기기에 SafeArea 각각 적용

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

SwiftUI Project9 : CustomTabView (geometryReader) - undefined - 베젤이 없는 기종과 있는 기기에 SafeArea 각각 적용

굉장히 보기 안 좋습니다. 이럴 때는 frame에 코드를 작성해 주면 되는데 자세한 코드 설명은 아래 링크를 클릭해주세요.

 

 

swift
UNFOLDED
Rectangle()
.foregroundColor(.white)
.frame(height: UIApplication.shared.windows.first?.safeAreaInsets.bottom == 0 ? 0 : 20)

ZStack Circleoffset부분도 수정해줘야겠죠?

swift
UNFOLDED
.offset(x: self.CalculatePosition(tabIndex: self.tabIndex, geo: geo), y: UIApplication.shared.windows.first?.safeAreaInsets.bottom == 0 ? 0 : -20)

전체코드

swift
FOLDED

 

 

읽어주셔서 감사합니다🤟

 

본 게시글의 전체코드 GitHub 👇🏻

 

Seogun95/SwiftUI_CustomTabView_TUT

SwiftUI_CustomTabView_TUT. Contribute to Seogun95/SwiftUI_CustomTabView_TUT development by creating an account on GitHub....

github.com


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.