WWDC Quick Look 💓 By SwiftGGTeam
Meet Focus filters

Meet Focus filters

观看原视频

Highlight

iOS 16 和 macOS Ventura 为 Focus(专注模式)引入了 Focus Filters 机制。之前 Focus 只能控制系统层面的通知行为,现在它可以深入到每个应用的内部逻辑。


核心内容

Focus 在 iOS 15、macOS Monterey 和 watchOS 8 中出现。它先解决的是系统层面的打扰控制:工作时只允许同事通知,个人时间只让少数 App 提醒用户(00:18)。

到了 iOS 16 和 macOS Ventura,Focus filters 把这个能力交给 App。用户启用某个 Focus 后,App 可以按当前场景切换内容、账号、外观、通知和角标。日历可以只显示个人日历,邮件可以只显示当前 Focus 相关的邮箱和通知(01:04)。

演讲里的示例是聊天 App。一个用户可能同时有工作账号和个人账号。Personal Focus 启用时,App 只显示个人账号,把状态改成个人时间,并让界面跟随用户配置的外观;Work Focus 启用时,App 再切回工作账号和工作通知(02:52)。

开发者要做三件事。第一,用 App Intents 定义一个 SetFocusFilterIntent,告诉系统这个 App 支持 Focus filter。第二,把用户可配置的选项声明成参数。第三,在 Focus 变化时执行 perform(),把系统传回来的参数应用到 App 或 Extension(03:41)。


详细内容

定义 Focus filter 的入口

04:57)Focus filter 本质上是一个遵守 SetFocusFilterIntent 的 Swift 类型。系统在安装 App 时读取它的静态标题和描述,并把它显示在 Focus 设置页面。

// Implementing SetFocusFilterIntent

import AppIntents

struct ExampleChatAppFocusFilter: SetFocusFilterIntent {

    static var title: LocalizedStringResource = "Set account, status & look"
    static var description: LocalizedStringResource? = """
        Select an account, set your status, and configure
        the look of Example Chat App.
    """
}

关键点:

  • import AppIntents 引入 Focus filters 所在的 App Intents 能力。
  • ExampleChatAppFocusFilter 是 App 提供给系统的 Focus filter 类型。
  • SetFocusFilterIntent 表明这个 Intent 用来设置某个 Focus 下的 App 行为。
  • title 会出现在 Focus 设置里的卡片副标题位置。
  • description 会在用户点进配置页面后提供额外说明。
  • 标题和描述是 static,系统在 App 安装时读取它们。

把可配置项声明成参数

07:02)用户能配置什么,由 @Parameter 决定。参数可以是 BoolStringFloat 等标准类型,也可以是 App 自己定义的 Entity。非 optional 参数需要提供默认值。

// Defining your Parameters & Entities

import AppIntents

struct ExampleChatAppFocusFilter: SetFocusFilterIntent {

    @Parameter(title: "Use Dark Mode", default: false)
    var alwaysUseDarkMode: Bool

    @Parameter(title: "Status Message")
    var status: String?

    @Parameter(title: "Selected Account")
    var account: AccountEntity?

    // ...
}

关键点:

  • alwaysUseDarkMode 是必填布尔参数,所以用 default: false 给出默认值。
  • status 是可选字符串,用户可以不为某个 Focus 设置状态消息。
  • account 是 App 自己的 AccountEntity,用于让用户选择账号。
  • 每个 @Parametertitle 都会显示在系统设置界面,帮助用户理解这个选项。
  • 参数的值由用户在每个 Focus 的设置中填写,App 不需要自己构建这套配置 UI。

让设置页面显示具体配置

08:43)Focus filter 配置完成后,系统设置页会显示动态标题和副标题。displayRepresentation 用来描述“配置了哪些参数”和“这些参数的当前值”。

// Display Representation

struct ExampleChatAppFocusFilter: SetFocusFilterIntent {
    // ...
  
    var localizedDarkModeString: String {
        return self.alwaysUseDarkMode ? "Dark" : "Dynamic"
    }

