返回博客

SwiftUI macOS Menu 自定义标签只渲染第一个 Text

给 macOS 原生工具栏做一个「分支切换 chip」,设计稿是图标 + 分支名 + 下拉箭头三段横排。用 Menu + .menuStyle(.borderlessButton) 实现,label: 里写了一个 HStack { Text("⎇"); Text(branch); Text("▾") }——看起来非常自然,运行却只显示 ,后面两段文字一片空白。

调了半天数据绑定,branch 值是对的,在旁边放一个普通 Text(branch) 完全正常。那为什么放进 Menulabel 就哑火?

现象

Menu {
    // 菜单选项 ...
} label: {
    HStack(spacing: 4) {
        Text("⎇")
        Text(branch)   // 只有这个会被渲染
        Text("▾")      // 静默消失
    }
    .font(.system(size: 12, design: .monospaced))
    .padding(.horizontal, 9).padding(.vertical, 3)
    .background(...)
}
.menuStyle(.borderlessButton)
.menuIndicator(.hidden)
.fixedSize()

运行结果:chip 里只出现 branch 完全不可见,但同一个 branch 绑定到侧边栏的 Text(branch) 却正常显示。

根因

.menuStyle(.borderlessButton)Menu 在 macOS 上渲染自定义 label 时,只会合成 label 视图树中的第一个 Text 节点,后续 Text 子视图静默丢弃。这是 AppKit/SwiftUI 桥接层在处理 borderlessButton 样式时的一个已知渲染限制,与数据绑定无关。

坑:borderlessButton Menu 的 label 只渲染第一个 Text

把多个 Text 放进 Menu { } label: { HStack { Text(...); Text(...); Text(...) } } 并搭配 .menuStyle(.borderlessButton) 时,第 2 个及以后的 Text 子视图会被静默吞掉,不报错、不警告、数据也是对的,表现完全像数据 bug。

.fixedSize() / .menuIndicator(.hidden) 对此没有任何影响,加不加都一样。

正解

有两种修法,按需选择:

方案 A:合并成单个 Text(快速修复)

把三段文字用字符串插值合并成一个 Text,失去独立样式控制,但 99% 的 chip 场景够用:

} label: {
    Text("⎇ \(branch)  ▾")
        .font(.system(size: 12, design: .monospaced))
        .foregroundStyle(Color.accentColor)
        .lineLimit(1)
        .padding(.horizontal, 9).padding(.vertical, 3)
        .background(...)
}

方案 B:换成 Button + .popover(完整控制)

Buttonlabel 对多 Text HStack 没有任何限制,这个 bug 是 Menu 专属的。换成 Button + 自定义 .popover 还能彻底解锁菜单行样式(双列布局、右对齐标签、激活高亮等),这些是系统 Menu 的菜单项无法做到的:

@State private var isOpen = false
 
Button { isOpen.toggle() } label: {
    HStack(spacing: 4) {
        Text("⎇")
        Text(branch)
        Text("▾")
    }
    .font(.system(size: 12, design: .monospaced))
}
.buttonStyle(.plain)
.popover(isPresented: $isOpen, arrowEdge: .bottom) {
    // 自定义下拉行:名称左对齐,状态标签右对齐,当前项高亮
    VStack(alignment: .leading, spacing: 0) {
        ForEach(branches) { b in
            BranchRow(branch: b, isCurrent: b.name == branch)
                .onTapGesture { branch = b.name; isOpen = false }
        }
    }
    .padding(6)
}
正解:单 Text 或换用 Button + popover
  • 只需展示文字 chip:把所有段拼成一个 Text("⎇ \(branch) ▾") 即可。
  • 需要自定义下拉行样式:用 Button { } label: { HStack { ... } } + .popover,完全绕开 Menu 的渲染限制,同时获得完整的行样式控制权。

一句话外卖

SwiftUI macOS Menu + .menuStyle(.borderlessButton) 的 label 只能放单个 Text,多 Text HStack 会静默截断——想要多段文字或自定义下拉行,换成 Button + .popover