从 Swift 函数中的异步调用返回数据

Posted

技术标签:

【中文标题】从 Swift 函数中的异步调用返回数据【英文标题】:Returning data from async call in Swift function 【发布时间】:2014-08-08 12:28:59 【问题描述】:

我在我的 Swift 项目中创建了一个实用程序类,用于处理所有 REST 请求和响应。我已经构建了一个简单的 REST API,所以我可以测试我的代码。我创建了一个需要返回 NSArray 的类方法,但是因为 API 调用是异步的,所以我需要从异步调用中的方法返回。问题是异步返回无效。 如果我在 Node 中执行此操作,我会使用 JS 承诺,但我无法找到适用于 Swift 的解决方案。

import Foundation

class Bookshop 
    class func getGenres() -> NSArray 
        println("Hello inside getGenres")
        let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
        println(urlPath)
        let url: NSURL = NSURL(string: urlPath)
        let session = NSURLSession.sharedSession()
        var resultsArray:NSArray!
        let task = session.dataTaskWithURL(url, completionHandler: data, response, error -> Void in
            println("Task completed")
            if(error) 
                println(error.localizedDescription)
            
            var err: NSError?
            var options:NSJSONReadingOptions = NSJSONReadingOptions.MutableContainers
            var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: options, error: &err) as NSDictionary
            if(err != nil) 
                println("JSON Error \(err!.localizedDescription)")
            
            //NSLog("jsonResults %@", jsonResult)
            let results: NSArray = jsonResult["genres"] as NSArray
            NSLog("jsonResults %@", results)
            resultsArray = results
            return resultsArray // error [anyObject] is not a subType of 'Void'
        )
        task.resume()
        //return "Hello World!"
        // I want to return the NSArray...
    

【问题讨论】:

这个错误在 Stack Overflow 上很常见,我写了一系列博客文章来处理它,从programmingios.net/what-asynchronous-means开始 【参考方案1】:

你可以传递回调,并在异步调用中调用回调

类似:

class func getGenres(completionHandler: (genres: NSArray) -> ()) 
    ...
    let task = session.dataTaskWithURL(url) 
        data, response, error in
        ...
        resultsArray = results
        completionHandler(genres: resultsArray)
    
    ...
    task.resume()

然后调用这个方法:

override func viewDidLoad() 
    Bookshop.getGenres 
        genres in
        println("View Controller: \(genres)")     
    

【讨论】:

谢谢。我的最后一个问题是如何从我的视图控制器调用这个类方法。代码目前是这样的:override func viewDidLoad() super.viewDidLoad() var genres = Bookshop.getGenres() // Missing argument for parameter #1 in call //var genres:NSArray //Bookshop.getGenres(genres) NSLog("View Controller: %@", genres) 【参考方案2】:

Swiftz 已经提供了 Future,它是 Promise 的基本构建块。 Future 是一个不会失败的 Promise(这里所有的术语都是基于 Scala 的解释,where a Promise is a Monad)。

https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift

希望最终会扩展为完整的 Scala 风格的 Promise(我可能会在某个时候自己编写它;我相信其他 PR 会受到欢迎;如果 Future 已经到位,这并不难)。

