简体   繁体   English

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

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

I am trying to modify the global variable currentWeather (of type CurrentWeather) using this function, which is meant to update said variable with the information retrieved from the URL and return a bool signifying its success. 我正在尝试使用此函数修改全局变量currentWeather(类型CurrentWeather),该函数旨在使用从URL检索的信息更新所述变量,并返回表示其成功的布尔值。 However, the function is returning false, as currentWeather is still nil. 但是,该函数返回false,因为currentWeather仍然为零。 I recognize that the dataTask is asynchronous, and that the task is running in the background parallel to the application, but I don't understand what this means for what I'm trying to accomplish. 我认识到dataTask是异步的,并且该任务在与应用程序并行的背景下运行,但是我不明白这对我要完成的工作意味着什么。 I also am unable to update currentWeather after the do block, as weather is no longer recognized after exiting the block. 我也无法在do块之后更新currentWeather,因为退出该块后无法再识别天气。 I did try using "self.currentWeather", but was told it was an unresolved identifier (perhaps because the function is also global, and there is no "self"?). 我曾尝试使用“ self.currentWeather”,但被告知这是一个无法解析的标识符(可能是因为该函数也是全局的,并且没有“ self”?)。

The URL is not currently valid because I took out my API key, but it is working as expected otherwise, and my CurrentWeather struct is Decodable. 该URL当前无效,因为我拿出了我的API密钥,但否则它可以正常工作,并且CurrentWeather结构是可分解的。 Printing currentWeatherUnwrapped is also consistently successful. 打印currentWeatherUnwrapped也始终成功。

I did look around Stack Overflow and through Apple's official documentation and was unable to find something that answered my question, but perhaps I wasn't thorough enough. 我确实查看了Stack Overflow,并查看了Apple的官方文档,但找不到能回答我问题的内容,但也许我不够彻底。 I'm sorry if this is a duplicate question. 很抱歉,如果这是一个重复的问题。 Direction to any further relevant reading is also appreciated! 任何进一步相关阅读的方向也表示赞赏! I apologize for the lack of conformity to best coding practices - I'm not very experienced at this point. 对于缺乏与最佳编码实践的一致性,我深表歉意。 Thank you all so much! 非常感谢大家!

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
}

When you do an asynchronous call like this, your function will return long before your dataTask will have any value to return. 当您执行这样的异步调用时,您的函数将返回很长时间,而dataTask将没有任何值要返回。 What you need to do is use a completion handler in your function. 您需要做的是在函数中使用完成处理程序。 You can pass it in as a parameter like this: 您可以像这样将其作为参数传递:

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

}

Then calling that function looks like this: 然后调用该函数如下所示:

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

You fundamentally misunderstand how async functions work. 您从根本上误解了异步功能是如何工作的。 You function returns before the URLSession's dataTask has even begun to execute. 您的函数将在URLSession's dataTask甚至开始执行之前返回。 A network request may take multiple seconds to complete. 网络请求可能需要几秒钟才能完成。 You ask it to fetch some data for you, give it a block of code to execute ONCE THE DATA HAS DOWNLOADED, and then go on with your business. 您要求它为您获取一些数据,为它提供一个代码块,以在数据下载后立即执行,然后继续您的业务。

You can be certain that the line after the dataTask's resume() call will run before the new data has loaded. 您可以确定dataTask的resume()调用之后的行将在加载新数据之前运行。

You need to put code that you want to run when the data is available inside the data task's completion block. 当数据任务的完成块中有可用数据时,您需要将要运行的代码放入。 (Your statement print(currentWeatherUnwrapped) will run once the data has been read successfully.) (成功读取数据后,您的语句print(currentWeatherUnwrapped)将运行。)

All you need is a closure. 您只需要关闭即可。

You cant have synchronous return statement to return the response of web service call which in itself is asynchronous in nature. 您不能拥有同步的return语句来返回本质上是异步的Web服务调用的响应。 You need closures for that. 您需要为此关闭。

You can modify your answer as below. 您可以如下修改答案。 Because you have not answered to my question in comment I have taken liberty to return the wether object rather than returning bool which does not make much sense. 因为您未在评论中回答我的问题,所以我自由地返回了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()
    }

Assuming currentWeather is a static variable in your CurrentWeather class you can update your global variable as well as return the actual data to caller as shown above 假设currentWeatherCurrentWeather类中的静态变量,则可以更新全局变量并将实际数据返回给调用方,如上所示

EDIT: 编辑:

