Jak podać zlokalizowany opis z typem błędu w Swift?

202

Definiuję niestandardowy typ błędu za pomocą składni Swift 3 i chcę podać przyjazny dla użytkownika opis błędu zwracanego przez localizedDescriptionwłaściwość Errorobiektu. Jak mogę to zrobić?

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

Czy istnieje sposób na localizedDescriptionzwrócenie mojego niestandardowego opisu błędu („Przyjazny dla użytkownika opis błędu.”)? Zauważ, że obiekt błędu jest tutaj typu, Errora nie typu MyError. Mogę oczywiście rzutować obiekt na MyError

(error as? MyError)?.localizedDescription

ale czy istnieje sposób, aby działał bez rzutowania na mój typ błędu?

Evgenii
źródło

Odpowiedzi:

401

Jak opisano w informacjach o wersji Xcode 8 beta 6,

Szybko zdefiniowane typy błędów mogą zapewniać zlokalizowane opisy błędów, przyjmując nowy protokół LocalizedError.

W Twoim przypadku:

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.

Możesz podać jeszcze więcej informacji, jeśli błąd zostanie przekonwertowany na NSError(co zawsze jest możliwe):

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

Przyjmując CustomNSErrorprotokół, błąd może zapewnić userInfosłownik (a także a domaini code). Przykład:

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.
źródło
7
Czy jest jakiś powód, dla którego uczynić pierwszy i przedłużyć go później? Czy jest różnica, jeśli zrobiłeś to w pierwszej kolejności? MyErrorErrorLocalizedErrorLocalizedError
Gee.E
9
@ Gee.E: Nie ma znaczenia. To tylko sposób na uporządkowanie kodu (jedno rozszerzenie dla każdego protokołu). Porównaj stackoverflow.com/questions/36263892/… , stackoverflow.com/questions/40502086 /... lub natashatherobot.com/using-swift-extensions .
Martin R
4
Ach, sprawdź. Rozumiem co teraz mówisz. Sekcja „Zgodność protokołu” na natashatherobot.com/using-swift-extensions jest rzeczywiście dobrym przykładem tego, co masz na myśli. Dzięki!
Gee.E
1
@MartinR Jeśli mój błąd zostanie przekonwertowany na NSError, jak mogę przekazać słownik z błędu, do którego można uzyskać dostęp jako informacja użytkownika NSError?
BangOperator,
18
Uważaj, aby wpisać var errorDescription: String?zamiast String. Wystąpił błąd w implementacji LocalizedError. Patrz SR-5858 .
ethanhuang13
35

Dodałbym również, jeśli twój błąd ma takie parametry

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

możesz wywołać te parametry w swoim zlokalizowanym opisie w następujący sposób:

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

Możesz nawet skrócić to w ten sposób:

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
źródło
4

Istnieją teraz dwa protokoły adopcji błędów, które typ błędu może zastosować w celu dostarczenia dodatkowych informacji do Objective-C - LocalizedError i CustomNSError. Oto przykładowy błąd, który przyjmuje oba:

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

}
matowy
źródło
2
Czy możesz dokonać edycji? Twoje przykłady niewiele pomagają zrozumieć wartość każdego z nich. Lub po prostu usuń, ponieważ odpowiedź MartinR oferuje dokładnie to ...
Honey
3

Użycie struct może być alternatywą. Trochę elegancji ze statyczną lokalizacją:

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
źródło
0

Oto bardziej eleganckie rozwiązanie:

  enum ApiError: String, LocalizedError {

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

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

  }
Witalij Gozhenko
źródło
4
Może to być bardziej eleganckie w czasie wykonywania, ale etap statycznej lokalizacji nie wyodrębni tych ciągów dla tłumaczy; zobaczysz "Bad entry in file – Argument is not a literal string"błąd podczas uruchamiania exportLocalizationslub w genstringscelu utworzenia listy tekstu do przetłumaczenia.
savinola,
@savinola zgadza się, statyczna lokalizacja nie będzie działać w takim przypadku. Być może używanie switch + casejest tylko opcją ...
Witalij Gozhenko,
Użycie surowych wartości zapobiegnie również użyciu powiązanych wartości dla któregokolwiek z twoich błędów
Brody Robertson