macOS 原生应用的窗口与导航管理

SwiftUI 提供了 NavigationSplitViewNavigationStackTabView 等多种导航容器,它们在 macOS、iPadOS 和 iOS 上的行为各有不同,组合方式也不止一种。

这篇文章整理了我在实际开发中对 macOS 原生应用导航管理的观察与总结,希望能帮助你在项目初期做出更清晰的架构选择。

创建三列应用

如果你的应用始终需要三列结构——比如 Notes 的「文件夹 → 笔记列表 → 编辑器」,或 Mail 的「邮箱 → 邮件列表 → 邮件正文」——那么 NavigationSplitView 的三列形态就是最自然的方案:

NavigationSplitView {
    // sidebar
} content: {
    // 中间列
} detail: {
    // 详情列
}

在这种场景下,系统的原生体验非常稳定,toolbar、标题栏与分栏折叠行为都与系统预期一致,几乎不需要额外干预。

备忘录
邮件

混合列数场景:谨慎切换容器形态

实际项目中更常见的情况是:应用的某些模块需要两列,某些模块需要三列。如果你尝试在模块切换时通过条件渲染来直接切换 NavigationSplitView 的列数结构(比如从二列变三列),可能会遇到明显的重布局感——界面跳动,过渡不够丝滑。

更稳妥的做法是分层处理:在顶层维护「模块导航」的逻辑(决定当前进入哪个功能模块),在模块内部各自管理自己的分栏结构。这样全局容器的形态不会频繁变化,用户感知到的是模块内部的内容切换,而非窗口骨架的重组。

创建两列应用

对于两列结构的 macOS 应用,常见的实现方式有两种,各有适用场景。

方案一:TabView + .sidebarAdaptable + NavigationStack

TabView {
    Tab("Library", systemImage: "books.vertical") {
        NavigationStack { ... }
    }
    Tab("Settings", systemImage: "gear") {
        NavigationStack { ... }
    }
}
.tabViewStyle(.sidebarAdaptable)

左侧呈现为系统托管的侧边栏,右侧通常用 NavigationStack 处理页面层级。Music 就是采用这个方式:

但是需要注意,Podcast 应用和 Journey 应用都不是 Tabview,原因是

  • Tabview 的分组 header 不支持折叠,但是 Podcast 的侧边栏有侧叠功能 —— 这是使用 NavigationSplitView 并自定义实现的侧边栏 UI。
  • Journel 应用侧边栏元素更多,明显不是 Tabview 的效果。
这个不是 Tabview,因为 Tabview 没有折叠功能

值得注意的一点是: 无论是 NavigationStack 还是 NavigationSplitView,在 macOS 上默认都没有 iOS 风格的推进动画。在 Finder、系统设置、备忘录中看到的内容切换,都是近乎即时的替换,而非动画过渡。

只有 App Store 是例外 —— 那种有过渡感的体验更像是定制转场,不是默认行为。

方案二:两列 NavigationSplitView

NavigationSplitView {
    // 自定义 sidebar
} detail: {
    // 主内容区
}

固定的 sidebar + detail 两列结构,适合「列表 + 主内容」型的经典布局。备忘录、系统设置等应用是这一模式的典型代表。这两种方案在 macOS 上都能获得原生的侧边栏折叠按钮和一致的系统外观,视觉上的差异并不大。

在两列 NavigationSplitView 中,当 detail 内需要「再深入一层」的页面层级时(比如从文章列表点进文章详情,再点进评论页),仅靠 NavigationSplitView 本身是不够的。

这时需要在 detail 列内部建立明确的 NavigationStack 上下文:

NavigationSplitView {
    // sidebar
} detail: {
    NavigationStack {
        ArticleListView()
            .navigationDestination(for: Article.self) { article in
                ArticleDetailView(article: article)
            }
    }
}

Tabview 与 NavigationSplitView 的区别

差异一:在 iPad 和 iOS 平台外观差异

虽然 TabView + .sidebarAdaptable 和两列 NavigationSplitView 在 macOS 上外观接近,但在 iPadOS 和 iOS 上,它们的行为差异会明显放大。

TabView 的跨平台行为:

  • iPhone 上默认表现为底部 Tab Bar
  • iPad 和 macOS 上可通过 .sidebarAdaptable 适配为侧边栏

NavigationSplitView 的跨平台行为:

  • iPad 宽度足够时呈现侧边栏或双栏/三栏
  • iPhone(紧凑宽度)上会折叠为单列导航流
  • 在 iOS 上不会产生底部导航——底部导航的语义始终来自 TabView

差异二:侧边栏选项外观自定义能力 ⭐️⭐️⭐️

TabView(.sidebarAdaptable) 的侧边栏由系统完全托管,无法自定义图标颜色、行间距、Section 分组、badge 样式等。

NavigationSplitView 的 sidebar 列则是一个完全可定制的 SwiftUI 视图。你可以自由使用 ListScrollView、自定义行组件、彩色图标、任意 Section 分组,甚至在底部放置用户头像行——一切由你掌控。

Tabview 增强

从 iOS 18 开始,Tabview 组件新增了多个修饰器,用于实现不同的增强功能。

关键区别:Footer 是内容的一部分(tab 多了会被滚走),BottomBar 是固定的 UI 层(始终可见)。用户行放在 tabViewSidebarBottomBar 是正确的选择。

tabViewSidebarBottomBar(常驻)

tabViewSidebarBottomBar 会自动添加一条分割线,没找到隐藏的办法。

音乐应用明显也是使用 Tabview 组件:

  • 左下角添加用户信息显示
  • 分组标题不支持折叠

tabViewSidebarHeader(常驻)

将内容显示在顶部

tabViewSidebarFooter(常驻)

tabViewSidebarFooter 是跟随 tab 列表内容排列的,不固定在底部,而是默认显示在最后一行下面: