修改全局变量内部闭包 (Swift 4)
Modify Global Variable Inside Closure (Swift 4)
我正在尝试使用此函数修改全局变量 currentWeather(CurrentWeather 类型),这意味着使用从 URL 和 return 中检索到的信息更新所述变量,一个 bool 表示它的成功。然而,这个函数是 returning false,因为 currentWeather 仍然是 nil。我认识到 dataTask 是异步的,并且该任务在后台与应用程序并行 运行,但我不明白这对我要完成的任务意味着什么。我也无法在 do 块之后更新 currentWeather,因为在退出块后不再识别天气。我确实尝试使用 "self.currentWeather",但被告知这是一个未解析的标识符(可能是因为该函数也是全局的,并且没有 "self"?)。
URL 当前无效,因为我取出了我的 API 密钥,但它按预期工作,否则我的 CurrentWeather 结构是可解码的。打印 currentWeatherUnwrapped 也一直是成功的。
我确实查看了 Stack Overflow 和 Apple 的官方文档,但未能找到可以回答我问题的内容,但也许我还不够彻底。如果这是一个重复的问题,我很抱歉。也感谢任何进一步相关阅读的指导!对于不符合最佳编码实践,我深表歉意——我在这一点上不是很有经验。非常感谢大家!
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
return currentWeather != nil
}
您从根本上误解了异步函数的工作原理。您在 URLSession's
dataTask
甚至开始执行之前运行 returns。网络请求可能需要几秒钟才能完成。你要求它为你获取一些数据,给它一个代码块,让它在数据下载后执行,然后继续你的业务。
您可以确定 dataTask 的 resume()
调用之后的行将 运行 在新数据加载之前。
当数据可用时,您需要将想要的代码放入 运行 数据任务的完成块中。 (一旦数据读取成功,您的语句 print(currentWeatherUnwrapped)
将 运行。)
当您执行这样的异步调用时,您的函数将 return 远早于您的 dataTask 对 return 具有任何值。您需要做的是在函数中使用完成处理程序。您可以像这样将其作为参数传递:
func getCurrentWeather(completion: @escaping(CurrentWeather?, Error?) -> Void) {
//Data task and such here
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
completion(nil, err)
return
}
//You don't need a do try catch if you use try?
let weather = try? JSONDecoder().decode(CurrentWeather.self, from: data)
completion(weather, err)
}.resume()
}
然后调用该函数如下所示:
getCurrentWeather(completion: { (weather, error) in
guard error == nil, let weather = weather else {
if weather == nil { print("No Weather") }
if error != nil { print(error!.localizedDescription) }
return
}
//Do something with your weather result
print(weather)
})
你只需要一个闭包。
您不能对 return Web 服务调用的响应使用同步 return 语句,这本身就是异步的。为此你需要闭包。
您可以修改您的答案如下。因为你没有在评论中回答我的问题,所以我自由地 return 天气对象而不是 returning bool ,这没有多大意义。
func getCurrentWeather (completion : @escaping((CurrentWeather?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
completion(CurrentWeather.currentWeather)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
completion(nil)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}
假设 currentWeather
是您 CurrentWeather
中的静态变量 class 您可以更新全局变量以及 return 调用方的实际数据,如上所示
编辑:
正如 Duncan 在下面的评论中所指出的,上面的代码在后台线程中执行完成块。所有 UI 操作只能在主线程上完成。因此,在更新 UI.
之前切换线程非常重要
两种方式:
1- 确保在主线程上执行完成块。
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather)
}
这将确保将来使用您的 getCurrentWeather
的任何人都不必担心切换线程,因为您的方法会处理它。如果您的完成块仅包含更新 UI 的代码,则很有用。使用这种方法的完成块中较长的逻辑会给主线程带来负担。
2 - Else 在更新 UI 元素时作为参数传递给 getCurrentWeather
的完成块中确保将这些语句包装在
中
DispatchQueue.main.async {
//your code to update UI
}
编辑 2:
正如 Leo Dabus 在下面的评论中所指出的,我应该 运行 完成块而不是 guard let url = URL(string: jsonUrlString) else { return false }
那是一个复制粘贴错误。我复制了 OP 的问题,然后匆忙意识到有一个 return 语句。
虽然在这种情况下将错误作为参数是可选的,并且完全取决于您设计错误处理模型的方式,但我很欣赏 Leo Dabus 建议的想法,这是更通用的方法,因此更新了我的答案以获得错误作为参数。
现在有些情况下我们可能还需要发送我们的自定义错误,例如如果 guard let data = data else { return }
returns false 而不是简单地调用 return 你可能需要 return 您自己的错误,表示无效输入或类似内容。
因此我冒昧地声明了我自己的自定义错误,您也可以使用该模型来处理您的错误处理
enum CustomError : Error {
case invalidServerResponse
case invalidURL
}
func getCurrentWeather (completion : @escaping((CurrentWeather?,Error?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidURL)
}
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
if err != nil {
DispatchQueue.main.async {
completion(nil,err)
}
return
}
guard let data = data else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidServerResponse)
}
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather,nil)
}
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
DispatchQueue.main.async {
completion(nil,jsonErr)
}
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}
正如您所指出的,data ask
是 async
,这意味着您不知道它何时会完成。
一种选择是通过不提供 return 值,而是提供 callback/closure 来将包装函数 getCurrentWeather
也修改为异步。那么你将不得不在其他地方处理异步性质。
在您的场景中您可能想要的另一个选项是使 data task
synchronous
像这样:
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
let dispatchGroup = DispatchGroup() // <===
dispatchGroup.enter() // <===
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
dispatchGroup.leave() // <===
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
dispatchGroup.leave() // <===
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
dispatchGroup.leave() // <===
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
dispatchGroup.wait() // <===
return currentWeather != nil
}
wait
函数可以带参数,可以定义超时时间。 https://developer.apple.com/documentation/dispatch/dispatchgroup 否则您的应用可能会永远等待。然后您将能够定义一些操作以将其呈现给用户。
顺便说一句,我制作了一个功能齐全的天气应用程序只是为了学习,所以请在 GitHub https://github.com/erikmartens/NearbyWeather 上查看。希望那里的代码可以为您的项目提供帮助。它也可以在应用商店中获得。
编辑:请理解这个答案是为了展示如何使异步调用同步。我并不是说这是处理网络调用的好习惯。这是一个 hacky 解决方案,当你 绝对必须 有一个函数的 return 值时,即使它在内部使用异步调用。
我正在尝试使用此函数修改全局变量 currentWeather(CurrentWeather 类型),这意味着使用从 URL 和 return 中检索到的信息更新所述变量,一个 bool 表示它的成功。然而,这个函数是 returning false,因为 currentWeather 仍然是 nil。我认识到 dataTask 是异步的,并且该任务在后台与应用程序并行 运行,但我不明白这对我要完成的任务意味着什么。我也无法在 do 块之后更新 currentWeather,因为在退出块后不再识别天气。我确实尝试使用 "self.currentWeather",但被告知这是一个未解析的标识符(可能是因为该函数也是全局的,并且没有 "self"?)。
URL 当前无效,因为我取出了我的 API 密钥,但它按预期工作,否则我的 CurrentWeather 结构是可解码的。打印 currentWeatherUnwrapped 也一直是成功的。
我确实查看了 Stack Overflow 和 Apple 的官方文档,但未能找到可以回答我问题的内容,但也许我还不够彻底。如果这是一个重复的问题,我很抱歉。也感谢任何进一步相关阅读的指导!对于不符合最佳编码实践,我深表歉意——我在这一点上不是很有经验。非常感谢大家!
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
return currentWeather != nil
}
您从根本上误解了异步函数的工作原理。您在 URLSession's
dataTask
甚至开始执行之前运行 returns。网络请求可能需要几秒钟才能完成。你要求它为你获取一些数据,给它一个代码块,让它在数据下载后执行,然后继续你的业务。
您可以确定 dataTask 的 resume()
调用之后的行将 运行 在新数据加载之前。
当数据可用时,您需要将想要的代码放入 运行 数据任务的完成块中。 (一旦数据读取成功,您的语句 print(currentWeatherUnwrapped)
将 运行。)
当您执行这样的异步调用时,您的函数将 return 远早于您的 dataTask 对 return 具有任何值。您需要做的是在函数中使用完成处理程序。您可以像这样将其作为参数传递:
func getCurrentWeather(completion: @escaping(CurrentWeather?, Error?) -> Void) {
//Data task and such here
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
completion(nil, err)
return
}
//You don't need a do try catch if you use try?
let weather = try? JSONDecoder().decode(CurrentWeather.self, from: data)
completion(weather, err)
}.resume()
}
然后调用该函数如下所示:
getCurrentWeather(completion: { (weather, error) in
guard error == nil, let weather = weather else {
if weather == nil { print("No Weather") }
if error != nil { print(error!.localizedDescription) }
return
}
//Do something with your weather result
print(weather)
})
你只需要一个闭包。
您不能对 return Web 服务调用的响应使用同步 return 语句,这本身就是异步的。为此你需要闭包。
您可以修改您的答案如下。因为你没有在评论中回答我的问题,所以我自由地 return 天气对象而不是 returning bool ,这没有多大意义。
func getCurrentWeather (completion : @escaping((CurrentWeather?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
completion(CurrentWeather.currentWeather)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
completion(nil)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}
假设 currentWeather
是您 CurrentWeather
中的静态变量 class 您可以更新全局变量以及 return 调用方的实际数据,如上所示
编辑:
正如 Duncan 在下面的评论中所指出的,上面的代码在后台线程中执行完成块。所有 UI 操作只能在主线程上完成。因此,在更新 UI.
之前切换线程非常重要两种方式:
1- 确保在主线程上执行完成块。
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather)
}
这将确保将来使用您的 getCurrentWeather
的任何人都不必担心切换线程,因为您的方法会处理它。如果您的完成块仅包含更新 UI 的代码,则很有用。使用这种方法的完成块中较长的逻辑会给主线程带来负担。
2 - Else 在更新 UI 元素时作为参数传递给 getCurrentWeather
的完成块中确保将这些语句包装在
DispatchQueue.main.async {
//your code to update UI
}
编辑 2:
正如 Leo Dabus 在下面的评论中所指出的,我应该 运行 完成块而不是 guard let url = URL(string: jsonUrlString) else { return false }
那是一个复制粘贴错误。我复制了 OP 的问题,然后匆忙意识到有一个 return 语句。
虽然在这种情况下将错误作为参数是可选的,并且完全取决于您设计错误处理模型的方式,但我很欣赏 Leo Dabus 建议的想法,这是更通用的方法,因此更新了我的答案以获得错误作为参数。
现在有些情况下我们可能还需要发送我们的自定义错误,例如如果 guard let data = data else { return }
returns false 而不是简单地调用 return 你可能需要 return 您自己的错误,表示无效输入或类似内容。
因此我冒昧地声明了我自己的自定义错误,您也可以使用该模型来处理您的错误处理
enum CustomError : Error {
case invalidServerResponse
case invalidURL
}
func getCurrentWeather (completion : @escaping((CurrentWeather?,Error?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidURL)
}
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
if err != nil {
DispatchQueue.main.async {
completion(nil,err)
}
return
}
guard let data = data else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidServerResponse)
}
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather,nil)
}
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
DispatchQueue.main.async {
completion(nil,jsonErr)
}
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}
正如您所指出的,data ask
是 async
,这意味着您不知道它何时会完成。
一种选择是通过不提供 return 值,而是提供 callback/closure 来将包装函数 getCurrentWeather
也修改为异步。那么你将不得不在其他地方处理异步性质。
在您的场景中您可能想要的另一个选项是使 data task
synchronous
像这样:
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
let dispatchGroup = DispatchGroup() // <===
dispatchGroup.enter() // <===
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
dispatchGroup.leave() // <===
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
dispatchGroup.leave() // <===
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
dispatchGroup.leave() // <===
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
dispatchGroup.wait() // <===
return currentWeather != nil
}
wait
函数可以带参数,可以定义超时时间。 https://developer.apple.com/documentation/dispatch/dispatchgroup 否则您的应用可能会永远等待。然后您将能够定义一些操作以将其呈现给用户。
顺便说一句,我制作了一个功能齐全的天气应用程序只是为了学习,所以请在 GitHub https://github.com/erikmartens/NearbyWeather 上查看。希望那里的代码可以为您的项目提供帮助。它也可以在应用商店中获得。
编辑:请理解这个答案是为了展示如何使异步调用同步。我并不是说这是处理网络调用的好习惯。这是一个 hacky 解决方案,当你 绝对必须 有一个函数的 return 值时,即使它在内部使用异步调用。