Using delegates, operations, and queues - objective-c

I am using the AWS SDK for iOS to upload and download files to and from local hard drive to Amazon S3 storage. I am capable of making this work but I am unable to get the S3 delegate to respond properly to alert me when operations have finished or resulted in an error.
I have an array of files that I want to upload. For each file I create a NSOperation where the main routine consist mostly of:
AmazonCredentials * credentials = [[AmazonCredentials alloc] initWithAccessKey:ACCESS_KEY_ID withSecretKey:SECRET_KEY];
putObjectRequest = [[S3PutObjectRequest alloc] initWithKey:pathCopy inBucket:[self bucket]];
putObjectRequest.filename = pathSource;
putObjectRequest.credentials=credentials;
[putObjectRequest setDelegate:s3Delegate];
Here, the delegate (s3Delegate) is created as a regular AmazonServiceRequestDelegate which should be able to fire off responses when an operation has finished. Each of my NSOperations are added to my NSOperationQueue which executes operations non-concurrently. If I use the delegate [putObjectRequest setDelegate:s3Delegate] the operations are not working. If I remove the use of the delegate the operations are performed correctly but I am unable to receive any responses to the operations as I do not have a delegate.
If I remove the use of the NSOperationQueue completely and use the [putObjectRequest setDelegate:s3Delegate] the delegate works perfectly.
My question is what am I doing wrong with using a delegate in a queue? Since the delegate is perfectly capable of performing while not in a queue could this be related to not performing on the main thread? I really want to be able to use the queue to limit the number of non-concurrent operations, however I am unable to figure this out. I hope someone has an idea of what is going on here and any example code would be greatly appreciated. Thanks!
Cheers, Trond

It seems that the aws sdk behaves asynchronously after the time you set your delegate.
So in order to have your asynchronous aws stuff work in a (asynchronous) NSOperation, you got to put some magic to wait for AWS to complete:
In your .h NSOperation file, add a boolean:
#interface UploadOperation : NSOperation <AmazonServiceRequestDelegate> {
#private
BOOL _doneUploadingToS3;
}
and in your .m file, your main method will look like this:
- (void) main
{
.... do your stuff …..
_doneUploadingToS3 = NO;
S3PutObjectRequest *por = nil;
AmazonS3Client *s3Client = [[AmazonS3Client alloc] initWithAccessKey:ACCESS_KEY withSecretKey:SECRET_KEY];
s3Client.endpoint = endpoint;
#try {
por = [[[S3PutObjectRequest alloc] initWithKey:KEY inBucket:BUCKET] autorelease];
por.delegate = self;
por.contentType = #"image/jpeg";
por.data = _imageData;
[s3Client putObject:por];
}
#catch (AmazonClientException *exception) {
_doneUploadingToS3 = YES;
}
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (!_doneUploadingToS3);
por.delegate = nil;
.... continue with your stuff ….
}
do not forget to implement your delegate methods
-(void)request:(AmazonServiceRequest *)request didCompleteWithResponse:(AmazonServiceResponse *)response
{
_doneUploadingToS3 = YES;
}
-(void)request:(AmazonServiceRequest *)request didFailWithError:(NSError *)error
{
_doneUploadingToS3 = YES;
}
-(void)request:(AmazonServiceRequest *)request didFailWithServiceException:(NSException *)exception
{
_doneUploadingToS3 = YES;
}
- (void) request:(AmazonServiceRequest *)request didSendData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite
{
// Do what you want
}
-(void)request:(AmazonServiceRequest *)request didReceiveResponse:(NSURLResponse *)response
{
// Do what you want
}
-(void)request:(AmazonServiceRequest *)request didReceiveData:(NSData *)data
{
// Do what you want
}
Note: this magic can work for any stuff that performs asynchronously but have to be implemented in a NSOperation.

Related

Obj-C return to a block from a delegate method?

