Несколько вызовов функций, но однократное выполнение асинхронной задачи
Я играю с новым WeatherKit + WidgetKit от Apple. К сожалению, я не могу найти решение для комбинации следующих трех проблем:
- WidgetKit требует загрузки всех данных в функцию (без динамического обновления пользовательского интерфейса)
- WidgetKit по какой-то причине дважды или более вызывает getTimeline при загрузке (Форумы разработчиков)
- Я хочу, чтобы мои запросы 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
, он может быть вызван дважды или более примерно в одно и то же время. Пока первая задача еще не завершена, cachedWeather все еще пуст/устарел. Это приводит к многократному выполнению задачи, что, в свою очередь, означает отправку нескольких запросов в Apple.
В обычном представлении SwiftUI в приложении я бы работал с чем-то вроде ObservableObject и запускал запрос только в том случае, если ни один из них еще не запущен. Пользовательский интерфейс будет обновляться на основе ObservableObject. В WidgetKit это невозможно, как упоминалось выше.
Вопрос: Может ли кто-нибудь помочь мне понять, как запустить задачу при первом вызове и если задача уже/все еще выполняется при втором вызове?getWeather()
приходит вызов, использовать уже запущенную задачу вместо запуска новой?
2 ответа
Если я правильно понимаю вопрос, для этого и нужен актер. Попробуй это:
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()
}
}
}
}
Как вы увидите, таймер пытается запустить задачу каждую секунду, но если задача запущена, попытка отменяется; вы можете запустить задачу только в том случае, если она еще не запущена. Актер делает все это совершенно безопасным и связным.
Что касается вашего комментария:
Отсутствует то, что если timeConsumingThing вызывается во время работы, мне все равно нужен результат в конечном итоге ... В идеале второй вызов просто «подпишется» на ту же запущенную асинхронную задачу.
Я думаю, мы можем подражать этому, добавив в микс настоящую публикацию и подписку. Во-первых, позвольте мне выделить реальную задачу и заставить ее возвращать результат; это должно быть ваше взаимодействие с WeatherKit:
func timeConsumingTaskWithResult() async throws -> Date {
try await Task.sleep(nanoseconds: 5_000_000_000)
return Date.now
}
Теперь я немного изменю актера, чтобы новые вызывающие абоненты были вынуждены ждать следующего результата от последнего взаимодействия с 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
Как видите, каждую секунду к нашему актеру кто-то звонит. Каждый вызывающий абонент в конечном итоге получает результат, и все они являются одним и тем же результатом.
Я хочу поделиться другим подходом. Использование AsyncStream на издателе имеет побочный эффект публикации первого результата (который равен нулю) и не работает для меня в нужном мне сценарии. Вы, конечно, можете использоватьdropFirst
илиcompactMap
но есть другой способ.
Мы можем использовать тот факт, чтоTask
имеет асинхронный режимvalue
собственность, которую вы можете ждать. Если мы сохраним ссылку на нашу задачу, для которой нам нужен только один экземпляр, то мы сможем дождаться ее результата.
actor WeatherService {
private var timeConsumingTask: Task<Date, Never>?
func timeConsumingTaskWithResult() async -> Date {
print("Triggering at \(Date.now)")
defer { print("RESULT! \(date)") }
if let timeConsumingTask {
return await timeConsumingTask.value
} else {
let t = Task {
try! await Task.sleep(for: .seconds(5))
return Date.now
}
timeConsumingTask = t
return await t.value
}
}
}
Использование вышеуказанной службы погоды и запросtimeConsumingTaskWithResult
несколько раз выдал следующее:
Triggering at 2023-06-02 12:20:44 +0000
Triggering at 2023-06-02 12:20:44 +0000
Triggering at 2023-06-02 12:20:45 +0000
Triggering at 2023-06-02 12:20:45 +0000
Triggering at 2023-06-02 12:20:45 +0000
Triggering at 2023-06-02 12:20:45 +0000
Triggering at 2023-06-02 12:20:45 +0000
Triggering at 2023-06-02 12:20:46 +0000
Triggering at 2023-06-02 12:20:46 +0000
Triggering at 2023-06-02 12:20:46 +0000
Triggering at 2023-06-02 12:20:46 +0000
RESULT! 2023-06-02 12:20:50 +0000
RESULT! 2023-06-02 12:20:50 +0000
RESULT! 2023-06-02 12:20:50 +0000
RESULT! 2023-06-02 12:20:50 +0000
RESULT! 2023-06-02 12:20:50 +0000
RESULT! 2023-06-02 12:20:50 +0000
RESULT! 2023-06-02 12:20:50 +0000
RESULT! 2023-06-02 12:20:50 +0000
RESULT! 2023-06-02 12:20:50 +0000
RESULT! 2023-06-02 12:20:50 +0000
RESULT! 2023-06-02 12:20:50 +0000
Вы увидите, что значение Result всегда одинаково.
Конечно, в реальном примере вы фактически сохраните значение из задачи, но это иллюстрирует суть. Сохраните ссылку на свою задачу и дождитесь ее значения.