Last active
September 18, 2025 04:56
-
-
Save BrentMifsud/86e7413649be57948cbec3cc17c231a3 to your computer and use it in GitHub Desktop.
A view modifier that fires off only the first time a view loads.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
/// A SwiftUI `ViewModifier` that ensures provided operations run only once, | |
/// on the view’s first appearance. | |
/// | |
/// This modifier supports both synchronous and asynchronous work: | |
/// - A synchronous closure (`syncOperation`) that runs immediately on the main actor when the view first appears. | |
/// - An asynchronous closure (`asyncOperation`) that is executed in a `Task` with a configurable `TaskPriority`. | |
/// | |
/// The modifier tracks whether the initial load has already occurred and prevents subsequent executions | |
/// when the view reappears (e.g., due to navigation, tab switching, or transient hierarchy changes). | |
/// | |
/// Behavior details: | |
/// - If a synchronous operation is provided, it executes first on the initial appearance. | |
/// - If an asynchronous operation is provided, it is launched with the specified priority and awaited via a `.task` | |
/// modifier to tie its lifecycle to the view. | |
/// - The asynchronous task is stored to ensure it is awaited and not duplicated. | |
/// - The operations are guaranteed to execute on the main actor as annotated. | |
/// | |
/// Use the accompanying convenience APIs on `View`: | |
/// - `onFirstAppear(perform:)` for synchronous one-time work. | |
/// - `initialTask(priority:operation:)` for asynchronous one-time work. | |
/// | |
/// Example: | |
/// ```swift | |
/// struct ContentView: View { | |
/// var body: some View { | |
/// List { /* ... */ } | |
/// .onFirstAppear { | |
/// // One-time setup on first appearance | |
/// } | |
/// .initialTask(priority: .userInitiated) { | |
/// // One-time async load on first appearance | |
/// await viewModel.load() | |
/// } | |
/// } | |
/// } | |
/// ``` | |
/// | |
/// Notes: | |
/// - This modifier is intended for "run once per view instance" scenarios. | |
/// If the view identity changes (e.g., with `.id(_)`), the operations will run again for the new identity. | |
/// - Prefer this over `.task` alone when you need strict "first appearance only" semantics. | |
/// - Both closures are `@MainActor` and `@Sendable`, aligning with Swift Concurrency best practices. | |
/// | |
/// Platform: SwiftUI on Apple platforms supporting Swift Concurrency. | |
struct OnInitialLoadModifier: ViewModifier { | |
private let syncOperation: (@MainActor @Sendable () -> Void)? | |
private let priority: TaskPriority | |
private let asyncOperation: (@MainActor @Sendable () async -> Void)? | |
@State private var task: Task<Void, Never>? | |
@State private var initialLoadComplete: Bool = false | |
private init( | |
sync: (@Sendable @MainActor () -> Void)?, | |
priority: TaskPriority = .medium, | |
async: (@Sendable @MainActor () async -> Void)? | |
) { | |
self.syncOperation = sync | |
self.priority = priority | |
self.asyncOperation = async | |
} | |
init(syncOperation: @Sendable @escaping @MainActor () -> Void) { | |
self.init(sync: syncOperation, async: nil) | |
} | |
init( | |
priority: TaskPriority = .medium, | |
asyncOperation: @Sendable @escaping @MainActor () async -> Void | |
) { | |
self.init(sync: nil, priority: priority, async: asyncOperation) | |
} | |
func body(content: Content) -> some View { | |
content | |
.onAppear { | |
guard !initialLoadComplete else { return } | |
initialLoadComplete = true | |
if let syncOperation { | |
syncOperation() | |
} | |
if let asyncOperation { | |
task = Task(priority: priority) { | |
await asyncOperation() | |
} | |
} | |
} | |
.task(id: task, priority: priority) { | |
await task?.value | |
} | |
} | |
} | |
extension View { | |
/// Adds a modifier that executes a synchronous closure only once, the first time the view appears. | |
/// | |
/// Use this to perform one-time setup work that must run on the main actor, | |
/// such as configuring state, kicking off initial navigation, or sending | |
/// analytics. Subsequent appearances of the same view instance will not | |
/// trigger the closure again. | |
/// | |
/// - Important: The "run once" guarantee is per view identity. If the view’s | |
/// identity changes (for example, by using `.id(_:)` with a new value), the | |
/// closure will run again for the new instance. | |
/// - Threading: The `operation` closure is annotated `@MainActor` and will be | |
/// executed on the main thread. | |
/// - Reentrancy: The closure runs before any associated asynchronous initial task | |
/// configured via `initialTask(priority:operation:)`. | |
/// | |
/// - Parameter operation: A `@MainActor` and `@Sendable` closure to execute once on first appearance. | |
/// - Returns: A view that triggers `operation` one time upon its initial appearance. | |
/// | |
/// ### Example | |
/// ```swift | |
/// struct ContentView: View { | |
/// var body: some View { | |
/// List { | |
/// // ... | |
/// } | |
/// .onFirstAppear { | |
/// // Perform one-time setup | |
/// viewModel.configure() | |
/// } | |
/// } | |
/// } | |
/// ``` | |
func onFirstAppear(perform operation: @Sendable @escaping @MainActor () -> Void) -> some View { | |
modifier(OnInitialLoadModifier(syncOperation: operation)) | |
} | |
/// Adds a modifier that executes an asynchronous task only once, the first time the view appears. | |
/// | |
/// Use this to kick off one-time async work (e.g., loading remote data, prefetching resources, | |
/// or initializing an async dependency) that should not repeat when the view reappears due to | |
/// navigation or hierarchy changes. The task is launched with the specified priority and is tied | |
/// to the view’s lifecycle using SwiftUI’s `.task`, ensuring it is awaited and not duplicated. | |
/// | |
/// Behavior: | |
/// - The task runs only on the view’s initial appearance for the current view identity. | |
/// - If combined with `onFirstAppear(perform:)`, the synchronous work runs first, followed by this async task. | |
/// - The `operation` is executed on the main actor and must be `@Sendable`. | |
/// - The task is stored and awaited to prevent overlapping executions. | |
/// | |
/// Notes: | |
/// - "Run once" is per view identity. If the view identity changes (e.g., using `.id(_:)` with a new value), | |
/// the task will run again for the new instance. | |
/// - Prefer this modifier when you need strict "first appearance only" semantics rather than using `.task` directly. | |
/// | |
/// - Parameters: | |
/// - priority: The `TaskPriority` used when launching the asynchronous task. Defaults to `.medium`. | |
/// - operation: A `@MainActor` and `@Sendable` asynchronous closure to perform one-time work on first appearance. | |
/// - Returns: A view that triggers `operation` one time upon its initial appearance. | |
/// | |
/// ### Example | |
/// ```swift | |
/// struct ContentView: View { | |
/// @StateObject private var viewModel = ViewModel() | |
/// | |
/// var body: some View { | |
/// List(viewModel.items) { item in | |
/// Text(item.title) | |
/// } | |
/// .initialTask(priority: .userInitiated) { | |
/// await viewModel.loadItems() | |
/// } | |
/// } | |
/// } | |
/// ``` | |
func initialTask( | |
priority: TaskPriority = .medium, | |
operation: @Sendable @escaping @MainActor () async -> Void | |
) -> some View { | |
modifier(OnInitialLoadModifier(priority: priority, asyncOperation: operation)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment