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

时间:2017-11-13 20:07:16

标签: ios swift closures global-variables weather-api

我正在尝试使用此函数修改全局变量currentWeather(类型为CurrentWeather),这意味着使用从URL检索的信息更新所述变量并返回表示其成功的bool。但是,函数返回false,因为currentWeather仍为零。我认识到dataTask是异步的,并且任务是在与应用程序并行的后台运行,但我不明白这对我想要实现的目的意味着什么。在do块之后我也无法更新currentWeather,因为退出块后天气不再被识别。我确实尝试使用" self.currentWeather",但被告知它是一个未解析的标识符(也许是因为该函数也是全局的,并且没有" self"?)。

网址目前无效,因为我拿出了我的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
}

4 个答案:

答案 0 :(得分:1)

当您执行这样的异步调用时,您的函数将在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)
})

答案 1 :(得分:0)

您从根本上误解了异步功能的工作原理。在URLSession's dataTask开始执行之前,您将返回函数。网络请求可能需要几秒钟才能完成。您要求它为您提取一些数据,给它一段代码来执行ONCE THE DATA HAS LOADLOADED,然后继续您的业务。

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

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

答案 2 :(得分:0)

你需要的只是一个闭包。

您无法使用同步return语句来返回Web服务调用的响应,这本身就是异步的。你需要关闭它。

您可以按照以下方式修改答案。因为你在评论中没有回答我的问题,所以我已经自由地回归了那些没有多大意义的bool而不是返回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的问题,并且匆匆地意识到有一个回复声明。

虽然在这种情况下作为参数的错误是可选的,并且完全取决于你如何设计你的错误处理模型,但我很欣赏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()
    }

答案 3 :(得分:-1)

正如您所指出的,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上查看。希望那里的代码可以帮助您完成项目。它也可以在应用程序商店中找到。

编辑:请理解,此答案旨在说明如何使异步调用同步。我不是说这是处理网络呼叫的好习惯。这是一个 hacky 解决方案,用于绝对必须从函数返回值,即使它在内部使用异步调用。