You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
核心问题分析
  1. 同步函数与异步逻辑不匹配:getAirports是同步函数,但内部通过Task启动异步任务,函数会直接返回初始空的tempData,异步任务完成后修改的tempData根本无法被Widget获取,这是数据丢失/空白的核心原因。
  2. Actor状态管理混乱:tempDataModel、transData、tempData等全局状态在异步调用中被频繁修改,没有保证线程安全,多次更新时会出现竞态条件,导致数据覆盖或重复。
  3. 数组清空时机错误: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()
    }
}
额外优化建议
  1. Widget时间线适配:在Widget的TimelineProvider中调用异步的getAirports(),等待数据返回后再生成时间线条目,确保每次更新都拿到完整数据。
  2. 并行请求优化:使用TaskGroup并行请求多个机场数据,提升加载速度:
nonisolated func getAirports() async -> [MetarDataModel] {
    let service = self
    let airportCodes = await service.readMetarA
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 23:45:22