I am converting my application from Syncronous to Asyncronous HTTP requests and have ran into a problem that looks like it will require quite a big reworking of how the application handles its data. Let me try to explain
Previously it was like this:
-Class1
, Class2
and Class3
were all subclasses of UIViewController
-Helper class
-Content display class
They do broadly different things but the common trait is their interaction with the helper class. They gather details of a request in a number of different ways from a user and then eventually send a request to the helper class.
When it was done syncronously the helper class would return the data. Each class would then interpret the data (XML files) and pass them on to the Content display class via a segue
So something broadly like this:
Class1:
//Get user input
SomeData *data = [helperclass makerequest];
id vcData = [data process];
[self performSegueWithIdentifier:@"segueIdentifier"];
---
- (void)prepareForSegue:(UIStoryboardSegue *)segue
{
DestinationViewController *destination = (DestinationViewController *)segue.destinationViewController;
destination.data = vcData;
}
Content display class:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.data presentdata];
}
Now it looks like this
I dealt with this problem by first making it work with Class1 with a view to deploying the fix to class2 and class3. So class1 and helper now interact like this
Class1:
//Get user input
SomeData *data = [helperclass makerequestWithSender:self];
id vcData = [data process];
[self performSegueWithIdentifier:@"segueIdentifier"];
---
- (void)prepareForSegue:(UIStoryboardSegue *)segue
{
DestinationViewController *destination = (DestinationViewController *)segue.destinationViewController;
destination.data = vcData;
}
Now the biggest problem I am facing is how to get the data from helperclass back to Class1
. I managed to get it to work by doing
(void)makeRequestWithSender:(Class1*)sender
{
[NSURLConnection sendAsynchronousRequest:...
{
[sender sendData:data];
}
}
However, when I have came to roll this out to the other 2 GUI classed which will compose the request I am having difficulty with. My first thought was to set sender:(id)
but that fails at the line [sender sendData:data]
telling me that id
does not have an method sendData:
or similar.
Hopefully I wasn't too vague here and you guys can help. If required I will be able to post code snippets but for now can anyone help with a better suggestion about how to structure the code for this request?
You basically want to use the 'observer pattern' or a (maybe) slightly changed setup, so you can use delegation.
Observer pattern
You gain the mechanic via the NSNotificationCenter and NSNotifications. Your 3 different UIViewController subclasses each subscribe to a specific NSNotification and you notify them via posting a notification via the NSNotificationCenter.
The following code is an example of how you can approach the problem in your viewcontroller subclasses:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// subscribe to a specific notification
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(doSomethingWithTheData:) name:@"MyDataChangedNotification" object:nil];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// do not forget to unsubscribe the observer, or you may experience crashes towards a deallocated observer
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
...
- (void)doSomethingWithTheData:(NSNotification *)notification {
// you grab your data our of the notifications userinfo
MyDataObject *myChangedData = [[notification userInfo] objectForKey:@"myChangedDataKey"];
...
}
In your helper class, after the data changed you have to inform the observers, e.g.
-(void)myDataDidChangeHere {
MyDataObject *myChangedData = ...;
// you can add you data to the notification (to later access it in your viewcontrollers)
[[NSNotificationCenter defaultCenter] postNotificationName:@"MyDataChangedNotification" object:nil userInfo:@{@"myChangedDataKey" : myChangedData}];
}
via @protocol
Presuming all your UIViewController subclasses reside in a parent viewcontroller, you can implement a protocol in your helper class and make the parent viewcontroller the delegate. Then the parent viewcontroller may inform the child uiviewcontrollers via passing a message.
Your helper class declaration could look like this (presuming ARC):
@protocol HelperDelegate;
@interface Helper : NSObject
@property (nonatomic, weak) id<HelperDelegate> delegate;
...
@end
@protocol HelperDelegate <NSObject>
-(void)helper:(Helper *)helper dataDidChange:(MyDataObject*)data;
@end
In the helper implementation you would inform the delegate via:
...
if ([self.delegate respondsToSelector:@selector(helper:dataDidChange:)]) {
[self.delegate helper:self dataDidChange:myChangedDataObject];
}
...
Your parent viewcontroller would need to be the delegate of the helper class and implement its protocol; a rough sketch, in the declaration
@interface ParentViewController : UIViewController <HelperDelegate>
and for the implementation in short version
// you alloc init your helper and assign the delegate to self, also of course implement the delegate method
-(void)helper:(Helper *)helper dataDidChange:(MyDataObject*)data {
[self.myCustomChildViewController doSomethingWithTheNewData:data];
}
Besides..
You might ask yourself which method to prefer. Both are viable, the main difference is that via the observer pattern you get more objects to be informed 'at once', whereas a protocol can only have one delegate and that one has to forward the message if needed. There are a lot of discussions around about pros and cons. I'd suggest you read up on them once you made up your mind (sorry ain't got enough reputation to post more than two links, so please search on stackoverflow). If something is unclear, please ask.
Some reasonable ideas here. To elaborate/add my opinion:
First, which object ought to tell the downloader (HelperClass
) to begin downloading? My practice is to do this in the view controller that will present the data. So I generally start network requests after a segue (like in viewWillAppear:
of the presented vc), not before.
Next, when one class needs to execute code provided for another, I first think about if it makes sense to do it using a block
. Very often (not always) blocks make more sense and provide more readable code than, say, delegate, notification, KVO, etc. I think NSURLConnection completion, for example, is better suited to blocks than delegate. (and Apple kind of agrees, having introduced + (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler
).
So my pattern for your app would be this:
// Class1.m
// when user has completed providing input
...
// don't do any request yet. just start a segue
[self performSegueWithIdentifier:@"ToContentDisplayClass" sender:self];
...
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// don't do a request yet, just marshall the data needed for the request
// and send it to the vc who actually cares about the request/result
if ([segue.identifier isEqualToString:@"ToContentDisplayClass"]) {
NSArray *userInput = // collect user input in a collection or custom object
ContentDisplayClass *vc = segue.destinationViewController;
vc.dataNeededForRequest = userInput;
}
...
Then in ContentDisplayClass.m
// this is the class that will present the result, let it make the request
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
HelperClass *helper = [[HelperClass alloc]
initWithDataNeededForRequest:self.dataNeededForRequest];
// helper class forms a request using the data provided from the original vc,
// then...
[helper sendRequestWithCompletion:^(NSURLResponse *response, NSData *data, NSError *error) {
if (!error) {
// interpret data, update view
self.label.text = // string we pulled out of data
} else {
// present an AlertView? dismiss this vc?
}
}];
This depends on HelperClass
implementing the block
form of NSURLConnection
// HelperClass.m
- (id)initWithDataNeededForRequest:(id)dataNeededForRequest {
// standard init pattern, set properties from the param
}
- (void)sendRequestWithCompletion:(void (^)(NSURLResponse *, NSData *, NSError *))completion {
NSURLRequest *request = ...
// the stuff we need to formulate the request has been setup in init
// use NSURLConnection block method
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:completion];
}
Edit - there are several rationale's for making the VC transition before starting the network request:
1) Build the standard behavior around the success case: unless the app is about testing network connections, the success case is that the request works.
2) The cardinal principal for an app is to be responsive, to do something sensible immediately upon user actions. So when the user does something to initiate the request, an immediate vc transition is good. (what instead? a spinner?). The newly presented UI might even reduce the perceived latency of the request by giving user something new to look at while it runs.
3) What should an app do when a request fails? If the app doesn't really need the request to be useful, then doing nothing is a good option, so you'd want to be on the new vc. More typically, the request is necessary to proceed. The UI should be "responsive" to request failure, too. Typical behavior is to present an alert that offers some form of "retry" or "cancel". For either choice, the place the UI wants to be is on the new vc. Retry is more obvious, because that's where it always is when it tries to fetch the data. For cancel, the way to be "responsive" to cancel is to go back to the old vc, a vc transition back isn't ugly, it's what the user just asked for.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With