swift 多个函数调用,但异步任务执行一次

soat7uwm  于 2023-01-04  发布在  Swift
关注(0)|答案(1)|浏览(151)

我正在摆弄苹果新推出的WeatherKit + WidgetKit,可惜我似乎找不到解决以下三个问题的方法:

  1. WidgetKit要求加载getTimeline函数中的所有数据(UI无动态更新)
    1.加载时,WidgetKit出于某种原因调用getTimeline两次或更多次(Developer Forums
    1.我希望将WeatherKit请求保持在最低限度
    上面的前两个问题迫使我在一个我无法控制的函数(getTimeline)中获取天气数据。
    我的获取天气的函数已经缓存了Weather对象,并确保只有该高速缓存太旧时才请求新数据。
private func getWeather() async -> Weather? {
    // if cachedWeather is not older than 2 hours return it instead of fetching new data
    if let cachedWeather = self.cachedWeather,
        cachedWeather.currentWeather.date > Date().addingTimeInterval(-7200)  {
        return cachedWeather
    }

    return try? await Task { () -> Weather in
        let fetchedWeather = try await WeatherService.shared.weather(for: self.location)
        cachedWeather = fetchedWeather
        return fetchedWeather
    }.value
}

如果我从getTimeline中调用getWeather(),它可能会在几乎相同的时间被调用两次或更多次。只要第一个任务还没有完成,cachedWeather就仍然是空的/过时的。这会导致任务的多次执行,这反过来意味着向苹果发送了多个请求。
在应用的普通SwiftUI视图中,我会使用ObservableObject之类的东西,只有在getWeather()中没有正在运行的东西时才触发请求。UI将基于ObservableObject更新。在WidgetKit中,如上所述,这是不可能的。

**问题:**有人能帮我弄清楚如何在第一次调用时触发getWeather()中的任务,以及如果在第二次getWeather()调用到来时任务已经/仍在运行,则使用已经运行的任务而不是触发新的任务吗?

5uzkadbs

5uzkadbs1#

如果我没理解错的话,这就是演员的职责。试试这个:

import UIKit

actor MyActor {
    var running = false
    func doYourTimeConsumingThing() async throws {
        guard !running else { print("oh no you don't"); return }
        running = true
        print("starting at", Date.now.timeIntervalSince1970)
        try await Task.sleep(nanoseconds: 5_000_000_000) // real task goes here
        print("finished at", Date.now.timeIntervalSince1970)
        running = false
    }
}

class ViewController: UIViewController {
    let actor = MyActor()
    var timer: Timer?
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            Task { [weak self] in
                try? await self?.actor.doYourTimeConsumingThing()
            }
        }
    }
}

正如您将看到的,计时器尝试每秒启动一次任务,但是如果任务正在运行,则尝试被退回;你只能在任务还没有运行的时候启动它。actor使这一切变得非常安全和线程一致。
关于你的评论:
缺少的是,如果timeConsumingThing在运行时被调用,我最终仍然需要结果...理想情况下,第二次调用将只是“订阅”同一个正在运行的异步任务。
我想我们可以通过添加一个实际的发布和订阅来模拟这一点。首先,让我分离出实际的任务并使其返回一个结果;这应该是你的WeatherKit交互

func timeConsumingTaskWithResult() async throws -> Date {
    try await Task.sleep(nanoseconds: 5_000_000_000)
    return Date.now
}

现在,我将稍微修改actor,以便新调用者必须等待最新WeatherKit交互返回的下一个结果:

actor MyActor {
    var running = false
    @Published var latestResult: Date?
    func doYourTimeConsumingThing() async throws -> Date? {
        if !running {
            running = true
            latestResult = try await timeConsumingTaskWithResult()
            running = false
        }
        for await result in $latestResult.values {
            return result
        }
        fatalError("shut up please, compiler")
    }
}

最后,测试台与以前基本相同,但现在我将获得每次触发计时器时所做调用的结果,并在获得结果时打印它:

class ViewController: UIViewController {
    let actor = MyActor()
    var timer: Timer?
    override func viewDidLoad() {
        super.viewDidLoad()
        timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            Task { [weak self] in
                print("calling at", Date.now)
                if let result = try? await self?.actor.doYourTimeConsumingThing() {
                    print("RESULT!", result)
                }
            }
        }
    }
}

结果是:

calling at 2022-08-28 15:35:39 +0000
calling at 2022-08-28 15:35:40 +0000
calling at 2022-08-28 15:35:41 +0000
calling at 2022-08-28 15:35:42 +0000
calling at 2022-08-28 15:35:43 +0000
calling at 2022-08-28 15:35:44 +0000
RESULT! 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:46 +0000
RESULT! 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:47 +0000
RESULT! 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:48 +0000
RESULT! 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:49 +0000
RESULT! 2022-08-28 15:35:45 +0000
calling at 2022-08-28 15:35:50 +0000
RESULT! 2022-08-28 15:35:45 +0000
RESULT! 2022-08-28 15:35:50 +0000
calling at 2022-08-28 15:35:51 +0000
calling at 2022-08-28 15:35:52 +0000
RESULT! 2022-08-28 15:35:50 +0000
calling at 2022-08-28 15:35:53 +0000
RESULT! 2022-08-28 15:35:50 +0000
calling at 2022-08-28 15:35:54 +0000
RESULT! 2022-08-28 15:35:50 +0000
calling at 2022-08-28 15:35:55 +0000
RESULT! 2022-08-28 15:35:50 +0000
calling at 2022-08-28 15:35:56 +0000
RESULT! 2022-08-28 15:35:50 +0000
RESULT! 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:35:58 +0000
RESULT! 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:35:59 +0000
RESULT! 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:36:00 +0000
RESULT! 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:36:01 +0000
RESULT! 2022-08-28 15:35:57 +0000
calling at 2022-08-28 15:36:02 +0000
RESULT! 2022-08-28 15:35:57 +0000
RESULT! 2022-08-28 15:36:02 +0000
calling at 2022-08-28 15:36:03 +0000

正如您所看到的,每秒都有人调用我们的actor。每个调用者最终都会得到一个结果,而且它们都是“相同”的结果2022-08-28 15:35:45,因为这是耗时任务返回的时间。从那时起,最近的调用者都开始得到2022-08-28 15:35:50,因为这是“下一个”耗时任务返回的时间。如我前面的示例所示,它被选通,因此只有在从上一次调用返回后才能被调用。

相关问题