One of the great new features in iOS 16 is Focus Filters, which allows users to change the way your app behaves when they are in Focus (such as Do Not Disturb).
In Streaks 8, we added a Focus Filter called "Up Next". When the linked Focus mode is enabled, only the selected tasks are shown within Streaks.
You create a Focus Filter as follows:
- Go to Settings app
- Open Focus
- Select an existing Focus mode, such as Work.
- Select Add Filter under the Focus Filters heading.
- Select the relevant focus filter (in this example, the Streaks: Up Next filter).
- Choose the relevant filter settings (in this case, the category of tasks to show).
- Select Add.
Now, when that Focus mode is enabled on your device, if you launch Streaks it will update so it only shows tasks from the selected category.
In Streaks, we implemented the focus filter as follows:
- Create the filter (extending from
SetFocusFilterIntent
) - Create the relevant value app entities / enums
- Check and update focus settings as necessary
Extend from the SetFocusFilterIntent
Protocol
Firs, we created the filter intent in our main app target. You can alternatively use an App Intents extension, but this isn't required.
import AppIntents
struct UpNextFocusFilterIntent: SetFocusFilterIntent {
// The focus filter title as it appears in the Settings app
static var title: LocalizedStringResource = "Up Next"
// The description as it appears in the Settings app
static var description: LocalizedStringResource? = "Filter tasks"
// How a configured filter appears on the Focus details screen
var displayRepresentation: DisplayRepresentation {
taskCategory?.displayRepresentation ?? "No category"
}
// A custom parameter called Category
@Parameter(title: "Category")
var taskCategory: CategoryAppEntity?
func perform() async throws -> some IntentResult {
// Use the parameter to update the state of the app
return .result()
}
}
The perform()
method is quite critical here, as you need to then use the parameters somehow.
In Streaks, we save the selected category to the app settings, and update the UI accordingly based on the saved setting.
Your parameters might be completely different: you can also use enums, or if you wanted to be able to select multiple categories you change the parameter to be an array.
Create The App Entity
In the above filter, we're using a custom entity called CategoryAppEntity
. To simplify this example, I've removed the image code used in displayRepresentation
(see below for more details about this).
import AppIntents
struct CategoryAppEntity: AppEntity {
let id: UUID
let title: String
var displayRepresentation: DisplayRepresentation {
.init(stringLiteral: title)
}
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Category"
static var defaultQuery = CategoryAppEntityQuery()
}
As with all AppEntity
instances, you need to also provide a query to satisfy defaultQuery
:
import AppIntents
struct CategoryAppEntityQuery: EntityQuery {
init() { }
func suggestedEntities() async throws -> [CategoryAppEntity] {
[] // Implement this
}
func entities(for identifiers: [TaskFilterID]) async throws -> [CategoryAppEntity] {
let allEntities = try await suggestedEntities()
return allEntities.filter { identifiers.contains($0.id) }
}
}
Responding to Changes
Your app doesn't explicitly know when a focus mode has been switched on or off; rather, you only know if the selected parameters have changed.
In other words, if a user has an identical focus filter for two different focus modes, you won't ever know if they switch between those two focus modes.
If your app is active when the parameters change, then the perform()
method of your SetFocusFilterIntent
will be called.
If your app isn't active, you need to check the settings when your app becomes active. In Streaks, we achieve this as follows:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// ...
func sceneDidBecomeActive(_ scene: UIScene) {
self.updateFocusFilterSettings()
// ...
}
func updateFocusFilterSettings() {
Task {
let filterIntent = try await UpNextFocusFilterIntent.current
_ = try await filterIntent.perform()
}
}
}
If you need to know about Focus Filter changes while your app is in the background, you need to use an App Intents extension instead.
Additional Enhancements
In the following screenshot, we enhanced the display of the configuration screen in the system Settings app in two ways:
- Images beside each category
- Categories are grouped into sections
To add the image, you need to change the displayRepresentation
to include an image:
struct TaskCategoryAppEntity: AppEntity {
// ...
let symbolName: String
var displayRepresentation: DisplayRepresentation {
.init(
title: .init(stringLiteral: title),
image: .init(systemName: symbolName)
)
}
// ...
}
This example uses an SF Symbol as the image (in Streaks we have our own icon set and custom drawing code for the color and circle shape).
To group the category parameter into multiple sections, you need to use DynamicOptionsProvider
, and specify this in the optionsProvider
argument in @Parameter
:
import AppIntents
struct UpNextFocusFilterIntent: SetFocusFilterIntent {
// ...
@Parameter(title: "Category", optionsProvider: CategoryDynamicOptionsProvider())
var taskCategory: CategoryAppEntity?
// ...
}
struct CategoryDynamicOptionsProvider: DynamicOptionsProvider {
func results() async throws -> IntentItemCollection<CategoryAppEntity> {
// Retrieve the list of options and build the sections
return .init(
sections: [
// section 1,
// section 2,
// ...
]
)
}
}
Now, when displaying the configuration page, this options provider is used for the category parameter.
Hopefully this article gives you a good idea of how to add Focus Filters to your app. The specifics of how to retrieve categories or update the UI have been omitted, since they are specific to your app.
If you want to see the Streaks focus filter in action, you can download it on the App Store.