Como posso usar o NSError no meu aplicativo para iPhone?

228

Estou trabalhando na captura de erros no meu aplicativo e estou pensando em usá-lo NSError. Estou um pouco confuso sobre como usá-lo e como preenchê-lo.

Alguém poderia fornecer um exemplo de como eu preencho e uso NSError?

Nic Hubbard
fonte

Respostas:

473

Bem, o que eu costumo fazer é ter meus métodos que poderiam gerar erros no tempo de execução fazer referência a um NSErrorponteiro. Se algo realmente der errado nesse método, eu posso preencher a NSErrorreferência com dados de erro e retornar nulo do método.

Exemplo:

- (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;
}

Podemos então usar o método como este. Nem se preocupe em inspecionar o objeto de erro, a menos que o método retorne zero:

// 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.

Conseguimos acessar os erros localizedDescriptionporque definimos um valor para NSLocalizedDescriptionKey.

O melhor lugar para obter mais informações é a documentação da Apple . É realmente bom.

Há também um tutorial simples e agradável sobre Cocoa Is My Girlfriend .

Alex
fonte
37
este é o exemplo mais engraçado, sempre
yow mingow
Esta é uma resposta bastante impressionante, embora haja alguns problemas no ARC e na conversão de idpara a BOOL. Qualquer leve variação compatível com ARC seria muito apreciada.
NSTJ
6
@ TomJowett Eu ficaria muito chateado se acabarmos não sendo capazes de acabar com a fome no mundo simplesmente porque a Apple nos pressionou a mudar para o mundo mais novo do ARC.
Manav
1
o tipo de retorno pode ser BOOL. Retorne NOem caso de erro e, em vez de verificar o valor de retorno, basta verificar error. Se nilseguir em frente, se != nillidar com isso.
Gabriele Petronella
8
-1: você realmente precisa incorporar o código que verifica **errorse não é nulo. Caso contrário, o programa lançará um erro que é completamente hostil e não torna aparente o que está acontecendo.
FreeAsInBeer
58

Gostaria de adicionar mais algumas sugestões com base na minha implementação mais recente. Eu olhei para alguns códigos da Apple e acho que meu código se comporta da mesma maneira.

As postagens acima já explicam como criar objetos NSError e devolvê-los, para não me preocupar com essa parte. Vou apenas tentar sugerir uma boa maneira de integrar erros (códigos, mensagens) em seu próprio aplicativo.


Eu recomendo criar um cabeçalho que seja uma visão geral de todos os erros do seu domínio (por exemplo, aplicativo, biblioteca etc.). Meu cabeçalho atual é assim:

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

Agora, ao usar os valores acima para erros, a Apple criará uma mensagem de erro padrão básica para seu aplicativo. Um erro pode ser criado como o seguinte:

+ (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;
}

A mensagem de erro padrão gerada pela Apple ( error.localizedDescription) para o código acima será semelhante à seguinte:

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

O exposto acima já é bastante útil para um desenvolvedor, pois a mensagem exibe o domínio em que o erro ocorreu e o código de erro correspondente. Os usuários finais não têm idéia do 1002significado do código de erro , então agora precisamos implementar algumas boas mensagens para cada código.

Para as mensagens de erro, precisamos manter a localização em mente (mesmo que não implementemos mensagens localizadas imediatamente). Eu usei a seguinte abordagem no meu projeto atual:


1) crie um stringsarquivo que conterá os erros. Arquivos de strings são facilmente localizáveis. O arquivo pode ter a seguinte aparência:

FSError.strings

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

2) Adicione macros para converter códigos inteiros em mensagens de erro localizadas. Eu usei 2 macros no meu arquivo Constants + Macros.h. Eu sempre incluo esse arquivo no cabeçalho do prefixo ( MyApp-Prefix.pch) por conveniência.

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) Agora é fácil mostrar uma mensagem de erro amigável com base em um código de erro. Um exemplo:

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" 
            message:FS_ERROR_LOCALIZED_DESCRIPTION(error.code) 
            delegate:nil 
            cancelButtonTitle:@"OK" 
            otherButtonTitles:nil];
