Custom TabView in SwiftUI
Updated:
Custom TabView in SwiftUI
There are majority of apps use either a tab bar or a navigation view and those two components in SwiftUI are not that customizable. Actually, model our custom tab view based off of apple’s API for the default tab view
The majority of features in Custom TabView
-
Generics
-
ViewBuilder
-
PreferenceKey
-
MatchedGeometryEffect
// General style tabView
import SwiftUI
struct AppTabBarView: View {
// MARK: - PROPERTY
@State private var selection: String = "home"
// MARK: - BODY
var body: some View {
TabView(selection: $selection) {
Color.red
.tabItem {
Image(systemName: "house")
Text("Home")
}
Color.blue
.tabItem {
Image(systemName: "heart")
Text("Favorite")
}
Color.orange
.tabItem {
Image(systemName: "person")
Text("Profile")
}
}
}
}
// MARK: - PREVIEW
struct AppTabBarView_Previews: PreviewProvider {
static var previews: some View {
AppTabBarView()
}
}
// in TabBarItem
import Foundation
import SwiftUI
// struct TabBarItem: Hashable {
// let iconName: String
// let title: String
// let color: Color
// }
// Model is handy when you don't know the actual data tat you're going to get
// TabBar specifically we actually have all that data in our code
// We have all of the data already it will actually be easier to make this tab bar item and enum instead of struct
enum TabBarItem: Hashable {
case home, favorites, profile, messages
var iconName: String {
switch self {
case .home: return "house"
case .favorites: return "heart"
case .profile: return "person"
case .messages: return "message"
}
}
var title: String {
switch self {
case .home: return "Home"
case .favorites: return "Favorites"
case .profile: return "Profile"
case .messages: return "Messages"
}
}
var color: Color {
switch self {
case .home: return Color.red
case .favorites: return Color.blue
case .profile: return Color.green
case .messages: return Color.orange
}
}
}
// in TabBarItemsPreferenceKey
import Foundation
import SwiftUI
// MARK: - Create PreferenceKey
struct TabBarItemsPreferenceKey: PreferenceKey {
static var defaultValue: [TabBarItem] = []
static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) {
value += nextValue()
}
}
// MARK: - ViewModifier
struct TabBarItemViewModifier: ViewModifier {
let tab: TabBarItem
@Binding var selection: TabBarItem
func body(content: Content) -> some View {
content
.opacity(selection == tab ? 1.0 : 0.0)
.preference(key: TabBarItemsPreferenceKey.self, value: [tab])
}
}
// MARK: - Extenstion
extension View {
func tabBarItem(tab: TabBarItem, selection: Binding<TabBarItem>) -> some View {
self
.modifier(TabBarItemViewModifier(tab: tab, selection: selection))
}
}
// in CustomTabBarContainerView
import SwiftUI
struct CustomTabBarContainerView<Content:View>: View {
@Binding var selection: TabBarItem
let content: Content
@State private var tabs: [TabBarItem] = []
init(selection: Binding<TabBarItem>, @ViewBuilder content: () -> Content) {
self._selection = selection
self.content = content()
}
var body: some View {
ZStack(alignment: .bottom) {
content
.ignoresSafeArea()
CustomTabBarView(tabs: tabs, selection: $selection, localSelection: selection)
} //: ZSTACK
.onPreferenceChange(TabBarItemsPreferenceKey.self) { value in
self.tabs = value
}
}
}
struct CustomTabBarContainerView_Previews: PreviewProvider {
static let tabs: [TabBarItem] = [
.home, .favorites, .profile, .messages
]
static var previews: some View {
CustomTabBarContainerView(selection: .constant(tabs.first!)) {
Color.red
}
}
}
// in CustomTabBarView
import SwiftUI
// MARK: - VIEW
struct CustomTabBarView: View {
// MARK: - PROPERTY
let tabs: [TabBarItem]
@Binding var selection: TabBarItem
@Namespace private var namespace
@State var localSelection: TabBarItem
// MARK: - BODY
var body: some View {
// tabBarVersion1
tabBarVersion2
.onChange(of: selection) { newValue in
withAnimation(.easeInOut) {
localSelection = newValue
}
}
}
}
// MARK: - PREVIEW
struct CustomTabBarView_Previews: PreviewProvider {
static let tabs: [TabBarItem] = [
.home, .favorites, .profile
]
static var previews: some View {
VStack {
Spacer()
CustomTabBarView(tabs: tabs, selection: .constant(tabs.first!), localSelection: tabs.first!)
}
}
}
// MARK: - EXTENSTION
extension CustomTabBarView {
private func tabView(tab: TabBarItem) -> some View {
VStack {
Image(systemName: tab.iconName)
.font(.subheadline)
Text(tab.title)
.font(.system(size: 10, weight: .semibold, design: .rounded))
} //: VSTACK
.foregroundColor(selection == tab ? tab.color : Color.gray)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
.background(selection == tab ? tab.color.opacity(0.2) : Color.clear)
.cornerRadius(10)
}
private var tabBarVersion1: some View {
HStack {
ForEach(tabs, id: \.self) { tab in
tabView(tab: tab)
.onTapGesture {
switchToTab(tab: tab)
}
}
} //: HSTACK
.padding(6)
.background(Color.white.ignoresSafeArea(edges: .bottom))
}
private func switchToTab(tab: TabBarItem) {
selection = tab
}
}
// tabBarVersion2
extension CustomTabBarView {
private func tabView2(tab: TabBarItem) -> some View {
VStack {
Image(systemName: tab.iconName)
.font(.subheadline)
Text(tab.title)
.font(.system(size: 10, weight: .semibold, design: .rounded))
} //: VSTACK
.foregroundColor(localSelection == tab ? tab.color : Color.gray)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
.background(
ZStack {
if localSelection == tab {
RoundedRectangle(cornerRadius: 10)
.fill(tab.color.opacity(0.2))
.matchedGeometryEffect(id: "background_rectangle", in: namespace)
}
} //: ZSTACK
)
}
private var tabBarVersion2: some View {
HStack {
ForEach(tabs, id: \.self) { tab in
tabView2(tab: tab)
.onTapGesture {
switchToTab(tab: tab)
}
}
} //: HSTACK
.padding(6)
.background(Color.white.ignoresSafeArea(edges: .bottom))
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 5)
.padding(.horizontal)
}
}
import SwiftUI
struct AppTabBarView: View {
// MARK: - PROPERTY
@State private var selection: String = "home"
@State private var tabSelection: TabBarItem = .home
// MARK: - BODY
var body: some View {
CustomTabBarContainerView(selection: $tabSelection) {
Color.blue
.tabBarItem(tab: .home, selection: $tabSelection)
Color.red
.tabBarItem(tab: .favorites, selection: $tabSelection)
Color.green
.tabBarItem(tab: .profile, selection: $tabSelection)
Color.orange
.tabBarItem(tab: .messages, selection: $tabSelection)
}
}
}
// MARK: - PREVIEW
struct AppTabBarView_Previews: PreviewProvider {
static var previews: some View {
AppTabBarView()
}
}
// MARK: - EXTENSTION
extension AppTabBarView {
private var defaultTabView: some View {
TabView(selection: $selection) {
Color.red
.tabItem {
Image(systemName: "house")
Text("Home")
}
Color.blue
.tabItem {
Image(systemName: "heart")
Text("Favorite")
}
Color.orange
.tabItem {
Image(systemName: "person")
Text("Profile")
}
} //: TAB
}
}
🗃 Reference
SwiftUI Thinking - https://youtu.be/FxW9Dxt896U
Leave a comment