Highlight
Session 聚焦在 UINavigationBar 的全面升级,分三个板块:界面可发现性、文档支持、搜索增强。
核心内容
iPad App 早期常把主要功能塞进左右两侧的 bar button item。屏幕变大以后,这个布局会浪费导航栏中间的空间。用户接上键盘、触控板或把 App 跑到 Mac Catalyst 时,也会期待工具栏像桌面 App 一样直接暴露常用操作。
Apple 在 iOS 16 给 UINavigationBar 增加了面向桌面级 iPad 的一组能力。它先把导航栏分成三种风格,再允许开发者把成组工具放进导航栏中央区域。这样,绘图、插入、格式化等操作不必藏在更多菜单里,可以成为可自定义的工具栏项。(02:00)
第二个变化面向文档 App。标题不再只是一个字符串。开发者可以给标题加菜单,提供复制、移动、重命名、导出和打印等动作,也可以从标题菜单发起拖放和分享。(08:43)
第三个变化是搜索。iPadOS 16 默认把搜索栏内联到导航栏里,减少它占用的垂直空间。UISearchController 新增搜索建议能力,用户输入时,App 可以同步给出可点击的建议项。(13:12)
这些变化还有一个额外收益:优化 Mac 时,UINavigationBar 会把 leading、center、trailing 三组内容自动翻译成 NSToolbar,并尊重中央工具组的自定义规则。(07:59)
详细内容
选择导航栏风格
UINavigationItem 在 iOS 16 增加了 style 属性。session 介绍了三种风格。(02:13)
navigator:默认风格,标题居中,行为和传统导航栏一致。browser:标题移到左侧,适合 Files、Safari 这类历史导航和当前位置同样重要的界面。editor:标题同样左对齐,并提供返回按钮,适合从文档选择器进入的编辑界面。
演讲没有给出设置 style 的代码片段。按 API 关系,可以把它理解为给当前页面的 navigationItem 选择布局策略:
// 概念示例:根据页面类型选择导航栏风格
navigationItem.style = .editor
关键点:
navigationItem是当前 view controller 的导航项。.editor对应 session 中的文档编辑场景。- 选择
browser或editor后,标题移到 leading 区域,导航栏中央会释放出更多空间。
用 UIBarButtonItemGroup 组织中央工具项
浏览器和编辑器风格释放了导航栏中间区域。iOS 16 用 centerItemGroups 接收一组 UIBarButtonItemGroup,让工具项以组为单位显示、移动、折叠和溢出。(03:23)
最简单的固定组可以从一个 UIBarButtonItem 创建:
let insertGroup = UIBarButtonItem(title: "Insert", image: UIImage(systemName: "photo"), primaryAction: UIAction { _ in }).creatingFixedGroup()
关键点:
UIBarButtonItem定义单个按钮,标题是Insert,图标来自 SF Symbols 的photo。primaryAction是点击按钮时执行的动作。creatingFixedGroup()把按钮包装成固定组。- 固定组总是优先出现在导航栏里,用户不能把它移动或移除。(04:47)
如果一个组必须保留在工具栏里,但允许用户改变位置,可以创建 movable group:
// Creating the 'Draw' group
// Convenient form of
// UIBarButtonItemGroup.movableGroup(customizationIdentifier:representativeItem:items:)
let drawGroup = UIBarButtonItem(title: "Draw", …)
.creatingMovableGroup(customizationIdentifier: "Draw")
关键点:
Draw是这个工具组在界面里的名称。creatingMovableGroup(customizationIdentifier:)创建可移动组。- 可移动组不能被移除,但可以被用户重新排序。
customizationIdentifier用来跟踪并保存用户调整后的位置。(05:13)
需要多按钮、可移除、可折叠的工具组时,用 optional group:
let shapeGroup = UIBarButtonItemGroup.optionalGroup(
customizationIdentifier: "Shapes",
representativeItem: UIBarButtonItem(title: "Shapes", image: UIImage(systemName: "square.on.circle")),
items: [
UIBarButtonItem(title: "Square", image: UIImage(systemName: "square"), primaryAction: UIAction { _ in }),
UIBarButtonItem(title: "Circle", image: UIImage(systemName: "circle"), primaryAction: UIAction { _ in }),
UIBarButtonItem(title: "Rectangle", image: UIImage(systemName: "rectangle"), primaryAction: UIAction { _ in }),
UIBarButtonItem(title: "Diamond", image: UIImage(systemName: "diamond"), primaryAction: UIAction { _ in }),
])
关键点:
optionalGroup表示这个组可以被移动,也可以被用户移除。customizationIdentifier: "Shapes"让 UIKit 保存这个组的自定义状态。representativeItem是空间不足时显示的代表按钮。representativeItem没有 action 时,UIKit 可以合成一个菜单,让用户选择组内项目。(05:45)items里的四个按钮分别对应 Square、Circle、Rectangle、Diamond。
最后,把这些组交给导航项:
navigationItem.customizationIdentifier = "com.jetpack.blueprints.maineditor"
navigationItem.centerItemGroups = [
// groups in the default customization
UIBarButtonItem(title: "Insert", image: UIImage(systemName: "photo"), primaryAction: UIAction { _ in }).creatingFixedGroup(),
UIBarButtonItem(title: "Draw", image: UIImage(systemName: "scribble"), primaryAction: UIAction { _ in }).creatingMovableGroup(customizationIdentifier: "Draw"),
.optionalGroup(customizationIdentifier: "Shapes",
representativeItem: UIBarButtonItem(title: "Shapes", image: UIImage(systemName: "square.on.circle")),
items: [
UIBarButtonItem(title: "Square", image: UIImage(systemName: "square"), primaryAction: UIAction { _ in }),
UIBarButtonItem(title: "Circle", image: UIImage(systemName: "circle"), primaryAction: UIAction { _ in }),
UIBarButtonItem(title: "Rectangle", image: UIImage(systemName: "rectangle"), primaryAction: UIAction { _ in }),
UIBarButtonItem(title: "Diamond", image: UIImage(systemName: "diamond"), primaryAction: UIAction { _ in }),
]),
.optionalGroup(customizationIdentifier: "Text",
items: [
UIBarButtonItem(title: "Label", image: UIImage(systemName: "character.textbox"), primaryAction: UIAction { _ in }),
UIBarButtonItem(title: "Text", image: UIImage(systemName: "text.bubble"), primaryAction: UIAction { _ in }),
]),
// additional group not in the default customization
.optionalGroup(customizationIdentifier: "Format",
isInDefaultCustomization: false,
representativeItem: UIBarButtonItem(title: "BIU", image: UIImage(systemName: "bold.italic.underline")),
items:[
UIBarButtonItem(title: "Bold", image: UIImage(systemName: "bold"), primaryAction: UIAction { _ in }),
UIBarButtonItem(title: "Italic", image: UIImage(systemName: "italic"), primaryAction: UIAction { _ in }),
UIBarButtonItem(title: "Underline", image: UIImage(systemName: "underline"), primaryAction: UIAction { _ in }),
])
]
关键点:
navigationItem.customizationIdentifier开启导航栏自定义,并给这套配置一个唯一标识。- UIKit 会基于这个标识自动保存和恢复用户自定义结果。(07:02)
centerItemGroups是导航栏中央区域的默认工具组列表。Insert是固定组,始终保留。Draw是可移动组,保留但可调整位置。Shapes和Text是 optional group,可被用户移除。Format设置了isInDefaultCustomization: false,表示它可用,但默认不显示在导航栏里。(07:34)
给标题增加文档菜单
文档 App 里,标题通常代表当前文档。iOS 16 允许通过 titleMenuProvider 给标题加菜单。启用后,系统建议菜单可以包含 duplicate、move、rename、export、print 五个命令,具体项目会根据 responder chain 过滤。(09:04)
navigationItem.titleMenuProvider = { suggestedActions in
var children = suggestedActions
children += [
UIAction(title: "Comments", image: UIImage(systemName: "text.bubble")) { _ in }
]
return UIMenu(children: children)
}
关键点:
titleMenuProvider是一个闭包,用来返回最终显示的标题菜单。suggestedActions是 UIKit 根据当前环境提供的建议菜单项。children先保留系统建议项,再追加自定义的Comments动作。UIMenu(children:)把系统项和自定义项组合成标题菜单。
从标题菜单支持拖放和分享
标题菜单还能显示文档头部,并提供拖放和分享入口。关键对象是 UIDocumentProperties。(10:06)
let url = <#T##URL#>
let documentProperties = UIDocumentProperties(url: url)
if let itemProvider = NSItemProvider(contentsOf: url) {
documentProperties.dragItemsProvider = { _ in
[UIDragItem(itemProvider: itemProvider)]
}
documentProperties.activityViewControllerProvider = {
UIActivityViewController(activityItems: [itemProvider], applicationActivities: nil)
}
}
navigationItem.documentProperties = documentProperties
关键点:
UIDocumentProperties(url:)用文件 URL 描述当前文档,UIKit 可以据此获取元数据和预览。NSItemProvider(contentsOf:)把文档包装成可拖放、可分享的数据提供者。dragItemsProvider返回UIDragItem数组,启用从标题菜单拖出文档的能力。activityViewControllerProvider返回UIActivityViewController,启用分享表单。navigationItem.documentProperties把这些文档信息交给导航栏。
实现内联重命名
UIKit 提供两种重命名入口:设置 UINavigationItem.renameDelegate 使用系统内联 UI,或者实现 UIResponder.rename(_:) 自己提供重命名界面。session 的代码演示了内联方案。(11:57)
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.renameDelegate = self
}
}
extension ViewController: UINavigationItemRenameDelegate {
func navigationItem(_ navigationItem: UINavigationItem, didEndRenamingWith title: String) {
// Try renaming our document, the completion handler will have the updated URL or return an error.
documentBrowserViewController.renameDocument(at: <#T##URL#>, proposedName: title, completionHandler: <#T##(URL?, Error?) -> Void#>)
}
}
关键点:
navigationItem.renameDelegate = self启用标题内联重命名。ViewController通过 extension 遵守UINavigationItemRenameDelegate。navigationItem(_:didEndRenamingWith:)接收用户确认后的新标题。- 文件型 App 可以调用
UIDocumentBrowserViewController的重命名 API,把新标题应用到文档 URL。
添加搜索建议
iPadOS 16 默认把搜索栏放进导航栏内部。用户激活搜索后,App 可以提供跟随输入变化的建议项。(13:23)
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
}
}
extension ViewController: UISearchResultsUpdating {
func fetchQuerySuggestions(for searchController: UISearchController) -> [(String, UIImage?)] {
let queryText = searchController.searchBar.text
// Here you would decide how to transform the queryText into search results. This example just returns something fixed.
return [("Sample Suggestion", UIImage(systemName: "rectangle.and.text.magnifyingglass"))]
}
func updateSearch(_ searchController: UISearchController, query: String) {
// This method is used to update the search UI from our query text change
// You should also update internal state related to when the query changes, as you might for when the user changes the query by typing.
searchController.searchBar.text = query
}
func updateSearchResults(for searchController: UISearchController) {
let querySuggestions = self.fetchQuerySuggestions(for: searchController)
searchController.searchSuggestions = querySuggestions.map { name, icon in
UISearchSuggestionItem(localizedSuggestion: name, localizedDescription: nil, iconImage: icon)
}
}
func updateSearchResults(for searchController: UISearchController, selecting searchSuggestion: UISearchSuggestion) {
if let suggestion = searchSuggestion.localizedSuggestion {
updateSearch(searchController, query: suggestion)
}
}
}
关键点:
searchController.searchResultsUpdater = self把搜索更新回调交给当前对象。fetchQuerySuggestions(for:)根据searchBar.text生成建议数据,示例返回固定建议。updateSearchResults(for:)在查询变化时被调用,用来更新searchSuggestions。UISearchSuggestionItem定义一条建议的文本、描述和图标。updateSearchResults(for:selecting:)在用户选择建议时被调用。- 示例把选中的建议文本写回搜索框,和用户手动输入查询走同一条更新路径。
核心启发
-
把绘图 App 的常用工具放到导航栏中央:画布 App 可以把插入图片、画笔、形状、文字、格式工具拆成 fixed、movable、optional groups。用
centerItemGroups提供默认组合,用customizationIdentifier保存用户的工具栏排列。 -
给文档标题加项目级操作菜单:笔记、白板、文档编辑器可以把评论、导出、打印、重命名放进
titleMenuProvider。入口贴近标题,用户知道这些动作作用于当前文档。 -
让文档从标题处可拖放、可分享:文件管理、设计稿、PDF 阅读器可以用
UIDocumentProperties和NSItemProvider支持从标题菜单拖出当前文档,或直接打开分享表单。 -
把重命名做成内联操作:文档型 App 可以设置
renameDelegate,让用户直接在导航栏标题处改名。完成后在navigationItem(_:didEndRenamingWith:)里调用真实的文档重命名逻辑。 -
给搜索框补上可点击建议:资料库、收藏夹、项目管理 App 可以用
UISearchController.searchSuggestions提供最近搜索、常用过滤条件或匹配结果,让键盘和触控板用户更快完成查询。
关联 Session
- Build a desktop-class iPad app — 展示如何把本 session 的导航栏能力和编辑、多窗口等功能组合成完整 App。
- Adopt desktop-class editing interactions — 继续讲 Find and Replace、Edit menu 和选择交互,补齐桌面级编辑体验。
- Bring multiple windows to your SwiftUI app — 讲 SwiftUI 多窗口管理,适合需要把 iPad App 扩展到桌面工作流的项目。
- Adopt Variable Color in SF Symbols — 讲 SF Symbols 可变颜色,可用于提升导航栏和工具栏图标状态表达。
评论
GitHub Issues · utterances