WWDC Quick Look 💓 By SwiftGGTeam
Meet desktop-class iPad

Meet desktop-class iPad

观看原视频

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 中的文档编辑场景。
  • 选择 browsereditor 后,标题移到 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 是可移动组,保留但可调整位置。
  • ShapesText 是 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 阅读器可以用 UIDocumentPropertiesNSItemProvider 支持从标题菜单拖出当前文档,或直接打开分享表单。

  • 把重命名做成内联操作:文档型 App 可以设置 renameDelegate,让用户直接在导航栏标题处改名。完成后在 navigationItem(_:didEndRenamingWith:) 里调用真实的文档重命名逻辑。

  • 给搜索框补上可点击建议:资料库、收藏夹、项目管理 App 可以用 UISearchController.searchSuggestions 提供最近搜索、常用过滤条件或匹配结果,让键盘和触控板用户更快完成查询。

关联 Session

评论

GitHub Issues · utterances