WWDC Quick Look 💓 By SwiftGGTeam
Meet Focus filters

Meet Focus filters

Watch original video

Highlight

iOS 16 and macOS Ventura introduce Focus Filters mechanism for Focus (focus mode). Previously, Focus could only control notification behavior at the system level, but now it can drill down into the internal logic of each application.


Core Content

Focus comes in iOS 15, macOS Monterey, and watchOS 8. It first solves the system-level interruption control: only colleagues are allowed to notify when working, and only a few apps are allowed to remind users during personal time (00:18).

With iOS 16 and macOS Ventura, Focus filters give this capability to Apps. After the user activates a Focus, the App can switch content, accounts, appearance, notifications and badges according to the current scene. The calendar can only display the personal calendar, and the email can only display the current Focus-related mailboxes and notifications (01:04).

The example in the talk is a chat app. A user may have both a work account and a personal account. When Personal Focus is enabled, the App only displays personal accounts, changes the status to personal time, and allows the interface to follow the appearance configured by the user; when Work Focus is enabled, the App switches back to work accounts and work notifications (02:52).

Developers have to do three things. First, use App Intents to define aSetFocusFilterIntent, tells the system that this App supports Focus filter. Second, declare user-configurable options as parameters. Third, execute when Focus changesperform(), apply the parameters returned by the system to the App or Extension (03:41).


Detailed Content

Define the entrance of Focus filter

(04:57) Focus filter is essentially a complianceSetFocusFilterIntentSwift type. The system reads the app’s static title and description when it is installed and displays it on the Focus settings page.

