如何使用 DispatchQueue.global().async 从主线程发布值?

How to publish values from the main thread using DispatchQueue.global().async?

我目前正在使用 SwiftUI 开发应用程序。

我想在标志为真时显示进度状态。

在我的代码中,标志在 while 循环期间具有 true 状态,并且包装在 DispatchQueue.main.sync 方法中的方法有效。 但是我在下面收到错误消息:

Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

云我怎么解决这个问题?


代码如下:

AppState.swift

@Published var isLoading:Bool = false
@Published var weatherInfos:[WeatherInfos]?


func getTemperatureAsync(date: String) {

    DispatchQueue.global().async {

        var counter:Int = 0
        var totalTemp:Float = 0.0

        while counter < 100
        {
            self.isLoading = true // I get error here

            counter += 1
            totalTemp += self.getTemperature(date: date)

            self.isLoading = false // I get error here
        }
    }
}


func getTemperature(date: String) -> Float{

    var temperature: Float = 0.0
    let semaphore = DispatchSemaphore(value: 0)

    let endpoint: String = "https://sample.com/api/weather/?&date=\(date)"
    let url = URL(string: endpoint)
    var urlRequest = URLRequest(url: url!)
    urlRequest.addValue("token xxxxxxxxxxxx", forHTTPHeaderField: "authorization")
    // set up the session
    let config = URLSessionConfiguration.default
    let session = URLSession(configuration: config)

    let task = session.dataTask(with: urlRequest) {(data, response, error) in
        guard error == nil else {
            print("error calling GET")
            return
        }
        // make sure we got data
        guard let responseData = data else {
            print("Error: did not receive data")
            return
        }
        DispatchQueue.main.sync {
            do{
                self.weatherInfos = try JSONDecoder().decode([WeatherInfos].self, from: responseData)

                for info in self.weatherAveInfos!{
                    temperature += info.ave_temp
                }
            }catch{
                print("Error: did not decode")
                return
            }
        }
        semaphore.signal()
    }
    task.resume()
    semaphore.wait()

    return temperature
}

JsonModel.swift

struct WeatherInfos:Codable,Identifiable {
    var id: Int
    var ave_temp: Float
}

Progress.swift

struct Progress: View {
    var body: some View {
        ProgressView("Loading...")
    }
}

MainView.swift

import SwiftUI

struct InformationView: View {

@EnvironmentObject var appState: AppState

    var body: some View {
       if appState.isLoding {
           Progress()
       }
       Button(action:{
            appState.getTemperatureAsync(date: "2020-11-01")
       }){
            Text("show progress")
         }
    }
}

Xcode:版本 12.0.1

iOS: 14.0

尝试以下操作(在循环中报告 isLoading 没有多大意义,因为在迭代时它会立即再次将 false 更改为 true)。

DispatchQueue.global(qos: .background).async {

    var counter:Int = 0
    var totalTemp:Float = 0.0

    DispatchQueue.main.async {
      self.isLoading = true
    }

    while counter < 100
    {
        counter += 1
        totalTemp += self.getTemperature(date: date)
    }

    DispatchQueue.main.async {
      self.isLoading = false
    }

}

您需要将更新发送回主队列。问题是您用信号量阻塞了主队列,因此尝试将更新分派回被阻塞的线程将导致死锁。

但是无论如何你都应该删除那个信号量。它解决了死锁风险,但也使其更快,完全避免阻塞 UI 。然后你应该采用异步模式。

因为这是 SwiftUI,我建议使用 Combine 来实现这种异步行为。例如,我可能会使用 dataTaskPublisher 来执行请求:

func weatherPublisher(for date: Date) -> AnyPublisher<Float, Error> {
    URLSession.shared.dataTaskPublisher(for: url(for: date))
        .map(\.data)
        .decode(type: WeatherReport.self, decoder: decoder)
        .map(\.temperature)
        .receive(on: RunLoop.main)
        .eraseToAnyPublisher()
}

如果您想检索多个日期的天气,请使用序列:

func weatherSequence(for dates: [Date]) -> AnyPublisher<Float, Error> {
    Publishers.Sequence(sequence: dates.map { self.weatherPublisher(for: [=11=]) })
        .flatMap(maxPublishers: .max(4)) { [=11=] }
        .eraseToAnyPublisher()
}

如果你想计算平均值:

class AverageTemperature: ObservableObject {
    var weatherRequests: AnyCancellable?
    @Published var average: Float = .nan
    private var values: [Float] = []

    func start() {
        let dates = ...
        weatherRequests = weatherSequence(for: dates).sink { completion in
            switch completion {
            case .finished:
                self.average = self.values.reduce(0, +) / Float(self.values.count)

            case .failure(let error):
                print("failed", error)
            }
        } receiveValue: { value in
            self.values.append(value)
        }
    }

    func weatherSequence(for dates: [Date]) -> AnyPublisher<Float, Error> {
        Publishers.Sequence(sequence: dates.map { self.weatherPublisher(for: [=12=]) })
            .flatMap(maxPublishers: .max(4)) { [=12=] }
            .eraseToAnyPublisher()
    }

    func weatherPublisher(for date: Date) -> AnyPublisher<Float, Error> {
        URLSession.shared.dataTaskPublisher(for: url(for: date))
            .map(\.data)
            .decode(type: WeatherReport.self, decoder: decoder)
            .map(\.temperature)
            .receive(on: RunLoop.main)
            .eraseToAnyPublisher()
    }

    func url(for date: Date) -> URL {
        var components = URLComponents(string: endPoint)!
        components.queryItems = [
            URLQueryItem(name: "date", value: formatter.string(from: date))
        ]
        return components.url!
    }
}

现在,我知道我的示例与您的不同(我的示例假设端点 returns 为单个温度,并且我对来自单独 API 调用的值进行平均),所以不要得到迷失在这里的细节。但大局是:

  • 使用dataTaskPublisher异步执行单个请求;
  • Sequence让他们都运行并发,但是用maxPublishers来约束并发度;和
  • 使用 sink 收集结果并进行任意计算。