在您的特定情况下,我可能会创建一个Result<[Book]>(基于Alexandros Salazar's version of Result)。那么您的方法签名将是:

class func fetchGenres() -> Future<Result<[Book]>> 

注意事项

我不建议在 Swift 中为函数添加 get 前缀。它将破坏与 ObjC 的某些类型的互操作性。 我建议在将结果作为Future 返回之前一直解析到Book 对象。这个系统有几种失败的方式,如果你在将它们包装成Future之前检查所有这些东西会更方便。对于您的 Swift 代码的其余部分,访问 [Book] 比提交 NSArray 要好得多。

【讨论】:

Swiftz 不再支持Future。但是看看github.com/mxcl/PromiseKit,它与 Swiftz 配合得很好! 我花了几秒钟才意识到你不是写 Swift 而是写了 Swiftz 听起来“Swiftz”是 Swift 的第三方函数库。由于您的答案似乎基于该库,因此您应该明确说明。 (例如,“有一个名为 'Swiftz' 的第三方库支持像 Futures 这样的功能结构,如果你想实现 Promises,应该作为一个很好的起点。”)否则你的读者会想知道你为什么拼错了“斯威夫特”。 请注意github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift 不再工作了。 @Rob get 前缀表示 ObjC 中的按引用返回(例如在 -[UIColor getRed:green:blue:alpha:] 中)。当我写这篇文章时,我担心进口商会利用这一事实(例如自动返回一个元组)。事实证明他们没有。当我写这篇文章时,我可能还忘记了 KVC 支持访问器的“get”前缀(这是我学过并忘记了好几次的东西)。所以同意了;我没有遇到任何领先的get 破坏事情的情况。这只会误导那些知道 ObjC “get”含义的人。【参考方案3】:

在 Swift 5.5(iOS 15、macOS 12)中引入,我们现在将使用 async-await 模式:

func fetchGenres() async throws -> [Genre] 
    …
    let (data, _) = try await URLSession.shared.dataTask(for: request)
    return try JSONDecoder().decode([Genre].self, from: data)

我们可以这样称呼它:

let genres = try await fetchGenres()

async-await 语法比我在下面的原始答案中概述的传统完成处理程序模式更加简洁和自然。

有关详细信息,请参阅Meet async/await in Swift。


历史模式是使用完成处理程序闭包。

例如,我们经常会使用Result:

func fetchGenres(completion: @escaping (Result<[Genre], Error>) -> Void) 
    ...
    URLSession.shared.dataTask(with: request)  data, _, error in 
        if let error = error 
            DispatchQueue.main.async 
                completion(.failure(error))
            
            return
        

        // parse response here

        let results = ...
        DispatchQueue.main.async 
            completion(.success(results))
        
    .resume()

你会这样称呼它:

fetchGenres  results in
    switch results 
    case .failure(let error):
        print(error.localizedDescription)

    case .success(let genres):
        // use `genres` here, e.g. update model and UI            
    


// but don’t try to use `genres` here, as the above runs asynchronously

注意,上面我将完成处理程序分派回主队列以简化模型和 UI 更新。一些开发者不同意这种做法,要么使用任何队列URLSession,要么使用他们自己的队列(要求调用者自己手动同步结果)。

但这在这里并不重要。关键问题是使用完成处理程序来指定异步请求完成时要运行的代码块。


注意,上面我不再使用NSArray(我们不再使用those bridged Objective-C types)。我假设我们有一个Genre 类型并且我们大概使用JSONDecoder,而不是JSONSerialization 来解码它。但是这个问题没有足够的关于底层 JSON 的信息来详细介绍这里,所以我省略了它以避免混淆核心问题,即使用闭包作为完成处理程序。

【讨论】:

您也可以在 Swift 4 及更低版本中使用 Result,但您必须自己声明枚举。我多年来一直在使用这种模式。 是的,当然,我也是。但它看起来只是随着 Swift 5 的发布而被 Apple 接受。他们只是迟到了。【参考方案4】:

Swift 4.0

对于异步请求响应,您可以使用完成处理程序。见下文,我已经使用完成句柄范例修改了解决方案。

func getGenres(_ completion: @escaping (NSArray) -> ()) 

        let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
        print(urlPath)

        guard let url = URL(string: urlPath) else  return 

        let task = URLSession.shared.dataTask(with: url)  (data, response, error) in
            guard let data = data else  return 
            do 
                if let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary 
                    let results = jsonResult["genres"] as! NSArray
                    print(results)
                    completion(results)
                
             catch 
                //Catch Error here...
            
        
        task.resume()
    

你可以像下面这样调用这个函数:

getGenres  (array) in
    // Do operation with array

【讨论】:

【参考方案5】:

@Alexey Globchastyy 的回答的 Swift 3 版本:

class func getGenres(completionHandler: @escaping (genres: NSArray) -> ()) 
...
let task = session.dataTask(with:url) 
    data, response, error in
    ...
    resultsArray = results
    completionHandler(genres: resultsArray)

...
task.resume()

【讨论】:

【参考方案6】:

我希望你不会仍然坚持这一点,但简短的回答是你不能在 Swift 中做到这一点。

另一种方法是返回一个回调,该回调将在数据准备就绪后立即提供您需要的数据。

【讨论】:

他也可以迅速做出承诺。但是苹果目前推荐的 aproceh 是使用 callbackclosures,就像你指出的那样,或者像旧的可可 API 一样使用 delegation 你对 Promises 的看法是正确的。但是 Swift 并没有为此提供原生 API,所以他必须使用 PromiseKit 或其他替代方案。【参考方案7】:

有 3 种创建回调函数的方法,即: 1.完成处理程序 2. 通知 3. 代表

完成处理程序 当源可用时执行并返回内部块集,处理程序将等待响应到来,以便之后更新UI。

通知 整个应用程序都会触发大量信息,Listner 可以检索并使用该信息。通过项目获取信息的异步​​方式。

代表 调用委托时会触发一组方法,必须通过方法本身提供Source

【讨论】:

【参考方案8】:

Swift 5.5,基于异步/等待的解决方案

原始发帖人提供的原始测试 URL 不再起作用,所以我不得不稍作改动。这个解决方案基于我发现的一个笑话 API。该 API 返回一个笑话,但我将其作为字符串数组 ([String]) 返回,以使其与原始帖子尽可能一致。


class Bookshop 
    class func getGenres() async -> [String] 
        print("Hello inside getGenres")
        let urlPath = "https://geek-jokes.sameerkumar.website/api?format=json"
        print(urlPath)
        let url = URL(string: urlPath)!
        let session = URLSession.shared
        
        typealias Continuation = CheckedContinuation<[String], Never>
        let genres = await withCheckedContinuation  (continuation: Continuation) in
            let task = session.dataTask(with: url)  data, response, error in
                print("Task completed")
                
                var result: [String] = []
                defer 
                    continuation.resume(returning: result)
                
                
                if let error = error 
                    print(error.localizedDescription)
                    return
                
                guard let data = data else  
                    return
                
                
                do 
                    let jsonResult = try JSONSerialization.jsonObject(with: data, options: [.mutableContainers])
                    print("jsonResult is \(jsonResult)")
                    if let joke = (jsonResult as? [String: String])?["joke"] 
                        result = [joke]
                    
                 catch 
                    print("JSON Error \(error.localizedDescription)")
                    print("data was \(String(describing: String(data: data, encoding: .utf8)))")
                    return
                
            
            task.resume()
        
        
        return genres
    


async 
    let final = await Bookshop.getGenres()
    print("Final is \(final)")

withCheckedContinuation 是您使 Swift async 函数实际在单独的任务/线程中运行的方式。

【讨论】:

【参考方案9】:

斯威夫特 5.5:

TL;DR:Swift 5.5 尚未发布(在撰写本文时)。要使用 swift 5.5,请从 here 下载 swift 工具链开发快照并添加编译器标志 -Xfrontend -enable-experimental-concurrency。阅读更多here

这可以通过async/await 功能轻松实现。

为此,您应该将您的函数标记为async,然后在withUnsafeThrowingContinuation 块内执行操作,如下所示。

class Bookshop 
  class func getGenres() async throws -> NSArray 
    print("Hello inside getGenres")
    let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
    print(urlPath)
    let url = URL(string: urlPath)!
    let session = URLSession.shared
    return try await withUnsafeThrowingContinuation  continuation in
      let task = session.dataTask(with: url, completionHandler: data, response, error -> Void in
        print("Task completed")
        if(error != nil) 
          print(error!.localizedDescription)
          continuation.resume(throwing: error!)
          return
        
        do 
          let jsonResult = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any]
          let results: NSArray = jsonResult!["genres"] as! NSArray
          continuation.resume(returning: results)
         catch 
          continuation.resume(throwing: error)
        
      )
      task.resume()
    
  