    var displayRepresentation: DisplayRepresentation {
        var titleList: [LocalizedStringResource] = [], subtitleList: [String] = []
        if let account = self.account {
            titleList.append("Account")
            subtitleList.append(account.displayName)
        }
        if let status = self.status {
            titleList.append("Status")
            subtitleList.append(status)
        }
        titleList.append("Look")
        subtitleList.append(self.localizedDarkModeString)
    
        let title = LocalizedStringResource("Set \(titleList, format: .list(type: .and))")
        let subtitle = LocalizedStringResource("\(subtitleList.formatted())")

        return DisplayRepresentation(title: title, subtitle: subtitle)
    }
  
    // ...
}

关键点:

  • localizedDarkModeString 把布尔值转换成用户能读懂的外观描述。
  • titleList 保存已经配置的参数名称,例如 Account、Status、Look。
  • subtitleList 保存参数值,例如账号显示名、状态消息、Dark 或 Dynamic。
  • accountstatus 是 optional,只在用户设置后才进入显示内容。
  • alwaysUseDarkMode 是必填参数,所以 Look 永远会显示。
  • DisplayRepresentationtitlesubtitle 会变成 Focus 设置卡片上的主文案和副文案。

在 Focus 变化时更新 App

09:23)系统判断 App 需要响应 Focus 变化时,会调用 Focus filter 的 perform()。如果 App 正在运行,调用发生在 App 内;如果 App 没有运行,开发者可以实现 Extension,让系统启动 Extension 处理这次变化。只更新 App 自己视图时,App 内实现通常就够了;需要改 widget、通知或角标时,应考虑 Extension。

11:24perform() 被调用时,参数已经填入用户在 Focus 设置里配置的值。示例把这些值组装成 AppData,再交给 App 的模型层更新界面和行为。

// Implementing Perform on your Focus filter

import AppIntents

struct ExampleChatAppFocusFilter: SetFocusFilterIntent {
    // ...

    func perform() async throws -> some IntentResult {
        let myData = AppData(
            alwaysUseDarkMode: self.alwaysUseDarkMode,
            status: self.status,
            account: self.account
        )
        myModel.shared.updateAppWithData(myData)
        return .result()
    }
  
    // ...
}

关键点:

  • perform() 是 Focus transition 后的执行入口。
  • self.alwaysUseDarkMode 读取用户为当前 Focus 配置的外观选项。
  • self.status 读取当前 Focus 下的状态消息。
  • self.account 读取当前 Focus 绑定的账号。
  • AppData 是示例 App 自己的数据结构,用来集中传递这些配置。
  • updateAppWithData(myData) 把 Focus 配置应用到 App。
  • return .result() 告诉系统这次 Intent 执行完成。

主动查询当前 Focus filter

11:47)有时 App 需要主动读取当前 Focus filter,而不是等待系统调用 perform()。这时可以访问具体 Focus filter 类型上的 current

// Calling Current

import AppIntents

func updateCurrentFilter() async throws {
    do {
        let currentFilter = try await ExampleChatAppFocusFilter.current
        let myData = AppData(
            myRequiredBoolValue: currentFilter.myRequiredBoolValue,
            myOptionalStringValue: currentFilter.myOptionalStringValue,
            myOptionalAppEnum: currentFilter.myOptionalAppEnum,
            myAppEntity: currentFilter.myAppEntity
        )
        myModel.shared.updateAppWithData(myData)
    } catch let error {
        print("Error loading current filter: \(error.localizedDescription)")
        throw error
    }
}

关键点:

  • ExampleChatAppFocusFilter.current 异步返回当前生效的 Focus filter 参数。
  • try await 表明读取当前配置可能失败,也可能需要等待系统结果。
  • currentFilter.myRequiredBoolValue 读取必填参数。
  • currentFilter.myOptionalStringValuemyOptionalAppEnummyAppEntity 读取可选参数或自定义对象。
  • updateAppWithData(myData) 复用和 perform() 相同的更新路径。
  • catch 中记录错误并重新抛出,避免静默丢失读取失败。

用 App Context 过滤通知

12:16)Focus filter 还可以把 App 当前上下文交还给系统。一个典型用途是通知过滤:App 在 FocusFilterAppContext 中设置 filterPredicate,通知内容再提供 filterCriteria。如果两者不匹配,通知会被静音。