I'm writing a mac app that runs its own web server, using the GCDWebServer library (https://github.com/swisspol/GCDWebServer). My app delegate handles GET requests like so:
__weak typeof(self) weakSelf = self;
[webServer addDefaultHandlerForMethod:#"GET"
requestClass:[GCDWebServerRequest class]
processBlock:^GCDWebServerResponse *(GCDWebServerRequest* request) {
return [weakSelf handleRequest:request];
}];
And then the handleRequest method returns the response data, something like:
return [GCDWebServerDataResponse responseWithHTML:#"<html><body><p>Hello World!</p></body></html>"];
So far so good. Except now I want the handleRequest method to use NSSpeechSynthesizer to create an audio file with some spoken text in it, and then wait for the speechSynthesizer:didFinishSpeaking method to be called before returning to the processBlock.
// NSSpeechSynthesizerDelegate method:
- (void)speechSynthesizer:(NSSpeechSynthesizer *)sender didFinishSpeaking:(BOOL)success
{
NSLog(#"did finish speaking, success: %d", success);
// return to processBlock...
}
Problem is, I have no idea how to do this. Is there a way to return from the speechSynthesizer:didFinishSpeaking method into the processBlock defined above?
You need to run the speech synthesizer on a separate thread with its own run loop, and use a lock to allow your request thread to wait for the operation to complete on the speech thread.
Assuming the web server maintains its own thread(s) and runloop, you can use your app's main thread to run the speech synthesizer, and you can use NSCondition to signal completion to the web response thread.
A basic (untested) example (without error handling):
#interface SynchroSpeaker : NSObject<NSSpeechSynthesizerDelegate>
- (id)initWithText:(NSString*)text outputUrl:(NSURL*)url;
- (void)run;
#end
#implementation SynchroSpeaker
{
NSCondition* _lock;
NSString* _text;
NSURL* _url;
NSSpeechSynthesizer* _synth;
}
- (id)initWithText:(NSString*)text outputUrl:(NSURL*)url
{
if (self = [super init])
{
_text = text;
_url = url;
_lock = [NSCondition new];
}
return self;
}
- (void)run
{
NSAssert(![NSThread isMainThread], #"This method cannot execute on the main thread.");
[_lock lock];
[self performSelectorOnMainThread:#selector(startOnMainThread) withObject:nil waitUntilDone:NO];
[_lock wait];
[_lock unlock];
}
- (void)startOnMainThread
{
NSAssert([NSThread isMainThread], #"This method must execute on the main thread.");
[_lock lock];
//
// Set up your speech synethsizer and start speaking
//
}
- (void)speechSynthesizer:(NSSpeechSynthesizer *)sender didFinishSpeaking:(BOOL)success
{
//
// Signal waiting thread that speaking has completed
//
[_lock signal];
[_lock unlock];
}
#end
It's used like so:
- (id)handleRequest:(id)request
{
SynchroSpeaker* speaker = [[SynchroSpeaker alloc] initWithText:#"Hello World" outputUrl:[NSURL fileURLWithPath:#"/tmp/foo.dat"]];
[speaker run];
////
return response;
}
GCDWebServer does run into its own threads (I guess 2 of them) - not in the main one. My solution needed to run code in Main Thread when calling the ProcessBlock.
I found this way that suits my needs:
First declare a weak storage for my AppDelegate: __weak AppDelegate *weakSelf = self;. Doing so I can access all my properties within the block.
Declare a strong reference to AppDelegate from within the block like so: __strong AppDelegate* strongSelf = weakSelf;
Use NSOperationQueue to align the operation on mainThread:
[[NSOperationQueue mainQueue] addOperationWithBlock:^ {
//Your code goes in here
NSLog(#"Main Thread Code");
[strongSelf myMethodOnMainThread];
}];
In this way myMethodOnMainThread surely will run where it's supposed to.
For sake of clarity I quote my relevant code section:
webServer = [[GCDWebServer alloc] init];
webServer.delegate = self;
__weak AppDelegate *weakSelf = self;
// Add a handler to respond to GET requests
[webServer addDefaultHandlerForMethod:#"GET"
requestClass:[GCDWebServerRequest class]
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
__strong AppDelegate* strongSelf = weakSelf;
[[NSOperationQueue mainQueue] addOperationWithBlock:^ {
//Your code goes in here
NSLog(#"Main Thread Code");
[strongSelf myMethodOnMainThread];
}];
GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithJSONObject:packet];
completionBlock(response);
}];
GCWebServer supports fully asynchronous responses as of version 3.0 and later [1].
[webServer addDefaultHandlerForMethod:#"GET"
requestClass:[GCDWebServerRequest class]
asyncProcessBlock:^(GCDWebServerRequest* request, GCDWebServerCompletionBlock completionBlock) {
// 1. Trigger speech synthesizer on main thread (or whatever thread it has to run on) and save "completionBlock"
// 2. Have the delegate from the speech synthesizer call "completionBlock" when done passing an appropriate response
}];
[1] https://github.com/swisspol/GCDWebServer#asynchronous-http-responses

How can i implement the promise pattern with ReactiveCocoa?

I am new to iOS development coming from a JS background with EmberJS. I want to port my EmberJS App to an iOS App. Therefore i would like to use similiar structures in my iOS App. As EmberJS makes heavy use of promises i searched for something similar for iOS and stumbled upon ReactiveCocoa. It is said in the introduction of ReactiveCocoa that this framework can be used to implement Promises. I tried it but it does not work properly. I wanted to start with a quite simple example:
Make an asynchronous network request (to fill a UITableViewController). Return a promise from this method.
Subscribe to this promise and reload the TableView when it is finished.
I want to do it this way, because i will have to perform several things after the data has been loaded successfully. My approach works basically but i am experiencing the following issues:
My TableView does not reload immediately after the request has been finished.
I am seeing the Log Statements in my subscribeCompleted immediately after the request finished. But the TableView stays blank.
The TableView loads the data after a few seconds of waiting.
If i start scrolling the TableView after i have seen the Log output, the TableView is suddenly loaded.
I suspect this may happen because i am fetching the data in a background thread. I think the resolve of the promise (subscribeCompleted) may happen in the background thread too and Cocoa Touch may not like this. Am i right? But if this is the case, how am i supposed implement a promise?
I hope you can help me getting started with ReactiveCocoa. Thx! :-)
UPDATE:
I managed to fix it by wrapping the to reloadData in a dispatch_async(dispatch_get_main_queue(), ^{... But still i am not sure wether this is the best way to go or what is recommended by ReactiveCocoa. So i am still keen on hearing some answers :-)
// this method wants to use the promise
- (void) loadDataAndPerformActionsAfterwards{
RACSignal *signal = [self fetchObjects];
[signal subscribeCompleted:^{
NSLog(#"Entered subscribeCompleted block signal!");
NSLog(#"Number of objects: %i", self.objects.count);
[self.tableView reloadData];
}];
}
// this method returns a promise. I omitted some parts but it shows basically how i go about resolving the promise.
- (RACSignal*) fetchMoviesForCurrentFormState{
return [RACSignal createSignal:^RACDisposable*(id<RACSubscriber> subscriber) {
NSLog(#"RAC createSignal Block called");
NSString *requestURL = #"...";
NSURL *urlObj = [NSURL URLWithString: requestURL];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData* data = [NSData dataWithContentsOfURL: urlObj];
if(data){
[self performSelectorOnMainThread:#selector(fetchedData:)
withObject:data waitUntilDone:YES];
[subscriber sendCompleted];
}else{
// Not implemented yet: handle the error case
[subscriber sendCompleted];
}
});
// actually i do not know yet what i should return here. Copied from a basic example.
return nil;
}];
}
You're right that this is an issue with threading. However, you don't need to drop down to the level of GCD.
Signals can be "delivered" onto another thread, which just invokes any subscription callbacks there:
- (void) loadDataAndPerformActionsAfterwards {
[[[self
fetchObjects]
deliverOn:RACScheduler.mainThreadScheduler]
subscribeCompleted:^{
NSLog(#"Entered subscribeCompleted block signal!");
NSLog(#"Number of objects: %i", self.objects.count);
[self.tableView reloadData];
}];
}
You may take a look into RXPromise. It's an Objective-C implementation of the Promises/A+ specification with a couple more features. (I'm the author).
A solution utilizing the RXPromise library would look as follows:
- (void) loadDataAndPerformActionsAfterwards {
[self fetchMovie]
.thenOn(dispatch_queue_get_main(), ^id(id fetchedMovie) {
self.model = fetchedObjects;
[self.tableView reloadData];
}, nil);
}
This assumes, method fetchMovie returns a Promise.
How do you get this? Well, you can easily wrap any asynchronous method or operation into one that returns a Promise. This works for any signal approach: completion blocks, callback functions, delegates, KVO, Notification, etc.
For example, a simplified implementation for NSURLConnection's async convenience class method (in practice you should check the response and do better error handling):
- (RXPromise*) fetchMovie {
RXPromise* promise = [[RXPromise alloc] init];
NSMutableRequest* request = ...;
[NSURLConnection sendAsynchronousRequest:request
queue:networkQueue
completionHandler:^(NSURLResponse* response, NSData* data, NSError* error){
if (error) {
[promise rejectWithReason:error];
}
else {
[promise fulfillWithValue:data];
}
}];
return promise;
}
You might want to use an approach using the NSURLConnection delegates, or an approach utilizing a NSOperation subclass. This enables you to implement cancellation:
- (RXPromise*) fetchObjects {
RXPromise* promise = [[RXPromise alloc] init];
NSMutableRequest* request = ...;
HTTPOperation* op =
[[HTTPOperation alloc] initWithRequest:request
queue:networkQueue
completionHandler:^(NSURLResponse* response, NSData* data, NSError* error){
if (error) {
[promise rejectWithReason:error];
}
else {
[promise fulfillWithValue:data];
}
}];
promise.then(nil, ^id(NSError* error){
[op cancel];
return nil;
});
[op start];
return promise;
}
Here, the HTTPOperation object will listen to its own promise for an error signal. If it receives one, for example a cancel message send from another object to the promise, the handler then "forwards" the cancel message to the operation.
A View Controller for example can now cancel a running HTTPOperation as follows:
- (void) viewWillDisappear:(BOOL)animate {
[super viewWillDisappear:animate];
[self.fetchObjectsPromise cancel];
self.fetchObjectPromise = nil;
}

Task running in Background does not trigger delegates

My application has to poll a server for a maximum of 10 minutes (using RestKit), even if the application is sent to the background. (polling always starts while the application is in the foreground)
I have a View Controller (not the RootViewController) that listens to applicationDidEnterBackground.
Also, there's a class "Order" that has a method "poll" which is used to send a request to the server, and several other callback methods for "timeout", "request cancel", "handle response", etc.
- (void)poll
{
RKRequest* request = [[RKClient sharedClient] requestWithResourcePath:#"/foo.php" delegate:self];
request.backgroundPolicy = RKRequestBackgroundPolicyContinue;
[request send];
NSLog(#"I am your RKClient singleton : %#", [RKClient sharedClient]);
}
- (void)requestDidStartLoad:(RKRequest *)request {
NSLog(#"requestDidStartLoad");
}
- (void)requestDidTimeout:(RKRequest *)request {
NSLog(#"requestDidTimeout");
}
- (void)request:(RKRequest *)request didFailLoadWithError:(NSError *)error {
NSLog(#"didFailLoadWithError");
}
- (void)request:(RKRequest*)request didLoadResponse:(RKResponse*)response
{
}
While the app is in the foreground, everything works fine and the callbacks are triggered.
When my application enters the background i want to continue polling the server. I use this method, "poll" is called, but no callbacks are triggered..
- (void)applicationDidEnterBackground:(NSNotification *) notification
{
Order *order = [[Order alloc] init];
UIApplication *app = [UIApplication sharedApplication];
__block UIBackgroundTaskIdentifier taskId;
taskId = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:taskId];
}];
if (taskId == UIBackgroundTaskInvalid) {
return;
}
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while(YES)
{
sleep(1);
[order poll];
}
[app endBackgroundTask:taskId];
});
[order release];
}
What am I doing wrong?
I don't know this RKClient you're using but probably it's based on NSURLConnection API. This asynchronous API calls delegates only if it's running inside a run-loop; from NSURLConnection documentation:
Messages to the delegate will be sent on the thread that calls this method. For the connection to work correctly the calling thread’s run loop must be operating in the default run loop mode.
Unfortunately GCD doesn't guarantee you to run a block inside a thread which executes a run-loop. The suggestion in such case is that you run your "poll" inside a NSOperation which is optimized for this kind of situations.

Perform block inside a NSOperation

I have a method in some class which performs some task using a block. When I execute that method using NSInvocationOperation then control never goes to the block. I tried logging inside the block but that is never called actually. But if I simply call that method with instance of that class then everything works as expected.
Don’t blocks run inside NSOperation?
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:myClassObj selector:#selector(myClassMethod:) object:obj1];
[[AppDelegate sharedOpQueue] addOperation:op];
[op release];
- (void)myClassMethod:(id)obj
{
AnotherClass *otherClass = [[AnotherClass allco] init]
[otherClass fetchXMLWithCompletionHandler:^(WACloudURLRequest* request, xmlDocPtr doc, NSError* error)
{
if(error){
if([_delegate respondsToSelector:#selector(handleFail:)]){
[_delegate handleFail:error];
}
return;
}
if([_delegate respondsToSelector:#selector(doSomeAction)]){
[_delegate doSomeAction];
}
}];
}
- (void) fetchXMLWithCompletionHandler:(WAFetchXMLHandler)block
{
_xmlBlock = [block copy];
[NSURLConnection connectionWithRequest:request delegate:self];
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if(_xmlBlock) {
const char *baseURL = NULL;
const char *encoding = NULL;
xmlDocPtr doc = xmlReadMemory([_data bytes], (int)[_data length], baseURL, encoding, (XML_PARSE_NOCDATA | XML_PARSE_NOBLANKS));
NSError* error = [WAXMLHelper checkForError:doc];
if(error){
_xmlBlock(self, nil, error);
} else {
_xmlBlock(self, doc, nil);
}
xmlFreeDoc(doc);
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
if(_xmlBlock) {
_xmlBlock(self, nil, error);
}
}
You are performing your NSConnection asynchronously (which you don't need to do in an NSOperation because you should already be on a background thread).
After your call to fetchXMLWithCompletionHandler, your method ends. This signals that the NSOperation is finished and it gets released and it's thread gets either reused for something else or, more likely, released as well. This means that by the time you get your callbacks, your initial object doesn't exist anymore!
There are two solutions :
1) Use NSURLConnection synchronously. This will wait in your myClassMethod until it has got a response.
2) Learn about NSOperations's concurrent mode. I don't know if this will work with NSInvocationOperation though :( And it's fairly complicated compared to option (1).
I would use method (1) - you have already created a background thread to perform your operation in, why bother creating another one to do your connection request?
There are two ways of fixing your problem:
The easy way out
is — as Dean suggests — using +[NSURLConnection sendSynchronousRequest:returningResponse:error:], as you already are on a different thread. This has you covered — I'd say — 80-90% of the time, is really simple to implement and Just Works™.
The other way
is only slightly more complicated and has you covered for all the cases where the first method does not suffice — by visiting the root of your problem:
NSURLConnection works in conjunction with the runloop — and the threads managed by NSOperationQueue don't necessarily use (or even have!) an associated runloop.
While calling +[NSURLConnection connectionWithRequest:delegate:] will implicitly create a runloop, if needed, it does not cause the runloop to actually run!
This is your responsibility, when the NSOperationQueue you use is not the queue associated with the main thread.
To do so, change your implementation of fetchXMLWithCompletionHandler: to look similar to the following:
- (void)fetchXMLWithCompletionHandler:(WAFetchXMLHandler)block
{
self.xmlHandler = block; // Declare a #property for the block with the copy attribute set
self.mutableXMLData = [NSMutableData data]; // again, you should have a property for this...
self.currentConnection = [NSURLConnection connectionWithRequest:request delegate:self]; // having a #property for the connection allows you to cancel it, if needed.
self.connectionShouldBeRunning = YES; // ...and have a BOOL like this one, setting it to NO in connectionDidFinishLoad: and connection:didFailWithError:
NSRunLoop *loop = [NSRunLoop currentRunLoop];
NSDate *neverExpire = [NSDate distantFuture];
BOOL runLoopDidIterateRegularly = YES;
while( self.connectionShouldBeRunning && runLoopDidIterateRegularly ) {
runLoopDidIterateRegularly = [loop runMode:NSDefaultRunLoopMode beforeDate:neverExpire];
}
}
With these small changes, you're good to go. Bonus: this is really flexible and (eventually) reusable throughout all your code — if you move the XML-parsing out of that class and make your handler simply take an NSData, an NSError and (optionally) an NSURLResponse.
Since you probably don't want the clients of your loader to see and possibly mess with the properties I just suggested you should add, you can declare them in a class continuation.

Delaying an -(id)init instance; is it possible?

I've been trying to get a PDF from an NSURL that is changed during a
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
The change in NSURL logs perfectly, but the view is loaded before the app has a chance to act upon that change. Is there a way to delay the reading of the change in URL by simply moving the code to the
viewDidLoad
section, or do I have to drastically change everything? Here's my -(id)init method:
- (id)init {
if (self = [super init]) {
CFURLRef pdfURL = (CFURLRef)[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:appDelegate.baseURL ofType:#"pdf"]];
pdf = CGPDFDocumentCreateWithURL((CFURLRef)pdfURL);
}
return self;
}
When you need to work with network the proven approach is to use asynchronous calls. This is because of the nature of a network connection; it is unpredictable, not always reliable, the time you need to spend to get the result from the server can vary from millisecond to minutes.
I would make a data model class, MyPDFModel, with an asynchronous method, that should run a thread to get the file from the server:
- (void)requestPDFWithURL:(NSURL*)fileURL
{
[NSThread detachNewThreadSelector:#selector(requestPDFWithURLThreaded:) toTarget:self fileURL];
}
- (void)requestPDFWithURLThreaded:(NSURL*)fileURL
{
NSAutoreleasePool* pool = [NSAutoreleasePool new];
// do whatever you need to get either the file or an error
if (isTheFileValid)
[_delegate performSelectorOnMainThread:#selector(requestDidGetPDF:) withObject:PDFFile waitUntilDone:NO];
else
[_delegate performSelectorOnMainThread:#selector(requestDidFailWithError:) withObject:error waitUntilDone:NO];
[pool release];
}
Meanwhile the UI should display an activity indicator.
The MyPDFModelDelegate protocol should have two methods:
- (void)requestDidGetPDF:(YourPDFWrapperClass*)PDFDocument;
- (void)requestDidFailWithError:(NSError*)error;
YourPDFWrapperClass is used to return an autoreleased document.
The delegate can let the UI know that the data has been updated, for example by posting a notification if the delegate is a part of the data model.
This is just an example, the implementation can be different depending on your needs, but I think you will get the idea.
P.S. Delaying an init is a very bad idea.