NSURLConnection timing out - ios

I've been having intermittent problems with NSURLConnection requests timing out in our iPhone app. It seems to be occurring more of late. Once it enters this state, it stays in that state. The only resolution seems to be killing the app and restarting it.
Observations:
The core code that executes the NSURLConnection has not changed (except for some custom user-agent code recently added).
Have yet to find a reproducible case, but timeouts seem to occur after the app has been sitting in the background for a while, particularly if running on 3G (no WiFi).
Apache on server is logging no requests from client while it's experiencing these timeouts.
Some indications that other apps, like Mail and Safari are affected (i.e., experiencing timeouts), although not consistently.
3G coverage is solid where I'm at, not to rule out a transitory issue triggering the problem (assumed not likely).
All requests are going to our own API server, and are restful POST requests.
We use our own NSTimer-based timeout, due to the issues with timeoutInterval and POST requests. I've tried playing around with increasing the timeout value -- problem still occurs.
Other miscellaneous stuff:
App was recently converted to ARC.
Running app under iOS 5.1.1.
App uses latest versions of UrbanAirship, TestFlight and Flurry SDKs.
Also using ARC branch of TouchXML to parse responses.
As you can see below, the code runs on the main thread. I assumed something is blocking on that thread, but the stack traces I see when suspending the app suggest the main thread is fine. I take it that NSURLConnection is using its own thread and that must be blocked.
#define relnil(v) (v = nil)
- (id) initWebRequestController
{
self = [super init];
if (self)
{
//setup a queue to execute all web requests on synchronously
dispatch_queue_t aQueue = dispatch_queue_create("com.myapp.webqueue", NULL);
[self setWebQueue:aQueue];
}
return self;
}
- (void) getStuffFromServer
{
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
dispatch_sync([self webQueue], ^{
error_block_t errorBlock = ^(MyAppAPIStatusCode code, NSError * error){
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestController:self didEncounterErrorGettingPointsWithCode:code andOptionalError:error];
});
};
parsing_block_t parsingBlock = ^(CXMLDocument * doc, error_block_t errorHandler){
NSError * error = nil;
CXMLNode * node = [doc nodeForXPath:#"apiResult/data/stuff" error:&error];
if (error || !node) {
errorHandler(MyAppAPIStatusCodeFailedToParse, error);
}
else {
stuffString = [node stringValue];
}
if (stuffString) {
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestController:self didFinishGettingStuff:stuffString];
});
}
else {
errorHandler(MyAppAPIStatusCodeFailedToParse, error);
}
};
NSURL * url = [[NSURL alloc] initWithString:[NSString stringWithFormat:MyAppURLFormat_MyAppAPI, #"stuff/getStuff"]];
NSMutableURLRequest * urlRequest = [[NSMutableURLRequest alloc] initWithURL:url];
NSMutableDictionary * postDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:
[[NSUserDefaults standardUserDefaults] objectForKey:MyAppKey_Token], #"token",
origin, #"from",
destination, #"to",
transitTypeString, #"mode",
time, #"time",
nil];
NSString * postString = [WebRequestController httpBodyFromDictionary:postDictionary];
[urlRequest setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
[urlRequest setHTTPMethod:#"POST"];
if (urlRequest)
{
[self performAPIRequest:urlRequest withRequestParameters:postDictionary parsing:parsingBlock errorHandling:errorBlock timeout:kTimeout_Standard];
}
else
{
errorBlock(MyAppAPIStatusCodeInvalidRequest, nil);
}
relnil(url);
relnil(urlRequest);
});
});
}
- (void) performAPIRequest: (NSMutableURLRequest *) request
withRequestParameters: (NSMutableDictionary *) requestParameters
parsing: (parsing_block_t) parsingBlock
errorHandling: (error_block_t) errorBlock
timeout: (NSTimeInterval) timeout
{
NSAssert([self apiConnection] == nil, #"Requesting before previous request has completed");
NSString * postString = [WebRequestController httpBodyFromDictionary:requestParameters];
[request setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
NSString * erVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleShortVersionString"];
NSString * erBuildVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:#"CFBundleVersion"];
if ([erBuildVersion isEqualToString:erVersion] || [erBuildVersion isEqualToString:#""]) {
erBuildVersion = #"";
} else {
erBuildVersion = [NSString stringWithFormat:#"(%#)", erBuildVersion];
}
NSString * iosVersion = [[UIDevice currentDevice] systemVersion];
NSString * userAgent = [NSString stringWithFormat:#"MyApp/%#%# iOS/%#", erVersion, erBuildVersion, iosVersion];
[request setValue:userAgent forHTTPHeaderField:#"User-Agent"];
[request setTimeoutInterval:(timeout-3.0f)];
dispatch_sync(dispatch_get_main_queue(), ^{
NSURLConnection * urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
if (urlConnection)
{
[self setApiConnection:urlConnection];
requestParseBlock = [parsingBlock copy];
requestErrorBlock = [errorBlock copy];
NSMutableData * aMutableData = [[NSMutableData alloc] init];
[self setReceivedData:aMutableData];
relnil(aMutableData);
[urlConnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[urlConnection start];
relnil(urlConnection);
NSTimer * aTimer = [NSTimer scheduledTimerWithTimeInterval:timeout target:self selector:#selector(timeoutTimerFired:) userInfo:nil repeats:NO];
[self setTimeoutTimer:aTimer];
}
else
{
errorBlock(MyAppAPIStatusCodeInvalidRequest, nil);
}
});
//we want the web requests to appear synchronous from outside of this interface
while ([self apiConnection] != nil)
{
[NSThread sleepForTimeInterval:.25];
}
}
- (void) timeoutTimerFired: (NSTimer *) timer
{
[[self apiConnection] cancel];
relnil(apiConnection);
relnil(receivedData);
[self requestErrorBlock](MyAppAPIStatusCodeTimeout, nil);
requestErrorBlock = nil;
requestParseBlock = nil;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[self requestErrorBlock](MyAppAPIStatusCodeFailedToConnect, error);
relnil(apiConnection);
relnil(receivedData);
[[self timeoutTimer] invalidate];
relnil(timeoutTimer);
requestErrorBlock = nil;
requestParseBlock = nil;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[receivedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
MyAppAPIStatusCode status = MyAppAPIStatusCodeFailedToParse;
CXMLDocument *doc = [[self receivedData] length] ? [[CXMLDocument alloc] initWithData:[self receivedData] options:0 error:nil] : nil;
DLog(#"response:\n%#", doc);
if (doc)
{
NSError * error = nil;
CXMLNode * node = [doc nodeForXPath:#"apiResult/result" error:&error];
if (!error && node)
{
status = [[node stringValue] intValue];
if (status == MyAppAPIStatusCodeOK)
{
[self requestParseBlock](doc, [self requestErrorBlock]);
}
else if (status == MyAppAPIStatusCodeTokenMissingInvalidOrExpired)
{
[Definitions setToken:nil];
[self requestMyAppTokenIfNotPresent];
[Definitions logout];
dispatch_async(dispatch_get_main_queue(), ^{
[[self delegate] webRequestControllerDidRecivedExpiredTokenError:self];
});
}
else
{
[self requestErrorBlock](status, nil);
}
}
else
{
[self requestErrorBlock](status, nil);
}
}
else
{
status = MyAppAPIStatusCodeUnexpectedResponse;
[self requestErrorBlock](status, nil);
}
relnil(doc);
relnil(apiConnection);
relnil(receivedData);
[[self timeoutTimer] invalidate];
relnil(timeoutTimer);
requestErrorBlock = nil;
requestParseBlock = nil;
}
URLs below are some screenshots of the queues/threads when the app was in the problematic state. Note, I believe thread 10 is related to the cancel performed on the previous timeout, although the mutex wait is curious. Also, the bit in thread 22 about Flurry does not consistently appear when experiencing the problem on other occasions.
Stack trace screenshots:
http://img27.imageshack.us/img27/5614/screenshot20120529at236.png
http://img198.imageshack.us/img198/5614/screenshot20120529at236.png
Perhaps I'm overlooking something obviously wrong in those traces, as I'm relatively new to iOS/Apple development.
All of this would be much simpler to solve if I had the source for NSURLConnection and related code, but such as it is, I'm taking stabs in the dark at this point.

Removing the TestFlight 1.0 SDK seemed to fix the problem. TestFlight also confirmed that they're working on a fix. Given that it's been over a month since the bug was originally confirmed by others, I wonder how close we are to getting a fix.

Related

AVPlayer stalling on large video files using resource loader delegate

I am using this approach to save the buffer data of the AVPlayer for video files. Found as the answer in this question Saving buffer data of AVPlayer.
iPhone and iPad - iOS 8.1.3
I made the necessary changes to play video and it is working very nicely except when I try to play a very long video (11-12 minutes long and about 85mb in size) the video will stall roughly 4 minutes after the connection finishes loading. I get an event for playbackBufferEmpty and a player item stalled notification.
This is the gist of the code
viewController.m
#property (nonatomic, strong) NSMutableData *videoData;
#property (nonatomic, strong) NSURLConnection *connection;
#property (nonatomic, strong) AVURLAsset *vidAsset;
#property (nonatomic, strong) AVPlayerItem *playerItem;
#property (nonatomic, strong) AVPlayerLayer *avlayer;
#property (nonatomic, strong) NSHTTPURLResponse *response;
#property (nonatomic, strong) NSMutableArray *pendingRequests;
/**
Startup a Video
*/
- (void)startVideo
{
self.vidAsset = [AVURLAsset URLAssetWithURL:[self videoURLWithCustomScheme:#"streaming"] options:nil];
[self.vidAsset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
self.pendingRequests = [NSMutableArray array];
// Init Player Item
self.playerItem = [AVPlayerItem playerItemWithAsset:self.vidAsset];
[self.playerItem addObserver:self forKeyPath:#"status" options:NSKeyValueObservingOptionNew context:NULL];
self.player = [[AVPlayer alloc] initWithPlayerItem:self.playerItem];
// Init a video Layer
self.avlayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
[self.avlayer setFrame:self.view.frame];
[self.view.layer addSublayer:self.avlayer];
}
- (NSURL *)getRemoteVideoURL
{
NSString *urlString = [#"http://path/to/your/long.mp4"];
return [NSURL URLWithString:urlString];
}
- (NSURL *)videoURLWithCustomScheme:(NSString *)scheme
{
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:[self getRemoteVideoURL] resolvingAgainstBaseURL:NO];
components.scheme = scheme;
return [components URL];
}
/**
NSURLConnection Delegate Methods
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSLog(#"didReceiveResponse");
self.videoData = [NSMutableData data];
self.response = (NSHTTPURLResponse *)response;
[self processPendingRequests];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSLog(#"Received Data - appending to video & processing request");
[self.videoData appendData:data];
[self processPendingRequests];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(#"connectionDidFinishLoading::WriteToFile");
[self processPendingRequests];
[self.videoData writeToFile:[self getVideoCachePath:self.vidSelected] atomically:YES];
}
/**
AVURLAsset resource loader methods
*/
- (void)processPendingRequests
{
NSMutableArray *requestsCompleted = [NSMutableArray array];
for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests)
{
[self fillInContentInformation:loadingRequest.contentInformationRequest];
BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest];
if (didRespondCompletely)
{
[requestsCompleted addObject:loadingRequest];
[loadingRequest finishLoading];
}
}
[self.pendingRequests removeObjectsInArray:requestsCompleted];
}
- (void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *)contentInformationRequest
{
if (contentInformationRequest == nil || self.response == nil)
{
return;
}
NSString *mimeType = [self.response MIMEType];
CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mimeType), NULL);
contentInformationRequest.byteRangeAccessSupported = YES;
contentInformationRequest.contentType = CFBridgingRelease(contentType);
contentInformationRequest.contentLength = [self.response expectedContentLength];
}
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest
{
long long startOffset = dataRequest.requestedOffset;
if (dataRequest.currentOffset != 0)
{
startOffset = dataRequest.currentOffset;
}
// Don't have any data at all for this request
if (self.videoData.length < startOffset)
{
NSLog(#"NO DATA FOR REQUEST");
return NO;
}
// This is the total data we have from startOffset to whatever has been downloaded so far
NSUInteger unreadBytes = self.videoData.length - (NSUInteger)startOffset;
// Respond with whatever is available if we can't satisfy the request fully yet
NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);
[dataRequest respondWithData:[self.videoData subdataWithRange:NSMakeRange((NSUInteger)startOffset, numberOfBytesToRespondWith)]];
long long endOffset = startOffset + dataRequest.requestedLength;
BOOL didRespondFully = self.videoData.length >= endOffset;
return didRespondFully;
}
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
if (self.connection == nil)
{
NSURL *interceptedURL = [loadingRequest.request URL];
NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:interceptedURL resolvingAgainstBaseURL:NO];
actualURLComponents.scheme = #"http";
NSURLRequest *request = [NSURLRequest requestWithURL:[actualURLComponents URL]];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection setDelegateQueue:[NSOperationQueue mainQueue]];
[self.connection start];
}
[self.pendingRequests addObject:loadingRequest];
return YES;
}
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
NSLog(#"didCancelLoadingRequest");
[self.pendingRequests removeObject:loadingRequest];
}
/**
KVO
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == StatusObservationContext)
{
AVPlayerStatus status = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];
if (status == AVPlayerStatusReadyToPlay) {
[self initHud];
[self play:NO];
} else if (status == AVPlayerStatusFailed)
{
NSLog(#"ERROR::AVPlayerStatusFailed");
} else if (status == AVPlayerItemStatusUnknown)
{
NSLog(#"ERROR::AVPlayerItemStatusUnknown");
}
} else if (context == CurrentItemObservationContext) {
} else if (context == RateObservationContext) {
} else if (context == BufferObservationContext){
} else if (context == playbackLikelyToKeepUp) {
if (self.player.currentItem.playbackLikelyToKeepUp)
}
} else if (context == playbackBufferEmpty) {
if (self.player.currentItem.playbackBufferEmpty)
{
NSLog(#"Video Asset is playable: %d", self.videoAsset.isPlayable);
NSLog(#"Player Item Status: %ld", self.player.currentItem.status);
NSLog(#"Connection Request: %#", self.connection.currentRequest);
NSLog(#"Video Data: %lu", (unsigned long)self.videoData.length);
}
} else if(context == playbackBufferFull) {
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
The problem seems to be that some time after the connection finishes loading, the player item buffer goes empty. My thought at the moment is that something is being deallocated when the connection finishes loading and messing up the playerItem buffer.
However at the time the buffer goes empty the playerItem status is good, the video asset is playable, the video data is good
If I throttle the wifi through charles and slow down the connection, the video will play as long as the connection does not finish loading within a few minutes of the end of the video.
If I set the connection nil on the finished loading event, the resource loader will fire up a new connection when shouldWaitForLoadingOfRequestedResource fires again. In this case the loading starts all over again and the video will continue playing.
I should mention that this long video plays fine if I play it as a normal http url asset, and also plays fine after being saved to the device and loaded from there.
when the resource loader delegate fires up the NSURLConnection, the connection takes over saving the NSData to the pending requests and processing them. when the connection finished loading, the resource loader regains responsibility for handling the loading requests. the code was adding the loading request to the pending requests array but the issue was that they were not being processed. changed the method to the following and it works.
//AVAssetResourceLoader
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
if(isLoadingComplete == YES)
{
//NSLog(#"LOADING WAS COMPLETE");
[self.pendingRequests addObject:loadingRequest];
[self processPendingRequests];
return YES;
}
if (self.connection == nil)
{
NSURL *interceptedURL = [loadingRequest.request URL];
NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:interceptedURL resolvingAgainstBaseURL:NO];
actualURLComponents.scheme = #"http";
self.request = [NSURLRequest requestWithURL:[actualURLComponents URL]];
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
[self.connection setDelegateQueue:[NSOperationQueue mainQueue]];
isLoadingComplete = NO;
[self.connection start];
}
[self.pendingRequests addObject:loadingRequest];
return YES;
}

Easier way to send JSON Data with Objective-C to PHP server?

I have a PHP RESTful server that I connect to to invoke methods based on the URL path called from my objective-c program. I use the ASIHTTPRequest and SBJson.
Everything works well but I'm doing requests in different controllers and now duplicated methods are in all those controllers. What is the best way to abstract that code (requests) in a single class, so I can easily re-use the code, so I can keep the controllers more simple.
Please save me from what I feel to be a redundant and repetitive coding sequence.
I have tried to make a singleton class, but each view controller requires different request connect points, and most view controllers require specific actions to be called when the succeeded method is called and Im not sure how to handle it all.
Here is what my code template currently looks like in the Objective-c program:
//*1) Somewhere at the top of my .m file I have this*
//These are the suffixes to the URL path that I connect to at my server,
//depending on the action required
NSString *const RequestCreateCustomer = #"Create/Customer";
NSString *const RequestUpdateCustomer = #"Update/Customer";
NSString *const RequestDeleteCustomer = #"Delete/Customer";
//*2) Then I have my connection invocation code*
//Method invocation, all of them look something like this
-navigationButton{
...
[self retrieveWithRequestStringType:RequestUpdateCustomer];
}
-(void)retrieveWithRequestStringType:(NSString*)typeOfRequest{
NSLog(#"Retrieve %# method called", typeOfRequest);
NSString *urlString = [NSString stringWithFormat:#"%#/Secure/CB/%#", #"http://www.defaultStapleURLToMyServer.com/CB", typeOfRequest];
NSString *encodedUrlString = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [[NSURL alloc] initWithString:encodedUrlString];
serverRequest = nil;
serverRequest = [ASIFormDataRequest requestWithURL:url];
[serverRequest addRequestHeader:#"Content-Type" value:#"application/json"];
[serverRequest addRequestHeader:#"Request-Method" value:#"POST"];
//Normally at this point depending on the request type, I prepare some data that needs to be sent along with the request
NSMutableDictionary *completeDataArray = [[NSMutableDictionary alloc] init];
if([typeOfRequest isEqualToString:RequestCreateCustomer]){
[serverRequest setUserInfo:[NSDictionary dictionaryWithObject:RequestCreateCustomer forKey:#"RequestType"]];
if( ! [self validateAndPrepareAllData:&completeDataArray]){
return;
}
}
else if([typeOfRequest isEqualToString:RequestUpdateCustomer]){
[serverRequest setUserInfo:[NSDictionary dictionaryWithObject:RequestUpdateCustomer forKey:#"RequestType"]];
if( ! [self validateAndPrepareAllData:&completeDataArray]){
return;
}
}
else if([typeOfRequest isEqualToString:RequestDeleteCustomer]){
[serverRequest setUserInfo:[NSDictionary dictionaryWithObject:RequestDeleteCustomer forKey:#"RequestType"]];
if( ! [self validateAndPrepareCustomerIdData:&completeDataArray]){
return;
}
}
NSString *jsonString = [completeDataArray JSONRepresentation];
[serverRequest appendPostData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];
[serverRequest setDelegate:self];
[serverRequest setDidFinishSelector:#selector(requestSucceeded:)];
[serverRequest setDidFailSelector:#selector(requestFailed:)];
[serverRequest startAsynchronous];
}
//*3) And heres the connection did succeed, and connection did fail methods*
-(void)requestSucceeded:(ASIHTTPRequest*)request{
NSInteger statusCode = [[[request responseHeaders] objectForKey:#"StatusCode"] intValue];
NSLog(#"StatusCode: %#", [[request responseHeaders] objectForKey:#"StatusCode"]);
NSString *myString = [[NSString alloc] initWithData:[request responseData] encoding:NSUTF8StringEncoding];
NSDictionary *JSONDictionary = [myString JSONValue];
switch (statusCode) {
case 400:
case 401:
{
NSLog(#"display error message");
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Error" message:[[request.responseString JSONValue] objectForKey:#"Message"] delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles: nil];
[alert show];
break;
}
case 200:
NSLog(#"status code = 200 so successful");
//Created customer request succeeded
if([[[request userInfo] objectForKey:#"RequestType"] isEqualToString:RequestCreateCustomer]){
[self.delegate addedCustomerVCWithCustomer:[[CustomerDataModel alloc] initWithJSONData:[JSONDictionary objectForKey:#"CustomerObject"]]];
[self cancelModalView];
}
//Edit customer request succeeded
else if([[[request userInfo] objectForKey:#"RequestType"] isEqualToString:RequestUpdateCustomer]){
[self.delegate editedCustomerVCWithCustomer:[[CustomerDataModel alloc] initWithJSONData:[JSONDictionary objectForKey:#"CustomerObject"]]];
[self cancelModalView];
}
//Delete customer request succeeded
else if([[[request userInfo] objectForKey:#"RequestType"] isEqualToString:RequestDeleteCustomer]){
[self.delegate deletedCustomer:customer];
[self cancelModalView];
}
break;
default:
[SVProgressHUD showErrorWithStatus:[NSString stringWithFormat:#"went to none: %d or %#", (int)statusCode, [[request responseHeaders] objectForKey:#"StatusCode"]]];
NSLog(#"went to none: %d or %#", (int)statusCode, [[request responseHeaders] objectForKey:#"StatusCode"]);
break;
}
[SVProgressHUD dismiss];
}
-(void)requestFailed:(ASIHTTPRequest*)request{
if([[[request userInfo] objectForKey:#"RquestType"] isEqualToString:RequestCreateCustomer]){
NSLog(#"retrieving states failed so trying again");
[self addCustomerRequest];
}
else if(){
//... you get the point
}
}
Can someone help out with an alternative solution?
create a new class that has blocks/delegate (but prefers block) on it.. let's say Request.h and Request.m.. and make a public method with a block parameter.. example
this should be public methods
typedef void(^CompletionBlock)(id results);
typedef void(^ErrorBlock)(NSError *error);
- (void)setCompetionBlock:(CompletionBlock)block; and
- (void)setErrorBlock:(ErrorBlock)error;
and make a variable name.. this should be private variables
CompletionBlock completionBlock;
ErrorBlock errorBlock;
and declare this methods in your .m
- (void)setCompletionBlock:(CompletionBlock)aCompletionBlock {
completionBlock = [aCompletionBlock copy];
}
- (void)setErrorBlock:(ErrorBlock)aError {
errorBlock = [aError copy];
}
- (void)reportSuccess:(id)results {
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock(results);
});
}
}
- (void)reportFailure:(NSError *)error {
if (errorBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
errorBlock(error);
});
}
}
and call [self reportSuccess:jsonSuccess]; under your success methods, in your code it is -(void)requestSucceeded:(ASIHTTPRequest*)request{ and [self reportFailure:NSerror*] under -(void)requestFailed:(ASIHTTPRequest*)request
and to call this class
Request *req = [Request alloc]init];
[req retrieveWithRequestStringType:#"sample"];
[req setCompletion:^(id result){
}];
[req setErrorBlock:(NSError *error) {
}];

NSOperation + setCompletionBlock

I have few different questions about NSOperation and NSOperationQueue and I know guys that yours answers will help me;
I have to load a big amount of images and I have created my own loader based on NSOperation, NSOperationQueue and NSURLConnection (asynchronous loading);
Questions:
If I set maxConcurrentOperationCount (for example 3) for queue (NSOperationQueue), does it mean that only 3 operations performed in the same time even queue has 100 operations?
When I set property maxConcurrentOperationCount for queue sometimes "setCompletionBlock" doesn't work and count (operationCount) only increases; Why?
MyLoader:
- (id)init
{
self = [super init];
if (self) {
_loadingFiles = [NSMutableDictionary new];
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 3;
_downloadQueue.name = #"LOADER QUEUE";
}
return self;
}
- (void)loadFile:(NSString *)fileServerUrl handler:(GetFileDataHandler)handler {
if (fileServerUrl.length == 0) {
return;
}
if ([_loadingFiles objectForKey:fileServerUrl] == nil) {
[_loadingFiles setObject:fileServerUrl forKey:fileServerUrl];
__weak NSMutableDictionary *_loadingFiles_ = _loadingFiles;
MyLoadOperation *operation = [MyLoadOperation new];
[operation fileServerUrl:fileServerUrl handler:^(NSData *fileData) {
[_loadingFiles_ removeObjectForKey:fileServerUrl];
if (fileData != nil) {
handler(fileData);
}
}];
[operation setQueuePriority:NSOperationQueuePriorityLow];
[_downloadQueue addOperation:operation];
__weak NSOperationQueue *_downloadQueue_ = _downloadQueue;
[operation setCompletionBlock:^{
NSLog(#"completion block :%i", _downloadQueue_.operationCount);
}];
}
}
MyOperation:
#interface MyLoadOperation()
#property (nonatomic, assign, getter=isOperationStarted) BOOL operationStarted;
#property(nonatomic, strong)NSString *fileServerUrl;
#property(nonatomic, copy)void (^OnFinishLoading)(NSData *);
#end
#implementation MyLoadOperation
- (id)init
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
}
return self;
}
- (void)fileServerUrl:(NSString *)fileServerUrl
handler:(void(^)(NSData *))handler {
#autoreleasepool {
self.fileServerUrl = fileServerUrl;
[self setOnFinishLoading:^(NSData *loadData) {
handler(loadData);
}];
[self setOnFailedLoading:^{
handler(nil);
}];
self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]
initWithURL:self.url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:25];
[request setValue:#"" forHTTPHeaderField:#"Accept-Encoding"];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
_data = [[NSMutableData alloc] init];
}
}
- (void)main {
#autoreleasepool {
[self stop];
}
}
- (void)start {
[self setOperationStarted:YES];
[self willChangeValueForKey:#"isFinished"];
_finished = NO;
[self didChangeValueForKey:#"isFinished"];
if ([self isCancelled])
{
[self willChangeValueForKey:#"isFinished"];
_finished = YES;
_executing = NO;
[self didChangeValueForKey:#"isFinished"];
}
else
{
[self willChangeValueForKey:#"isExecuting"];
_finished = NO;
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
}
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return _executing;
}
- (BOOL)isFinished {
return _finished;
}
- (void)cancel {
[self.connection cancel];
if ([self isExecuting])
{
[self stop];
}
[super cancel];
}
#pragma mark -NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[_data appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if ([self OnFinishLoading]) {
[self OnFinishLoading](_data);
}
if (![self isCancelled]) {
[self stop];
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
;
if (![self isCancelled]) {
[self stop];
}
}
- (void)stop {
#try {
__weak MyLoadOperation *self_ = self;
dispatch_async(dispatch_get_main_queue(), ^{
[self_ completeOperation];
});
}
#catch (NSException *exception) {
NSLog(#"Exception! %#", exception);
[self completeOperation];
}
}
- (void)completeOperation {
if (![self isOperationStarted]) return;
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
You must start the connection in the Operation's start method, and not in fileServerUrl:handler:.
I would remove this method altogether, and only provide an init method with all required parameters where you can completely setup the operation. Then, in method start start the connection.
Additionally, it's not clear why you override main.
Modifying the state variables _executing and _finished could be more concise and more clear (you don't need to set them initially, since the are already initialized to NO). Only set them in the "final" method completeOperation including KVO notifications.
You also do not need a #try/#catch in stop, since function dispatch_async() does not throw Objective-C exceptions.
Your cancel method is not thread safe, and there are also a few other issues. I would suggest the following changes:
#implementation MyOperation {
BOOL _executing;
BOOL _finished;
NSError* _error; // remember the error
id _result; // the "result" of the connection, unless failed
completion_block_t _completionHandler; //(your own completion handler)
id _self; // strong reference to self
}
// Use the "main thread" as the "synchronization queue"
- (void) start
{
// Ensure start will be called only *once*:
dispatch_async(dispatch_get_main_queue(), ^{
if (!self.isCancelled && !_finished && !_executing) {
[self willChangeValueForKey:#"isExecuting"];
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
_self = self; // keep a strong reference to self in order to make
// the operation "immortal for the duration of the task
// Setup connection:
...
[self.connection start];
}
});
}
- (void) cancel
{
dispatch_async(dispatch_get_main_queue, ^{
[super cancel];
[self.connection cancel];
if (!_finished && !_executing) {
// if the op has been cancelled before we started the connection
// ensure the op will be orderly terminated:
self.error = [[NSError alloc] initWithDomain:#"MyOperation"
code:-1000
userInfo:#{NSLocalizedDescriptionKey: #"cancelled"}];
[self completeOperation];
}
});
}
- (void)completeOperation
{
[self willChangeValueForKey:#"isExecuting"];
self.isExecuting = NO;
[self didChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
self.isFinished = YES;
[self didChangeValueForKey:#"isFinished"];
completion_block_t completionHandler = _completionHandler;
_completionHandler = nil;
id result = self.result;
NSError* error = self.error;
_self = nil;
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
completionHandler(result, error);
});
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if ([self onFinishLoading]) {
[self onFinishLoading](self.result);
}
[self completeOperation];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
if (self.error == nil) {
self.error = error;
}
[self completeOperation];
}
In answer to your questions:
Yes, a maxConcurrentOperationCount of three means that only three will run at a time. Doing network requests like this is perfect example of when you'd want to use maxConcurrentOperationCount, because failure to do so would result in too many network requests trying to run, most likely resulting in some of the connections failing when using a slower network connection.
The main issue here, though, is that you're calling your operation's fileServerUrl method (which is starting the connection) from MyLoader. You've disconnected the request from the operation's start (defeating the purpose of maxConcurrentCount of 3 and possibly confusing the state of the operation).
The start method should be initiating the connection (i.e. don't start the request until one of those three available concurrent operations is available). Furthermore, since you cannot pass the URL and the handler to the start method, you should move your logic that saves those values to a customized rendition of your init method.
There are other minor edits we might suggest to your operation (main not needed, operationStarted is a little redundant, simplify the _executing/_finished handling, etc.), but the starting of the connection in fileServerUrl rather than being initiated by the start method is the key issue.
Thus:
- (id)initWithServerUrl:(NSString *)fileServerUrl
handler:(void(^)(NSData *))handler
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
// do your saving of `fileServerURL` and `handler` here, e.g.
self.fileServerUrl = fileServerUrl;
self.OnFinishLoading:^(NSData *loadData) {
handler(loadData);
}];
[self setOnFailedLoading:^{
handler(nil);
}];
}
return self;
}
- (void)startRequest {
self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:25];
[request setValue:#"" forHTTPHeaderField:#"Accept-Encoding"];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
_data = [[NSMutableData alloc] init];
}
- (void)start {
if ([self isCancelled])
{
[self willChangeValueForKey:#"isFinished"];
_finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
[self setOperationStarted:YES]; // personally, I'd retire this and just reference your `executing` flag, but I'll keep it here for compatibility with the rest of your code
[self willChangeValueForKey:#"isExecuting"];
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
[self startRequest];
}
For the first question, the answer is yes, if set 3 as a max number of operations, only 3 can be running togheter.
The second is bit strange problem and I'm not totally sure that this answer will be correct. When you leave operations to an NSOperationQueue, you can't be sure on which thread they will be executed, this lead a huge problem with async connection. When you start an NSURLConnection as usual you receive the delegate callbacks without a problem, that is because the connection is running on a thread with a living run loop. If you start the connection on a secondary thread, callbacks will be called on that thread, but if you don't keep the run loop alive they will be never received.That's where probably my answer isn't correct, GCD should take care of living run loops, because GCD queues runs on living threads. But if not, the problem could be that operations are started on a different thread, the start method is called, but the callbacks are never called. Try to check if the thread is always the main thread.

Correct usage of NMSSH

When a session is created, a shell is started and commands are written: no responses are recieved and the callback methods for the buffer is never called, what did i miss?:
(Executing a single command using channel:execute works)
-(void) createSessionWithAdress:(NSString*)address username:(NSString*)user password:(NSString*)pass{
session = [NMSSHSession connectToHost:address withUsername:user];
if (session.isConnected) {
[session authenticateByPassword:pass];
if (session.isAuthorized) {
NSError *err = nil;
session.channel.delegate = self;
//self.receiveView.text = [session.channel execute:#"ls" error:&err]; // works
[session.channel startShell:&err];
NSLog(#"Authentication succeeded");
}
}
}
- (void)channel:(NMSSHChannel *)channel didReadData:(NSString *)message{
NSLog(#"Read data!");
receiveView.text = [NSString stringWithFormat:#"%# \n%#",receiveView.text,message];
}
- (void)channel:(NMSSHChannel *)channel didReadError:(NSString *)error{
receiveView.text = [NSString stringWithFormat:#"%# \n%#",receiveView.text,error];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField{
NSLog(#"RETURN PRESSED");
NSError* err = nil;
bool commandSucess = [session.channel write:sendView.text error:&err];
[session.channel write:#"/n" error:&err];
if (commandSucess) {
NSLog(#"Command written successfully");
}else{
NSLog(#"Command not written successfully");
}
return YES;
}
PTY mode needs to be enabled, and there were som issues in the library (should be fixed now)

Integrating MBProgressHUD with two classes

Im am trying to properly integrate MBProgressHUD in a project while i am downloading and processing some data. Forgive me my ignorance if i am asking stupid things, but i'm a complete noob...
I have a "Data" class that is handling my HTTPRequests, so i thing here is the proper place to put some stuff in:
-(void)resolve
{
// Create the request.
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[[NSURL alloc] initWithString:[self url]]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
NSLog(#"Resolving content from: %#",[self url]);
// create the connection with the request
// and start loading the data
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (theConnection) {
// Create the NSMutableData to hold the received data.
// receivedData is an instance variable declared elsewhere.
receivedData = [[NSMutableData data] retain];
} else {
NSLog(#"Content - resolve: connection failed");
}
// Here is the part that it makes me crazy...
HUD = [[MBProgressHUD showHUDAddedTo:self.view animated:YES] retain];
return;
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// This method is called when the server has determined that it
// has enough information to create the NSURLResponse.
// It can be called multiple times, for example in the case of a
// redirect, so each time we reset the data.
// receivedData is an instance variable declared elsewhere.
expectedLength = [response expectedContentLength];
currentLength = 0;
HUD.mode = MBProgressHUDModeDeterminate;
[receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
currentLength += [data length];
HUD.progress = currentLength / (float)expectedLength;
// Append the new data to receivedData.
// receivedData is an instance variable declared elsewhere.
[receivedData appendData:data];
return;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[HUD hide:YES];
// release the connection, and the data object
[connection release];
// receivedData is declared as a method instance elsewhere
[receivedData release];
// inform the user
NSLog(#"Content - Connection failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
return;
}
Now i know that putting the "showHUDaddedTo" in the -(void)resolve is not the right way...
I am calling this data function from a view controller with an IBaction like this:
-(IBAction) btnClicked:(id) sender {
if ([[issue status] intValue] == 1 ) // issue is not downloaded
{
[(Content *)[issue content] resolve];
//HUD = [[MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES] retain];
}
else // issue is downloaded - needs to be archived
{
NSError * error = nil;
[[NSFileManager defaultManager] removeItemAtPath:[(Content *)[issue content] path] error:&error];
if (error) {
// implement error handling
}
else {
Content * c = (Content *)[issue content];
[c setPath:#""];
[issue setStatus:[NSNumber numberWithInt:1]];
[buttonView setTitle:#"Download" forState:UIControlStateNormal];
[gotoIssue setTitle:#"..." forState:UIControlStateNormal];
// notify all interested parties of the archived content
[[NSNotificationCenter defaultCenter] postNotificationName:#"contentArchived" object:self]; // make sure its persisted!
}
}
}
Simply said: i would like to call all the MBProgressHUD stuff from my Conten.m file in the moment i push the download button in my IssueController.m file. I think you see now where my problem is: i am not a coder ;-) Any help is appreciated.
Cheers,
sandor
I would try and use the HUD method showWhileExecuting. Wrap your calls up in a new method and call that to download your information. When your method finishes, so will your HUD.
Also, is there a reason why you are retaining your calls for the HUD? I have not seen that before and I'm not sure if you need to retain those as they are autoreleased.

Resources