// Set filterPredicate on an App context

import AppIntents

struct ExampleChatAppFocusFilter: SetFocusFilterIntent {

    var appContext: FocusFilterAppContext {
        let allowedAccountList = [account.identifier]
        let predicate = NSPredicate(format: "SELF IN %@", allowedAccountList)
        return FocusFilterAppContext(notificationFilterPredicate: predicate)
    }
}

关键点:

  • appContext 返回系统需要的额外上下文。
  • allowedAccountList 保存当前 Focus 允许打扰用户的账号标识符。
  • NSPredicate(format: "SELF IN %@", allowedAccountList) 定义通知过滤条件。
  • FocusFilterAppContext(notificationFilterPredicate: predicate) 把过滤条件交给系统。
  • 这个上下文可以作为 perform() 的结果返回,也可以在需要时通过 invalidate 让系统重新读取。

13:53)发送通知时,App 要把通知所属账号写入 filterCriteria。演讲示例里,当前 Personal Focus 只允许个人账号;一条工作账号通知带着 work-account-identifier,因此会被系统静音。

// Pass filterCriteria on UNNotificationContent

let content = UNMutableNotificationContent()
content.title = "Curt Rothert"
content.subtitle = "Slide Feedback"
content.body = "The run through today was great. I had few comments about slide 22 and 28."
content.filterCriteria = "work-account-identifier"

关键点:

  • UNMutableNotificationContent() 创建本地通知内容。
  • titlesubtitlebody 填写通知正文。
  • filterCriteria 写入这条通知所属的上下文标识。
  • 系统会把 filterCriteria 与 Focus filter 提供的 filterPredicate 匹配。
  • 远程通知也可以在 JSON payload 中设置 filter criteria。

14:29)角标也应该跟随当前 Focus。演讲提到 UNUserNotificationCenter 新增 setBadgeCount,开发者可以把角标改成当前 Focus 下真正重要的数量,减少无关提醒。


核心启发

1. 多账号 App 的 Focus 账号切换

  • 做什么:让聊天、邮件、客服或社交 App 在 Work Focus 下只显示工作账号,在 Personal Focus 下只显示个人账号。
  • 为什么值得做:演讲明确提到多账号 App 适合把某个 Focus 关联到某个账号,避免用户在错误场景里看到无关内容。
  • 怎么开始:把账号建模成 AppEntity,在 SetFocusFilterIntent 中声明 @Parameter(title: "Selected Account") var account: AccountEntity?,再在 perform() 里刷新当前账号和列表。

2. 根据 Focus 切换状态和外观

  • 做什么:在 Work Focus 自动设置“工作中”的状态消息,在 Personal Focus 使用更轻量的界面布局或外观。
  • 为什么值得做:示例 Focus filter 同时包含 statusalwaysUseDarkMode,说明 Focus filter 不限于过滤内容,也能控制 App 行为和外观。
  • 怎么开始:为状态消息和外观分别声明 @Parameter,用 displayRepresentation 把用户配置展示清楚,避免设置页只显示模糊名称。

3. 给通知加上下文过滤

  • 做什么:通知发送前写入账号、项目或频道标识,让系统在当前 Focus 下只突出相关通知。
  • 为什么值得做:演讲说明 filterPredicatefilterCriteria 不匹配时,通知会被静音,适合减少非当前场景的打扰。
  • 怎么开始:在 FocusFilterAppContext 中返回 notificationFilterPredicate,在本地通知的 UNMutableNotificationContent.filterCriteria 或远程通知 payload 中写入同一套标识。

4. 让角标只反映当前 Focus 的待处理事项

  • 做什么:Work Focus 下角标只统计工作账号未读数,Personal Focus 下角标只统计个人内容。
  • 为什么值得做:演讲把角标列为 App 向系统提供额外上下文的方式,用来减少不相关提醒。
  • 怎么开始:在 Focus 变化后重新计算当前 Focus 的重要计数,调用 UNUserNotificationCentersetBadgeCount 更新角标。

关联 Session

评论

GitHub Issues · utterances