Comment puis-je utiliser NSError dans mon application iPhone?

228

Je travaille à détecter des erreurs dans mon application et j'envisage d'utiliser NSError. Je suis un peu confus sur la façon de l'utiliser et de le remplir.

Quelqu'un pourrait-il fournir un exemple sur la façon dont je remplis puis utilise NSError?

Nic Hubbard
la source

Réponses:

473

Eh bien, ce que je fais habituellement, c'est que mes méthodes qui pourraient provoquer des erreurs au moment de l'exécution prennent une référence à un NSErrorpointeur. Si quelque chose se passe vraiment mal dans cette méthode, je peux remplir la NSErrorréférence avec des données d'erreur et retourner nil à partir de la méthode.

Exemple:

- (id) endWorldHunger:(id)largeAmountsOfMonies error:(NSError**)error {
    // begin feeding the world's children...
    // it's all going well until....
    if (ohNoImOutOfMonies) {
        // sad, we can't solve world hunger, but we can let people know what went wrong!
        // init dictionary to be used to populate error object
        NSMutableDictionary* details = [NSMutableDictionary dictionary];
        [details setValue:@"ran out of money" forKey:NSLocalizedDescriptionKey];
        // populate the error object with the details
        *error = [NSError errorWithDomain:@"world" code:200 userInfo:details];
        // we couldn't feed the world's children...return nil..sniffle...sniffle
        return nil;
    }
    // wohoo! We fed the world's children. The world is now in lots of debt. But who cares? 
    return YES;
}

Nous pouvons alors utiliser la méthode comme celle-ci. Ne vous embêtez même pas à inspecter l'objet d'erreur à moins que la méthode ne renvoie nil:

// initialize NSError object
NSError* error = nil;
// try to feed the world
id yayOrNay = [self endWorldHunger:smallAmountsOfMonies error:&error];
if (!yayOrNay) {
   // inspect error
   NSLog(@"%@", [error localizedDescription]);
}
// otherwise the world has been fed. Wow, your code must rock.

Nous avons pu accéder aux erreurs localizedDescriptioncar nous avons défini une valeur pour NSLocalizedDescriptionKey.

Le meilleur endroit pour plus d'informations est la documentation d'Apple . C'est vraiment bien.

Il existe également un didacticiel simple et agréable sur Cocoa Is My Girlfriend .

Alex
la source
37
ceci est l'exemple le plus drôle, jamais
ming yeow
c'est une réponse assez impressionnante, bien qu'il y ait quelques problèmes dans ARC et le cast idà un BOOL. Toute légère variation compatible ARC serait très appréciée.
NSTJ
6
@ TomJowett Je serais vraiment énervé si nous ne pouvions pas mettre fin à la faim dans le monde simplement parce qu'Apple nous a poussés à passer au nouveau monde ARC uniquement.
Manav
1
le type de retour peut être BOOL. Retour NOen cas d'erreur et au lieu de vérifier la valeur de retour, il suffit de vérifier error. Si nilallez-y, != nilmanipulez-le.
Gabriele Petronella
8
-1: Vous devez vraiment incorporer du code qui vérifie que ce **errorn'est pas nul. Sinon, le programme lancera une erreur qui est complètement hostile et ne rend pas évident ce qui se passe.
FreeAsInBeer
58

Je voudrais ajouter quelques suggestions supplémentaires basées sur ma dernière mise en œuvre. J'ai regardé du code d'Apple et je pense que mon code se comporte de la même manière.

Les articles ci-dessus expliquent déjà comment créer des objets NSError et les renvoyer, donc je ne vais pas m'embêter avec cette partie. Je vais juste essayer de suggérer un bon moyen d'intégrer les erreurs (codes, messages) dans votre propre application.


Je recommande de créer 1 en-tête qui sera un aperçu de toutes les erreurs de votre domaine (ex: application, bibliothèque, etc.). Mon en-tête actuel ressemble à ceci:

FSError.h

FOUNDATION_EXPORT NSString *const FSMyAppErrorDomain;

enum {
    FSUserNotLoggedInError = 1000,
    FSUserLogoutFailedError,
    FSProfileParsingFailedError,
    FSProfileBadLoginError,
    FSFNIDParsingFailedError,
};

FSError.m

#import "FSError.h" 

NSString *const FSMyAppErrorDomain = @"com.felis.myapp";

Maintenant, lorsque vous utilisez les valeurs ci-dessus pour les erreurs, Apple crée un message d'erreur standard de base pour votre application. Une erreur pourrait être créée comme suit:

+ (FSProfileInfo *)profileInfoWithData:(NSData *)data error:(NSError **)error
{
    FSProfileInfo *profileInfo = [[FSProfileInfo alloc] init];
    if (profileInfo)
    {
        /* ... lots of parsing code here ... */

        if (profileInfo.username == nil)
        {
            *error = [NSError errorWithDomain:FSMyAppErrorDomain code:FSProfileParsingFailedError userInfo:nil];            
            return nil;
        }
    }
    return profileInfo;
}

