Reputation: 1575
I have been given the following design for a tabbar:
I'm pretty close to replicating it, here is how my implementation looks:
Where I am struggling however is the finer detail of the alignment of the icons. So here is my code for the TabBarView:
struct TabBarView: View {
@ObservedObject var viewModel: TabBarViewModel
var body: some View {
HStack {
HStack(alignment: .bottom) {
tabOption(inactiveIcon: Image.Icons.Stores.notSelected, activeIcon: Image.Icons.Stores.selected, title: "Stores", tabNumber: 1, height: 20.8)
Spacer()
tabOption(inactiveIcon: Image.Icons.Menu.notSelected, activeIcon: Image.Icons.Menu.selected,title: "Menu", tabNumber: 2, height: 26)
Spacer()
tabOption(inactiveIcon: Image.Icons.Account.notSelected, activeIcon: Image.Icons.Account.selected,title: "Account", tabNumber: 3, height: 26)
Spacer()
tabOption(inactiveIcon: Image.Icons.Basket.notSelected, activeIcon: Image.Icons.Basket.selected,title: "Basket", tabNumber: 4, height: 23.11)
}
.frame(height: 39)
}
.frame(height: 64)
}
func tabOption(inactiveIcon: Image, activeIcon: Image, title: String, tabNumber: Int, height: CGFloat) -> some View {
Button {
viewModel.selectTab(tabNumber)
} label: {
VStack {
if viewModel.selectedTab == tabNumber {
activeIcon
.resizable()
.renderingMode(.template)
.foregroundColor(.snappyBlue)
.aspectRatio(contentMode: .fit)
.frame(height: height)
} else {
inactiveIcon
.resizable()
.renderingMode(.template)
.foregroundColor(.snappyBlue)
.aspectRatio(contentMode: .fit)
.frame(height: height)
}
Text(title)
.font(.Caption1.semiBold())
.foregroundColor(.black)
}
}
}
}
So each button or option is in a VStack, and I am then placing these buttons within an HStack. In fact, I have 2 HStacks, one embedded in the other (the outer one represents the entire tab bar area, the inner one contains the actual icons and has a set height to ensure that the labels align).
However, if you look closer at the design the icons are all slightly different heights. They are not aligned across the bottom, rather they are aligned vertically, i.e. the vertical centre of the stores icon is aligned with the vertical centre of the menu item. How can I go about aligning these as desired?
It feels right to have the buttons in separate VStacks, and the text needs to be aligned at the bottom, but it then seems difficult to align the images.
Here is how my solution looks in view hierarchy:
Upvotes: 0
Views: 704
Reputation: 298
If you want to keep the buttons in separate VStack
s but have them align vertically, all you need to do is surround the icon by Spacer
s. By default, if there are multiple spacers with no frame assignment being used in a view, the extra space will be split up equally among them (the same way that you're using them in the HStack
among the tabOption
views), pushing the label to the bottom and the icon to the center of the remaining space. By doing this, you should only need to wrap each TabOption
in a single HStack
, with the height of the stack specified.
First, consider switching from using integers to represent your tabs to an enum, which can more accurately model your tabs.
enum Tab {
case stores
case menu
case account
case basket
var title: String {
switch self {
case .stores: return "Stores"
case .menu: return "Menu"
case .account: return "Account"
case .basket: return "Basket"
}
var activeIcon: Image {
switch self {
case .stores: return Image.Icons.Stores.selected
case .menu: return Image.Icons.Menu.selected
case .account: return Image.Icons.Account.selected
case .basket: return Image.Icons.Basket.selected
}
var inactiveIcon: Image {
switch self {
case .stores: return Image.Icons.Stores.notSelected
case .menu: return Image.Icons.Menu.notSelected
case .account: return Image.Icons.Account.notSelected
case .basket: return Image.Icons.Basket.notSelected
}
}
If you use this enum, you could clean the tabOption
func up quite a bit, perhaps by converting it to a reusable struct like so.
struct TabOption: View {
var tab: Tab
var isSelected: Bool
var height: CGFloat
var onSelect: (Tab) -> Void
var body: some View {
Button {
onSelect(tab)
} label: {
VStack(spacing: 0) {
// These spacers will be of equal height, pushing
// your view to the vertical center.
Spacer() <-----
isSelected ? tab.activeIcon : tab.inactiveIcon
.resizable()
.renderingMode(.template)
.foregroundColor(.snappyBlue)
.aspectRatio(contentMode: .fit)
.frame(height: height)
Spacer() <-----
Text(tab.title)
.font(.Caption1.semiBold())
.foregroundColor(.black)
}
}
}
}
If you use this struct, you would create the view like so:
HStack {
TabOption(tab: .store, isSelected: viewModel.selectedTab == .store, height: 20.8) {
viewModel.selectTab($0)
}
Spacer()
TabOption(tab: .menu, isSelected: viewModel.selectedTab == .store, height: 26) {
viewModel.selectTab($0)
}
Spacer()
TabOption(tab: .account, isSelected: viewModel.selectedTab == .store, height: 26) {
viewModel.selectTab($0)
}
Spacer()
TabOption(tab: .basket, isSelected: viewModel.selectedTab == .store, height: 23.11) {
viewModel.selectTab($0)
}
}
.frame(height: 64)
There are better ways to handle notifying the view model of tab selection than I've shown you here, but this would do the trick.
Upvotes: 2