How to Implement a Focus Filter in SwiftUI
Conform a struct to FocusFilterIntent, declare @Parameter properties users configure per Focus mode, then call FocusFilterIntent.current in your app to read the active configuration and adjust behavior accordingly.
import AppIntents
struct MyFocusFilter: FocusFilterIntent {
static var title: LocalizedStringResource = "My App Filter"
static var description: LocalizedStringResource = "Customize My App for this Focus."
@Parameter(title: "Show Urgent Only")
var urgentOnly: Bool = false
func perform() async throws -> some IntentResult {
// Called when the user activates a Focus with this filter
return .result()
}
}
// Read the active filter anywhere in your app:
let filter = try? await MyFocusFilter.current
Full implementation
Focus Filters use the App Intents framework. You define a FocusFilterIntent subtype, expose @Parameter values users can configure per Focus mode in iOS Settings, then observe those values inside your SwiftUI views. The system calls perform() whenever a matching Focus activates, and you read the stored configuration via MyFocusFilter.current to drive your UI.
import SwiftUI
import AppIntents
// MARK: - Focus Filter Intent
struct TaskAppFocusFilter: FocusFilterIntent {
static var title: LocalizedStringResource = "Task App Filter"
static var description: LocalizedStringResource =
"Configure Task App behavior for this Focus mode."
/// Shown in iOS Settings › Focus › [Mode] › App Filters › Task App
@Parameter(title: "Show Urgent Tasks Only", default: false)
var urgentOnly: Bool
@Parameter(title: "Selected Project", optionsProvider: ProjectOptionsProvider())
var project: String?
func perform() async throws -> some IntentResult {
// Called by the system when this Focus activates.
// Use UserDefaults / AppStorage to persist and notify the main app.
UserDefaults.standard.set(urgentOnly, forKey: "focusUrgentOnly")
UserDefaults.standard.set(project, forKey: "focusProject")
return .result()
}
}
// MARK: - Options Provider
struct ProjectOptionsProvider: DynamicOptionsProvider {
func results() async throws -> [String] {
// Replace with your real data source
["Inbox", "Work", "Personal", "Side Project"]
}
}
// MARK: - App State Observable
@Observable
final class FocusFilterState {
var urgentOnly: Bool = false
var activeProject: String? = nil
@MainActor
func refresh() async {
guard let filter = try? await TaskAppFocusFilter.current else {
urgentOnly = false
activeProject = nil
return
}
urgentOnly = filter.urgentOnly
activeProject = filter.project
}
}
// MARK: - SwiftUI View
struct TaskListView: View {
@State private var focusState = FocusFilterState()
let allTasks: [TaskItem] = TaskItem.samples
var filtered: [TaskItem] {
allTasks
.filter { !focusState.urgentOnly || $0.isUrgent }
.filter { focusState.activeProject == nil || $0.project == focusState.activeProject }
}
var body: some View {
NavigationStack {
List(filtered) { task in
TaskRow(task: task)
}
.navigationTitle(focusState.activeProject ?? "All Tasks")
.toolbar {
if focusState.urgentOnly {
ToolbarItem(placement: .topBarTrailing) {
Label("Focus Active", systemImage: "moon.fill")
.labelStyle(.iconOnly)
.foregroundStyle(.indigo)
}
}
}
}
.task {
await focusState.refresh()
}
// Re-read when app returns to foreground (Focus may have changed)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
Task { await focusState.refresh() }
}
}
}
// MARK: - Supporting Types
struct TaskItem: Identifiable {
let id = UUID()
let title: String
let project: String
let isUrgent: Bool
static let samples = [
TaskItem(title: "Submit PR", project: "Work", isUrgent: true),
TaskItem(title: "Buy groceries", project: "Personal", isUrgent: false),
TaskItem(title: "Fix crash bug", project: "Work", isUrgent: true),
TaskItem(title: "Read chapter 3", project: "Personal", isUrgent: false),
TaskItem(title: "Update README", project: "Side Project", isUrgent: false),
]
}
struct TaskRow: View {
let task: TaskItem
var body: some View {
HStack {
if task.isUrgent {
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.red)
.accessibilityLabel("Urgent")
}
VStack(alignment: .leading) {
Text(task.title).font(.body)
Text(task.project).font(.caption).foregroundStyle(.secondary)
}
}
}
}
// MARK: - Preview
#Preview {
TaskListView()
}
How it works
-
FocusFilterIntentconformance —TaskAppFocusFiltertells iOS that your app supports Focus Filters. iOS surfaces this in Settings › Focus › [Mode] › App Filters so users can configure it per Focus without touching your app. -
@Parameterdeclarations —urgentOnly: Boolandproject: String?are the knobs users set in Settings.DynamicOptionsProvideron theprojectparameter lets you supply a live list of projects from your data store. -
perform()callback — iOS calls this on your intent when the matching Focus activates. Here we write the active configuration toUserDefaultsso the main app can read it instantly, even if it was already in memory. -
FocusFilterIntent.current— ReadingTaskAppFocusFilter.currentinsideFocusFilterState.refresh()fetches the intent instance that is currently active. If no Focus with your filter is active it throws, which we handle withtry?and reset to defaults. -
Foreground re-read via
UIApplication.didBecomeActiveNotification— The user may enable or disable a Focus while your app is backgrounded. Re-fetching.currentwhen the app comes to the foreground keeps the UI in sync without any polling.
Variants
Suppress notifications inside the app during Focus
// Add a parameter for in-app notification suppression
@Parameter(title: "Mute In-App Banners", default: false)
var muteBanners: Bool
// In your notification presentation delegate:
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler handler: @escaping (UNNotificationPresentationOptions) -> Void
) {
Task {
let mute = (try? await TaskAppFocusFilter.current)?.muteBanners ?? false
handler(mute ? [] : [.banner, .sound, .badge])
}
}
Tinting the UI to signal an active Focus
Inject focusState.urgentOnly into your environment and apply .tint(.indigo) conditionally at the root WindowGroup level. This gives users a subtle, app-wide signal that a Work Focus is constraining the view — without needing a dedicated banner or alert.
Common pitfalls
-
iOS 16 vs iOS 17:
FocusFilterIntentwas introduced in iOS 16, but theDynamicOptionsProviderAPI and the.currentasync property behave most reliably on iOS 17+. Wrap iOS 16 fallback paths in#available(iOS 17, *)guards if you support both. -
perform()runs in a short-lived extension process: Don't assume your main app's in-memory state is reachable fromperform(). Always persist configuration to a sharedUserDefaultssuite (App Group) or a shared file so the main process can read it on next foreground. -
Accessibility — don't hide content silently: If
urgentOnlyhides tasks, add a visible banner (ContentUnavailableView) explaining why fewer items appear, so VoiceOver users aren't left wondering where their data went.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement a focus filter in SwiftUI for iOS 17+. Use FocusFilterIntent, @Parameter, DynamicOptionsProvider, and FocusFilterIntent.current. Persist active filter config to a shared UserDefaults App Group. Make it accessible (VoiceOver labels, ContentUnavailableView fallback). Add a #Preview with realistic sample data.
Drop this prompt into Soarias during the Build phase after your screens are scaffolded — it wires Focus Filter support into your existing data layer without disrupting your view hierarchy.
Related
FAQ
Does this work on iOS 16?
Partially. FocusFilterIntent shipped in iOS 16, so the basic conformance compiles. However, DynamicOptionsProvider and the async .current property work reliably only on iOS 17+. If you target iOS 16, replace DynamicOptionsProvider with a static optionsProvider and read the filter synchronously from UserDefaults instead of calling .current.
How do I test a Focus Filter without toggling my iPhone's Focus mode constantly?
Write a unit test that instantiates TaskAppFocusFilter, sets its urgentOnly and project properties directly, calls perform() with await, then asserts the expected UserDefaults values. For manual testing, add a developer settings screen in #if DEBUG that lets you fake-write the same UserDefaults keys and call focusState.refresh().
What's the UIKit equivalent?
The FocusFilterIntent API is framework-agnostic — it lives in App Intents, not SwiftUI. In a UIKit app you adopt the same FocusFilterIntent conformance and read .current in applicationDidBecomeActive(_:), then update your UIViewController state accordingly. No SwiftUI required.
Last reviewed: 2026-05-11 by the Soarias team.