WWDC26:把 SwiftUI Toolbar 真正握在手心里

引子

你有没有过这种时刻:同一套 .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-)

相关推荐
茶底世界之下1 小时前
四角变换后的最小包围视口,为什么不能只把宽高取整?
ios·swift
茶底世界之下1 天前
预乘 Alpha 图层合成为什么会越叠越暗?
架构·swift
茶底世界之下3 天前
时间线效果层,为什么不能只复用一个处理器?
ios·swift
Cc.Y3 天前
Swift深度链接的极简之道
ios·cocoa·swift
传奇开心果编程4 天前
【SwiftUI提高练中学】第9课 安全与隐私
swiftui·移动开发·swift·编程语言理论·开发范式·编程框架·声明式ui
CocoaKier4 天前
苹果商店详情顶部头图已面向所有开发者开放!
ios·apple
茶底世界之下4 天前
动态切换 nearest / linear 时,为什么不能只替换 sampler
ios·swift
应用市场4 天前
把旧 Pixel 变成相册备份中转站(上):Mac 到安卓的照片传输工具设计——流式上传、sha256 校验、adb forward 与 Bonjour
android·macos·adb·kotlin·swift
传奇开心果编程4 天前
【声明式UI开发实用技术学与练】第8课 状态提升与下放
学习·flutter·react native·ui·swiftui·composer
东坡肘子4 天前
Swift Server,又多了一个 Google -- 肘子的 Swift 周报 #156
人工智能·swiftui·swift