SwiftUI Widget更新后清空数组致数据丢失问题求助
问题概述
我正在将应用从UIKit迁移至SwiftUI,同时把网络请求更新为async/await方案。开发了一款大尺寸Widget用于显示7个机场的天气信息,机场标识符存储在App Group共享UserDefaults容器中。测试时设置每分钟更新一次时间线(正常为20分钟),初始加载时数据完整正确,但时间线更新后数据不全,几次更新后Widget屏幕变空白。
主App更新共享UserDefaults中的机场信息后会调用WidgetCenter.shared.reloadAllTimelines,这部分逻辑正常(另一小尺寸Widget使用同一容器无问题)。若移除数组清空操作,数据不会消失但会持续重复添加;尝试在代码不同位置使用removeAll()或[]清空数组,均出现相同问题。
我对async/await的执行流程理解可能存在偏差,附上MetarService actor代码,其中getAirports为非隔离同步函数,内部通过Task异步遍历机场获取数据,但函数直接返回tempData,疑似存在异步同步不匹配问题。曾尝试多种解决方案均无效,请求帮助解决此数组管理与异步数据获取问题。
actor MetarService: NSObject, XMLParserDelegate, URLSessionDelegate, ObservableObject { enum MetarFetcherError: Error { case invalidServerResponse case missingData } @Published var metarArray = [String]() @Published var metarDataModel: [MetarDataModel] = [] var tempDataModel: [MetarDataModel] = [] func fetchMetars(metarAPTs: String) async throws -> [MetarDataModel] { let wxUrl = URL(string: "https://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&hoursBeforeNow=3&mostRecent=true&stationString=" + metarAPTs)! let (data, response) = try await URLSession.shared.data(from: wxUrl) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw MetarFetcherError.invalidServerResponse } guard let xml = SWXMLHash.parse(data) as XMLIndexer? else { throw MetarFetcherError.missingData } var noDataResponse = (xml["response"]["data"].element?.attribute(by: "num_results")?.text) ?? "0" var myairport = "" var myfltcat = "" var mymetarImage = "" var myobstime: NSString = "" var myvis = "" var mywinddir = "" var mywindspd = "" var mywind = "" if noDataResponse == "1" && (xml["response"]["data"]["METAR"]["observation_time"].element?.text) != nil { if (xml["response"]["data"]["METAR"]["station_id"].element?.text) != nil { myairport = xml["response"]["data"]["METAR"]["station_id"].element!.text } else { myairport = "MSNG" } if (xml["response"]["data"]["METAR"]["flight_category"].element?.text) != nil { myfltcat = xml["response"]["data"]["METAR"]["flight_category"].element!.text } else { myfltcat = "MISNG" } switch myfltcat { case "VFR": mymetarImage = "sun.max.circle.fill" case "MVFR": mymetarImage = "cloud.sun.circle.fill" case "IFR": mymetarImage = "cloud.fog.circle.fill" case "LIFR": mymetarImage = "smoke.circle.fill" default: mymetarImage = "person.crop.circle.badge.questionmark" } if (xml["response"]["data"]["METAR"]["observation_time"].element?.text) != nil { myobstime = xml["response"]["data"]["METAR"]["observation_time"].element!.text as NSString if myobstime.length < 16 { myobstime = "MISNG" } else { myobstime = myobstime.substring(with: NSRange(location: 11, length: 5)) as NSString } } if (xml["response"]["data"]["METAR"]["visibility_statute_mi"].element?.text) != nil { myvis = xml["response"]["data"]["METAR"]["visibility_statute_mi"].element!.text let intVis = (myvis as NSString) .integerValue myvis = String(intVis) + "SM" } else { myvis = "0" } if (xml["response"]["data"]["METAR"]["wind_dir_degrees"].element?.text) != nil { mywinddir = xml["response"]["data"]["METAR"]["wind_dir_degrees"].element!.text if mywinddir.contains("VRB") { mywinddir = "VRB" } else if mywinddir.count <= 2 && mywinddir.count > 0 { mywinddir = "0" + mywinddir } } else { mywinddir = "MISNG" } if (xml["response"]["data"]["METAR"]["wind_speed_kt"].element?.text) != nil { mywindspd = xml["response"]["data"]["METAR"]["wind_speed_kt"].element!.text if mywindspd == "0" { mywind = "Calm" } else if mywindspd.count == 1 { mywindspd = "0" + mywindspd mywind = mywinddir + "/" + mywindspd + "KT" } else if mywindspd.count > 1 { mywind = mywinddir + "/" + mywindspd + "KT" } } else { mywind = "MISNG" } } self.tempDataModel.append(MetarDataModel(metarImage: mymetarImage, mairport: myairport, mobstime: myobstime as String, mfltcat: myfltcat, mvis: myvis, mwind: mywind)) self.metarDataModel = self.tempDataModel tempDataModel = [] return metarDataModel } func readMetarApts() -> [String] { let defaults = UserDefaults(suiteName: "group.userdefaults.shared.FRAT")! var defaultairport = "" var wxAirport1 = "" var wxAirport2 = "" var wxAirport3 = "" var wxAirport4 = "" var wxAirport5 = "" var wxAirport6 = "" if ((defaults.value(forKey: "icaoIdent") as! String).isEmpty) { defaultairport = "KSFO" } else { defaultairport = defaults.value(forKey: "icaoIdent") as! String } wxAirport1 = defaults.value(forKey: "wxAirport1") as! String wxAirport2 = defaults.value(forKey: "wxAirport2") as! String wxAirport3 = defaults.value(forKey: "wxAirport3") as! String wxAirport4 = defaults.value(forKey: "wxAirport4") as! String wxAirport5 = defaults.value(forKey: "wxAirport5") as! String wxAirport6 = defaults.value(forKey: "wxAirport6") as! String metarArray.append(defaultairport) metarArray.append(wxAirport1) metarArray.append(wxAirport2) metarArray.append(wxAirport3) metarArray.append(wxAirport4) metarArray.append(wxAirport5) metarArray.append(wxAirport6) metarArray = metarArray.sorted() let returnArray = metarArray metarArray = [] return returnArray }// end of readAirports function var transData = [MetarDataModel]() var tempData = [MetarDataModel]() nonisolated func getAirports() -> ([MetarDataModel] ){ Task{ let tempArray = await readMetarApts() for apts in tempArray { let zData = try await self.fetchMetars(metarAPTs: apts) if zData .isEmpty { let errorData = MetarDataModel(metarImage: "sun.max.circle.fill", mairport: "DATA", mobstime: "CHK", mfltcat: "MSNG", mvis: "WiFi", mwind: "") tempData = [errorData] } else { transData.append(contentsOf: zData) tempData = transData } // else Closure } //Task Closure } // apts in tempArray Closure tempData = transData return tempData.sorted() } // end of getAirports function } // end of MetarService Class
核心问题分析
- 同步函数与异步逻辑不匹配:
getAirports是同步函数,但内部通过Task启动异步任务,函数会直接返回初始空的tempData,异步任务完成后修改的tempData根本无法被Widget获取,这是数据丢失/空白的核心原因。 - Actor状态管理混乱:
tempDataModel、transData、tempData等全局状态在异步调用中被频繁修改,没有保证线程安全,多次更新时会出现竞态条件,导致数据覆盖或重复。 - 数组清空时机错误:
fetchMetars每次调用都会清空tempDataModel,异步遍历多个机场时,前一次的结果会被后一次覆盖,最终只保留最后一个机场的数据。
修复方案
1. 修正getAirports为异步函数
将getAirports改为异步函数,让Widget的时间线更新逻辑能等待所有异步请求完成后再获取完整数据。
2. 重构状态管理,避免全局变量竞态
去掉多余的全局临时数组,每次请求创建局部数组存储结果,保证每次请求的独立性,避免状态污染。
3. 优化fetchMetars逻辑
让fetchMetars仅负责单机场数据的解析和返回,不修改全局状态,避免多次调用时的结果覆盖。
修复后的完整代码
actor MetarService: NSObject, XMLParserDelegate, URLSessionDelegate, ObservableObject { enum MetarFetcherError: Error { case invalidServerResponse case missingData } @Published var metarArray = [String]() @Published var metarDataModel: [MetarDataModel] = [] // 单机场数据获取,仅返回结果,不修改全局状态 func fetchMetar(for airportCode: String) async throws -> MetarDataModel { let urlString = "https://www.aviationweather.gov/adds/dataserver_current/httpparam?dataSource=metars&requestType=retrieve&format=xml&hoursBeforeNow=3&mostRecent=true&stationString=\(airportCode)" guard let wxUrl = URL(string: urlString) else { throw MetarFetcherError.invalidServerResponse } let (data, response) = try await URLSession.shared.data(from: wxUrl) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw MetarFetcherError.invalidServerResponse } let xml = SWXMLHash.parse(data) let noDataResponse = xml["response"]["data"].element?.attribute(by: "num_results")?.text ?? "0" guard noDataResponse == "1", let _ = xml["response"]["data"]["METAR"]["observation_time"].element?.text else { // 无数据时返回错误模型 return MetarDataModel(metarImage: "person.crop.circle.badge.questionmark", mairport: airportCode, mobstime: "MISNG", mfltcat: "MSNG", mvis: "MISNG", mwind: "MISNG") } // 解析字段 let myairport = xml["response"]["data"]["METAR"]["station_id"].element?.text ?? "MSNG" let myfltcat = xml["response"]["data"]["METAR"]["flight_category"].element?.text ?? "MISNG" let mymetarImage: String switch myfltcat { case "VFR": mymetarImage = "sun.max.circle.fill" case "MVFR": mymetarImage = "cloud.sun.circle.fill" case "IFR": mymetarImage = "cloud.fog.circle.fill" case "LIFR": mymetarImage = "smoke.circle.fill" default: mymetarImage = "person.crop.circle.badge.questionmark" } let myobstime: String if let obsTimeStr = xml["response"]["data"]["METAR"]["observation_time"].element?.text, obsTimeStr.count >= 16 { myobstime = String(obsTimeStr.dropFirst(11).prefix(5)) } else { myobstime = "MISNG" } let myvis: String if let visStr = xml["response"]["data"]["METAR"]["visibility_statute_mi"].element?.text, let intVis = Int(visStr) { myvis = "\(intVis)SM" } else { myvis = "0" } let mywind: String if let windDirStr = xml["response"]["data"]["METAR"]["wind_dir_degrees"].element?.text, let windSpdStr = xml["response"]["data"]["METAR"]["wind_speed_kt"].element?.text, let windSpd = Int(windSpdStr) { var formattedWindDir = windDirStr if formattedWindDir != "VRB" && formattedWindDir.count <= 2 && !formattedWindDir.isEmpty { formattedWindDir = String(format: "%03d", Int(formattedWindDir) ?? 0) } if windSpd == 0 { mywind = "Calm" } else { mywind = "\(formattedWindDir)/\(String(format: "%02d", windSpd))KT" } } else { mywind = "MISNG" } return MetarDataModel(metarImage: mymetarImage, mairport: myairport, mobstime: myobstime, mfltcat: myfltcat, mvis: myvis, mwind: mywind) } func readMetarApts() -> [String] { let defaults = UserDefaults(suiteName: "group.userdefaults.shared.FRAT")! let defaultairport = (defaults.string(forKey: "icaoIdent") ?? "").isEmpty ? "KSFO" : defaults.string(forKey: "icaoIdent")! let airports = [ defaultairport, defaults.string(forKey: "wxAirport1") ?? "", defaults.string(forKey: "wxAirport2") ?? "", defaults.string(forKey: "wxAirport3") ?? "", defaults.string(forKey: "wxAirport4") ?? "", defaults.string(forKey: "wxAirport5") ?? "", defaults.string(forKey: "wxAirport6") ?? "" ].filter { !$0.isEmpty } // 过滤空字符串 return airports.sorted() } // 改成异步函数,等待所有机场数据获取完成后返回 nonisolated func getAirports() async -> [MetarDataModel] { let service = self let airportCodes = await service.readMetarApts() var results = [MetarDataModel]() for code in airportCodes { do { let metar = try await service.fetchMetar(for: code) results.append(metar) } catch { // 捕获错误,添加错误模型 let errorData = MetarDataModel(metarImage: "sun.max.circle.fill", mairport: code, mobstime: "CHK", mfltcat: "MSNG", mvis: "WiFi", mwind: "") results.append(errorData) } } // 更新Actor内的发布状态(如果需要主App监听) await service.metarDataModel = results.sorted() return results.sorted() } }
额外优化建议
- Widget时间线适配:在Widget的
TimelineProvider中调用异步的getAirports(),等待数据返回后再生成时间线条目,确保每次更新都拿到完整数据。 - 并行请求优化:使用
TaskGroup并行请求多个机场数据,提升加载速度:
nonisolated func getAirports() async -> [MetarDataModel] { let service = self let airportCodes = await service.readMetarA
相关产品推荐
相关产品推荐

