Background:
Please don't mark this as duplicate. I have tried every other related post and they didn't work for me. I have looked at countless examples (StackOverflow/MBProgressHUD Demo/etc.) trying to get this to work. I feel like most examples are outdated as some methods are deprecated. This is the closest I have got.
What I want the code to do:
The MBProgress HUD should display the default loading screen with "Preparing" before I start connecting to JSON. When I connect, I want the mode to change to AnnularDeterminate when I receive a response. Once it's finished loading, I want the progress bar to display my progress of adding the data to a dictionary. Once that is done, change the mode to CustomView displaying a checkmark image and "Completed".
The Issue:
The MBProgress HUD will display. However, it only shows the default loading screen with "Preparing..." underneath it and then it will say "Completed" with the checkmark custom view after progress is at 1.0. There is no Determinate view with my updated progress in between.
My Code
- (void)viewDidLoad
{
buildingsDataArray = [[NSMutableArray alloc] init];
MBProgressHUD *HUD = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
HUD.delegate = self;
HUD.label.text = NSLocalizedString(#"Preparing...", #"HUD preparing title");
[self connect];
NSLog(#"End of setup");
}
- (void)connect
{
NSURLRequest *request =[NSURLRequest requestWithURL:[NSURL URLWithString:buildingList]];
connection = [NSURLConnection connectionWithRequest:request delegate:self];
[connection start];
NSLog(#"End of connect");
}
// Connection delegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSLog(#"connection received response");
[MBProgressHUD HUDForView:self.view].mode = MBProgressHUDModeAnnularDeterminate;
NSLog(#"Changed mode");
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[webData appendData:data];
NSLog(#"connection received data");
[MBProgressHUD HUDForView:self.view].progress = 0.0f;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(#"Fail with error - %# %#", [error localizedDescription], [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
if ([[error localizedDescription] isEqualToString:#"The Internet connection appears to be offline."]) {
//do something
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSArray *allDataArray = [NSJSONSerialization JSONObjectWithData:webData options:0 error:nil];
float count = [allDataArray count];
float c=1;
if ([buildingsDataArray count]!=0)
[buildingsDataArray removeAllObjects];
for (NSDictionary *dataDict in allDataArray){ //from 1 to 6
if ([[dataDict objectForKeyedSubscript:#"id"] intValue] != 0)
[buildingsDataArray addObject:dataDict];
usleep(200000);
float p = c/count;
[MBProgressHUD HUDForView:self.view].progress = p;
NSLog(#"Progress: %f", [MBProgressHUD HUDForView:self.view].progress);
});
c++;
}
[MBProgressHUD HUDForView:self.view].customView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Checkmark.png"]];
[MBProgressHUD HUDForView:self.view].label.text = NSLocalizedString(#"Completed", #"HUD done title");
[MBProgressHUD HUDForView:self.view].mode = MBProgressHUDModeCustomView;
[[MBProgressHUD HUDForView:self.view] hideAnimated:YES afterDelay:1.0f];
}
And here is my console output
End of connect
End of setup
connection received response
Changed mode
connection received data
connection finished loading
Progress: 0.166667
Progress: 0.333333
Progress: 0.500000
Progress: 0.666667
Progress: 0.833333
Progress: 1.000000
Additional comments
When I try adding
dispatch_async(dispatch_get_main_queue()
outside all of the MBProgress method calls inside my connection delegate,
I get the default loading screen again. But after progress reaches 1.0, the determinate loading screen view shows (fully loaded) with "Completed" and then disappears after delay 1.0. There is still no loading process in between. Also, the checkmark image never shows. I don't think this is the way to do it.
The output when I do this is:
End of connect
End of doSetup
connection received response
connection received data
connection finished loading
Changed mode
Progress: 0.166667
Progress: 0.333333
Progress: 0.500000
Progress: 0.666667
Progress: 0.833333
Progress: 1.000000
Here is a useful reference that touches base on UICalls on different threads:
Is there a way to make drawRect work right NOW?
This is what worked:
NSURLConnection was being called on the main thread. So all of my UI updates weren't occurring until after the connection delegate calls were finished.
So to fix this problem, I put the connection in a background thread.
- (void)viewDidLoad{
//...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
NSURL *URL = [NSURL URLWithString:buildingList];
NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:URL];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection setDelegateQueue:[[NSOperationQueue alloc] init]];
[connection start];
});
}
Inside the connection delegate methods, I surrounded each MBProgress call with
dispatch_async(dispatch_get_main_queue(), ^{
//...
}
Hope this helps others. I spent all day on this and it was such a simple misunderstanding...
Related
I am creating a work queue to perform tasks in the background. The code is below. The problem is that selector called by the timer is called twice every period, by 2 different timers.
The queue (UpdateController) is created in didFinishLaunchingWithOptions of the AppDelegate:
...
[self setUpdateController:[[FFUpdateController alloc] initWithRootDetailViewController:rdvc]];
[[self updateController] start];
...
Here’s the UpdateController initializer
- (id) initWithRootDetailViewController:(FFRootDetailViewController*)rdvc
{
if (self = [super init])
{
_rootDetailViewController = rdvc;
_updateQueue = [[NSOperationQueue alloc] init];
}
return self;
}
Here’s UpdateController start
- (void) start
{
//sweep once a minute for updates
[self setTimer:[NSTimer scheduledTimerWithTimeInterval:60.0 target:self selector:#selector(sweepForUpdates:) userInfo:nil repeats:YES]];
}
Here is sweepForUpdates, the selector called by the timer:
- (void) sweepForUpdates:(NSTimer*)timer
{
FormHeader* fh;
NSInvocationOperation* op;
NSInteger sectionIdx = [[self dataController] sectionIndexForFormTypeWithTitle:SFTShares];
NSInteger headerCount = [[self dataController] numberOfRowsInSection:sectionIdx];
NSArray* changed;
NSMutableDictionary* params;
NSLog(#"Notice - sweepForUpdates(1) called:");
for (NSInteger i = 0; i < headerCount; i++)
{
fh = [[self dataController] formHeaderAtIndexPath:[NSIndexPath indexPathForRow:i inSection:sectionIdx]];
changed = [[self dataController] formDatasModifiedSince:[fh modifiedAt] ForFormHeader:fh];
if ([changed count])
{
NSLog(#"Error - sweepForUpdates(2) update: changes to update found");
params = [[NSMutableDictionary alloc] init];
[params setObject:fh forKey:#"formHeader"];
[params setObject:[self rootDetailViewController] forKey:#"rootDetailViewController"];
op = [[NSInvocationOperation alloc] initWithTarget:[FFParseController sharedInstance] selector:#selector(updateRemoteForm:) object:params];
if ([[[self updateQueue] operations] count])
{
[op addDependency:[[[self updateQueue] operations] lastObject]];
}
[[self updateQueue] addOperation:op];
}
else
{
NSLog(#"Error - sweepForUpdates(3) save: no changes found");
}
}
NSLog(#"Notice - sweepForUpdates(4) done:");
}
In this case there are 2 objects to examine for updates. Here is the console output for 1 sweep:
2015-02-16 09:22:28.569 formogen[683:806645] Notice - sweepForUpdates(1) called:
2015-02-16 09:22:28.580 formogen[683:806645] Error - sweepForUpdates(3) save: no changes found
2015-02-16 09:22:28.583 formogen[683:806645] Error - sweepForUpdates(3) save: no changes found
2015-02-16 09:22:28.584 formogen[683:806645] Notice - sweepForUpdates(4) done:
2015-02-16 09:22:29.249 formogen[683:806645] Notice - sweepForUpdates(1) called:
2015-02-16 09:22:29.254 formogen[683:806645] Error - sweepForUpdates(3) save: no changes found
2015-02-16 09:22:29.256 formogen[683:806645] Error - sweepForUpdates(3) save: no changes found
2015-02-16 09:22:29.256 formogen[683:806645] Notice - sweepForUpdates(4) done:
Neither object has updates, which is correct. But I do not understand why the selector is called twice.
Thanks
Add logging to start. You probably call it more than once.
Note that UpdateController can never deallocate, because the timer is retaining it. That may be ok, but keep that in mind if you believe you're deallocating it (and its timer).
I was successful in creating XPC service and communicating with XPC service by sending messages from main application. But what I want to know is, whether its possible to initiate a communication from XPC service to the main application. The Apple documentation says XPC is bidirectional. It would be much appreciated if someone can point me in right direction with an example.
Please note,
I want to launch the XPC from main application.
communicate with XPC from main application.
when some events occur, XPC should send a message to main application.
I succeeded in first two, but couldn't find any resource on the third one.
Thanks. :)
Figured everything out. The following should be a decent example:
ProcessorListener.h (included in both client and server):
#protocol Processor
- (void) doProcessing: (void (^)(NSString *response))reply;
#end
#protocol Progress
- (void) updateProgress: (double) currentProgress;
- (void) finished;
#end
#interface ProcessorListener : NSObject<NSXPCListenerDelegate, Processor>
#property (weak) NSXPCConnection *xpcConnection;
#end
ProcessorListener.m: (Included in just the server)
#import "ProcessorListener.h"
#implementation ProcessorListener
- (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection
{
[newConnection setExportedInterface: [NSXPCInterface interfaceWithProtocol:#protocol(Processor)]];
[newConnection setExportedObject: self];
self.xpcConnection = newConnection;
newConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol: #protocol(Progress)];
// connections start suspended by default, so resume and start receiving them
[newConnection resume];
return YES;
}
- (void) doProcessing: (void (^)(NSString *g))reply
{
dispatch_async(dispatch_get_global_queue(0,0), ^{
for(int index = 0; index < 60; ++index)
{
[NSThread sleepWithTimeInterval: 1];
[[self.xpcConnection remoteObjectProxy] updateProgress: (double)index / (double)60 * 100];
}
[[self.xpcConnection remoteObjectProxy] finished];
}
// nil is a valid return value.
reply(#"This is a reply!");
}
#end
MainApplication.m (your main app):
#import "ProcessorListener.h"
- (void) executeRemoteProcess
{
// Create our connection
NSXPCInterface * myCookieInterface = [NSXPCInterface interfaceWithProtocol: #protocol(Processor)];
NSXPCConnection * connection = [[NSXPCConnection alloc] initWithServiceName: kServiceName];
[connection setRemoteObjectInterface: myCookieInterface];
connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:#protocol(Progress)];
connection.exportedObject = self;
[connection resume];
id<Processor> theProcessor = [connection remoteObjectProxyWithErrorHandler:^(NSError *err)
{
NSAlert *alert = [[NSAlert alloc] init];
[alert addButtonWithTitle: #"OK"];
[alert setMessageText: err.localizedDescription];
[alert setAlertStyle: NSWarningAlertStyle];
[alert performSelectorOnMainThread: #selector(runModal) withObject: nil waitUntilDone: YES];
}];
[theProcessor doProcessing: ^(NSString * response)
{
NSLog(#"Received response: %#", response);
}];
}
#pragma mark -
#pragma mark Progress
- (void) updateProgress: (double) currentProgress
{
NSLog(#"In progress: %f", currentProgress);
}
- (void) finished
{
NSLog(#"Has finished!");
}
#end
Note that this code is is a condensed version based on my working code. It may not compile 100% but shows the key concepts used. In the example, the doProcessing runs async to show that the callbacks defined in the Progress protocol still get executed even after the initial method has return and the Received response: This is a reply! has been logged.
I am trying to download small PDF files 1-2Mb maybe 5 Mb at most. I am working on SelectionViewController.m class which has a UIProgressView called progressBar and it also implements the NSURLConnectionDataDelegate protocol.
The funny thing is that my progress bar goes from 0.0 to 1.0 suddenly. I would like to increment this slowly so it looks nice.
Here is my code:
-(void)downloadPDFToMyDocumentsFrom:(NSString*) PDFUrl filename:(NSString *) title {
NSURL *url = [[NSURL alloc] initWithString:PDFUrl];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
NSLog(#"%#", self);
fileName = [title stringByAppendingPathExtension:#"pdf"];
filePath = [[SearchFeed pathToDocuments] stringByAppendingPathComponent:fileName];
}
//handling incoming data
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)recievedData{
if(self.data == nil)
{
self.data = [[NSMutableData alloc] initWithCapacity:2048];
}
[self.data appendData:recievedData];
NSNumber *resourceLength = [NSNumber numberWithUnsignedInteger:[self.data length]];
float progress = [resourceLength floatValue] /[fileLength floatValue];
self.progressBar.progress = progress;
}
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
long length = [response expectedContentLength];
fileLength = [NSNumber numberWithUnsignedInteger:length];
//lastProgress = 0.0;
//currentLength = 0.0;
NSLog(#"%f ------------------------------------------------------- is the fileLength", [fileLength floatValue]);
}
//handling connection progress
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
//WRITE code to set the progress bar to 1.0
//self.progressBar.progress = 1.0;
// [fileStream close];
[self.data writeToFile:filePath atomically:YES];
//[listFileAtPath:self.filePath];
connection = nil;
NSLog(#"%f %f-------------------------------------------- Finished Loading ", lastProgress, [fileLength floatValue]);
}
I have put some NSLog statements to help me debug and it seems like its downloading all at once.
Here is the output of all the NSLog:
2013-09-05 20:30:04.856 Revista[63246:c07] <SelectionViewController: 0x7180000>
2013-09-05 20:30:04.859 Revista[63246:c07] 63561.000000 ------------------------------------------------------- is the fileLength
2013-09-05 20:30:04.861 Revista[63246:c07] 0.000000 63561.000000-------------------------------------------- Finished Loading
Do you guys have any idea on how to make it download in pieces and make the progress bar move smoothly?
Nevermind, my network is too fast! That's why I couldn't see the download in progress because it is lightening quick.
Thanks to Comcast!
I am new to cocoa programming , I am trying to download binary file from a url to disk. Unfortunately the methods are not calledback for some reason. The call to downloadFile is made from a background thread started via [self performSelectorOnBackground] etc. Any ideas what am I doing wrong ?
NOTE
When I make call to downloadFile from main UI thread, it seems to work, what's up with background thread ?
-(BOOL) downloadFile
{
BOOL downloadStarted = NO;
// Create the request.
NSURLRequest *theRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:versionLocation]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// Create the connection with the request and start loading the data.
NSURLDownload *theDownload = [[NSURLDownload alloc] initWithRequest:theRequest
delegate:self];
if (theDownload) {
// Set the destination file.
[theDownload setDestination:#"/tmp" allowOverwrite:YES];
downloadStarted = YES;
} else {
// inform the user that the download failed.
downloadStarted = NO;
}
return downloadStarted;
}
- (void)download:(NSURLDownload *)download didFailWithError:(NSError *)error
{
// Release the connection.
[download release];
// Inform the user.
NSLog(#"Download failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)downloadDidFinish:(NSURLDownload *)download
{
// Release the connection.
[download release];
// Do something with the data.
NSLog(#"%#",#"downloadDidFinish");
}
I think you should start the run loop to which your NSURLDownload object is attached. By default it will use the current thread's run loop, so you probably should do something like this after initialization of NSURLDownload object:
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
while (!self.downloaded && !self.error && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]])
;
Properties self.downloaded and self.error should be set in your callbacks.
In main thread run loop probably started by the NSApplication object.
I am attempting to update a "status label", NSTextField, with the current (X) of total (Y) when downloading files from an NSURLConnection. Below is some code that is working, but not 100%, or the way I would like.
X = runningCurrent
Y = runningTotal
The following code updates the (Y) or ofTotal correctly, however, the (X) or current jumps all over the place and does not increment 1, 2, 3 .. etc.
ApplicationController
- (void) updateLabelWithCurrent:(int)current ofTotal:(int)total
{
[txtStatus setStringValue:[NSString stringWithFormat:#"Downloading %i of %i",current,total]];
[txtStatus setNeedsDisplay:YES];
}
XML Data Source
for (int x = 0; x < [catArray count]; x++)
{
/* download each file to the corresponding category sub-directory */
[[WCSWallpaperDownloader alloc]
initWithWallpaperURL: [NSURL URLWithString:[[catArray objectAtIndex:x] objectForKey:#"imageUrl"]]
andFileOutput: [NSString stringWithFormat:#"%#/%#_0%i.jpg",cat,catName,x] withCurrent:x ofTotal:[catArray count]];
}
WCSWallpaperDownloader
- (id)initWithWallpaperURL:(NSURL *)imageUrl andFileOutput:(NSString*)fileOutput withCurrent:(int)current ofTotal:(int)total
{
self = [super init];
if (self)
{
appController = [[ApplicationController alloc] init];
self.fileOut = fileOutput;
NSURLRequest *imageRequest =
[NSURLRequest requestWithURL:imageUrl cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:1800.0];
[[NSURLConnection alloc] initWithRequest:imageRequest delegate:self];
runningCurrent = current;
runningTotal = total;
}
return self;
}
#pragma mark NSURLConenction
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
receivedData = [[NSMutableData data] retain];
[receivedData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[receivedData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
/* release the connection, and the data object */
[connection release];
[receivedData release];
NSLog(#"Connection failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSErrorFailingURLStringKey]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
/* updates the status label with the current download of total objects being downloaded */
[appController updateLabelWithCurrent: runningCurrent ofTotal: runningTotal];
/* skip existing files */
if ( ! [MANAGER fileExistsAtPath:fileOut] )
{
[receivedData writeToFile:fileOut atomically:YES];
[receivedData release];
}
[[appController txtStatus] setStringValue:#""];
}
Solution
The following code correctly increments the download status as each object finishes.
- (void) incrementStatusLabelWithTotal:(int)total
{
runningCurrent++;
[txtStatus setStringValue:[NSString stringWithFormat:#"Downloading %i of %i",runningCurrent,total]];
}
It looks like you are setting off your downloads one by one, but they are not finishing in the same order - so you create each object telling it that it is loading item X of Y, but if the object downloading item 6 finishes before the object downloading item 4, your X is going to go, as you say, all over the place.
Each wallpaper downloader should just tell the appController that it has finished, and let the appController hold the number of items that have been downloaded so far, and the total number.
In fact, the wallpaper downloaders don't really need to know how many downloads are happening, or which particular number they are. Your XML data source should be telling your "app controller" the total number of downloads, and then each downloader, as it finishes, should tell the controller that it is done.
So, your current init method would just be:
- (id)initWithWallpaperURL:(NSURL *)imageUrl andFileOutput:(NSString*)fileOutput
I'm not sure you should be allocating a new instance of appController each time in this method - the rest of the code looks like there should be a single one of these which is displaying one label, effectively a delegate for the downloader? Perhaps this should be assigned by the XML data source when it creates each object?
After the download is complete, your connectionDidFinishLoading method would be something like this:
[appController downloaderDidFinishDownloading:self];
Which would call a method in your appController that looks something like this:
-(void)downloaderDidFinishDownloading:(WCSWallpaperDownloader*)downloader
{
completedDownloads++;
[txtStatus setStringValue:[NSString stringWithFormat:#"Downloaded %i of %i",completedDownloads,totalDownloads]];
}
Where completedDownloads and totalDownloads are ivars in your app controller class.