web-dev-qa-db-fra.com

La conversion de ErrorType en NSError perd les objets associés

In Swift 2.0 NSError est conforme au protocole ErrorType.

Pour une erreur définie de manière personnalisée, nous pouvons spécifier le ou les objets associés dans certains cas, comme ci-dessous.

enum LifeError: ErrorType {
    case BeBorn
    case LostJob(job: String)
    case GetCaughtByWife(wife: String)
    ...
}

Nous pouvons confortablement faire ce qui suit:

do {
    try haveAffairWith(otherPerson)
} catch LifeError.GetCaughtByWife(let wife) {
    ...
}

Cependant, si nous voulons qu'il passe à d'autres endroits en tant que NSError, il perd ses informations d'objet d'association.

println("\(LifeError.GetCaughtByWife("Name") as NSError)")

impressions:

Error Domain=... Code=1 "The operation couldn't be completed". (... error 1)

et son userInfo est nil.

Où mon wife est-il associé au ErrorType?

57
Ben Lu

Nouveau dans Xcode 8 : CustomNSErrorprotocole .

enum LifeError: CustomNSError {
    case beBorn
    case lostJob(job: String)
    case getCaughtByWife(wife: String)

    static var errorDomain: String {
        return "LifeError"
    }

    var errorCode: Int {
        switch self {
        case .beBorn:
            return 0
        case .lostJob(_):
            return 1
        case .getCaughtByWife(_):
            return 2
        }
    }

    var errorUserInfo: [String : AnyObject] {
        switch self {
        case .beBorn:
            return [:]
        case .lostJob(let job):
            return ["Job": job]
        case .getCaughtByWife(let wife):
            return ["Wife": wife]
        }
    }
}
43
Igor Camilo

Un ErrorType ne peut pas vraiment être converti en NSError, vous devez prendre les données associées et les empaqueter dans un NSError vous-même.

