iOS8 Weirdness Part4: URLSessionTask

This post is one of those short ones. Again I’m confused how switching from one iOS to another may transform your day to a horror. So what is this all about, if you are typical user of NSURLConnection you should acutally know that Apple introduces NSURLSession as a new way to handle networking. And if somethings goes wrong you need to handle it somehow…

And this is where all things starting to be tricky; typically in iOS7 when you get an error from your request you handle it in some fail block passing NSError so my today talk is actually about NSError. Sometimes you need to handle paritcular error in way that is more readable for your user eg.: connecting to VPN, Timeouting etc. So let’s say the error that is contained in NSError class need to be modified to something more readable eg by using CODE and DOMAIN to specify what we should actually show to the user. But in most of cases NSError contains quiet good description and it’s even localized! But not in iOS8…

Some time ago i saw empty UIAlertView in some of the app i was working on. So i start digging what acutally happens, few minutes with debugger show me that NSURLSessionTask retuns NSError if something fails but it do not contains NSLocalizedDescriptionKey in userInfo the key is acutally missing! And using localizedDescription property also retuns some very unfriendly messages. So lets see what you will see in debugger:

iOS7.1:

1
2
3
4
5
6
7
(lldb) po self.userInfo
{
    NSErrorFailingURLKey = "https://yourservername.com:8080/login/";
    NSErrorFailingURLStringKey = "https://yourservername.com:8080/login/";
    NSLocalizedDescription = "A server with the specified hostname could not be found.";
    NSUnderlyingError = "Error Domain=kCFErrorDomainCFNetwork Code=-1003 \"A server with the specified hostname could not be found.\" UserInfo=0x7ce56df0 {NSErrorFailingURLKey=https://yourservername.com:8080/login/, NSErrorFailingURLStringKey=https://yourservername.com:8080/login/, NSLocalizedDescription=A server with the specified hostname could not be found.}";
}

As you can see we can find NSLocalizedDescription ther. Now let’s try to run same code on iOS8:

1
2
3
4
5
6
7
8
(lldb) po self.userInfo
{
    NSErrorFailingURLKey = "https://yourservername.com:8080/login/";
    NSErrorFailingURLStringKey = "https://yourservername.com:8080/login/";
    NSUnderlyingError = "Error Domain=kCFErrorDomainCFNetwork Code=-1003 \"The operation couldn\U2019t be completed. (kCFErrorDomainCFNetwork error -1003.)\" UserInfo=0x7c31fad0 {_kCFStreamErrorDomainKey=12, _kCFStreamErrorCodeKey=8}";
    "_kCFStreamErrorCodeKey" = 8;
    "_kCFStreamErrorDomainKey" = 12;
}

As i said no NSLocalizedDescription at all, the worst thing is that no matter error your connection will generate, it will be always called the same “The operation couldn’t be completed.” So if you have some production build and you try to get some info from your user, you are in serious trouble. The only way to fix this for now is to build some extension that will convert CODE and DOMAIN to apropriate error message.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@import UIKit;

//some typical error codes
const NSInteger kServerNotFoundCode = -1003;
const NSInteger kVPNCode = -1005;
const NSInteger kTimeoutCode = -1001;

@implementation NSError (Utilities)

- (void)getDisplayTitle:(NSString * __autoreleasing *)title message:(NSString * __autoreleasing *)message

{

    *title = NSLocalizedString(@"error_label_title",nil);

      if ([self.domain isEqualToString:NSURLErrorDomain] == YES) {

            switch( self.code ) {

              case kVPNCode :
                  *message = NSLocalizedString(@"error_label_message_vpnnotenabled", nil);
                  break;

              case kServerNotFoundCode:
                  *message = NSLocalizedString(@"error_label_message_servernotfound", nil);
                  break;

              case kTimeoutCode:
                  *message = NSLocalizedString(@"error_label_message_requesttimeout", nil);
                  break;

                  case default:
                      *message = self.localizedDescription;
                      break;
            }

        }
        else {

           // unknow error or we don't know how to handle this display ugly info.
           *message = self.localizedDescription;
        }

}

And how to use it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)failWithError:(NSError *)error
{

   NSString *title, *message;

    [error getDisplayTitle:&title
                 message:&message];

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title
                                                    message:message
                                                   delegate:nil
                                          cancelButtonTitle:NSLocalizedString(@"error_label_ok", nil)
                                          otherButtonTitles:nil];
    [alert show];
}

Of course you may need to handle way more cases here so it is good to check all code list in documentation. In this example im using localization to support multiple languages by myself if you do not need to do that just simply type there your description.

And that’a all for today, next time some swift cool features.