Exploring SwiftUI: Executing a Task Only Once

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!