How to Build Focus Filters in iOS 16

Apple introduced the Focus Filter API in iOS 16, allowing apps to dynamically adjust their behavior and content based on a user's active Focus mode. By leveraging the App Intents framework, developers can enable users to filter notifications, hide specific accounts, or adjust interfaces to minimize distractions. This guide outlines the steps required to implement custom Focus filters in an iOS application.

1. Adopt the App Intents Framework

Focus filters rely entirely on the AppIntents framework rather than the legacy SiriKit framework. Ensure your deployment target is set to iOS 16.0 or later and import the framework into your Swift file:

import AppIntents

2. Implement the SetFocusFilterIntent Protocol

To create a Focus filter, define a struct that conforms to the SetFocusFilterIntent protocol. This protocol requires you to provide metadata, parameters for the user to configure, and an execution handler.

struct MailAccountFocusFilter: SetFocusFilterIntent {
    static var title: LocalizedStringResource = "Filter Mail Accounts"
    static var description: LocalizedStringResource? = "Select which mail account to display during this Focus."

    @Parameter(title: "Account", description: "The account to show")
    var account: AccountEntity?

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

    static var parameterSummary: some ParameterSummary {
        Summary("Show \(\.$account)") {
            \.$enableDarkMode
        }
    }

    func perform() async throws -> some IntentResult {
        // Apply the filter settings locally
        AccountManager.shared.activeAccount = account
        ThemeManager.shared.isDarkMode = enableDarkMode
        
        return .result()
    }
}

3. Create Supporting Entities

If your filter allows users to pick from dynamic options—such as a specific email account, calendar, or folder—you must define an AppEntity accompanied by an EntityQuery.

struct AccountEntity: AppEntity {
    static var defaultQuery = AccountQuery()
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Account"

    var id: String
    var name: String

    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(title: "\(name)")
    }
}

struct AccountQuery: EntityQuery {
    func entities(for identifiers: [String]) async throws -> [AccountEntity] {
        return AccountManager.shared.accounts(for: identifiers)
    }

    func suggestedEntities() async throws -> [AccountEntity] {
        return AccountManager.shared.allAccounts
    }
}

4. Respond to Focus Changes in the App

When a user switches Focus modes, the system triggers your intent's perform() method. If the app is already in the foreground or suspended in memory, you need to read the current state and adjust your UI.

You can inspect the active configuration at any time using the current property:

Task {
    if let currentFilter = try? await MailAccountFocusFilter.current {
        print("Active account: \(String(describing: currentFilter.account))")
    }
}

In SwiftUI, you can observe changes using the scene phase or listen for updates via NotificationCenter depending on your state management architecture.

5. Test the Implementation

To test your Focus filter:

  1. Run the app on an iOS 16 device or simulator.
  2. Open the device Settings app and navigate to Focus.
  3. Select an existing Focus (e.g., Work or Personal).
  4. Scroll down to the Focus Filters section and tap Add Filter.
  5. Locate your app under the App Filters list, configure the defined parameters, and save.
  6. Toggle the Focus mode on and off in Control Center to verify your app receives and applies the parameters correctly.