使用 URL 中的数据并了解 Swift 5 中的完成处理程序



我有代码,我正在创建一个URLSession并尝试返回从中获得的数据,以便它可以在URLSession所在的函数之外使用。我的代码如下所示:

var nameStr = ""
var ingStr = ""
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
let str = String(data: data, encoding: .utf8)!
let str2 = convertStringToDictionary(text: str)
nameStr = "Name: (str2!["title"] as! NSString)"
ingStr = "Ingredients: ((str2!["ingredientList"] as! NSString).components(separatedBy: ", "))"
print("First ", nameStr)
print(ingStr)

}
task.resume()
print("Second ", nameStr)
print(ingStr)

第一个打印语句按预期工作,但第二个仅打印"第二个"。我查看了来自 URLSession 的 Swift 返回数据,虽然我意识到它确实有一个可行的解决方案,但我不了解整个完成块部分,并且感觉其他人可能在同一条船上。有什么建议吗?

你说:

第一个打印语句按预期工作,但第二个仅打印"第二个"。我查看了来自 URLSession 的 Swift 返回数据......

这个问题的公认答案在这里也是正确答案。

你在代码片段中标记为"第一"和"第二"的内容实际上是倒退的。如果你仔细观察,你会发现"第二个"打印语句(还没有结果)实际上正在发生,然后你看到你的"第一个"打印语句!更重要的是,在收到并解析来自网络调用的响应之前,您的执行路径将到达"第二个"打印语句。

这就是问题的根源,即传递给方法的闭包中的代码"异步"发生dataTask即稍后。您不能在resume语句(您的"第二个"打印语句)之后立即print字符串,因为这些字符串尚未设置!同样,显然也无法return这些字符串,因为同样,直到您从此函数返回很久之后才会填充它们。这都是因为dataTask关闭还没有运行!另一个答案中描述的完成处理程序闭包是解决方案。

请考虑以下代码片段:

let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard
let data = data,
let string = String(data: data, encoding: .utf8),
let dictionary = convertStringToDictionary(text: string),
let title = dictionary["title"] as? String,
let ingredientsString = dictionary["ingredientList"] as? String
else { return }
let ingredients = ingredientsString.components(separatedBy: ", ")
// you’ve got `title` and `ingredients` here …
}
task.resume()
// … but not here, because the above runs asynchronously

这基本上是您的代码,经过重构以消除!强制解包运算符和NSString引用。但这不是故事的相关部分。正是这两个评论,我在其中显示了您在哪里有结果(即在闭包中)和您没有结果的地方(即,在resume之后)。如果您不熟悉异步编程,这可能看起来非常令人困惑。但这是一个重要的教训,要准确理解"异步"的含义。

。但我不明白整个完成块部分。 有什么建议吗?

是的,您的问题的答案在于理解这种"完成处理程序"模式。dataTask方法具有完成处理程序闭包参数,您必须在自己的方法中重复该模式。

那么,回到我上面的代码片段,你如何准确地"返回"稍后检索到的东西?答案是:从技术上讲,你什么都不return。相反,将完成处理程序闭包参数添加到方法中,并使用Result调用该方法,该对象要么是成功时的某个模型对象(例如,可能是Recipe对象),要么是失败时的Error对象。为了使调用方保持简单,我们将在主队列上调用该完成处理程序。

因此,也许:

func fetchRecipe(with url: URL, completion: @escaping (Result<Recipe, Error>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard
let data = data,
let string = String(data: data, encoding: .utf8),
let dictionary = convertStringToDictionary(text: string),
let title = dictionary["title"] as? String,
let ingredientsString = dictionary["ingredientList"] as? String
else { 
DispatchQueue.main.async {
completion(.failure(error ?? URLError(.badServerResponse)))
}
return
}

let ingredients = ingredientsString.components(separatedBy: ", ")
let recipe = Recipe(title: title, ingredients: ingredients)
DispatchQueue.main.async {
completion(.success(recipe))
}
}
task.resume()
}

这与我上面的代码片段基本相同,除了我:

  • fetchRecipe方法包裹它;
  • 我什么都不return;
  • 我为该方法提供了一个completion参数,这是一个完成处理程序闭包;
  • 当网络请求完成时,我称之为completion关闭,传回.failure().success(),显然取决于它是失败还是成功。

为了完整起见,这是我使用Recipe模型对象,用于将从服务器返回的所有内容包装在一个漂亮的简单对象中:

struct Recipe {
let title: String
let ingredients: [String]
}

你会这样称呼它:

fetchRecipe(with: url) { result in
// use `result` here …
switch result {
case .failure(let error):
print(error)
case .success(let recipe):
print(recipe)
// update your model and UI using `result` here …
}
}
// … but not here, because, again, the above runs asynchronously

我不得不说,将Data转换为String,然后转换为字典,然后使用字典键字符串文字感觉非常错误。通常,例如,我们会有一个返回 JSON 的服务器,我们只会使用JSONDecoder直接解析Data。但是你没有分享这个convertStringToDictionary,所以我无法就此提供任何有意义的建议。但是,一旦你有了眼前的问题,这是你要解决的问题。

最新更新