你可以像这样调用这个函数

@asyncHandler
func check() 
  do 
    let genres = try await Bookshop.getGenres()
    print("Result: \(genres)")
   catch 
    print("Error: \(error)")
  

请记住,在调用Bookshop.getGenres 方法时,调用者方法应为async 或标记为@asyncHandler

【讨论】:

【参考方案10】:
self.urlSession.dataTask(with: request, completionHandler:  (data, response, error) in
            self.endNetworkActivity()

            var responseError: Error? = error
            // handle http response status
            if let httpResponse = response as? HTTPURLResponse 

                if httpResponse.statusCode > 299 , httpResponse.statusCode != 422  
                    responseError = NSError.errorForHTTPStatus(httpResponse.statusCode)
                
            

            var apiResponse: Response
            if let _ = responseError 
                apiResponse = Response(request, response as? HTTPURLResponse, responseError!)
                self.logError(apiResponse.error!, request: request)

                // Handle if access token is invalid
                if let nsError: NSError = responseError as NSError? , nsError.code == 401 
                    DispatchQueue.main.async 
                        apiResponse = Response(request, response as? HTTPURLResponse, data!)
                        let message = apiResponse.message()
                        // Unautorized access
                        // User logout
                        return
                    
                
                else if let nsError: NSError = responseError as NSError? , nsError.code == 503 
                    DispatchQueue.main.async 
                        apiResponse = Response(request, response as? HTTPURLResponse, data!)
                        let message = apiResponse.message()
                        // Down time
                        // Server is currently down due to some maintenance
                        return
                    
                

             else 
                apiResponse = Response(request, response as? HTTPURLResponse, data!)
                self.logResponse(data!, forRequest: request)
            

            self.removeRequestedURL(request.url!)

            DispatchQueue.main.async(execute:  () -> Void in
                completionHandler(apiResponse)
            )
        ).resume()

