¿Cómo proporcionar una descripción localizada con un tipo de error en Swift?

203

Estoy definiendo un tipo de error personalizado con la sintaxis Swift 3 y quiero proporcionar una descripción fácil de usar del error que devuelve la localizedDescriptionpropiedad del Errorobjeto. ¿Cómo puedo hacerlo?

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.)"

¿Hay alguna forma de localizedDescriptionque devuelva mi descripción de error personalizada ("Una descripción fácil de usar del error")? Tenga en cuenta que el objeto de error aquí es de tipo Errory no MyError. Puedo, por supuesto, lanzar el objeto a MyError

(error as? MyError)?.localizedDescription

pero, ¿hay alguna manera de hacerlo funcionar sin transmitir a mi tipo de error?

Evgenii
fuente

Respuestas:

403

Como se describe en las notas de la versión Xcode 8 beta 6,

Los tipos de error definidos rápidamente pueden proporcionar descripciones de error localizadas mediante la adopción del nuevo protocolo LocalizedError.

En tu caso:

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.

Puede proporcionar aún más información si el error se convierte a NSError(que siempre es posible):

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.")

Al adoptar el CustomNSErrorprotocolo, el error puede proporcionar un userInfodiccionario (y también un domainy code). Ejemplo:

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
Martin R
fuente
77
¿Hay alguna razón por la que haces MyErrorun Errorprimero y luego lo extiendes LocalizedError? ¿Hay alguna diferencia si lo hiciste LocalizedErroren primer lugar?
Gee.E
9
@ Gee.E: No hay diferencia. Es solo una forma de organizar el código (una extensión para cada protocolo). Compare stackoverflow.com/questions/36263892/… , stackoverflow.com/questions/40502086/… o natashatherobot.com/using-swift-extensions .
Martin R
44
Ah, mira. Entiendo lo que estás diciendo ahora. La sección "Conformidad de protocolo" en natashatherobot.com/using-swift-extensions es un buen ejemplo de lo que quiere decir. ¡Gracias!
Gee.E
1
@MartinR Si mi error se convertiría a NSError, ¿cómo puedo pasar un diccionario de error al que se puede acceder como información de usuario de NSError?
BangOperator
18
Tenga cuidado de escribir en var errorDescription: String?lugar de String. Hay un error en la implementación de LocalizedError. Ver SR-5858 .
ethanhuang13
35

También agregaría, si su error tiene parámetros como este

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

Puede llamar a estos parámetros en su descripción localizada así:

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

Incluso puedes hacer esto más corto así:

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

Ahora hay dos protocolos de adopción de errores que su tipo de error puede adoptar para proporcionar información adicional a Objective-C: LocalizedError y CustomNSError. Aquí hay un error de ejemplo que los adopta a ambos:

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" }

}
mate
fuente
2
¿Puedes hacer una edición? Sus ejemplos no ayudan mucho a comprender el valor de cada uno. O simplemente elimínelo porque la respuesta de MartinR ofrece esto exactamente ...
Miel
3

Usar una estructura puede ser una alternativa. Un poco de elegancia con localización estática:

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)")
   }
}
Zafer Sevik
fuente
0

Aquí hay una solución más elegante:

  enum ApiError: String, LocalizedError {

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

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

  }
Vitaliy Gozhenko
fuente
44
Esto puede ser más elegante en tiempo de ejecución, pero el paso de localización estática no podrá extraer estas cadenas para traductores; verá un "Bad entry in file – Argument is not a literal string"error al ejecutar exportLocalizationso genstringsal crear su lista de texto traducible.
savinola
@savinola está de acuerdo, la localización estática no funcionará en tal caso. Quizás usar switch + casees la única opción ...
Vitaliy Gozhenko
El uso de valores sin formato también evitará el uso de valores asociados para cualquiera de sus errores
Brody Robertson