swiftasync-awaitswiftuiswift5swift-concurrency

Recurring function in Swift 5.5 using async/await


I want to keep firing a function 5 seconds after it completes.

Previously I would use this at the end of the function:

Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { self.function() }

But I am wanting to use Swift 5.5's async/await.

If I use something like this:

func loadInfo() async {
    async let info = someOtherAsyncFunc()
    self.info = try? await info
    await Task.sleep(5_000_000_000)
    await loadInfo()
}

I get a warning that the Function call causes an infinite recursion and it's not really cancellable.

This compiles fine:

func loadInfo() async {
    Task {
        async let info = someOtherAsyncFunc()
        self.info = try? await info
        await Task.sleep(5_000_000_000)
        if Task.isCancelled {
            print("Cancelled")
        }
        else
        {
            print("Not cancelled")
            await loadInfo()
        }
    }
}

and although it does fire every 5 seconds, it keeps running when my SwiftUI view is dismissed. I start it using:

.onAppear {
    loadInfo()
}

As it's all running on the same Task and not detached should it not all cancel when the view is removed?

What is the modern way to achieve this with async/await?


Solution

  • You can save the task in a @State variable, and then cancel it when the view disappears with onDisappear(perform:).

    Working example:

    struct ContentView: View {
        @State private var info: String?
        @State private var currentTask: Task<Void, Never>?
    
        var body: some View {
            NavigationView {
                VStack {
                    Text(info ?? "None (yet)")
                        .onAppear(perform: loadInfo)
                        .onDisappear(perform: cancelTask)
    
                    NavigationLink("Other view") {
                        Text("Some other view")
                    }
                }
                .navigationTitle("Task Test")
            }
            .navigationViewStyle(.stack)
        }
    
        private func loadInfo() {
            currentTask = Task {
                async let info = someOtherAsyncFunc()
                self.info = try? await info
                await Task.sleep(5_000_000_000)
                guard !Task.isCancelled else { return }
                loadInfo()
            }
        }
    
        private func cancelTask() {
            print("Disappear")
            currentTask?.cancel()
        }
    
        private func someOtherAsyncFunc() async throws -> String {
            print("someOtherAsyncFunc ran")
            return "Random number: \(Int.random(in: 1 ... 100))"
        }
    }