Le message d'erreur standard généré par Apple ( error.localizedDescription) pour le code ci-dessus ressemblera à ceci:

Error Domain=com.felis.myapp Code=1002 "The operation couldn’t be completed. (com.felis.myapp error 1002.)"

Ce qui précède est déjà très utile pour un développeur, car le message affiche le domaine où l'erreur s'est produite et le code d'erreur correspondant. Les utilisateurs finaux n'auront aucune idée de ce 1002que signifie le code d'erreur , alors nous devons maintenant implémenter de beaux messages pour chaque code.

Pour les messages d'erreur, nous devons garder à l'esprit la localisation (même si nous n'implémentons pas immédiatement les messages localisés). J'ai utilisé l'approche suivante dans mon projet actuel:


1) créez un stringsfichier qui contiendra les erreurs. Les fichiers de chaînes sont facilement localisables. Le fichier pourrait ressembler à ceci:

FSError.strings

"1000" = "User not logged in.";
"1001" = "Logout failed.";
"1002" = "Parser failed.";
"1003" = "Incorrect username or password.";
"1004" = "Failed to parse FNID."

2) Ajoutez des macros pour convertir les codes entiers en messages d'erreur localisés. J'ai utilisé 2 macros dans mon fichier Constants + Macros.h. J'inclus toujours ce fichier dans l'en-tête du préfixe ( MyApp-Prefix.pch) pour plus de commodité.

Constantes + Macros.h

// error handling ...

#define FS_ERROR_KEY(code)                    [NSString stringWithFormat:@"%d", code]
#define FS_ERROR_LOCALIZED_DESCRIPTION(code)  NSLocalizedStringFromTable(FS_ERROR_KEY(code), @"FSError", nil)

3) Il est maintenant facile d'afficher un message d'erreur convivial basé sur un code d'erreur. Un exemple:

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" 
            message:FS_ERROR_LOCALIZED_DESCRIPTION(error.code) 
            delegate:nil 
            cancelButtonTitle:@"OK" 
            otherButtonTitles:nil];