As pointed out by Duncan in comments below, the above code executes the completion block in background thread. 正如Duncan在下面的评论中指出的那样,以上代码在后台线程中执行完成块。 All the UI operations must be done only on main thread. 所有UI操作必须仅在主线程上完成。 Hence its very much essential to switch the thread before updating the UI. 因此,在更新UI之前切换线程非常重要。

Two ways : 两种方式:

1- Make sure you execute the completion block on main thread. 1-确保在主线程上执行完成块。

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

This will make sure that whoever uses your getCurrentWeather in future need not worry about switching thread because your method takes care of it. 这样可以确保将来使用getCurrentWeather都不必担心切换线程,因为您的方法会处理它。 Useful if your completion block contains only the code to update UI. 如果您的完成块仅包含更新UI的代码,则很有用。 Lengthier logic in completion block with this approach will burden the main thread. 用这种方法完成块中更长的逻辑将负担主线程。

2 - Else In completion block that you pass as a parameter to getCurrentWeather whenever you update UI elements make sure you wrap those statements in 2-在更新UI元素时,在完成块中作为参数传递给getCurrentWeather时,请确保将这些语句包装在

DispatchQueue.main.async {
    //your code to update UI
}

EDIT 2: 编辑2:

As pointed out by Leo Dabus in comments below, I should have run completion block rather than guard let url = URL(string: jsonUrlString) else { return false } That was a copy paste error. 正如Leo Dabus在下面的评论中指出的那样,我应该运行完成块而不是guard let url = URL(string: jsonUrlString) else { return false }这是一个复制粘贴错误。 I copied the OP's question and in a hurry din realize that there is a return statement. 我复制了OP的问题,并急忙意识到存在一个return语句。

Though having a error as a parameter is optional in this case and completely depends on how you designed your error handling model, I appreciate the idea suggested by Leo Dabus which is more general approach and hence updating my answer to have error as a parameter. 尽管在这种情况下将错误作为参数是可选的,并且完全取决于您设计错误处理模型的方式,但我感谢Leo Dabus提出的想法,该想法是更通用的方法,因此可以更新我的答案以将错误作为参数。

Now there are cases where we may need to send our custom error as well for example if guard let data = data else { return } returns false rather than simply calling return you may need to return a error of your own which says invalid input or something like that. 现在,在某些情况下,我们可能还需要发送自定义错误,例如,如果guard let data = data else { return }返回false而不是简单地调用return,那么您可能需要返回一个自己的错误,即输入无效或类似的错误像那样。

Hence I have taken a liberty to declare a custom errors of my own and you can as well use the model to deal with your error handling 因此,我可以自由声明自己的自定义错误,您也可以使用该模型来处理您的错误处理

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

As you pointed out, the data ask is async , meaning you do not know when it will be completed. 正如您所指出的, data askasync ,这意味着您不知道何时完成。

One option is to modify your wrapper function getCurrentWeather to be async as well by not providing a return value, but instead a callback/closure. 一种选择是通过不提供返回值,而是回调/关闭来将包装函数getCurrentWeather修改为异步。 Then you will have to deal with the async nature somewhere else though. 然后,您将不得不在其他地方处理异步特性。

The other option which is what you probably want in your scenario is to make the data task synchronous like so: 场景中您可能想要的另一个选项是使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
}

The wait function can take parameters, which can define a timeout. wait函数可以使用可以定义超时的参数。 https://developer.apple.com/documentation/dispatch/dispatchgroup Otherwise your app could be stuck waiting forever. https://developer.apple.com/documentation/dispatch/dispatchgroup否则,您的应用可能会永远等待。 You will then be able to define some action to present that to the user. 然后,您将能够定义一些动作来呈现给用户。

Btw I made a fully functional weather app just for learning, so check it out here on GitHub https://github.com/erikmartens/NearbyWeather . 顺便说一句,我制作了一个全功能的天气应用程序只是为了学习,因此请在GitHub https://github.com/erikmartens/NearbyWeather上查看。 Hope the code there can help you for your project. 希望那里的代码可以为您的项目提供帮助。 It's also available on the app store. 它也可以在应用商店中找到。

EDIT: Please understand that this answer is meant to show how to make async calls synchronous. 编辑:请理解,这个答案是为了显示如何使异步调用同步。 I am not saying this is good practice for handling network calls. 我并不是说这是处理网络呼叫的好习惯。 This is a hacky solution for when you absolutely must have a return value from a function even though it uses async calls inside. 当您绝对必须从函数中获得返回值时,即使该函数内部使用了异步调用,这也是一种骇人听闻的解决方案。

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

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