Como fornecer uma descrição localizada com um tipo de erro no Swift?

203

Estou definindo um tipo de erro personalizado com a sintaxe do Swift 3 e desejo fornecer uma descrição amigável do erro retornada pela localizedDescriptionpropriedade do Errorobjeto. Como eu posso fazer isso?

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

Existe uma maneira de localizedDescriptionretornar minha descrição personalizada do erro ("Uma descrição amigável do erro.")? Observe que o objeto de erro aqui é do tipo Errore não MyError. É claro que posso converter o objeto para MyError

(error as? MyError)?.localizedDescription

mas existe uma maneira de fazê-lo funcionar sem transmitir para o meu tipo de erro?

Evgenii
fonte

Respostas:

403

Conforme descrito nas notas de versão do Xcode 8 beta 6,

Os tipos de erro definidos por Swift podem fornecer descrições de erro localizadas adotando o novo protocolo LocalizedError.

No seu 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.

Você pode fornecer ainda mais informações se o erro for convertido para NSError(o que é sempre possível):

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

Ao adotar o CustomNSErrorprotocolo, o erro pode fornecer um userInfodicionário (e também um domaine code). Exemplo:

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
fonte
7
Existe uma razão para você fazer MyErrorum Errorprimeiro e estendê-lo LocalizedErrordepois? Existe alguma diferença se você fez isso LocalizedErrorem primeiro lugar?
Gee.E
9
@ Gee.E: Não faz diferença. É apenas uma maneira de organizar o código (uma extensão para cada protocolo). Compare stackoverflow.com/questions/36263892/… , stackoverflow.com/questions/40502086/… ou natashatherobot.com/using-swift-extensions .
Martin R #
4
Ah, verifique. Eu entendo o que você está dizendo agora. A seção "Conformidade do protocolo" em natashatherobot.com/using-swift-extensions é realmente um bom exemplo do que você quer dizer. Obrigado!
Gee.E
1
@ MartininR Se meu erro seria convertido para NSError, como posso passar um dicionário de erro que pode ser acessado como userInfo do NSError?
BangOperator
18
Cuidado ao digitar em var errorDescription: String?vez de String. Há um erro na implementação do LocalizedError. Veja SR-5858 .
precisa saber é o seguinte
35

Eu também acrescentaria, se o seu erro tiver parâmetros como este

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

você pode chamar esses parâmetros em sua descrição localizada assim:

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

Você pode até tornar isso mais curto assim:

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
fonte
4

Agora, existem dois protocolos de adoção de erros que seu tipo de erro pode adotar para fornecer informações adicionais ao Objective-C - LocalizedError e CustomNSError. Aqui está um exemplo de erro que adota os dois:

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
fonte
2
Você pode fazer uma edição? Seus exemplos não ajudam muito a entender o valor de cada um. Ou simplesmente excluí-lo, porque a resposta de MartinR oferece este exatamente ...
Mel
3

Usar uma estrutura pode ser uma alternativa. Um pouco de elegância com localização 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
fonte
0

Aqui está a solução mais elegante:

  enum ApiError: String, LocalizedError {

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

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

  }
Vitaliy Gozhenko
fonte
4
Isso pode ser mais elegante no tempo de execução, mas a etapa de localização estática falhará ao extrair essas seqüências de caracteres para tradutores; você verá um "Bad entry in file – Argument is not a literal string"erro ao executar exportLocalizationsou genstringscriar sua lista de texto traduzível.
Savinola
@savinola concorda, a localização estática não funcionará nesse caso. Talvez usar switch + caseseja apenas uma opção ...
Vitaliy Gozhenko
Usando valores brutos também irá impedir a utilização de valores associados para qualquer um dos seus erros
Brody Robertson