对齐作为 VStack 一部分的水平视图

Aligning horizontal views which are part of a VStack

我得到了以下标签栏设计:

我快要复制它了,这是我的实现方式:

然而,让我苦苦挣扎的是图标对齐的更精细细节。所以这是我的 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)
            }
        }
    }
}

所以每个按钮或选项都在一个 VStack 中,然后我将这些按钮放在一个 HStack 中。事实上,我有 2 个 HStack,一个嵌入另一个(外部的代表整个标签栏区域,内部的包含实际图标并设置高度以确保标签对齐)。

但是,如果您仔细观察设计,图标的高度都略有不同。它们不是在底部对齐,而是垂直对齐,即商店图标的垂直中心与菜单项的垂直中心对齐。我怎样才能根据需要对齐它们?

按钮在单独的VStacks中感觉不错,文本需要在底部对齐,但图像很难对齐。

这是我的解决方案在视图层次结构中的样子:

如果您想将按钮保持在单独的 VStack 中,但让它们垂直对齐,您需要做的就是用 Spacer 包围图标。默认情况下,如果在一个视图中有多个 space 没有使用框架分配,额外的 space 将在它们之间平均分配(与您在 HStacktabOption 个视图中),将标签推到底部,将图标推到其余 space 的中心。通过这样做,您应该只需要将每个 TabOption 包装在一个 HStack 中,并指定堆栈的高度。

首先,考虑从使用整数来表示您的选项卡切换到枚举,这可以更准确地为您的选项卡建模。

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
    }
}

如果你使用这个枚举,你可以清理 tabOption 函数,也许通过将它转换成一个可重用的结构,就像这样。

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

如果你使用这个结构,你会像这样创建视图:

HStack {
    TabOption(tab: .store, isSelected: viewModel.selectedTab == .store, height: 20.8) {
        viewModel.selectTab([=12=])
    }
    Spacer()
    TabOption(tab: .menu, isSelected: viewModel.selectedTab == .store, height: 26) {
        viewModel.selectTab([=12=])
    }
    Spacer()
    TabOption(tab: .account, isSelected: viewModel.selectedTab == .store, height: 26) {
        viewModel.selectTab([=12=])
    }
    Spacer()
    TabOption(tab: .basket, isSelected: viewModel.selectedTab == .store, height: 23.11) {
        viewModel.selectTab([=12=])
    }
}
.frame(height: 64)

有比我在这里向您展示的更好的方法来处理通知选项卡选择的视图模型,但这可以解决问题。