簡體   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