1

In a little iOS app, I need to retrieve some JSON from a server (similar to a DNS request), and use this response to initiate further connections. To do so, I have an object that cares for the JSON request, using NSURLSession. This object receives the main object (MainController) as delegate, and should call a method, when the JSON data is available. Here's the call in the MainController:

- (void)serverResolve:(NSString *)serverID withPass:(NSString *)pass {
    // Must retrieve the server list from JSON
    ServerListRetriever *slCom = [[ServerListRetriever alloc] init];
    slCom.delegate = self;
    [slCom searchServer:serverID usingPass:pass];
}

My problem is that - as soon as the completionHandler - comes into action, the MainController won't do what it should. And I have no idea why???

This is the ServerListRetriever's content (header):

#import "ServerListRetrieverDelegate.h"

@interface ServerListRetriever : NSObject
@property (assign, nonatomic) id <ServerListRetrieverDelegate> delegate;

- (void)searchServer:(NSString *)serverID usingPass:(NSString *)pass;
@end

... (and implementation)

#import "ServerListRetriever.h"
#define SERVER_LIST @"https://www.example.com/hosts.json"
@implementation ServerListRetriever

- (void)searchServer:(NSString *)serverID usingPass:(NSString *)pass
{
    // This will work...
    [self.delegate serverConnect:@"https://www.example.net/" withPass:pass];
    return;

    // And if removing the above two lines, this won't work
    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithURL:[NSURL URLWithString:SERVER_LIST]
            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                // Handle response
                if (error) {
                    [self.delegate serverListFailedWithError:error];
                } else {
                    // NSLog(@"Retrieved JSON: %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);

                    // Extract the server URL
                    NSError *jsonError = nil;
                    NSDictionary *parsedObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];

                    if (jsonError) {
                        [self.delegate serverListFailedWithError:jsonError];
                        return;
                    }

                    NSString *serverURL = [parsedObject objectForKey:serverID];
                    NSLog(@"Resolved server URL: %@", serverURL);

                    // Continue with connection
                    [self.delegate serverConnect:serverURL withPass:pass];
                }
            }] resume];
}

@end

The method serverConnect will be run in both cases, but when called by the completionHandler, a lot of thinks will not work correctly then. I have checked the delegate via debugging, and it has the same ID in both cases.

As I am still struggling with the memory management on iOS, losing some object to early could be an explanation. Yet, I lack the experience to locate the problem. Thanks for ideas and advice!

MrDream
  • 9
  • 2
BurninLeo
  • 4,240
  • 4
  • 39
  • 56
  • In the meanwhile, I also tried getting rid of the extra class `ServerListRetriever` and write it's code directly into the MainViewController. This means that no delegate is used at all. No change. – BurninLeo May 05 '17 at 22:00

2 Answers2

0

There's an ongoing debate about whether delegates should be strong or not. If they're weak, then you guarantee that you won't get retain loops, but that also means that if the delegate isn't being retained by somebody else, it can go away before asynchronous operations finish.

The way this is usually handled is by making a strong internal copy of the delegate, e.g.

@interface ServerListRetriever ()
@property (strong, nonatomic) id <ServerListRetrieverDelegate> strongDelegate;
@end

...

// When an operation is started:
self.strongDelegate = self.delegate;

// When the *last* operation ends (keep an array of operations or something):
self.strongDelegate = nil;

Or you can mark the public delegate property as strong, but you'll need to explicitly either nil out the ServerListRetriever's delegate when you're done with it or nil out the last reference to the ServerListRetriever object itself.

dgatwood
  • 10,129
  • 1
  • 28
  • 49
0

Actually, I do not yet fully understand the solution ... but the trick is to handle the response outside of the main thread. See Running NSURLSession completion handler on main thread

/**
 * Resolve the server URL by the server ID, and then connect.
 */
- (void)serverResolve:(NSString *)serverID withPass:(NSString *)pass {
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *download = [session dataTaskWithURL:[NSURL URLWithString:SERVER_LIST]
            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                // Handle response
                if (error) {
                    [self serverListFailedWithError:error];
                } else {
                    // Extract the server URL
                    NSError *jsonError = nil;
                    NSDictionary *parsedObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];

                    if (jsonError) {
                        [self serverListFailedWithError:jsonError];
                        return;
                    }

                    // Working out of the main thread did the trick 
                    dispatch_async(dispatch_get_main_queue(), ^{
                        NSString *serverURL = [parsedObject objectForKey:serverID];
                        [self serverConnect:serverURL withPass:pass];
                    });
                }
            }];

    [download resume];
}
Community
  • 1
  • 1
BurninLeo
  • 4,240
  • 4
  • 39
  • 56