do {
    try haveAffairWith(otherPerson)
} catch LifeError.GetCaughtByWife(let wife) {
    throw NSError(domain:LifeErrorDomain code:-1 userInfo:
        [NSLocalizedDescriptionKey:"You cheated on \(wife)")
}

EDIT: En fait, vous pouvez faire le cast de ErrorType à NSError, mais le NSError que vous obtenez de l'implémentation par défaut est assez primitif. Ce que je fais dans mon application, c'est accrocher l'application: willPresentError: dans mon délégué d'application et utiliser une classe personnalisée pour lire les ErrorType de mon application et décorer les NSErrors à retourner.

23
iluvcapra

La création d'un NSError dans chaque bloc catch peut entraîner de nombreux copier-coller pour convertir votre ErrorType personnalisé en NSError. Je l'ai résumé de manière similaire à @ powertoold .

protocol CustomErrorConvertible {
    func userInfo() -> Dictionary<String,String>?
    func errorDomain() -> String
    func errorCode() -> Int
}

Cette extension peut contenir du code, ce qui est courant pour les LifeError que nous avons déjà et d'autres types d'erreur personnalisés que nous pouvons créer.

extension CustomErrorConvertible {
    func error() -> NSError {
        return NSError(domain: self.errorDomain(), code: self.errorCode(), userInfo: self.userInfo())
    }
}

En route pour l'implémentation!

enum LifeError: ErrorType, CustomErrorConvertible {
    case BeBorn
    case LostJob(job: String)
    case GetCaughtByPolice(police: String)

    func errorDomain() -> String {
        return "LifeErrorDomain"
    }

    func userInfo() -> Dictionary<String,String>? {
        var userInfo:Dictionary<String,String>?
        if let errorString = errorDescription() {
            userInfo = [NSLocalizedDescriptionKey: errorString]
        }
        return userInfo
    }

    func errorDescription() -> String? {
        var errorString:String?
        switch self {
        case .LostJob(let job):
            errorString = "fired as " + job
        case .GetCaughtByPolice(let cops):
            errorString = "arrested by " + cops
        default:
            break;
        }
        return errorString
    }

    func errorCode() -> Int {
        switch self {
        case .BeBorn:
            return 1
        case .LostJob(_):
            return -9000
        case .GetCaughtByPolice(_):
            return 50
        }
    }
}

Et voici comment l'utiliser.

func lifeErrorThrow() throws {
    throw LifeError.LostJob(job: "L33tHax0r")
}

do {
    try lifeErrorThrow()
}
catch LifeError.BeBorn {
  print("vala morgulis")
}
catch let myerr as LifeError {
    let error = myerr.error()
    print(error)
}

Vous pouvez facilement déplacer certaines fonctions comme func userInfo() -> Dictionary<String,String>? de LifeError vers extension CustomErrorConvertible Ou une autre extension.

Au lieu de coder en dur les codes d'erreur comme ci-dessus, une énumération peut être préférable.

enum LifeError:Int {
  case Born
  case LostJob
}
14
orkoden

Ma solution à ce problème a été de créer une énumération conforme à Int, ErrorType:

enum AppError: Int, ErrorType {
    case UserNotLoggedIn
    case InternetUnavailable
}

Et puis étendez l'énumération pour vous conformer à CustomStringConvertible et à un protocole personnalisé appelé CustomErrorConvertible:

extension AppError: CustomStringConvertible, CustomErrorConvertible

protocol CustomErrorConvertible {
    var error: NSError { get }
}

Pour la description et l'erreur, j'ai allumé l'AppError. Exemple:

Description:    switch self {
            case .UserNotLoggedIn: return NSLocalizedString("ErrorUserNotLoggedIn", comment: "User not logged into cloud account.")
            case .InternetUnavailable: return NSLocalizedString("ErrorInternetUnavailable", comment: "Internet connection not available.")
            }

Error:    switch self {
            case .UserNotLoggedIn: errorCode = UserNotLoggedIn.rawValue; errorDescription = UserNotLoggedIn.description
            case .InternetUnavailable: errorCode = InternetUnavailable.rawValue; errorDescription = InternetUnavailable.description
            }

Et puis j'ai composé ma propre NSError:

return NSError(domain:NSBundle.mainBundle().bundleIdentifier!, code:errorCode, userInfo:[NSLocalizedDescriptionKey: errorDescription])
6
powertoold

La meilleure solution que j'ai trouvée est d'avoir un wrapper Objective-C pour convertir le ErrorType en NSError (via NSObject* parmeter) et extraire le userInfo. Très probablement, cela fonctionnerait également pour d'autres objets associés.

Dans mon cas, toutes les autres tentatives utilisant uniquement Swift ont abouti à l'obtention d'un niluserInfo.

Voici l'assistant Objective-C. Placez-le par exemple dans une classe MyErrorUtils exposée à Swift:

+ (NSDictionary*)getUserInfo:(NSObject *)error {
    NSError *nsError = (NSError *)error;
    if (nsError != nil) {
        return [nsError userInfo];
    } else {
        return nil;
    }
}

Ensuite, utilisez l'aide dans Swift comme ceci:

static func myErrorHandler(error: ErrorType) {

    // Note the as? cast to NSObject
    if let userInfo: [NSObject: AnyObject]? = 
        MyErrorUtils.getUserInfo(error as? NSObject) {

        let myUserInfo = userInfo["myCustomUserInfo"]

        // ... Error processing based on userInfo ...
    }

}

(J'utilise actuellement XCode 8 et Swift 2.3)

2
Peter Lamberg

J'ai aussi ce problème en utilisant PromiseKit et j'ai trouvé une solution de contournement qui peut être un peu moche mais qui semble fonctionner.

Je colle ici mon terrain de jeu pour que vous puissiez voir tout le processus.

import Foundation
import PromiseKit
import XCPlayground

let error = NSError(domain: "a", code: 1, userInfo: ["hello":"hello"])

// Only casting won't lose the user info

let castedError = error as ErrorType
let stillHaveUserInfo = castedError as NSError

// when using promises

func convert(error: ErrorType) -> Promise<Int> {
    return Promise<Int> {
        (fulfill, reject) in
        reject(error)
    }
}

let promiseA = convert(error)

// Seems to lose the user info once we cast back to NSError

promiseA.report { (promiseError) -> Void in
    let lostUserInfo = promiseError as NSError
}


// Workaround

protocol CastingNSErrorHelper {
    var userInfo: [NSObject : AnyObject] { get }
}

extension NSError : CastingNSErrorHelper {}

promiseA.report { (promiseError) -> Void in
    let castingNSErrorHelper = promiseError as! CastingNSErrorHelper
    let recoveredErrorWithUserInfo = castingNSErrorHelper as! NSError
}

XCPSetExecutionShouldContinueIndefinitely()
2
Charly Liu

Comme l'a souligné la réponse acceptée, il y a maintenant CustomNSError dans Swift 3, cependant, vous n'avez pas nécessairement besoin de l'utiliser. Si vous définissez votre type d'erreur comme ceci

@objc
enum MyErrorType: Int, Error { ... }

Ensuite, cette erreur peut être directement convertie en NSError:

let error: MyErrorType = ...
let objcError = error as NSError

Je viens de le découvrir aujourd'hui et je le partage avec le monde.

1
Mecki