[alert show];
Wolfgang Schreurs
fonte
9
Ótima resposta! Mas por que não colocar a descrição localizada no dicionário de informações do usuário a que pertence? [NSError errorWithDomain: código FSMyAppErrorDomain: FSProfileParsingFailedError userInfo: @ {NSLocalizedDescriptionKey: FS_ERROR_LOCALIZED_DESCRIPTION (error.code)}];
Richard Venable
1
Existe algum lugar em particular onde eu devo colocar o arquivo de string? De FS_ERROR_LOCALIZED_DESCRIPTION (), estou obtendo apenas o número (código de erro).
huggie
@ huggie: não tenho muita certeza do que você quer dizer. Normalmente, coloco essas macro que uso em todo o aplicativo em um arquivo chamado Constants+Macros.he importo esse arquivo no cabeçalho do prefixo ( .pcharquivo) para que fique disponível em qualquer lugar. Se você quer dizer que está usando apenas uma das duas macros, isso pode funcionar. Talvez a conversão de intpara NSStringnão seja realmente necessária, embora eu não tenha testado isso.
Wolfgang Schreurs
@ huggie: ai, acho que te entendo agora. As strings devem estar em um arquivo localizável ( .stringsarquivo), pois é para lá que a macro da Apple ficará. Leia sobre como usar NSLocalizedStringFromTableaqui: developer.apple.com/library/mac/documentation/cocoa/conceptual/…
Wolfgang Schreurs
1
@ huggie: Sim, eu usei as tabelas de string localizadas. O código na macro FS_ERROR_LOCALIZED_DESCRIPTIONverifica a cadeia localizável em um arquivo chamado FSError.strings. Você pode conferir o guia de localização da Apple em .stringsarquivos, se isso lhe for estranho.
Wolfgang Schreurs
38

Ótima resposta Alex. Um problema em potencial é a desreferência nula. Referência da Apple sobre como criar e retornar objetos 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
fonte
30

Objetivo-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
fonte
9

Consulte o seguinte tutorial

Espero que seja útil para você, mas antes você precisa ler a documentação do NSError

Este é um link muito interessante que encontrei recentemente ErrorHandling

Tirth
fonte
3

Tentarei resumir a ótima resposta de Alex e o argumento do jlmendezbonini, adicionando uma modificação que tornará tudo compatível com o ARC (até agora não é porque o ARC irá reclamar, pois você deve retornar id, o que significa "qualquer objeto", mas BOOLnão é um objeto tipo).

- (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;
}

Agora, em vez de verificar o valor de retorno de nossa chamada de método, verificamos se errorainda está nil. Se não for, temos um problema.

// 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
fonte
3
@Gabriela: A Apple afirma que, ao usar variáveis ​​indiretas para retornar erros, o próprio método sempre deve ter algum valor de retorno em caso de sucesso ou falha. A Apple pede que os desenvolvedores primeiro verifiquem o valor de retorno e somente se o valor de retorno for de alguma forma inválido, verifique se há erros. Veja a seguinte página: developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…
Wolfgang Schreurs 31/12/12
3

Outro padrão de design que eu já vi envolve o uso de blocos, o que é especialmente útil quando um método está sendo executado de forma assíncrona.

Digamos que tenhamos os seguintes códigos de erro definidos:

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

Você definiria seu método que pode gerar um erro da seguinte maneira:

- (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);
    }
}

E quando você o chama, não precisa se preocupar em declarar o objeto NSError (a conclusão do código fará isso por você) ou em verificar o valor retornado. Você pode fornecer apenas dois blocos: um que será chamado quando houver uma exceção e outro que será chamado quando for bem-sucedido:

[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ível
fonte
0

Bem, está um pouco fora do escopo da pergunta, mas, caso você não tenha uma opção para o NSError, sempre poderá exibir o erro de nível baixo:

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

que posso usar NSError.defaultError()sempre que não tiver um objeto de erro válido.

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