WWDC Quick Look 💓 By SwiftGGTeam
Discover search suggestions for Apple TV

Discover search suggestions for Apple TV

观看原视频

Highlight

tvOS 14 为 UISearchController 增加搜索建议,应用可以用 UISearchSuggestionItem 或自定义 UISearchSuggestion 在用户输入时显示带图标和无障碍描述的候选项,并在建议被选中后按内容类型刷新结果。


核心内容

Apple TV 上的搜索成本比手机高。用户拿着遥控器,在屏幕键盘上逐字移动焦点。搜索框文字太小、键盘占据太多空间、结果区域被压缩,都会让找一部影片或一张照片变慢。

tvOS 14 先改了系统搜索界面。搜索框文字变大,多种语言的键盘被优化成单行布局,结果区有了更多空间。只要应用已经使用 UISearchController,这些界面改进会随系统一起获得。

这场 session 的重点是新的搜索建议。用户输入 blue 时,应用不必等用户打完完整关键词。它可以立刻提供“blue video”或“blue photo”这样的候选项。建议本身还能携带图标和无障碍描述,所以用户选中建议后,应用可以知道这是视频建议,随后只展示视频结果。

演示项目是一个照片和视频混合的旅行 app。它先把搜索页放进 tab bar,再用 UISearchController 驱动 collection view 结果,最后接入 UISearchSuggestion,让建议列表跟着输入实时更新。


详细内容

把搜索页接入 tvOS 的 tab bar

01:40)演示从 SearchViewController 开始。这个控制器持有数据源,后续所有搜索结果和建议都从 appData 查询。

private let appData: AppData

init(appData: AppData) {
    self.appData = appData

    super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

关键点:

  • private let appData 把搜索数据源固定在控制器内部。
  • init(appData:) 让调用方在创建搜索页时传入同一个数据源。
  • super.init(nibName:bundle:) 使用纯代码方式创建控制器。
  • init?(coder:) 没有实现,说明这个示例不走 storyboard 解码路径。

01:51)搜索入口应该使用系统标准 tab bar 项。这样用户在 Apple TV 上可以直接识别它的用途。

// use the system standard search tab bar item
tabBarItem = UITabBarItem(tabBarSystemItem: UITabBarItem.SystemItem.search, tag: 0)

关键点:

  • UITabBarItem.SystemItem.search 使用系统搜索图标和文案。
  • tag: 0 是 tab bar item 的标识,示例里不承担额外逻辑。
  • 这段代码解决入口问题,还没有创建搜索输入框。

UISearchContainerViewController 承载搜索控制器

02:05)因为搜索控制器会作为子控制器加入页面,session 使用 UISearchContainerViewController 包住它。

private let searchController: UISearchController
private let searchContainerViewController: UISearchContainerViewController

关键点:

  • searchController 管理搜索栏、输入变化和建议。
  • searchContainerViewController 负责把搜索控制器嵌入当前页面。
  • 两个属性都放在 SearchViewController 内,生命周期跟搜索页一致。

02:11)搜索结果由已有的 searchResultsController 展示,再传给 UISearchController

self.searchController = UISearchController(searchResultsController: self.searchResultsController)
self.searchContainerViewController = UISearchContainerViewController(searchController: searchController)

关键点:

  • UISearchController(searchResultsController:) 把结果页交给系统搜索控制器管理。
  • UISearchContainerViewController(searchController:) 让搜索 UI 可以作为子控制器嵌入。
  • 后续只要更新结果控制器的 items,collection view 就能刷新。

02:16)子控制器要按 UIKit 的 containment 流程加入父控制器。

override func viewDidLoad() {
    addChild(searchContainerViewController)

    searchContainerViewController.view.frame = view.bounds
    view.addSubview(searchContainerViewController.view)
    searchContainerViewController.didMove(toParent: self)
}

