How to provide a localized description with an Err

2019-01-01 14:25发布

问题:

I am defining a custom error type with Swift 3 syntax and I want to provide a user-friendly description of the error which is returned by the localizedDescription property of the Error object. How can I do it?

public enum MyError: Error {
  case customError

  var localizedDescription: String {
    switch self {
    case .customError:
      return NSLocalizedString(\"A user-friendly description of the error.\", comment: \"My error\")
    }
  }
}

let error: Error = MyError.customError
error.localizedDescription
// \"The operation couldn’t be completed. (MyError error 0.)\"

Is there a way for the localizedDescription to return my custom error description (\"A user-friendly description of the error.\")? Note that the error object here is of type Error and not MyError. I can, of course, cast the object to MyError

(error as? MyError)?.localizedDescription

but is there a way to make it work without casting to my error type?

回答1:

As described in the Xcode 8 beta 6 release notes,

Swift-defined error types can provide localized error descriptions by adopting the new LocalizedError protocol.

In your case:

public enum MyError: Error {
    case customError
}

extension MyError: LocalizedError {
    public var errorDescription: String? {
        switch self {
        case .customError:
            return NSLocalizedString(\"A user-friendly description of the error.\", comment: \"My error\")
        }
    }
}

let error: Error = MyError.customError
print(error.localizedDescription) // A user-friendly description of the error.

You can provide even more information if the error is converted to NSError (which is always possible):

extension MyError : LocalizedError {
    public var errorDescription: String? {
        switch self {
        case .customError:
            return NSLocalizedString(\"I failed.\", comment: \"\")
        }
    }
    public var failureReason: String? {
        switch self {
        case .customError:
            return NSLocalizedString(\"I don\'t know why.\", comment: \"\")
        }
    }
    public var recoverySuggestion: String? {
        switch self {
        case .customError:
            return NSLocalizedString(\"Switch it off and on again.\", comment: \"\")
        }
    }
}

let error = MyError.customError as NSError
print(error.localizedDescription)        // I failed.
print(error.localizedFailureReason)      // Optional(\"I don\\\'t know why.\")
print(error.localizedRecoverySuggestion) // Optional(\"Switch it off and on again.\")

By adopting the CustomNSError protocol the error can provide a userInfo dictionary (and also a domain and code). Example:

extension MyError: CustomNSError {

    public static var errorDomain: String {
        return \"myDomain\"
    }

    public var errorCode: Int {
        switch self {
        case .customError:
            return 999
        }
    }

    public var errorUserInfo: [String : Any] {
        switch self {
        case .customError:
            return [ \"line\": 13]
        }
    }
}

let error = MyError.customError as NSError

if let line = error.userInfo[\"line\"] as? Int {
    print(\"Error in line\", line) // Error in line 13
}

print(error.code) // 999
print(error.domain) // myDomain


回答2:

I would also add, if your error has parameters like this

enum NetworkError: LocalizedError {
  case responseStatusError(status: Int, message: String)
}

you can call these parameters in your localized description like this:

extension NetworkError {
  var errorDescription: String {
    switch self {
    case .responseStatusError(status: let status, message: let message):
      return \"Error with status \\(status) and message \\(message) was thrown\"
  }
}

You can even make this shorter like this:

extension NetworkError {
  var errorDescription: String {
    switch self {
    case let .responseStatusError(status, message):
      return \"Error with status \\(status) and message \\(message) was thrown\"
  }
}


回答3:

There are now two Error-adopting protocols that your error type can adopt in order to provide additional information to Objective-C — LocalizedError and CustomNSError. Here\'s an example error that adopts both of them:

enum MyBetterError : CustomNSError, LocalizedError {
    case oops

    // domain
    static var errorDomain : String { return \"MyDomain\" }
    // code
    var errorCode : Int { return -666 }
    // userInfo
    var errorUserInfo: [String : Any] { return [\"Hey\":\"Ho\"] };

    // localizedDescription
    var errorDescription: String? { return \"This sucks\" }
    // localizedFailureReason
    var failureReason: String? { return \"Because it sucks\" }
    // localizedRecoverySuggestion
    var recoverySuggestion: String? { return \"Give up\" }

}


回答4:

Here is more elegant solution:

  enum ApiError: String, LocalizedError {

    case invalidCredentials = \"Invalid credentials\"
    case noConnection = \"No connection\"

    var localizedDescription: String { return NSLocalizedString(self.rawValue, comment: \"\") }

  }


回答5:

Using a struct can be an alternative. A little bit elegance with static localization:

import Foundation

struct MyError: LocalizedError, Equatable {

   private var description: String!

   init(description: String) {
       self.description = description
   }

   var errorDescription: String? {
       return description
   }

   public static func ==(lhs: MyError, rhs: MyError) -> Bool {
       return lhs.description == rhs.description
   }
}

extension MyError {

   static let noConnection = MyError(description: NSLocalizedString(\"No internet connection\",comment: \"\"))
   static let requestFailed = MyError(description: NSLocalizedString(\"Request failed\",comment: \"\"))
}

func throwNoConnectionError() throws {
   throw MyError.noConnection
}

do {
   try throwNoConnectionError()
}
catch let myError as MyError {
   switch myError {
   case .noConnection:
       print(\"noConnection: \\(myError.localizedDescription)\")
   case .requestFailed:
       print(\"requestFailed: \\(myError.localizedDescription)\")
   default:
      print(\"default: \\(myError.localizedDescription)\")
   }
}