NSURLSession delegation: How to implement my custom SessionDelegate class accordingly?
Asked Answered
B

3

5

Got a singleton class, so called RequestManager, which shall handle requests made by different modules and background tasks of my application.

@interface RequestFactory : NSObject

- (void)requestDataWith:(NSString *)token
                     id:(NSString *)id
                 sender:(id<RequestFactoryDelegate>)sender;
...

@end

Then I got another class, so called SessionDelegate, which shall handle all the callbacks during the request.

@interface SessionDelegate : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate>

@property (weak, nonatomic) id <RequestFactoryDelegate> delegate;

@end

My idea is to encapsulate the functions in these classes to not overload my classes, because I need a lot of helper classes with CommonCrypto and so on.

So I set quickly coded a protocol RequestFactoryDelegate to send the received data to the sender who initiated the origin request.

- (void)requestDataWith:(NSString *)token
                     id:(NSString *)id
                 sender:(id<RequestFactoryDelegate>)sender
{
    self.sessionDelegate.delegate = sender;

    NSMutableURLRequest *request = //create the request here

    NSURLSessionDataTask *dataTask = [self.defaultSession dataTaskWithRequest:request];
   [dataTask resume];
}

Well, it works if I have an object, let us call it senderA which sends the requests, because the set delegate is always senderA itself.

The problem occurs having another object, e.g. senderB which sends requests - not even at the same time - but very shortly after senderA send.

- (void)foo
{
    [requestFactory requestDataWith:token
                                 id:id
                             sender:senderA]; // let's assume this takes 20s

    [requestFactory requestDataWith:token
                                 id:id
                             sender:senderB]; // let's assume this takes 1s
 }

Because the request of senderA is still in progress, senderB sets the delegate to him and what happens is the delegate function of senderB is run twice.

 <senderB>
 <senderB>

Well... I really need to implement an own custom delegate (whether or not in the same class as the RequestFactory or not), but how to I handle the callback methods so I can respond properly to either senderA or senderB?

My last idea is to override the NSURLSessionTasks class and implement an own delegate property or block property or whatever.

Many thanks in advance.

Birdwell answered 24/9, 2013 at 21:19 Comment(4)
I think it's worth clarifying that the single delegate of a NSURLSession object MUST handle ALL tasks. Additionally NSURLSessionTasks cannot be subclassed - and also has no such "user context" property. IMHO, this is a design flaw - but alas, I'm not Apple. Your design needs to consider this.Fondness
TBH I really like this design pattern of NSURLSession since I know how to handle it properly. The idea all task use one session object is really cool, the advantages can you read up checking the WWDC2013 videos.Birdwell
Then why did you ask? Well, anyway - the issue is NOT that there is a session and one or more session tasks. That's actually a good separation. What I mean is exactly where you had your problems. You can solve that in a few ways. Using a dictionary as you did is a viable solution - but I consider it a "hack", due to the limitations given by the APIs. Nonetheless, I do the same ;)Fondness
@Fondness Actually you can add a "user context" to an NSURLSessionTask, by attaching an arbitrary property value to its NSURLRequest.Knute
F
5

In Objective-C, there is an alternative to subclassing that might be what you want here: associating objects.

It works like this: you can "attach" (associate) an object to another object with a custom key and later retrieve it. So in your case, you would do something like:

#include <objc/runtime.h>

// Top level of your .m file. The type and content of this
// variable don't matter much, we need the _address_ of it.
// See the first link of this answer for details.
static char kDelegateKey = 'd';

- (void)requestDataWith:(NSString *)token
                     id:(NSString *)id
                 sender:(id<RequestFactoryDelegate>)sender
{
    NSMutableURLRequest *request = //create the request here

    NSURLSessionDataTask *dataTask = [self.defaultSession dataTaskWithRequest:request];

   // Associate the sender with the dataTask. We use "assign" here
   // to avoid retain cycles as per the delegate pattern in Obj-C.
   objc_setAssociatedObject(dataTask, &kDelegateKey, sender, OBJC_ASSOCIATION_ASSIGN);

   [dataTask resume];
}

- (void)someOtherMethodWithDataTask:(NSURLSessionDataTask *)dataTask
{
    // Read the attached delegate.
    id<RequestFactoryDelegate> delegate = objc_getAssociatedObject(dataTask, &kDelegateKey);

    // Do something with the delegate.
}
Fiddling answered 24/9, 2013 at 21:46 Comment(8)
Thank you very much, proper answer from a Munich-being ;) By the way, is there another convenience way to handle the callback from the delegate?Birdwell
Well, instead of using an interface you can also use a block. Objective-C allows you to treat blocks like objects, so you can do: objc_setAssociatedObject(foo, &kKey, aBlock, OBJC_ASSOCIATION_COPY); MyBlockType block = objc_getAssocatedObject(foo, &kKey); block(bar);Fiddling
Well, your method does not work - I just implemented it, but the return value of the getAssociatedObject function is nil. Furthermore, what I meant is, is there another pattern one shall use implementing NSURLSession? For instance I did not find anything in the documentation of the class concerning custom delegation...Birdwell
Okay I found out that the associated object gets released and becomes nil calling it from another class - logical.Birdwell
Finally got a solution for the problem, quite simple. Do you know where is the best place to call the method finishTasksAndCancel? I mean it does not make sense to call it by the end of the function where you create the request... And as long as the requestFactory is a singleton for every kind of instance networking :/Birdwell
Sorry, I have no idea what finishTasksAndCancel is or does. And I haven't worked with NSURLSession yet.Fiddling
No problem, it lets all task finish and cancels the session afterwards. But I really like your approach, because I can use it for something else - love this runtime library :)Birdwell
Underrated answer; +1Longhair
K
11