[alert show];
Wolfgang Schreurs
la source
9
Très bonne réponse! Mais pourquoi ne pas mettre la description localisée dans le dictionnaire d'informations utilisateur où elle appartient? [NSError errorWithDomain: code FSMyAppErrorDomain: FSProfileParsingFailedError userInfo: @ {NSLocalizedDescriptionKey: FS_ERROR_LOCALIZED_DESCRIPTION (error.code)}];
Richard Venable
1
Y a-t-il un endroit particulier où je devrais mettre le fichier de chaîne? De FS_ERROR_LOCALIZED_DESCRIPTION () je reçois juste le numéro (code d'erreur).
huggie
@ huggie: pas vraiment sûr de ce que tu veux dire. Je place généralement ces macros que j'utilise dans toute l'application dans un fichier appelé Constants+Macros.het j'importe ce fichier dans l'en-tête de préfixe ( .pchfichier) afin qu'il soit disponible partout. Si vous voulez dire que vous n'utilisez qu'une des deux macros, cela pourrait fonctionner. Peut-être que la conversion de intvers NSStringn'est pas vraiment nécessaire, même si je n'ai pas testé cela.
Wolfgang Schreurs
@ huggie: ow, je pense que je vous comprends maintenant. Les chaînes doivent être dans un fichier localisable ( .stringsfichier), car c'est là que la macro d'Apple se penchera. Lisez à propos de l'utilisation NSLocalizedStringFromTableici: developer.apple.com/library/mac/documentation/cocoa/conceptual/…
Wolfgang Schreurs
1
@huggie: Oui, j'ai utilisé les tables de chaînes localisées. Le code de la macro FS_ERROR_LOCALIZED_DESCRIPTIONvérifie la chaîne localisable dans un fichier appelé FSError.strings. Vous voudrez peut-être consulter le guide de localisation d'Apple sur les .stringsfichiers si cela vous est étranger.
Wolfgang Schreurs
38

Excellente réponse Alex. Un problème potentiel est la déréférence NULL. Référence d'Apple sur la création et le retour d'objets NSError

...
[details setValue:@"ran out of money" forKey:NSLocalizedDescriptionKey];

if (error != NULL) {
    // populate the error object with the details
    *error = [NSError errorWithDomain:@"world" code:200 userInfo:details];
}
// we couldn't feed the world's children...return nil..sniffle...sniffle
return nil;
...
jlmendezbonini
la source
30

Objectif c

NSError *err = [NSError errorWithDomain:@"some_domain"
                                   code:100
                               userInfo:@{
                                           NSLocalizedDescriptionKey:@"Something went wrong"
                               }];

Swift 3

let error = NSError(domain: "some_domain",
                      code: 100,
                  userInfo: [NSLocalizedDescriptionKey: "Something went wrong"])
AlBeebe
la source
9

Veuillez vous référer au tutoriel suivant

J'espère que cela vous sera utile, mais avant de lire la documentation de NSError

Ceci est un lien très intéressant que j'ai trouvé récemment ErrorHandling

Tirth
la source
3

Je vais essayer de résumer la grande réponse d'Alex et le point de jlmendezbonini, en ajoutant une modification qui rendra tout compatible avec ARC (jusqu'à présent ce n'est pas puisque ARC se plaindra puisque vous devriez revenir id, ce qui signifie "n'importe quel objet", mais ce BOOLn'est pas un objet type).

- (BOOL) endWorldHunger:(id)largeAmountsOfMonies error:(NSError**)error {
    // begin feeding the world's children...
    // it's all going well until....
    if (ohNoImOutOfMonies) {
        // sad, we can't solve world hunger, but we can let people know what went wrong!
        // init dictionary to be used to populate error object
        NSMutableDictionary* details = [NSMutableDictionary dictionary];
        [details setValue:@"ran out of money" forKey:NSLocalizedDescriptionKey];
        // populate the error object with the details
        if (error != NULL) {
             // populate the error object with the details
             *error = [NSError errorWithDomain:@"world" code:200 userInfo:details];
        }
        // we couldn't feed the world's children...return nil..sniffle...sniffle
        return NO;
    }
    // wohoo! We fed the world's children. The world is now in lots of debt. But who cares? 
    return YES;
}

Maintenant, au lieu de vérifier la valeur de retour de notre appel de méthode, nous vérifions si errorc'est toujours nil. Si ce n'est pas le cas, nous avons un problème.

// initialize NSError object
NSError* error = nil;
// try to feed the world
BOOL success = [self endWorldHunger:smallAmountsOfMonies error:&error];
if (!success) {
   // inspect error
   NSLog(@"%@", [error localizedDescription]);
}
// otherwise the world has been fed. Wow, your code must rock.
Gabriele Petronella
la source
3
@Gabriela: Apple déclare que lors de l'utilisation de variables d'indirection pour renvoyer des erreurs, la méthode elle-même devrait toujours avoir une valeur de retour en cas de succès ou d'échec. Apple exhorte les développeurs à vérifier d'abord la valeur de retour et uniquement si la valeur de retour est en quelque sorte invalide, vérifiez les erreurs. Voir la page suivante: developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…
Wolfgang Schreurs
3

Un autre modèle de conception que j'ai vu implique l'utilisation de blocs, ce qui est particulièrement utile lorsqu'une méthode est exécutée de manière asynchrone.

Supposons que nous ayons défini les codes d'erreur suivants:

typedef NS_ENUM(NSInteger, MyErrorCodes) {
    MyErrorCodesEmptyString = 500,
    MyErrorCodesInvalidURL,
    MyErrorCodesUnableToReachHost,
};

Vous définiriez votre méthode qui peut générer une erreur comme ceci:

- (void)getContentsOfURL:(NSString *)path success:(void(^)(NSString *html))success failure:(void(^)(NSError *error))failure {
    if (path.length == 0) {
        if (failure) {
            failure([NSError errorWithDomain:@"com.example" code:MyErrorCodesEmptyString userInfo:nil]);
        }
        return;
    }

    NSString *htmlContents = @"";

    // Exercise for the reader: get the contents at that URL or raise another error.

    if (success) {
        success(htmlContents);
    }
}

Et puis, lorsque vous l'appelez, vous n'avez pas à vous soucier de déclarer l'objet NSError (l'achèvement du code le fera pour vous) ou de vérifier la valeur renvoyée. Vous pouvez simplement fournir deux blocs: un qui sera appelé lorsqu'il y aura une exception et un qui sera appelé quand il réussira:

[self getContentsOfURL:@"http://google.com" success:^(NSString *html) {
    NSLog(@"Contents: %@", html);
} failure:^(NSError *error) {
    NSLog(@"Failed to get contents: %@", error);
    if (error.code == MyErrorCodesEmptyString) { // make sure to check the domain too
        NSLog(@"You must provide a non-empty string");
    }
}];
Sensé
la source
0

Eh bien, c'est un peu hors de portée, mais si vous n'avez pas d'option pour NSError, vous pouvez toujours afficher l'erreur de bas niveau:

 NSLog(@"Error = %@ ",[NSString stringWithUTF8String:strerror(errno)]);
Mike.R
la source
0
extension NSError {
    static func defaultError() -> NSError {
        return NSError(domain: "com.app.error.domain", code: 0, userInfo: [NSLocalizedDescriptionKey: "Something went wrong."])
    }
}

que je peux utiliser NSError.defaultError()chaque fois que je n'ai pas d'objet d'erreur valide.

let error = NSError.defaultError()
print(error.localizedDescription) //Something went wrong.
Hemang
la source