【讨论】:

【参考方案11】:

swift中实现回调主要有3种方式

    闭包/完成处理程序

    代表

    通知

观察者也可用于在异步任务完成后获得通知。

【讨论】:

【参考方案12】:

每个优秀的 API 管理器都希望满足一些非常通用的要求: 将实现一个面向协议的 API 客户端。

APIClient 初始接口

protocol APIClient 
   func send(_ request: APIRequest,
              completion: @escaping (APIResponse?, Error?) -> Void) 


protocol APIRequest: Encodable 
    var resourceName: String  get 


protocol APIResponse: Decodable 

现在请检查完整的api结构

// ******* This is API Call Class  *****
public typealias ResultCallback<Value> = (Result<Value, Error>) -> Void

/// Implementation of a generic-based  API client
public class APIClient 
    private let baseEndpointUrl = URL(string: "irl")!
    private let session = URLSession(configuration: .default)

    public init() 

    

    /// Sends a request to servers, calling the completion method when finished
    public func send<T: APIRequest>(_ request: T, completion: @escaping ResultCallback<DataContainer<T.Response>>) 
        let endpoint = self.endpoint(for: request)

        let task = session.dataTask(with: URLRequest(url: endpoint))  data, response, error in
            if let data = data 
                do 
                    // Decode the top level response, and look up the decoded response to see
                    // if it's a success or a failure
                    let apiResponse = try JSONDecoder().decode(APIResponse<T.Response>.self, from: data)

                    if let dataContainer = apiResponse.data 
                        completion(.success(dataContainer))
                     else if let message = apiResponse.message 
                        completion(.failure(APIError.server(message: message)))
                     else 
                        completion(.failure(APIError.decoding))
                    
                 catch 
                    completion(.failure(error))
                
             else if let error = error 
                completion(.failure(error))
            
        
        task.resume()
    

    /// Encodes a URL based on the given request
    /// Everything needed for a public request to api servers is encoded directly in this URL
    private func endpoint<T: APIRequest>(for request: T) -> URL 
        guard let baseUrl = URL(string: request.resourceName, relativeTo: baseEndpointUrl) else 
            fatalError("Bad resourceName: \(request.resourceName)")
        

        var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true)!

        // Common query items needed for all api requests
        let timestamp = "\(Date().timeIntervalSince1970)"
        let hash = "\(timestamp)"
        let commonQueryItems = [
            URLQueryItem(name: "ts", value: timestamp),
            URLQueryItem(name: "hash", value: hash),
            URLQueryItem(name: "apikey", value: "")
        ]

        // Custom query items needed for this specific request
        let customQueryItems: [URLQueryItem]

        do 
            customQueryItems = try URLQueryItemEncoder.encode(request)
         catch 
            fatalError("Wrong parameters: \(error)")
        

        components.queryItems = commonQueryItems + customQueryItems

        // Construct the final URL with all the previous data
        return components.url!
    


