Skip to content

Instantly share code, notes, and snippets.

@BrentMifsud
Last active September 18, 2025 04:56
Show Gist options
  • Save BrentMifsud/86e7413649be57948cbec3cc17c231a3 to your computer and use it in GitHub Desktop.
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.
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