关键点:

  • addChild 先建立父子控制器关系。
  • searchContainerViewController.view.frame = view.bounds 让搜索容器填满当前页面。
  • view.addSubview 把搜索容器的 view 放到层级里。
  • didMove(toParent:) 完成 UIKit 子控制器添加流程。

03:17)搜索结果是 collection view,搜索栏需要跟结果滚动联动。

// scroll search controller allong with results collection view
searchController.searchControllerObservedScrollView = searchResultsController.collectionView

关键点:

  • searchControllerObservedScrollView 指向结果列表的 collectionView
  • 用户滚动结果时,搜索控制器可以跟着系统预期的 tvOS 行为移动。
  • 这一步只处理滚动关系,不处理搜索过滤。

用输入文字刷新搜索结果

03:43)搜索控制器通过 searchResultsUpdater 把输入变化交给当前控制器。

searchController.searchResultsUpdater = self

关键点:

  • self 需要遵守 UISearchResultsUpdating
  • 用户每次输入变化后,系统都会调用更新方法。
  • 这个入口后面也会用于处理建议被选中的场景。

04:00)最基础的搜索逻辑是读取搜索栏文本,然后用数据源过滤照片和视频。

func updateSearchResults(for searchController: UISearchController) {
    if let searchText = searchController.searchBar.text {
        // get search results for 'searchText' from data source
        let (results, _) = appData.searchResults(seachTerm: searchText, includePhotos: true, includeVideos: true)

        searchResultsController.items = results
    } else {
        // no search text, show unfiltered results
        searchResultsController.items = appData.allEntries
    }
}

关键点:

  • searchController.searchBar.text 取出当前输入内容。
  • appData.searchResults 同时搜索照片和视频。
  • searchResultsController.items = results 是界面刷新的关键赋值。
  • 没有输入时,示例回到 appData.allEntries,展示未过滤内容。

添加静态搜索建议

05:30)tvOS 14 新增 UISearchSuggestionItem。它是系统提供的建议模型,可以放文字、描述和图标。

let suggestion1 = UISearchSuggestionItem(localizedSuggestion: "Result1", localizedDescription: "Result1", iconImage: nil)
let suggestion2 = UISearchSuggestionItem(localizedSuggestion: "Result2", localizedDescription: "Result2", iconImage: nil)

searchController.searchSuggestions = [suggestion1, suggestion 2]

关键点:

  • localizedSuggestion 是显示给用户的建议文字。
  • localizedDescription 用于描述建议,session 明确提到它服务于 accessibility。
  • iconImage 可以为建议增加图标;这里先传 nil
  • searchController.searchSuggestions 接收建议数组,系统负责显示。

让业务对象遵守 UISearchSuggestion

07:05)如果 app 已经有自己的数据模型,可以让模型遵守 UISearchSuggestion。演示中的 SuggestedEntry 直接把名称和内容类型暴露给搜索 UI。

var localizedSuggestion: String? {
    return self.name
}

var iconImage: UIImage? {
    return self.isVideo ? UIImage(systemName: "video") : UIImage(systemName: "photo")
}

关键点:

  • localizedSuggestion 返回模型自己的 name,避免再创建一层建议对象。
  • iconImage 根据 isVideo 选择 videophoto 符号。
  • 这些 symbol image 用于在有限空间内区分内容类型。

07:20)建议项还要提供无障碍描述。示例把名称和内容类型一起本地化。

var localizedDescription: String? {
    if (self.isVideo) {
        return String.localizedStringWithFormat(NSLocalizedString("%@ - Video", comment: ""), self.name)
    }
    return String.localizedStringWithFormat(NSLocalizedString("%@ - Photo", comment: ""), self.name)
}

关键点:

  • localizedDescription 返回辅助描述,不只重复标题。
  • self.isVideo 决定描述里的内容类型。
  • NSLocalizedStringString.localizedStringWithFormat 让描述适配本地化。
  • 这与 session 开头提到的国际键盘和语言支持相呼应。

选中建议后按内容类型过滤

09:01)tvOS 14 扩展了 UISearchResultsUpdating。当用户选中搜索建议时,系统会把 UISearchSuggestion 传入新的更新方法。

func updateSearchResults(for searchController: UISearchController, selecting searchSuggestion: UISearchSuggestion) {
    if let searchText = searchController.searchBar.text {
        var includePhotos = true;
        var includeVideos = true;


    }
}

关键点:

  • 方法签名多了 selecting searchSuggestion 参数。
  • searchText 仍然来自搜索栏,代表用户当前输入。
  • includePhotosincludeVideos 先都设为 true,表示默认搜索两类内容。
  • 空白处留给建议解析和结果刷新逻辑。

09:13)演示把传入建议转回自己的 SuggestedEntry,读取它是否代表视频。

// check if the suggestion is for a photo or video
if let suggestedEntry = searchSuggestion as? SuggestedEntry {
    includeVideos = suggestedEntry.isVideo
    includePhotos = !includeVideos
}

关键点:

  • as? SuggestedEntry 只处理 app 自己创建的建议对象。
  • includeVideos = suggestedEntry.isVideo 让视频建议只显示视频。
  • includePhotos = !includeVideos 让照片建议只显示照片。
  • 这利用了建议对象携带的业务信息,不只依赖建议文字。

09:21)最后用更新后的类型开关重新查询数据源。

// filter the results by to include photos, videos, or both
let (results, _) = appData.searchResults(seachTerm: searchText, includePhotos: includePhotos, includeVideos: includeVideos)

searchResultsController.items = results

关键点:

  • searchText 保持用户输入的关键词。
  • includePhotosincludeVideos 决定结果类别。
  • appData.searchResults 返回过滤后的结果集合。
  • searchResultsController.items = results 立即刷新 collection view。

为不同输入方式预留空间

09:38)session 最后提醒,tvOS 键盘会随语言和输入设备变化。IR remote 会显示网格键盘;泰语环境会使用三行键盘。结果布局不能假设键盘永远只有一行。

关键点:

  • 前面在 03:17 设置的 searchController.searchControllerObservedScrollView 负责滚动联动;这里的重点是结果区域还要适配不同键盘布局。
  • 避免用自定义 UI 覆盖键盘,即使是 safe area 外的边缘区域也不推荐。
  • 如果建议需要区分内容类型,session 建议使用 symbol images。
  • SF Symbols 适合放进搜索建议这种空间有限的界面。

核心启发

  • 做什么:为视频 app 增加“关键词 + 类型”的搜索建议,例如“blue video”和“blue clip”。 为什么值得做UISearchSuggestion 可以携带内容类型,用户点建议后不需要再进入筛选面板。 怎么开始:让内容模型遵守 UISearchSuggestion,用 isVideo 生成 iconImage,在 updateSearchResults(for:selecting:) 里设置 includeVideos

  • 做什么:为照片浏览 app 做“地点 + 媒体类型”的建议。 为什么值得做:遥控器输入长地点名很慢,建议能减少键盘操作。 怎么开始:在普通 updateSearchResults(for:) 中按输入查询候选地点,把结果赋给 searchController.searchSuggestions

  • 做什么:为儿童内容 app 给建议项增加清晰图标和语音可读描述。 为什么值得做:session 明确展示了 iconImagelocalizedDescription,它们能在大屏界面和辅助功能场景里解释建议含义。 怎么开始:用 UIImage(systemName:) 返回内容类型符号,用 NSLocalizedString 生成“名称 - 类型”的描述。

  • 做什么:为国际化 tvOS app 检查不同键盘高度下的搜索结果布局。 为什么值得做:tvOS 会根据语言和遥控器类型显示单行、网格或三行键盘,固定高度结果区容易被压缩。 怎么开始:把结果放在 collection view 中,让 searchControllerObservedScrollView 指向它,并避免自定义浮层覆盖键盘。


关联 Session

评论

GitHub Issues · utterances