How can I wait for a NSURLConnection delegate to f

2019-02-20 06:32发布

This has been a hard one to search. I found a similar question, iOS 5 Wait for delegate to finish before populating a table?, but the accepted answer was 'Refresh the table view,' and that does not help me. The other results I found tended to be in c#.

I have an app that streams from iPhone to Wowza servers. When the user hits record, I generate a unique device id, then send it to a PHP script on the server that returns a JSON document with configuration settings (which includes the rtmp dump link).

The problem is, the delegate methods are asynchronous, but I need to get the config settings before the next lines of code in my - (IBAction)recordButtonPressed method, since that code is what sets the profile settings, and then records based on those settings.

I've realized I could make the NSURLConnection in -recordButtonPressed like I am currently, and then continue the setup code inside the delegate method connectionDidFinishLoading (or just encapsulate the setup and method call it from there) but that's sacrificing coherent design for functionality and that sucks.

Is there not some simple waitUntilDelegateIsFinished:(BOOL)nonAsyncFlag flag I can send to the delegator so I can have sequential operations that pull data from the web?

3条回答
欢心
2楼-- · 2019-02-20 06:58

I've realized I could make the NSURLConnection in -recordButtonPressed like I am currently, and then continue the setup code inside the delegate method connectionDidFinishLoading (or just encapsulate the setup and method call it from there) but that's sacrificing coherent design for functionality and that sucks.

You have analyzed and understood the situation and you have described its possible solutions perfectly. I just don't agree with your conclusions. This kind of thing happens all the time:

- (void) doPart1 {
    // do something here that will eventually cause part2 to be called
}

- (void) doPart2 {
}

You can play various games with invocations to make this more elegant and universal, but my advice would be, don't fight the framework, as what you're describing is exactly the nature of being asynchronous. (And do not use a synchronous request on the main thread, since that blocks the main thread, which is a no-no.)

Indeed, in an event-driven framework, the very notion "wait until" is anathema.

查看更多
做自己的国王
3楼-- · 2019-02-20 07:03
来,给爷笑一个
4楼-- · 2019-02-20 07:05

Wrap your asynchronous NSURLConnection request in a helper method which has a completion block as a parameter:

-(void) asyncDoSomething:(void(^)(id result)completionHandler ;

This method should be implemented in the NSURLConnectionDelegate. For details see the example implementation and comments below.

Elsewhere, in your action method:

Set the completion handler. The block will dispatch further on the main thread, and then perform anything appropriate to update the table data, unless the result was an error, in which case you should display an alert.

- (IBAction) recordButtonPressed 
{
    [someController asyncConnectionRequst:^(id result){
        if (![result isKindOfClass:[NSError class]]) {
            dispatch_async(dispatch_get_main_queue(), ^{
                // We are on the main thread!
                someController.tableData = result;
            });
        }    
    }];
}

The Implementation of the method asyncConnectionRequst: could work as follows: take the block and hold it in an ivar. When it is appropriate call it with the correct parameter. However, having blocks as ivars or properties will increase the risk to inadvertently introduce circular references.

But, there is a better way: a wrapper block will be immediately dispatched to a suspended serial dispatch queue - which is hold as an ivar. Since the queue is suspended, they will not execute any blocks. Only until after the queue will be resumed, the block executes. You resume the queue in your connectionDidFinish: and connectionDidFailWithError: (see below):

In your NSURLConnectionDelegate:

-(void) asyncConnectionRequst:(void(^)(id result)completionHandler 
{
    // Setup and start the connection:
    self.connection = ...
    if (!self.connection) {
        NSError* error = [[NSError alloc] initWithDomain:@"Me" 
                            code:-1234 
                        userInfo:@{NSLocalizedDescriptionKey: @"Could not create NSURLConnection"}];
            completionHandler(error);
        });
        return;
    }
    dispatch_suspend(self.handlerQueue);  // a serial dispatch queue, now suspended
    dispatch_async(self.handlerQueue, ^{
        completionHandler(self.result);
    });
    [self.connection start];
}

Then in the NSURLConnectionDelegate, dispatch a the handler and resume the handler queue:

- (void) connectionDidFinishLoading:(NSURLConnection*)connection {
    self.result = self.responseData;
    dispatch_resume(self.handlerQueue);
    dispatch_release(_handlerQueue), _handlerQueue = NULL;
}

Likewise when an error occurred:

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    self.result = error;
    dispatch_resume(self.handlerQueue);
    dispatch_release(_handlerQueue), _handlerQueue = NULL;
}

There are even better ways, which however involve a few more basic helper classes which deal with asynchronous architectures which at the end of the day make your async code look like it were synchronous:

-(void) doFourTasksInAChainWith:(id)input
{
    // This runs completely asynchronous!

    self.promise = [self asyncWith:input]
    .then(^(id result1){return [self auth:result1]);}, nil)
    .then(^(id result2){return [self fetch:result2];}, nil)
    .then(^(id result3){return [self parse:result3];}, nil)
    .then(^(id result){ self.tableView.data = result; return nil;}, ^id(NSError* error){ ... })

    // later eventually, self.promise.get should contain the final result        
}
查看更多
登录 后发表回答