
引子
你有没有过这种时刻:同一套 .toolbar { },在 Mac 上干干净净排成一排,一到 iPhone 上就「失踪」了半截------有的按钮还在,有的钻进了 ⋯ 菜单,你却说不清系统到底按什么规则做的选择。
Toolbar 大概是 SwiftUI 里我写得最多、吐槽得也最多的 API 之一。声明式、自适应,听起来很香;真要做跨平台产品时,你才会发现:系统替你做的「聪明决定」,有时刚好不是你想要的。
尤其现在这套设计语言把界面拆成内容层和控制层,工具栏几乎成了控制层的门面------它往哪摆、谁先露脸、滚动时收不收,都会直接影响手感。
这周我们聊的,就是 WWDC 之后那批新 API:让你在「交给系统」和「我说了算」之间,终于能拨得更准一点。
到底 SwiftUI 新升级的 Toolbar 有什么玄机?让我们马上来先睹为快吧?;-)

先从一个朴素例子说起。
Toolbar API 很声明式,也很会自适应;可一旦按钮一多,你就会想要更多控制权。比如这样一个塞满动作的工具栏------在 macOS、iOS、iPadOS 上,观感可能完全不是一回事:
swift
struct ContentView: View {
var body: some View {
NavigationStack {
Text("Hello, World!")
.navigationTitle("Hello")
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button("Action 1") {
}
}
ToolbarItemGroup {
Button("Action 2") {
}
Button("Action 3") {
}
Button("Action 4") {
}
Button("Action 5") {
}
}
}
}
}
}
同一段代码,平台会给出不同反应。在 macOS 上,这些动作往往都挤在右上角;

在 iOS 上,则可能一部分留在 trailing,另一部分被收进 overflow 菜单。

你写的是「一堆按钮」,系统交付的却是「它认为合适的布局」。

所以 SwiftUI 加了 visibilityPriority,可以挂在 ToolbarContent 上。可选值包括 automatic、low、high 这类优先级。空间紧张时,系统会参考优先级,优先折叠那些不那么重要的项:
swift
struct ContentView: View {
var body: some View {
NavigationStack {
Text("Hello, World!")
.navigationTitle("Hello")
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button("Action 1") {
}
}
.visibilityPriority(.high)
ToolbarItemGroup {
Button("Action 2") {
}
Button("Action 3") {
}
Button("Action 4") {
}
Button("Action 5") {
}
}
}
}
}
}
换句话说:你不必再靠碰运气猜谁会留下------可以明确告诉系统,「Action 1 比旁边那几个更重要」。


再看一个更绕一点的例子。把第二组放进 .secondaryAction 之后,平台差异会更扎眼:macOS 上,Action 1 通常在右上,其余一组可能居中出现在导航区域;

iOS 上,Action 1 会钉在 trailing,其它项则更容易被折叠起来。
swift
struct ContentView: View {
var body: some View {
NavigationStack {
Text("Hello, World!")
.navigationTitle("Hello")
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button("Action 1") {
}
}
ToolbarItemGroup(placement: .secondaryAction) {
Button("Action 2") {
}
Button("Action 3") {
}
Button("Action 4") {
}
Button("Action 5") {
}
}
}
}
}
}
自适应仍然在,但语义更清楚了:主操作归主操作,次要操作归次要操作。

跨平台时,这种「角色分工」往往比硬编码坐标更经得起折腾。

有时候你根本不想让系统猜。
某些动作本来就该住在 overflow 里------不常用,但要找得到。这时可以用专门的 ToolbarOverflowMenu:
swift
struct ContentView: View {
var body: some View {
NavigationStack {
Text("Hello, World!")
.navigationTitle("Hello")
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
Button("Action 1") {
}
}
ToolbarOverflowMenu {
Button("Action 2") {
}
Button("Action 3") {
}
Button("Action 4") {
}
Button("Action 5") {
}
}
}
}
}
}
它表达的意图很干脆:这组内容默认折叠。你不用再盯着预览猜「这次会不会被收进去」------你已经主动把它们放进菜单了。

(补充一句:按当前 SDK,ToolbarOverflowMenu 主要面向 iOS / visionOS;文章里说「全平台默认折叠」,更接近设计意图,落地时仍要以平台可用性为准。)
还有些按钮几乎不能丢,比如分享。
SwiftUI 为此提供了 .topBarPinnedTrailing:把项钉在顶部栏 trailing。通常只有在搜索展开、空间实在不够时,它才可能被挤掉:
swift
struct ContentView: View {
var body: some View {
NavigationStack {
Text("Hello, World!")
.navigationTitle("Hello")
.toolbar {
ToolbarItemGroup(placement: .topBarPinnedTrailing) {
Button("Action 1") {
}
}
ToolbarItemGroup(placement: .secondaryAction) {
Button("Action 2") {
}
Button("Action 3") {
}
Button("Action 4") {
}
Button("Action 5") {
}
}
}
}
}
}
优先级解决「谁更重要」,pin 解决「谁必须在场」。

一个管排序,一个管锚定,搭配起来很好用。
最后一块拼图,是滚动时的收纳。列表一长,你往往希望导航栏别一直霸占屏幕。可以这样写:
swift
struct ContentView: View {
var body: some View {
NavigationStack {
Text("Hello, World!")
.navigationTitle("Hello")
.toolbar {
ToolbarItemGroup(placement: .topBarPinnedTrailing) {
Button("Action 1") {
}
}
ToolbarItemGroup(placement: .secondaryAction) {
Button("Action 2") {
}
Button("Action 3") {
}
Button("Action 4") {
}
Button("Action 5") {
}
}
}
// 原文写作 toolbarMinimizeBehavior;
// 当前 SDK 符号为 toolbarMinimizationBehavior,且应加在栈内滚动内容上。
.toolbarMinimizationBehavior(.onScrollDown, for: .navigationBar)
}
}
}
向下滚时收起导航栏,内容区立刻更敞亮。

同类能力也可以作用在 tab、bottom、window 等工具栏上------滚动与 chrome 终于能更明确地「配合演出」。

回到开头那个烦恼:按钮忽然进了 ⋯,你却插不上话。现在这套新修饰符,把系统自适应和显式控制重新校准了一遍。你可以指定谁更重要、谁尽量一直可见、谁就该住在 overflow,以及滚动时工具栏怎么让路。
Toolbar 还是那个 Toolbar,但它不再只是「写完等系统发挥」的黑盒。把关键旋钮拧到自己顺手的位置,界面才会既像系统原生,又像你的产品。
希望这篇文章对大家有用。有问题欢迎给我留言。
感谢宝子们的观赏,下回再见!8-)