DevB1
DevB1

Reputation: 1575

Aligning horizontal views which are part of a VStack

I have been given the following design for a tabbar:

1

I'm pretty close to replicating it, here is how my implementation looks:

enter image description here

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:

enter image description here

Upvotes: 0

Views: 704

Answers (1)

Carter Foughty
Carter Foughty

Reputation: 298

If you want to keep the buttons in separate VStacks but have them align vertically, all you need to do is surround the icon by Spacers. 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

Related Questions