繁体   English   中英

修改全局变量内部闭包(Swift 4)

[英]Modify Global Variable Inside Closure (Swift 4)

我正在尝试使用此函数修改全局变量currentWeather(类型CurrentWeather),该函数旨在使用从URL检索的信息更新所述变量,并返回表示其成功的布尔值。 但是,该函数返回false,因为currentWeather仍然为零。 我认识到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
}

当您执行这样的异步调用时,您的函数将返回很长时间,而dataTask将没有任何值要返回。 您需要做的是在函数中使用完成处理程序。 您可以像这样将其作为参数传递:

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)
})

您从根本上误解了异步功能是如何工作的。 您的函数将在URLSession's dataTask甚至开始执行之前返回。 网络请求可能需要几秒钟才能完成。 您要求它为您获取一些数据,为它提供一个代码块,以在数据下载后立即执行,然后继续您的业务。

您可以确定dataTask的resume()调用之后的行将在加载新数据之前运行。

当数据任务的完成块中有可用数据时,您需要将要运行的代码放入。 (成功读取数据后,您的语句print(currentWeatherUnwrapped)将运行。)

您只需要关闭即可。

您不能拥有同步的return语句来返回本质上是异步的Web服务调用的响应。 您需要为此关闭。

您可以如下修改答案。 因为您未在评论中回答我的问题,所以我自由地返回了wether对象,而不是返回没有意义的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()
    }

假设currentWeatherCurrentWeather类中的静态变量,则可以更新全局变量并将实际数据返回给调用方,如上所示

编辑:

正如Duncan在下面的评论中指出的那样,以上代码在后台线程中执行完成块。 所有UI操作必须仅在主线程上完成。 因此,在更新UI之前切换线程非常重要。

两种方式:

1-确保在主线程上执行完成块。

DispatchQueue.main.async {
      completion(CurrentWeather.currentWeather)
}

这样可以确保将来使用getCurrentWeather都不必担心切换线程,因为您的方法会处理它。 如果您的完成块仅包含更新UI的代码,则很有用。 用这种方法完成块中更长的逻辑将负担主线程。

2-在更新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 }返回false而不是简单地调用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 ,这意味着您不知道何时完成。

一种选择是通过不提供返回值,而是回调/关闭来将包装函数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上查看。 希望那里的代码可以为您的项目提供帮助。 它也可以在应用商店中找到。

编辑:请理解,这个答案是为了显示如何使异步调用同步。 我并不是说这是处理网络呼叫的好习惯。 当您绝对必须从函数中获得返回值时,即使该函数内部使用了异步调用,这也是一种骇人听闻的解决方案。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM