I want to implement a progress bar in my application. The process happening is the app copying a directory into the iOS documents directory. Usually takes anywhere from 7-10 seconds (iPhone 4 tested). My understanding of progress bars is you update the bar as things are happening. But based on the copying of the directory code, I am not sure how to know how far along it is.
Can anyone offer any suggestions or examples on how this can be done? Progress bar code are below and the copying of the directory code.
Thanks!
UIProgressView *progressView = [[UIProgressView alloc] initWithProgressViewStyle: UIProgressViewStyleBar];
progressView.progress = 0.75f;
[self.view addSubview: progressView];
[progressView release];
//Takes 7-10 Seconds. Show progress bar for this code
if (![fileManager fileExistsAtPath:dataPath]) {
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
NSString *imageDataPath = [bundlePath stringByAppendingPathComponent:_dataPath];
if (imageDataPath) {
[fileManager copyItemAtPath:imageDataPath toPath_:dataPath error:nil];
}
}
define NSTimer *timer in your .h file
if (![fileManager fileExistsAtPath:dataPath]) {
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
NSString *imageDataPath = [bundlePath stringByAppendingPathComponent:_dataPath];
timer = [NSTimer timerWithTimeInterval:0.5 target:self selector:#selector(updateProgressView) userInfo:nil repeats:YES];
[timer fire];
if (imageDataPath) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[fileManager copyItemAtPath:imageDataPath toPath_:dataPath error:nil];
};
}
}
and add this method
- (void) updateProgressView{
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
NSString *imageDataPath = [bundlePath stringByAppendingPathComponent:_dataPath];
NSData *allData = [NSData dataWithContentsOfFile:imageDataPath];
NSData *writtenData = [NSData dataWithContentsOfFile:dataPath];
float progress = [writtenData length]/(float)[allData length];
[pro setProgress:progress];
if (progress == 1.0){
[timer invalidate];
}
}
If it takes this long because there are many files in that directory, you could copy the files one by one in a loop. To determine the progress, you could/should simply assume that copying each files takes the same amount of time.
Note that you don't want to block the UI for this 7-10 seconds, so you need to copy on a separate non-main thread. Setting the progress bar, like all UI code, needs to be done on the mean thread using:
dispatch_async(dispatch_get_main_queue(), ^
{
progressBar.progress = numberCopied / (float)totalCount;
});
The cast to float gives you slightly (depending on number of files) better accuracy, because a pure int division truncates the remainder.
Related
This is the function to download images , in this function i have to update the UI, the UILabels that shows total images and counter value (downloaded images).
thats why i am calling a thread.
-(void) DownloadImages
{
NSLog(#"Images count to download %lu",(unsigned long)[productID count]);
if ([productID count] > 0)
{
// Document Directory
NSString *myDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
for (i = 0; i < [productID count]; i++)
{
[NSThread detachNewThreadSelector: #selector(UpdateLabels) toTarget:self withObject:nil];
// [self UpdateLabels]; this does not work….
NSLog(#"image no %d", i);
//[self Status];
NSString *imageURL = [NSString stringWithFormat:#"http://serverPath/%#/OnlineSale/images/products/%#img.jpg",clientSite,[productID objectAtIndex:i]];
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]]];
// Reference path
NSString *imagePath = [NSString stringWithFormat:#"%#/%#-1-lrg.jpg",myDirectory,[productID objectAtIndex:i]];
NSData *imageData = [NSData dataWithData:UIImageJPEGRepresentation(image, 1.0f)];
[imageData writeToFile:imagePath atomically:YES];
}
NSLog(#"Downloading images completed...");
}
}
Problem:
- here for every new image i have to call every time a new NSThread, there may be thousands of images and it looks bad that there will be thousands threads.
in simulator i have tested it for 3000 images, but in my iPad when testing it gives the error,,,
App is Exited and terminated due to memory pressure.
i guess the error is due to this calling approach of new thread every time.
what i am doing wrong here.?????????????
i have searched there are two different options but i don't know which one is right option,
Options are:
- [self performSelectorInBackground:#selector(UpdateLabels) withObject:nil];
- [NSThread detachNewThreadSelector: #selector(UpdateLabels) toTarget:self withObject:nil];
and i also want to know that if i use
[NSThread cancelPreviousPerformRequestsWithTarget:self];
just after the call of selector, is it right approach or not..???
My suggestion is to use NSOperationQueue . you could then set the maxConcurrentOperationCount property to whatever you like.
In addition in your case is the best approach.
Make some research on what are your maxConcurrentOperationCount with how many concurrent thread your iphone could run without any issue.
NSOperationQueue will manage this for you run new thread with this limitation.
It should look something like this:
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
[myQueue setMaxConcurrentOperationCount:500];
for (i = 0; i < [productID count]; i++) {
[myQueue addOperationWithBlock:^{
// you image download here
}];
}
but check apple reference first.
i am downloading images from a server, this is my code.
-(void) DownloadImages
{
NSLog(#"Images count to download %lu",(unsigned long)[productID count]); // about 3000
if ([productID count] > 0)
{
// Document Directory
NSString *myDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
for (i = 0; i < [productID count]; i++)
{
#autoreleasepool // is this right of use autoreleasepool???
{
#autoreleasepool
{
[self performSelectorInBackground:#selector(UpdateUI) withObject:nil];
}
NSLog(#"image no %d", i);
NSString *imageURL = [NSString stringWithFormat:#"http://serverPath/%#/OnlineSale/images/products/%#img.jpg",clientSite,[productID objectAtIndex:i]];
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]]];
// Reference path
NSString *imagePath = [NSString stringWithFormat:#"%#/%#img.jpg",myDirectory,[productID objectAtIndex:i]];
NSData *imageData = [NSData dataWithData:UIImageJPEGRepresentation(image, 1.0f)];
[imageData writeToFile:imagePath atomically:YES];
}
}
NSLog(#"Downloading images completed...");
}
}
EDIT: this is UpdateUI method
-(void) UpdateUI
{
spinner.hidden = NO;
[spinner startAnimating];
progressCounter.hidden = NO;
status.text = #"Synchronising Product Images...";
progressCounter.text = [NSString stringWithFormat:#"%d / %lu", i,(unsigned long)[productID count]];
}
while processing this, when i am in simulator the maximum memory used is:
on more than 1500 images download.
but testing on iPad, the maximum memory is reached to 106.9 MB and app crashed with this message, just after 500 images download:
and screen shows this message:
i am stuck on this point from last two days, after scratching my head don't find any solution. please help me on this issue..
Edit:
using autoreleasedpool way is right or not???
First of all, I would suggest enclosing the whole for loop block inside of #autoreleasepool. It would look like this:
for (i = 0; i < [productID count]; i++)
{
#autoreleasepool // is this right of use autoreleasepool???
{
[self performSelectorInBackground:#selector(UpdateUI) withObject:nil];
NSLog(#"image no %d", i);
NSString *imageURL = [NSString stringWithFormat:#"http://serverPath/%#/OnlineSale/images/products/%#img.jpg",clientSite,[productID objectAtIndex:i]];
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]]];
// Reference path
NSString *imagePath = [NSString stringWithFormat:#"%#/%#img.jpg",myDirectory,[productID objectAtIndex:i]];
NSData *imageData = [NSData dataWithData:UIImageJPEGRepresentation(image, 1.0f)];
[imageData writeToFile:imagePath atomically:YES];
}
}
If what you tried to accomplish with your #autoreleasepool block was handling memory allocated in updateUI, that is not the right approach. You should instead enclose updateUI body inside of #autoreleasepool.
I would also suggest to post the content of updateUI; you could even comment that call out and see whether the download-only is causing the memory issue.
More generally, it seems pretty suspicious to me that you are updating the UI on a background thread. UI should be only updated on the main thread, as a general practice. (But without seeing updateUI definition, I cannot really say if this is ok or not).
About the different results you get with a device or the simulator, this is "normal". The simulator is totally unreliable when it come to memory handling, you could also detect leaks with the simulator that are not there on a device.
EDIT:
As an easy patch, try with this approach:
for (i = 0; i < [productID count]; i++)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self UpdateUI];
NSLog(#"image no %d", i);
NSString *imageURL = [NSString stringWithFormat:#"http://serverPath/%#/OnlineSale/images/products/%#img.jpg",clientSite,[productID objectAtIndex:i]];
UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]]];
// Reference path
NSString *imagePath = [NSString stringWithFormat:#"%#/%#img.jpg",myDirectory,[productID objectAtIndex:i]];
NSData *imageData = [NSData dataWithData:UIImageJPEGRepresentation(image, 1.0f)];
[imageData writeToFile:imagePath atomically:YES];
});
}
As you'll notice, I am dispatching each step in the loop individually. In this way, the mail loop will be able to update the UI. Notice that this implementation is not optimal, since dataWithContentsOfURL will take a little to execute (since it entails a network access) and should not be executed on the main thread (but you were doing it already like this). That should be dispatched on a background queue.
You should use more appropriate method for fetching Image data.
I suggest you to try NSURLDownload class.
Alternatively you could use one of popular 3rd party networking framework like AFNetworking
I output an m4a file from Novocaine's sample project. But I cannot open the file in iTunes. The file might be corrupted.
NSArray *pathComponents = [NSArray arrayWithObjects:
[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject],
#"My Recording.m4a",
nil];
NSURL *outputFileURL = [NSURL fileURLWithPathComponents:pathComponents];
NSLog(#"URL: %#", outputFileURL);
self.fileWriter = [[AudioFileWriter alloc]
initWithAudioFileURL:outputFileURL
samplingRate:self.audioManager.samplingRate
numChannels:self.audioManager.numInputChannels];
__block int counter = 0;
self.audioManager.inputBlock = ^(float *data, UInt32 numFrames, UInt32 numChannels) {
[wself.fileWriter writeNewAudio:data numFrames:numFrames numChannels:numChannels];
counter += 1;
if (counter > 800) { // roughly 5 seconds of audio
wself.audioManager.inputBlock = nil;
}
};
I did not change the code, just removed comment-out.
If you know possible solutions or suggestions, I was wondering if you could share me.
Novocaine https://github.com/alexbw/novocaine
This issue is discussed here:
https://github.com/alexbw/novocaine/issues/59
The original code terminates the recording with this line (inside a handler block):
wself.audioManager.inputBlock = nil;
I got it working by adding this line immediately after:
[wself.fileWriter stop];
The stop method of AudioFileWriter is private, so it is also necessary to add it to the public interface.
I've been trying to get this working for a day now and I'm still failing. I want to copy a number of files into the Documents folder of my app from the bundle when the app is installed, but this makes the user wait for a long time with the app showing the splash screen.
So I thought I'd make an initial UIAlertView with a UIProgressView as a subview that gets updated every time a file is copied into the documents folder. However the alert shows and the progress bar never gets updated. My logic was:
Set up the UIProgressView and UIAlertView as instance variables of my ViewController.
In ViewDidLoad, present the alert and set the delegate
In - (void)didPresentAlertView:(UIAlertView *)alertView perform a for loop that copies the files and updates the UI. the code was :
- (void)didPresentAlertView:(UIAlertView *)alertView{
NSString *src, *path;
src = // path to the Bundle folder where the docs are stored //
NSArray *docs = [[NSFileManager defaultManager]contentsOfDirectoryAtPath:src error:nil];
float total = (float)[docs count];
float index = 1;
for (NSString *filename in docs){
path = [src stringByAppendingPathComponent:filename];
if ([[NSFileManager defaultManager]fileExistsAtPath:path]) {
... // Copy files into documents folder
[self performSelectorOnMainThread:#selector(changeUI:) withObject:[NSNumber numberWithFloat:index/total] waitUntilDone:YES];
index++;
}
}
[alertView dismissWithClickedButtonIndex:-1 animated:YES];
}
And the code for ChangeUI is
- (void) changeUI: (NSNumber*)value{
NSLog(#"change ui %f", value.floatValue);
[progressBar setProgress:value.floatValue];
}
However this just updates the UI from 0 to 1, although the NSLog prints all the intermediate values. Does anyone here know what I'm doing wrong?
Thanks in advance.
The problem is that your loop is on the main thread, and thus the UI has no chance to update until the very end. Try doing the work on a background thread using GCD:
dispatch_async(DISPATCH_QUEUE_PRIORITY_DEFAULT, ^
{
NSString *src, *path;
src = // path to the Bundle folder where the docs are stored //
NSArray *docs = [[NSFileManager defaultManager]contentsOfDirectoryAtPath:src error:nil];
float total = (float)[docs count];
float index = 1;
for (NSString *filename in docs){
path = [src stringByAppendingPathComponent:filename];
if ([[NSFileManager defaultManager]fileExistsAtPath:path]) {
... // Copy files into documents folder
dispatch_async(dispatch_get_main_queue(), ^{ [self changeUI:[NSNumber numberWithFloat:index/total]]; } );
index++;
}
}
dispatch_async(dispatch_get_main_queue(), ^{ [alertView dismissWithClickedButtonIndex:-1 animated:YES]; } );
} );
I thought maybe the fastest way was to go with Sound Services. It is quite efficient, but I need to play sounds in a sequence, not overlapped. Therefore I used a callback method to check when the sound has finished. This cycle produces around 0.3 seconds in lag. I know this sounds very strict, but it is basically the main axis of the program.
EDIT: I now tried using AVAudioPlayer, but I can't play sounds in a sequence without using audioPlayerDidFinishPlaying since that would put me in the same situation as with the callback method of SoundServices.
EDIT2: I think that if I could somehow get to join the parts of the sounds I want to play into a large file, I could get the whole audio file to sound continuously.
EDIT3: I thought this would work, but the audio overlaps:
waitTime = player.deviceCurrentTime;
for (int k = 0; k < [colores count]; k++)
{
player.currentTime = 0;
[player playAtTime:waitTime];
waitTime += player.duration;
}
Thanks
I just tried a technique that I think will work well for you. Build an audio file with your sounds concatenated. Then build some meta data about your sounds like this:
#property (strong, nonatomic) NSMutableDictionary *soundData;
#synthesize soundData=_soundData;
- (void)viewDidLoad {
[super viewDidLoad];
_soundData = [NSMutableDictionary dictionary];
NSArray *sound = [NSArray arrayWithObjects:[NSNumber numberWithFloat:5.0], [NSNumber numberWithFloat:0.5], nil];
[self.soundData setValue:sound forKey:#"soundA"];
sound = [NSArray arrayWithObjects:[NSNumber numberWithFloat:6.0], [NSNumber numberWithFloat:0.5], nil];
[self.soundData setValue:sound forKey:#"soundB"];
sound = [NSArray arrayWithObjects:[NSNumber numberWithFloat:7.0], [NSNumber numberWithFloat:0.5], nil];
[self.soundData setValue:sound forKey:#"soundC"];
}
The first number is the offset of the sound in the file, the second is the duration. Then get your player ready to play like this...
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:#"%#/audiofile.mp3", [[NSBundle mainBundle] resourcePath]]];
NSError *error;
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
audioPlayer.numberOfLoops = -1;
if (audioPlayer == nil)
NSLog(#"%#", [error description]);
else {
[audioPlayer prepareToPlay];
}
}
Then you can build a low-level sound playing method like this ...
- (void)playSound:(NSString *)name withCompletion:(void (^)(void))completion {
NSArray *sound = [self.soundData valueForKey:name];
if (!sound) return;
NSTimeInterval offset = [[sound objectAtIndex:0] floatValue];
NSTimeInterval duration = [[sound objectAtIndex:1] floatValue];
audioPlayer.currentTime = offset;
[audioPlayer play];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, duration * NSEC_PER_SEC), dispatch_get_current_queue(), ^{
[audioPlayer pause];
completion();
});
}
And you can play sounds in rapid combination like this ...
- (IBAction)playAB:(id)sender {
[self playSound:#"soundA" withCompletion:^{
[self playSound:#"soundB" withCompletion:^{}];
}];
}
Rather than nesting blocks, you could build a higher-level method that takes a list of sound names and plays them one after the other, that would look like this:
- (void)playSoundList:(NSArray *)soundNames withCompletion:(void (^)(void))completion {
if (![soundNames count]) return completion();
NSString *firstSound = [soundNames objectAtIndex:0];
NSRange remainingRange = NSMakeRange(1, [soundNames count]-1);
NSArray *remainingSounds = [soundNames subarrayWithRange:remainingRange];
[self playSound:firstSound withCompletion:^{
[self playSoundList:remainingSounds withCompletion:completion];
}];
}
Call it like this...
NSArray *list = [NSArray arrayWithObjects:#"soundB", #"soundC", #"soundA", nil];
[self playSoundList:list withCompletion:^{ NSLog(#"done"); }];
I'm assuming you want to change the sequence or omit sounds sometimes. (Otherwise you would just build the asset with all three sounds in a row and play that).
There might be a better idea out there, but to get things very tight, you could consider producing that concatenated asset, pre-loading it - moving up all the latency to that one load, then seeking around it to change the sound.