如何在Swift 2中处理异步闭包的错误?最佳方法是什么?

7

我正在使用大量的异步网络请求(顺便提一下,在iOS中任何网络请求都需要异步处理),并且我正在寻找更好的方法来处理苹果的dataTaskWithRequest返回的错误,因为它不支持throws

我的代码如下:

func sendRequest(someData: MyCustomClass?, completion: (response: NSData?) -> ()) {
    let request = NSURLRequest(URL: NSURL(string: "http://google.com")!)

    if someData == nil {
        // throw my custom error
    }

    let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
        data, response, error in

        // here I want to handle Apple's error
    }
    task.resume()
}

我需要解析可能的自定义错误并处理dataTaskWithRequest中可能发生的连接错误。Swift 2引入了throws,但是你不能从苹果的闭包中抛出异常,因为它们不支持throw并且是异步运行的。

我只能想到一种方法,那就是在我的完成块中添加返回NSError,但据我所知,使用NSError是旧版Objective-C的方式。ErrorType只能与throws一起使用(我认为)。

在使用苹果网络闭包时,最好和最现代的处理错误的方法是什么?我理解任何异步网络函数中都无法使用throws?


你可以在不使用throws的情况下使用ErrorType,即在完成处理程序中返回它。也许你可能想看一下Alamofire如何处理响应/错误:https://github.com/Alamofire/Alamofire/blob/master/Source/Result.swift - doschi
3个回答

16

有很多方法可以解决这个问题,但我建议使用期望 Result 枚举 的完成块。这可能是最“Swift”的方式。

结果枚举只有两种状态:成功和错误,这是通常的两个可选返回值(数据和错误)所没有的优势,后者会导致4种可能的状态。

enum Result<T> {
    case Success(T)
    case Error(String, Int)
}

在完成块中使用结果枚举可以完成拼图。

let InvalidURLCode = 999
let NoDataCode = 998
func getFrom(urlString: String, completion:Result<NSData> -> Void) {
    // make sure the URL is valid, if not return custom error
    guard let url = NSURL(string: urlString) else { return completion(.Error("Invalid URL", InvalidURLCode)) }

    let request = NSURLRequest(URL: url)
    NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
       // if error returned, extract message and code then pass as Result enum
        guard error == nil else { return completion(.Error(error!.localizedDescription, error!.code)) }

        // if no data is returned, return custom error
        guard let data = data else { return completion(.Error("No data returned", NoDataCode)) }

        // return success
        completion(.Success(data))
    }.resume()
}

因为返回值是枚举类型,所以您应该根据它进行切换。
getFrom("http://www.google.com") { result in
    switch result {
    case .Success(let data):
        // handle successful data response here
        let responseString = String(data:data, encoding: NSASCIIStringEncoding)
        print("got data: \(responseString)");
    case .Error(let msg, let code):
        // handle error here
        print("Error [\(code)]: \(msg)")
    }
}

另一种解决方案是传递两个完成块,一个用于成功,一个用于错误。大致如下:

func getFrom(urlString: String, successHandler:NSData -> Void, errorHandler:(String, Int) -> Void)

1

这与Casey的回答非常相似,但是在Swift 5中,我们现在在标准库中有了Result(通用枚举)实现。

//Don't add this code to your project, this has already been implemented
//in standard library.
public enum Result<Success, Failure: Error> {
    case success(Success), failure(Failure)
}

它很容易使用,

URLSession.shared.dataTask(with: url) { (result: Result<(response: URLResponse, data: Data), Error>) in
    switch result {
    case let .success(success):
        handleResponse(success.response, data: success.data)
    case let .error(error):
        handleError(error)
    }
}

https://developer.apple.com/documentation/swift/result

https://github.com/apple/swift-evolution/blob/master/proposals/0235-add-result.md


0

有一种优雅的方法,可以利用类似JavaScript的Promise库或类似Scala的“Future和Promise”库。

使用Scala风格的futures和promises,代码如下:

您原来的函数

func sendRequest(someData: MyCustomClass?, completion: (response: NSData?) -> ())

可以实现如下。它还展示了如何创建一个promise,如何提前返回一个失败的future以及如何履行/拒绝一个promise:

func sendRequest(someData: MyCustomClass) -> Future<NSData> {
  guard let url = ... else {
    return Future.failure(MySessionError.InvalidURL)  // bail out early with a completed future
  }
  let request = ... // setup request
  let promise = Promise<NSData>()  
  NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
    guard let error = error else { 
      promise.reject(error) // Client error
    }
    // The following assertions should be true, unless error != nil
    assert(data != nil) 
    assert(response != nil)

    // We expect HTTP protocol:
    guard let response = response! as NSHTTPURLResponse else {
      promise.reject(MySessionError.ProtocolError)  // signal that we expected HTTP.
    }

    // Check status code:
    guard myValidStatusCodeArray.contains(response.statusCode) else {
      let message: String? = ... // convert the response data to a string, if any and if possible
      promise.reject(MySessionError.InvalidStatusCode(statusCode: response.statusCode, message: message ?? ""))
    }

    // Check MIME type if given:
    if let mimeType = response.MIMEType {
      guard myValidMIMETypesArray.contains(mimeType) else {
        promise.reject(MySessionError.MIMETypeNotAccepted(mimeType: mimeType))
      }
    } else {
      // If we require a MIMEType - reject the promise.
    }
    // transform data to some other object if desired, can be done in a later, too. 

    promise.fulfill(data!)
  }.resume()

  return promise.future!
}

你可能希望在请求成功时返回一个JSON响应。
现在,你可以按如下方式使用它:
sendRequest(myObject).map { data in 
  return try NSJSONSerialization.dataWithJSONObject(data, options: [])
}
.map { object in
   // the object returned from the step above, unless it failed.
   // Now, "process" the object: 
   ...
   // You may throw an error if something goes wrong:
   if failed {
       throw MyError.Failed
   }
}
.onFailure { error in
   // We reach here IFF an error occurred in any of the 
   // previous tasks.
   // error is of type ErrorType.
   print("Error: \(error)")
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接