// 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.
    """
}

Key points:

  • import AppIntentsIntroduce the App Intents capability where Focus filters are located. -ExampleChatAppFocusFilterIt is the Focus filter type provided by the App to the system. -SetFocusFilterIntentIndicates that this Intent is used to set the App behavior under a Focus. -titleIt will appear in the card subtitle position in Focus settings. -descriptionAdditional instructions will be provided after the user clicks on the configuration page.
  • The title and description arestatic, the system reads them when the app is installed.

Declare configurable items as parameters

(07:02) What can users configure, by@ParameterDecide. Parameters can beBoolStringFloatOther standard types, or it can be an Entity defined by the App itself. Non-optional parameters need to provide default values.

// 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?

    // ...
}

Key points:

  • alwaysUseDarkModeis a required Boolean parameter, so usedefault: falseGive default value. -statusIs an optional string, the user can not set the status message for a Focus. -accountIt’s the App’s ownAccountEntity, used to allow users to select accounts.
  • each@Parameteroftitlewill be displayed in the system settings interface to help users understand this option.
  • The value of the parameter is filled in by the user in each Focus setting, and the App does not need to build this configuration UI by itself.

Let the settings page display specific configurations

(08:43) After the Focus filter is configured, the system settings page will display dynamic titles and subtitles.displayRepresentationUsed to describe “which parameters are configured” and “the current values ​​of these parameters”.

// 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)
    }
  
    // ...
}

Key points:

  • localizedDarkModeStringConvert Boolean values ​​into user-readable appearance descriptions. -titleListSave the configured parameter names, such as Account, Status, Look. -subtitleListSave parameter values ​​such as account display name, status message, Dark or Dynamic. -accountandstatusIt is optional and only enters the display content after the user sets it. -alwaysUseDarkModeis a required parameter, so Look will always be displayed. -DisplayRepresentationoftitleandsubtitleWill become the main copy and sub-copy on the Focus settings card.

Update App when Focus changes

(09:23) When the system determines that the App needs to respond to Focus changes, it will call the Focus filterperform(). If the App is running, the call occurs within the App; if the App is not running, the developer can implement an Extension and let the system start the Extension to handle the change. When you only update the app’s own views, the in-app implementation is usually enough; when you need to change widgets, notifications, or badges, you should consider Extension.

11:24perform()When called, the parameters are filled with the values ​​configured by the user in the Focus settings. The example assembles these values ​​intoAppData, and then hand it over to the App’s model layer to update the interface and behavior.

// 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()
    }
  
    // ...
}

Key points:

  • perform()It is the execution entry after Focus transition. -self.alwaysUseDarkModeRead the appearance options configured by the user for the current Focus. -self.statusRead status messages under the current Focus. -self.accountRead the account currently bound to Focus. -AppDataIt is the sample App’s own data structure used to centrally transfer these configurations. -updateAppWithData(myData)Apply the Focus configuration to the app. -return .result()Tell the system that this Intent execution is complete.

Actively query the current Focus filter

(11:47) Sometimes the App needs to actively read the current Focus filter instead of waiting for a system callperform(). At this time, you can access the specific Focus filter typecurrent

// 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
    }
}

Key points:

  • ExampleChatAppFocusFilter.currentAsynchronously returns the currently effective Focus filter parameters. -try awaitIndicates that reading the current configuration may fail, or you may need to wait for system results. -currentFilter.myRequiredBoolValueRead required parameters. -currentFilter.myOptionalStringValuemyOptionalAppEnumandmyAppEntityRead optional parameters or custom objects. -updateAppWithData(myData)reuse andperform()Same update path. -catchRecords errors and rethrows them to avoid silent lost read failures.

Use App Context to filter notifications

(12:16) Focus filter can also return the current context of the App to the system. A typical use is notification filtering: Apps inFocusFilterAppContextMedium settingsfilterPredicate, the notification content will be provided againfilterCriteria. If the two don’t match, the notification will be muted.

// 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)
    }
}

Key points:

  • appContextReturns additional context required by the system. -allowedAccountListSaves the account identifier of the user currently allowed to interrupt Focus. -NSPredicate(format: "SELF IN %@", allowedAccountList)Define notification filter conditions. -FocusFilterAppContext(notificationFilterPredicate: predicate)Submit filtering conditions to the system.
  • This context can be used asperform()The result is returned, and you can also use invalidate to let the system re-read it when needed.

(13:53) When sending a notification, the App must write the account to which the notification belongsfilterCriteria. In the speech example, Personal Focus currently only allows personal accounts; a work account notification comes withwork-account-identifier, so it will be muted by the system.

// 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"

Key points:

  • UNMutableNotificationContent()Create local notification content. -titlesubtitleandbodyFill in the text of the notification. -filterCriteriaWrite the context ID to which this notification belongs.
  • The system willfilterCriteriaSupplied with Focus filterfilterPredicatematch.
  • Remote notifications can also set filter criteria in the JSON payload.

(14:29) The corner mark should also follow the current Focus. The speech mentionedUNUserNotificationCenterNewsetBadgeCount, developers can change the corner mark to the number that is really important under the current Focus to reduce irrelevant reminders.


Core Takeaways

1. Focus account switching for multi-account apps

  • What to do: Make chat, email, customer service or social apps display only work accounts under Work Focus, and only personal accounts under Personal Focus.
  • Why it’s worth doing: The speech clearly mentioned that multi-account apps are suitable for linking a certain Focus to a certain account to prevent users from seeing irrelevant content in wrong scenarios.
  • How ​​to start: Model the account asAppEntity,existSetFocusFilterIntentStatement in@Parameter(title: "Selected Account") var account: AccountEntity?, againperform()Refresh the current account and list.

2. Switch status and appearance according to Focus

  • What: Automatically set a “working” status message in Work Focus and use a more lightweight interface layout or appearance in Personal Focus.
  • Why it’s worth doing: example Focus filter also containsstatusandalwaysUseDarkMode, indicating that Focus filter is not limited to filtering content, but can also control App behavior and appearance.
  • How ​​to start: Declare separately for status message and appearance@Parameter,usedisplayRepresentationDisplay the user configuration clearly and avoid only vague names on the settings page.

3. Add contextual filtering to notifications

  • What to do: Write the account, project or channel ID before sending the notification, so that the system will only highlight relevant notifications under the current Focus.
  • Why it’s worth doing: Speech NotesfilterPredicateandfilterCriteriaWhen there is no match, the notification will be muted, which is suitable for reducing interruptions in non-current scenes.
  • How ​​to start: InFocusFilterAppContextreturn innotificationFilterPredicate, notified locallyUNMutableNotificationContent.filterCriteriaOr write the same set of identifiers in the remote notification payload.

4. Let the corner mark only reflect the pending items of the current Focus

  • What to do: The Work Focus subscript only counts work account unreads, and the Personal Focus subscript only counts personal content.
  • Why it’s worth doing: The presentation lists badges as a way for apps to provide additional context to the system to reduce irrelevant reminders.
  • How ​​to start: Recalculate the importance count of the current Focus after the Focus changes, callUNUserNotificationCenterofsetBadgeCountUpdate badge.

Comments

GitHub Issues · utterances