在 Swift 5.5 中使用 async/await 实现循环函数

9

我希望在函数完成后继续每隔5秒触发一次该函数。

以前,我会在函数末尾使用以下代码:

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

但是我想使用 Swift 5.5async/await 特性。

如果我使用类似下面的代码:

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

我收到一个警告,说函数调用导致无限递归,而且它不是真正可取消的。

这个可以成功编译:

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()
        }
    }
}

尽管它每5秒会触发一次,但当我的 SwiftUI 视图被关闭时,它仍在运行。我使用以下方式启动它:

.onAppear {
    loadInfo()
}

由于所有内容都在同一个 Task 上运行,而不是 detached,那么当视图被移除时,它不应该全部取消吗?

使用 async/await 实现这一点的现代方法是什么?

2个回答

6
您可以将任务保存在 @State 变量中,然后使用 onDisappear(perform:) 在视图消失时取消它。
示例:
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))"
    }
}

1
谢谢,这很棒。我不得不进行轻微修改才能编译。由于loadInfo()函数不是异步的,所以我需要使用.onAppear而不是.task - Darren
1
@Darren 谢谢,我已经更改了。对我来说,在 Xcode 13b3 上使用 .task(_:) 编译通过,但在 Xcode 13b5 上不行。 - George

3

async await的目的是让你以同步的方式编写异步代码。因此,您可以删除递归函数并简单地编写:

.task {
    repeat {
        // code you want to repeat
        print("Tick")

        try? await Task.sleep(for: .seconds(5)) // exception thrown when cancelled by SwiftUI when this view disappears.
    } while (!Task.isCancelled)
    print("Cancelled")  
}

我注意到打印操作总是在主线程上运行,但如果我将其移到自己的异步函数中,则可以正确地在不同的线程上运行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接