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 AppIntents2. 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:
- Run the app on an iOS 16 device or simulator.
- Open the device Settings app and navigate to Focus.
- Select an existing Focus (e.g., Work or Personal).
- Scroll down to the Focus Filters section and tap Add Filter.
- Locate your app under the App Filters list, configure the defined parameters, and save.
- Toggle the Focus mode on and off in Control Center to verify your app receives and applies the parameters correctly.