You can attach an arbitrary object to a task:

NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:url];
id whatever = // anything at all!
[NSURLProtocol setProperty:whatever forKey:@"thing" inRequest:req];
NSURLSessionDownloadTask* task = [[self session] dataTaskWithRequest:req];

And retrieve it later (in the delegate) like this:

NSURLRequest* req = task.originalRequest;
id thing = [NSURLProtocol propertyForKey:@"thing" inRequest:req];

The value here (whatever) can be any object. It can be a completion handler callback - I've done this and it works just fine.

Knute answered 14/11, 2013 at 17:48 Comment(7)
one can also use task.taskDescription propertyGunsel
@JohnSmith Not if it isn't an NSString. In my answer I give the example attaching a completion handler (a block) - you can't do that with a mere taskDescription. The point of my answer is that it is general.Knute
yes, of course, I pointed that out, for future readers, as a much more simpler example to fetch a property attached to a task. Thanks for a great answer, didn't know about that before.Gunsel
@JohnSmith But that's irrelevant to what the original questioner asked (and to what I answered). The existence of taskDescription is obvious, but it has nothing to do with what this page is about.Knute
ok. by the way I just verified that extra properties does not get remembered when the app reconnects the session.Gunsel
@JohnSmith Interesting. I never tried that. So that would be a weakness of my approach; I guess this is because a whole new URL request is generated. The "associated object" approach, however, should not suffer from this weakness. Thanks for pointing that out!Knute
For me, storing simple NSString objects as @Knute states works great, even if the app reconnects the session due to a background upload/download finished event. So it looks like these extra properties are serialized by the OS in an NSURLSession with a background configuration. Before, I tried without success storing simple NSString objects using the task.taskDescription property, but when the app reconnects the session, I could not retrieve the extra properties. Hopes this helps to others in the same scenario as me :-)Kalinin
F
5

In Objective-C, there is an alternative to subclassing that might be what you want here: associating objects.

It works like this: you can "attach" (associate) an object to another object with a custom key and later retrieve it. So in your case, you would do something like:

#include <objc/runtime.h>

// Top level of your .m file. The type and content of this
// variable don't matter much, we need the _address_ of it.
// See the first link of this answer for details.
static char kDelegateKey = 'd';

- (void)requestDataWith:(NSString *)token
                     id:(NSString *)id
                 sender:(id<RequestFactoryDelegate>)sender
{
    NSMutableURLRequest *request = //create the request here

    NSURLSessionDataTask *dataTask = [self.defaultSession dataTaskWithRequest:request];

   // Associate the sender with the dataTask. We use "assign" here
   // to avoid retain cycles as per the delegate pattern in Obj-C.
   objc_setAssociatedObject(dataTask, &kDelegateKey, sender, OBJC_ASSOCIATION_ASSIGN);

   [dataTask resume];
}

- (void)someOtherMethodWithDataTask:(NSURLSessionDataTask *)dataTask
{
    // Read the attached delegate.
    id<RequestFactoryDelegate> delegate = objc_getAssociatedObject(dataTask, &kDelegateKey);

    // Do something with the delegate.
}
Fiddling answered 24/9, 2013 at 21:46 Comment(8)
Thank you very much, proper answer from a Munich-being ;) By the way, is there another convenience way to handle the callback from the delegate?Birdwell
Well, instead of using an interface you can also use a block. Objective-C allows you to treat blocks like objects, so you can do: objc_setAssociatedObject(foo, &kKey, aBlock, OBJC_ASSOCIATION_COPY); MyBlockType block = objc_getAssocatedObject(foo, &kKey); block(bar);Fiddling
Well, your method does not work - I just implemented it, but the return value of the getAssociatedObject function is nil. Furthermore, what I meant is, is there another pattern one shall use implementing NSURLSession? For instance I did not find anything in the documentation of the class concerning custom delegation...Birdwell
Okay I found out that the associated object gets released and becomes nil calling it from another class - logical.Birdwell
Finally got a solution for the problem, quite simple. Do you know where is the best place to call the method finishTasksAndCancel? I mean it does not make sense to call it by the end of the function where you create the request... And as long as the requestFactory is a singleton for every kind of instance networking :/Birdwell
Sorry, I have no idea what finishTasksAndCancel is or does. And I haven't worked with NSURLSession yet.Fiddling
No problem, it lets all task finish and cancels the session afterwards. But I really like your approach, because I can use it for something else - love this runtime library :)Birdwell
Underrated answer; +1Longhair
B
0

Here is my solution.

I just just use the unique identifier of each sessionTask object. So my delegate object contains a dictionary with blocks as values to execute on success/failure and the identifier as keys to identify the correct execution block.

In the .h file I declared the dictionary and a method to add a key/value object:

@property (nonatomic, strong) NSDictionary *completionHandlerDictionary;

- (void)addCompletionHandler:(CompletionHandlerType)handler
                     forTask:(NSString *)identifier;

And in the .m file I call the handler block.

- (void)addCompletionHandler:(CompletionHandlerType)handler
                  forTask:(NSString*)identifier
{
    if ([self.completionHandlerDictionary objectForKey:identifier]) {
        NSLog(@"Error: Got multiple handlers for a single task identifier. This should   not happen.\n");
    }

    [self.completionHandlerDictionary setObject:handler forKey:identifier];
}

- (void)callCompletionHandlerForTask:(NSString *)identifier
{
    CompletionHandlerType handler = [self.completionHandlerDictionary   objectForKey:identifier];

    if (handler) {
        [self.completionHandlerDictionary removeObjectForKey: identifier];
        NSLog(@"Calling completion handler.\n");

        handler();
    }
}

That's it, simple as it is.

Birdwell answered 25/9, 2013 at 8:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.