Swift中如何将回调函数内的API返回值存储到外部变量
解决Swift异步回调中无法赋值外部变量的问题
嘿,我完全懂你遇到的这个困扰!作为Swift新手,异步操作的执行顺序确实很容易踩坑,咱们来一步步捋清楚问题出在哪,以及怎么解决~
问题根源:异步操作的执行顺序
你代码里的print(smth)输出nil,核心原因是**URLSession.dataTask是异步执行的**——当你调用getTime后,程序会立刻继续执行后面的print语句,而网络请求还在后台跑,回调闭包里的self.smth = smth根本还没来得及执行,所以这时候smth还是初始的nil。
另外你的代码还有两个小问题:
completionHandler定义成了返回String的闭包,但异步回调的返回值其实没有任何作用(因为dataTask的回调是在后台线程执行的,返回值不会被接收),完全可以改成无返回值的闭包。- 赋值时用了
self.smth? = smth,这是可选链赋值——如果smth初始是nil,这个赋值操作会直接被跳过,应该直接用self.smth = smth。
修正后的代码方案
方案1:优化回调闭包写法
先修改getTime函数,调整闭包类型并完善错误处理:
private func getTime(completionHandler: @escaping (_ steamId: String) -> Void) { // 先校验URL有效性,避免强制解包崩溃 guard let url = URL(string: urlTime) else { print("Invalid URL for time request") completionHandler("there is no data") return } let task = URLSession.shared.dataTask(with: url) { data, response, error in // 处理网络错误 if let error = error { print("Error fetching data: \(error.localizedDescription)") completionHandler("there is no data") return } // 校验响应状态码 guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { print("Unexpected status code: \(response?.description ?? "unknown")") completionHandler("there is no data") return } // 校验数据是否存在并解析 guard let data = data else { print("No data received from server") completionHandler("there is no data") return } do { let result = try JSONDecoder().decode(ResponseStats.self, from: data) let steamId = result.response.players[0].steamid ?? "there is no data" completionHandler(steamId) } catch { print("Failed to decode JSON: \(error)") completionHandler("there is no data") } } task.resume() }
然后修改调用逻辑,把需要使用smth的代码都放到回调闭包里面(如果要更新UI,记得切回主线程):
class ViewController: UIViewController { private var smth: String? override func viewDidLoad() { super.viewDidLoad() getData(from: url) getTime { steamId in // 直接赋值,不要用可选链 self.smth = steamId // 这里是异步回调执行的地方,能拿到正确的steamId print("Inside callback: \(self.smth)") // 如果要更新UI,必须切回主线程 DispatchQueue.main.async { // 比如更新UILabel的文本 // self.myLabel.text = self.smth } } // 这里的print会在网络请求完成前执行,所以仍然是nil print("Outside callback: \(smth)") } }
方案2:用Async/Await简化异步代码(推荐)
Swift 5.5+支持的Async/Await可以让异步代码写起来像同步代码一样清晰,完全避免回调嵌套的问题。我们可以把getTime改成异步函数:
private func getTime() async -> String { guard let url = URL(string: urlTime) else { print("Invalid URL") return "there is no data" } do { // 用await等待网络请求完成 let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { print("Unexpected status code") return "there is no data" } let result = try JSONDecoder().decode(ResponseStats.self, from: data) return result.response.players[0].steamid ?? "there is no data" } catch { print("Request failed: \(error)") return "there is no data" } }
然后在viewDidLoad里用Task来调用异步函数:
override func viewDidLoad() { super.viewDidLoad() getData(from: url) // 创建Task来执行异步代码 Task { // 用await等待getTime返回结果 self.smth = await getTime() print("After await: \(self.smth)") // 更新UI同样要切回主线程 DispatchQueue.main.async { // self.myLabel.text = self.smth } } }
关键总结
- 异步操作不会阻塞当前线程,所以调用异步函数后立刻访问结果变量,得到的肯定是初始值(比如
nil)。 - 所有需要使用异步结果的逻辑,必须放在回调闭包内,或者用
Async/Await等待结果返回后再执行。 - 更新UI的操作必须在主线程执行,所以异步回调里要记得用
DispatchQueue.main.async切换线程。
内容的提问来源于stack exchange,提问作者markliss
相关产品推荐
相关产品推荐

