修改全局变量内部闭包 (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 askasync,这意味着您不知道它何时会完成。

一种选择是通过不提供 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 值时,即使它在内部使用异步调用。