SwiftUI 中实现 Menu 菜单在有 checkmark 等 icon 情形下的文本对齐,如果使用 Button 或其他自定义 View 很难到达想要的效果,可以使用 Picker 和 Toggle。

在很多原生 macOS App 中经常可以看到下面这种菜单选项选中状态和非选中状态文本对齐的布局:

SwiftUI Menu checkmark align

这种布局要求在未选中状态时不显示左侧的 icon,但是文本和选中状态保持左对齐。这个布局看样子很简单对不对?在 SwiftUI 中使用 Menu + Label 似乎很容易实现。

Menu("菜单") {
    Button {
    } label: {
        Label("kanchuan1", systemImage: "")
    }
    
    Button {
    } label: {
        Label("kanchuan2", systemImage: "")
    }
    
    // 选中
    Button {
    } label: {
        Label("kanchuan", systemImage: "checkmark")
    }
}.labelStyle(.titleAndIcon)

你可能很快会发现,Label 在 systemImage 传空时会忽略 icon。那么我们自定义 buttonStyle 或直接自定义布局呢,比如像下面这样:

struct MenuLabel: View {
    var imageName: String
    var text: String
    
    var body: some View {
        HStack(spacing: 4.0) {
            if imageName.isEmpty {
                EmptyView()
                    .frame(width: 20, height: 20)
            } else {
                Image(systemName: imageName)
                    .frame(width: 20, height: 20)
            }
            Text(text)
        }
    }
}

很遗憾,无论是使用 EmptyView、Spacer、Color.clear 构造空白视图,还是通过 hidden、opacity(0) 隐藏视图,一旦放入 Menu 组件中显示,都不能起作用。更离普的是,当我们试图通过使用 Unicode 文本字符的方式替代 checkmark Image 时,竟然会发生将空格字符忽略显示的情况。

Text(\(isSelected ? "✔️" : " ")\(kanchuan))

Menu 对其内部的视图施加了强有力的布局约束,会导致子视图本身的布局不再受控制。

正确的方式是什么?

不要尝试使用 Button,而是使用 Picker 和 Toggle,这两个组件会自带对齐效果,像下面这样。

var options: [String] = ["kanchuan1", "kanchuan2", "kanchuan3", "kanchuan4"]
@State var selectedOption = "kanchuan1"
...
Menu {
    Picker("菜单", selection: $selectedOption) {
        ForEach(options, id: \.self) { option in
            Text(option)
        }
    }
    
    Toggle(isOn: Binding<Bool>(
        get: {
            return true or false
        },
        set: { oldValue, newValue in
        	// set
        }))
    {
        Text("kanchuan5")
    }
    
    Toggle(isOn: Binding<Bool>(
        get: {
            return true or false
        },
        set: { oldValue, newValue in
        	// set
        }))
    {
        Text("kanchuan6")
    }
} label: {
    Text("Menu")
}

相关讨论:

How to align SwiftUI menu-item with checkmark in macOS?

在 SwiftUI 中,想要实现和 UIKit 那样的高度自定义布局有时候还是差一点意思,似乎 Apple 更倾向开发者使用官方推荐的方案解决问题。这到底是好还是不好呢?