WWDC22: App 通过专注模式过滤器设置可显示的内容

发布
更新
字数 1811
阅读 10 分钟
阅读量 1514

自 iOS15 开始引入专注模式,系统会帮助用户在一段时间内专注于重要的事情。iOS 16 增强了这一功能:使用专注模式过滤器对通知、App 进行设置。

对 App 意味着什么

  • 有时 App 可能处理多个登录账号,那现在可以根据系统场景(专注模式)控制可显示的账号内容。
  • 过滤内容,如日历应用,在专注模式下,只会显示工作任务。
  • 过滤通知,如邮件应用,回家后不会推送与工作相关的邮件通知。
  • 甚至可以在专注模式下改变应用的外观、布局。

总之,使用专注模式过滤器,App 可以在不同场景下根据用户设置显示不同的内容,改善用户体验。

哪些内容可以自定义

这取决于 App 提供的参数。

  1. 使用 AppIntent 提供可以自定义的属性,了解 AppIntents 框架
  2. 属性在系统专注模式设置中作为过滤器显示出来
  3. 用户在专注模式设置中设置过滤器来设定 App 的运作方式

实践

实现 SetFocusFilterIntent

// 1. 导入 AppIntents framework
import AppIntents

// 2. 定义一个实现了 `SetFocusFilterIntent` 的结构体
struct ExampleChatAppFocusFilter: SetFocusFilterIntent {
    
    // 3. 设置标题
    static var title: LocalizedStringResource = "Set account, status & look"
    // 4. 和描述
    static var description: LocalizedStringResource? = """
        Select an account, set your status, and configure
        the look of Example Chat App.
    """
}

以上内容会结合 App 的图标,一起出现在系统的专注模式过滤器的设置中,以网格的形式呈现。注意标题和描述字符串都是静态的,系统在安装您的应用时就读取了他们,定义参数后可以动态设置显示内容

定义参数

  • 定义专注模式过滤器时需要提供一系列修饰为参数(@Parameter(title:, default:) )的属性,用于指定用户可以自定义的内容。
  • 参数需要命名并确定数据类型,可以是标准数据类型如 Bool, string, float 等
  • 自定义数据类型需要设定为实体 Entity ,即可以修饰为参数使用了,参考 https://developer.apple.com/videos/play/wwdc2022/10032
  • 参数可以被标记为可选,非可选的参数需要提供一个默认值
import AppIntents

struct ExampleChatAppFocusFilter: SetFocusFilterIntent {

    // 修饰为参数的属性,类型为 Bool,显示标题为 `Use Dark Mode` 用来描述参数
    // 非可选,默认值 `false` 
    @Parameter(title: "Use Dark Mode", default: false)
    var alwaysUseDarkMode: Bool
    
    // 可选 String 参数,没有默认值
    @Parameter(title: "Status Message")
    var status: String?

    // 可选 AccountEntity 实体参数
    @Parameter(title: "Selected Account")
    var account: AccountEntity?

    // ...
}

动态设置显示内容

用户设置过滤器后,系统过滤器设置页应该根据用户设置动态显示内容

struct ExampleChatAppFocusFilter: SetFocusFilterIntent {
    // ...
    
    // 一个显示给用户的动态文本片段,根据上文的参数动态返回
    var localizedDarkModeString: String {
        return self.alwaysUseDarkMode ? "Dark" : "Dynamic"
    }

    // 需要实现的属性,返回一个包含标题和副标题的 DisplayRepresentation 对象
    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())")
        
        // 返回 DisplayRepresentation 对象
        return DisplayRepresentation(title: title, subtitle: subtitle)
    }
  
    // ...
}

响应用户设置

用户在系统过滤器设置中完成配置后,系统会在专注模式发生变化时通知你的应用:

  • 应用运行中,FocusFilterIntent 会收到 perform 方法的调用
  • 没有运行,可以部署一个扩展,系统会在扩展中调用 perform 方法

所以 App 和扩展都可以调用 perform 方法,通常来讲,如果只是随着专注模式更改应用的界面外观,只需要在 App 内实现 perform 方法就可以了;如果你的 App 包含小组件、通知、标记这些都需要根据专注模式改变,那就需要部署一个扩展了,也就是说当应用需要更新任何内部视图之外的东西,就需要部署一个扩展了。

perform 方法

同上,perform 方法也需要在 App 的 focus filter 中实现

import AppIntents

struct ExampleChatAppFocusFilter: SetFocusFilterIntent {
    // ...

    func perform() async throws -> some IntentResult {
        // 定义一个 App 内的数据
        let myData = AppData(
            // 通过 `self.参数名` 的方式调用参数
            alwaysUseDarkMode: self.alwaysUseDarkMode,
            status: self.status,
            account: self.account
        )
        // 更新应用
        myModel.shared.updateAppWithData(myData)
        // 返回
        return .result()
    }
  
    // ...
}

但专注模式发生变化或过滤器参数发生变化时,系统就会调用perform 方法,更改 app 的配置,随后可以依此更改 App 的行为和外观。

有时需要主动查询当前的过滤器参数,可以访问 current 属性,例如示例中调用 ExampleChatAppFocusFilter.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
    }
}

App context

如果要在 App 视图之外响应专注模式过滤器的设置,例如过滤通知信息,设定 App 通知标记的数量,专注模式下只显示选中帐号相关的通知,就需要提供额外的上下文信息返回给系统。

提供方法:

  • 创建 AppContext 对象,作为 perform 函数结果的一部分返回给系统。
  • 可以在专注模式过滤器中随时返回 AppContext ,并通过调用 invalidate 强制系统获取更新值。

以过滤通知为例,为了决定在专注模式下哪些通知可以打断用户并被显示出来,需要在 AppContext 设置 filterPredicate 属性,即“过滤器谓词”,同时需要设置 UNNotification 一个新的字符串属性 filterCriteria ,如果通知的过滤器与过滤器谓词不匹配,该通知就会被静音。

import AppIntents

struct ExampleChatAppFocusFilter: SetFocusFilterIntent {

    // 在 Intent 中设置 appContext
    var appContext: FocusFilterAppContext {
        // 设置一个过滤器谓词,匹配被允许的帐号标识符
        let allowedAccountList = [account.identifier]
        let predicate = NSPredicate(format: "SELF IN %@", allowedAccountList)
        // 返回一个包含过滤器谓词的 `FocusFilterAppContext` 对象,但通知不是来自个人账户时就会被静音
        return FocusFilterAppContext(notificationFilterPredicate: predicate)
    }
}

// 发送一条会被静音的工作通知
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."
// 设置标识符,即上文用到的 `account.identifier`,但这是一个工作帐户的标识符,所以会被静音
content.filterCriteria = "work-account-identifier"

以上示例为本地通知,如果是远程通知可以在 JSON payload 字段中进行配置。

如果是设置通知的标记数量,可以使用新的 UserNotifications API,在 UNUserNotificationCenter 上调用 setBadgeCount 方法提供一个无符号整数即可。

本文根据 WWDC 22 Meet Focus filters 总结:https://developer.apple.com/videos/play/wwdc2022/10121/