// ******  API Request Encodable Protocol *****
public protocol APIRequest: Encodable 
    /// Response (will be wrapped with a DataContainer)
    associatedtype Response: Decodable

    /// Endpoint for this request (the last part of the URL)
    var resourceName: String  get 


// ****** This Results type  Data Container Struct ******
public struct DataContainer<Results: Decodable>: Decodable 
    public let offset: Int
    public let limit: Int
    public let total: Int
    public let count: Int
    public let results: Results

// ***** API Errro Enum ****
public enum APIError: Error 
    case encoding
    case decoding
    case server(message: String)



// ****** API Response Struct ******
public struct APIResponse<Response: Decodable>: Decodable 
    /// Whether it was ok or not
    public let status: String?
    /// Message that usually gives more information about some error
    public let message: String?
    /// Requested data
    public let data: DataContainer<Response>?


// ***** URL Query Encoder OR JSON Encoder *****
enum URLQueryItemEncoder 
    static func encode<T: Encodable>(_ encodable: T) throws -> [URLQueryItem] 
        let parametersData = try JSONEncoder().encode(encodable)
        let parameters = try JSONDecoder().decode([String: HTTPParam].self, from: parametersData)
        return parameters.map  URLQueryItem(name: $0, value: $1.description) 
    


// ****** HTTP Pamater Conversion Enum *****
enum HTTPParam: CustomStringConvertible, Decodable 
    case string(String)
    case bool(Bool)
    case int(Int)
    case double(Double)

    init(from decoder: Decoder) throws 
        let container = try decoder.singleValueContainer()

        if let string = try? container.decode(String.self) 
            self = .string(string)
         else if let bool = try? container.decode(Bool.self) 
            self = .bool(bool)
         else if let int = try? container.decode(Int.self) 
            self = .int(int)
         else if let double = try? container.decode(Double.self) 
            self = .double(double)
         else 
            throw APIError.decoding
        
    

    var description: String 
        switch self 
        case .string(let string):
            return string
        case .bool(let bool):
            return String(describing: bool)
        case .int(let int):
            return String(describing: int)
        case .double(let double):
            return String(describing: double)
        
    


/// **** This is your API Request Endpoint  Method in Struct *****
public struct GetCharacters: APIRequest 
    public typealias Response = [MyCharacter]

    public var resourceName: String 
        return "characters"
    

    // Parameters
    public let name: String?
    public let nameStartsWith: String?
    public let limit: Int?
    public let offset: Int?

    // Note that nil parameters will not be used
    public init(name: String? = nil,
                nameStartsWith: String? = nil,
                limit: Int? = nil,
                offset: Int? = nil) 
        self.name = name
        self.nameStartsWith = nameStartsWith
        self.limit = limit
        self.offset = offset
    


// *** This is Model for Above Api endpoint method ****
public struct MyCharacter: Decodable 
    public let id: Int
    public let name: String?
    public let description: String?



// ***** These below line you used to call any api call in your controller or view model ****
func viewDidLoad() 
    let apiClient = APIClient()

    // A simple request with no parameters
    apiClient.send(GetCharacters())  response in

        response.map  dataContainer in
            print(dataContainer.results)
        
    


【讨论】:

【参考方案13】:

这是一个可能有用的小用例:-

func testUrlSession(urlStr:String, completionHandler: @escaping ((String) -> Void)) 
        let url = URL(string: urlStr)!


        let task = URLSession.shared.dataTask(with: url)(data, response, error) in
            guard let data = data else  return 
            if let strContent = String(data: data, encoding: .utf8) 
            completionHandler(strContent)
            
        


        task.resume()
    

调用函数时:-

testUrlSession(urlStr: "YOUR-URL")  (value) in
            print("Your string value ::- \(value)")

【讨论】:

以上是关于从 Swift 函数中的异步调用返回数据的主要内容,如果未能解决你的问题,请参考以下文章

从 Swift 函数中的异步调用返回数据

从 Swift 函数中的异步调用返回数据

从 Swift 函数中的异步调用返回数据

从 Swift 函数中的异步调用返回数据

如何从 Swift 中的 void 闭包中返回一个值?

在 Swift 的异步调用中包含返回处理程序