Несколько вызовов функций, но однократное выполнение асинхронной задачи

Я играю с новым WeatherKit + WidgetKit от Apple. К сожалению, я не могу найти решение для комбинации следующих трех проблем:

  1. WidgetKit требует загрузки всех данных в функцию (без динамического обновления пользовательского интерфейса)
  2. WidgetKit по какой-то причине дважды или более вызывает getTimeline при загрузке (Форумы разработчиков)
  3. Я хочу, чтобы мои запросы 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 всегда одинаково.

Конечно, в реальном примере вы фактически сохраните значение из задачи, но это иллюстрирует суть. Сохраните ссылку на свою задачу и дождитесь ее значения.

Другие вопросы по тегам