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 👇🏻
'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 |