如何使用 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
收集结果并进行任意计算。
我目前正在使用 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
收集结果并进行任意计算。