I was working on a code where I wanted to run the task only once, as it was unnecessary to do a call on navigating back to the view again. I have already seen the onFirstAppear modifier by Matt Young, and I wanted to extend it for async/await syntax—syntactic sugar to avoid wrapping in a Task {}.

I have to ensure that a task is executed only once by using the task modifier in combination with a ViewModifier.

FirstTask ViewModifier

First, I define a FirstTask structure that conforms to the ViewModifier protocol. This struct will contain an action property that will hold the asynchronous closure and a @State property called hasAppeared that will be used to track whether the closure has already been executed.

struct FirstTask: ViewModifier {
  let action: @Sendable () async -> ()

  // Use this to ensure the block is only executed once
  @State private var hasAppeared = false

  func body(content: Content) -> some View {
    content.task {
    
      // Prevent the action from being executed more than once
      guard !hasAppeared else { return }
      hasAppeared = true
      await action()
    }
  }
}

In the body property of the FirstTask struct, I use the task modifier to execute the asynchronous closure. Before executing the closure, I check the hasAppeared property to ensure that the closure has not already been executed. If it has, it simply returns from the closure without executing it. If it has not, I set the hasAppeared property to true and then execute the closure.

onFirstTask Modifier

Then, I define an extension on the View protocol that takes an asynchronous closure as a parameter. This closure will contain the code that I want to execute only once.

extension View {
  func onFirstTask(_ action: @escaping @Sendable () async -> ()) -> some View {
    modifier(FirstTask(action: action))
  }
}

Example Usage

To use this code, I call the onFirstTask method on any view and pass in the asynchronous method.

struct AppleMusicSongListView: View {
  @StateObject private var viewModel = AppleMusicSongViewModel()

  var body: some View {
    VStack {
      VStack(alignment: .leading, spacing: 12) {
        ///
    }
    .onFirstTask {
      await viewModel.fetchLibrarySongs()
    }
  }
}

Complex Views

Suppose there are cases with complex view hierarchies where views might be reconstructed. In that case, I can leverage @StateObject to hold the isFirstAppear state and ensure that this state is retained across the view's appearances.

extension View {
  func onFirstTask(_ action: @escaping @Sendable () async -> ()) -> some View {
    modifier(FirstTask(action: action))
  }
}

private struct FirstTask: ViewModifier {
  let action: @Sendable () async -> ()

  // Use this to ensure the block is only executed once
  @StateObject private var hasAppeared = FirstAppearState()

  func body(content: Content) -> some View {
    content.task {
      // Prevent the action from being executed more than once
      guard !hasAppeared.value else { return }
      hasAppeared.value = true
      await action()
    }
  }
}

// Create a separate class to hold the isFirstAppear state
private class FirstAppearState: ObservableObject {
    @Published var value: Bool = true
}

Conclusion

This is a simple way to ensure an asynchronous code block is executed only once in SwiftUI. Try it in your code, and let me know how it works for you!

String Catalog

String Catalog - App Localization on Autopilot

Push to GitHub, and we'll automatically localize your app for 40+ languages, saving you hours of manual work.

Tagged in: