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!