Importing multiple photos from album, one of the delegate method is
// Here info is array of dictionary containing FileName/AssetURL etc
- (void)somePicker(SomePicker*)somePicker didFinishPickingMediaWithInfo:(NSArray *)info {
_importStatusView.center = self.view.center;
[self.view addSubview:_importStatusView];
[self dismissViewControllerAnimated:YES completion:^{
NSNumber *total = [NSNumber numberWithInteger:info.count];
[info enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSDictionary *imageInfo = (NSDictionary*)obj;
NSString *fileName = [imageInfo objectForKey:#"UIImagePickerControllerFileName"];
NSURL *imageURL = [imageInfo objectForKey:UIImagePickerControllerReferenceURL];
ALAssetsLibrary *assetLibrary=[[ALAssetsLibrary alloc] init];
[assetLibrary assetForURL:imageURL resultBlock:^(ALAsset *asset) {
NSLog(#"start");
ALAssetRepresentation *rep = [asset defaultRepresentation];
Byte *buffer = (Byte*)malloc(rep.size);
NSUInteger buffered = [rep getBytes:buffer fromOffset:0.0 length:rep.size error:nil];
NSData *data = [NSData dataWithBytesNoCopy:buffer length:buffered freeWhenDone:YES];
NSString *filePath = [_currentPath stringByAppendingPathComponent:fileName];
[data writeToFile:filePath atomically:YES];
//This also has no effect
//dispatch_async(dispatch_get_main_queue(), ^{
//_lblImportCountStatus.text = [NSString stringWithFormat:#"%d of %d",idx+1,[total integerValue]];
//NSLog(#"Label value->%#",_lblImportCountStatus.text); //This prints values but after everything is finished it prints all line at once i.e. at the end of the enumeration of all items
//});
//Update UI
NSNumber *current = [NSNumber numberWithInteger:idx+1];
NSDictionary *status = [NSDictionary dictionaryWithObjectsAndKeys:current,#"current", total,#"totalCount", nil];
[self performSelectorOnMainThread:#selector(updateImportCount:) withObject:status waitUntilDone:YES];
//_lblImportCountStatus.text = [NSString stringWithFormat:#"%d of %d",idx+1,[total integerValue]];
if(idx==info.count-1){
[_importStatusView removeFromSuperview];
}
NSLog(#"Finish");
} failureBlock:^(NSError *error) {
NSLog(#"Error: %#",[error localizedDescription]);
}];
}];
}];
}
My declaration for status view and label is
#property (strong, nonatomic) IBOutlet UIView *importStatusView; //View containing label
#property (weak, nonatomic) IBOutlet UILabel *lblImportCountStatus; //Label
Everything in above code is working fine and as expected, but the problem is a importStatusView is being added to the screen but lblImportCountStatus value is not displaying, though If I log the values it shows updated.
When enumeration is finished at the end all the NSLog gets printed for e.g. If I have imported 10 photos than at last it prints, i.e. dispatch_async(dispatch_get_main_queue() this function has no effect at all while enumeration is in progress.
Label value->1 of 10
Label value->2 of 10
Label value->3 of 10
Label value->4 of 10
Label value->5 of 10
Label value->6 of 10
Label value->7 of 10
Label value->8 of 10
Label value->9 of 10
Label value->10 of 10
What could be the issue ?
Update:
-(void)updateImportCount:(NSDictionary*)info{ //(NSNumber*)current forTotalItems:(NSNumber*)totalCount{
NSNumber *current = [info objectForKey:#"current"];
NSNumber *totalCount = [info objectForKey:#"totalCount"];
_lblImportCountStatus.text = [NSString stringWithFormat:#"%d of %d",[current integerValue],[totalCount integerValue]];
[_lblImportCountStatus setNeedsDisplay];
NSLog(#"Updating ui->%#",_lblImportCountStatus.text);
}
Above function works on main thread and updates but stil label is not shown it prints following NSLog
start
Updating ui->1 of 10
Finish
start
Updating ui->2 of 10
Finish
start
Updating ui->3 of 10
Finish
start
Updating ui->4 of 10
Finish
start
Updating ui->5 of 10
Finish
start
Updating ui->6 of 10
Finish
start
Updating ui->7 of 10
Finish
start
Updating ui->8 of 10
Finish
start
Updating ui->9 of 10
Finish
start
Updating ui->10 of 10
Finish
I have uploaded project at this location, please feel free to help.
All those blocks are executing on the main thread (the easiest way to verify this is using NSThread's +currentThread to get the current thread, and -isMainThread to check if it's the main thread. Anywhere you have code that you want to see what thread it's on, do something like this:
NSLog( #"enumeration block on main thread: %#",
[[NSThread currentThread] isMainThread] ? #"YES" : #"NO" );
I think the problem is, since this is all executing on the main thread, you're locking up the runloop, not giving the UI a chance to update.
The right way to fix this is probably to really do this processing on a separate thread (calling the code to update the UI via performSelectorOnMainThread:, as you're doing now). But, a quick hack to make it work would be to allow the runloop to run. At the end of updateImportCount:, do something like this:
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
It ain't pretty, but it will work.
Update:
In Cocoa (Mac OS and iOS) there a concept of a runloop. The main thread of your application drives an NSRunLoop which serves as the event loop. User actions -- taps, etc. -- are processed through this loop, as are things like timers, network connections, and other things. See the NSRunLoop Reference for more information on that.
In iOS, drawing also happens on the runloop. So when you call setNeedsDisplay on a view, that view is not redrawn immediately. Rather, it's simply flagged as needing redraw, and then on the next drawing cycle (the next trip through the runloop) the actual drawing takes place. The UIView reference has a brief description of this (see the section "The View Drawing Cycle". Quoting from that section:
When the actual content of your view changes, it is your
responsibility to notify the system that your view needs to be
redrawn. You do this by calling your view’s setNeedsDisplay or
setNeedsDisplayInRect: method of the view. These methods let the
system know that it should update the view during the next drawing
cycle. Because it waits until the next drawing cycle to update the
view, you can call these methods on multiple views to update them at
the same time.
When you return from whatever method was called in response to some user action (in this case, somePicker:didFinishPickingMediaWithInfo:, control returns to the runloop, and any views that need redrawing, are. However, if you do a bunch of processing on the main thread without returning, the runloop is basically stalled, and drawing won't take place. The code above basically gives the runloop some time to process, so drawing can take place. [NSDate date] returns the current date and time, right now, so that basically tells the runlooop "run until right now", which ends up giving it one cycle through the loop, giving you one drawing cycle, which is an opportunity for your label to be redrawn.
Related
I have a memory leak that seems to be coming from a retain cycle. The memory allocation size is increasing every time this code runs:
- (void)nextPhoto {
self.photoIndex++;
if (self.photoIndex >= [self.photos count]) {
self.photoIndex = 0;
}
__weak Photo *photo = [self.photos objectAtIndex:self.photoIndex];
[[SDWebImageManager sharedManager] downloadImageWithURL:[NSURL URLWithString:photo.thumbnailURLString] options:SDWebImageRetryFailed progress:nil
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
}];
}
The code is looping on a 2 second timer:
self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:#selector(nextPhoto) userInfo:nil repeats:YES];
The total memory use increases without bounds until I get a memory overuse event.
Why is this code causing a retain cycle? Is there a special way I need to handle self in this situation?
self.photos is an NSMutableArray
self.photoIndex is an NSInteger
SDWebImageManager is a well maintained library: https://github.com/rs/SDWebImage and I use it in numerous other locations with no issues
I don't see any problem involving a retain cycle here, even if you use self in the completion block. the block owner is SDWebImageManager so no problems here. a retain cycle could occur if you store your block in a property of your viewController, cause it then would own a block that retains it... It's not what is happening here imho.
Now your problem, i presume, comes from the UIImage. I depends of what you do in the block of course but if your storing the images then, yes every 2 seconds a new one is created and then it will fail eventually. You should keep a cache of images that has already been downloaded and try to download them only if needed... Add a NSDictionary with url as key and UIImage as value for example, this way you will only download your images once.
Ok I should have slept on this one... The function is actually working exactly as it should and it was self.photos that was increasing without bounds. Putting a limit on the size of that array fixed the "leak".
I'm having a problem where I'm unable to update UI when performing synchronous downloads. I would expect that using synchronous APIs would ensure that code executes in order (which it doesn't seem to be doing), which is really confusing me.
The following code is in a UICollectionView's didSelectItemAtIndexPath and is not wrapped in any asynchronous block or anything.
Any ideas on what I can do to be able to update the UI (most importantly a progress indicator) as these tasks occur? I think that the way it is currently laid out should work, but for some reason it's not able to update until the code has all 'executed'.
if ([internetReachable isReachable]) {
//does not become visible until after
self.circleProgress.alpha = 1.0;
//lots of downloading and saving with NSData dataWithContentsOfURL followed by this:
for (int i = 1; i < pages.count; i++) {
NSString *number;
if (i < 10) {
number = [NSString stringWithFormat:#"00%d", i];
}
else if (i < 100) {
number = [NSString stringWithFormat:#"0%d", i];
}
else {
number = [NSString stringWithFormat:#"%d", i];
}
NSURL *imageURL = [NSURL URLWithString:[NSString stringWithFormat:#"http://books.hardbound.co/%#/%#-%#.png", slug, slug, number]];
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
[df setObject:imageData forKey:[NSString stringWithFormat:#"%#-%#", slug, number]];
CGFloat progress = ((CGFloat)i / pages.count);
//only runs for the last iteration, rather than calling the method to update the progress indicator each iteration and allowing it to update before going back to the next iteration as I would expect
[self updateProgressBarWithAmount:[NSNumber numberWithFloat:progress]];
NSLog(#"progress after: %f", self.circleProgress.progress);
}
}
UI can only be executed on the main thread. Since the main thread is busy doing the downloading, it can't update the UI. It's almost never a good idea to perform any long running operations on the main thread. You should make the download asynchronous, and update the UI on the main thread.
The loop in the code you posted will only be executed after lots of downloading and saving with NSData dataWithContentsOfURL is performed, all the while the application will be unresponsive, and that's very poor UX. Take a look at this question for a much better implementation of a progress bar.
I am not by any means qualified to explain what exactly happens during each render loop and why updateProgress doesn't actually let a screen render occur before you block the main thread again, but I am able to provide a solution.
After you update the progress of the progress view, you want the changes to get rendered "right now". This means you have to tell the current run loop to run one iteration, and then return to you so you can do another long running task.
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
Call that whoever you want the progress view to update, and it will do a screen render and then return to you.
I got this from this answer
However, you really should be doing this asynchronously.
(Apologies for any typos, as this is being typed on my phone)
I'm learning to develop an IOS app. I'm having the following problem. I want to use a label to display a string. It takes a really long time for this string to be displayed (10-15 sec). Is this normal? The following code is inside the viewDidLoad function
NSLog(self.example); //displays almost immediately
_labelOutput.text= [NSString stringWithFormat:#"%#", self.example;//takes 15 seconds
The entire viewDidLoad function:
- (void)viewDidLoad
{
[super viewDidLoad];
double lat = 43.7000;
double lon = -79.4000;
NSArray *users = [[NSArray alloc] initWithObjects:#"user_1",#"user_2",#"user_3", nil];
id prediction = [[Prediction alloc] initWithUsers:users Lat:lat Lon:lon];
[prediction populate:^{
self.resName= [prediction generateRandom][#"id"];
NSLog([NSString stringWithFormat:#"%#", self.resName]);
_labelOutput.text= [NSString stringWithFormat:#"%#", self.resName];
}];
}
What does -[Prediction populate:] do with the block? My guess is it runs the block on a background thread or queue. You aren't allowed to modify the UI from a background thread or queue. Your app might crash or just act unpredictably. Your mysterious delay in updating the screen is a common symptom of this mistake.
You must only modify the UI from the main thread or queue. Try this:
[prediction populate:^{
self.resName= [prediction generateRandom][#"id"];
NSLog([NSString stringWithFormat:#"%#", self.resName]);
dispatch_async(dispatch_get_main_queue(), ^{
_labelOutput.text= [NSString stringWithFormat:#"%#", self.resName];
});
}];
ETA: Rob's answer is probably spot-on...if you'd posted that code initially I would have caught it as well.
Try setting the label's text in viewWillAppear or perhaps viewDidAppear instead.
Setting the "text" property of a label will normally trigger a [setNeedsDisplay] call automatically via key-value observing, and this notifies the system that the label's view needs to be redrawn on the next run loop. However, viewDidLoad is called before your view is actually visible. It's likely that because of this, either [setNeedsDisplay] is not being called, or is being ignored because the label is not yet visible...and thus, you have to wait for some other event to trigger re-drawing of subviews.
You could test this theory by adding a [self.labelOutput setNeedsDisplay] call yourself in viewDidAppear.
For swift 3 you'll want to use
DispatchQueue.main.async(execute: {
_labelOutput.LabelName.text = "Something"
})
I am new to iOS programming, and I could not find an answer out there already.
In Xcode 5, I am iterating over an array, and attempting to update a label with the values as they change.
here is the .h file...
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
#property (strong, nonatomic) NSArray *currentNumber;
#property (strong, nonatomic) IBOutlet UILabel *showLabel;
- (IBAction)start;
#end
here is the main part of the .m file...
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.currentNumber = [NSArray arrayWithObjects:#"1", #"2", #"3", #"4", nil];
}
This is where it gets tricky...
The following works perfectly...
- (IBAction)start {
self.showLabel.text = [NSString stringWithFormat:#"new text"];
}
#end
As does this...
- (IBAction)start {
for (NSString *p in self.currentNumber) {
NSLog(#"%#", p);
sleep(3);
}
}
#end
But when I replace the NSLog with setting the .text attribute, it "fails". The timing still happens, and the label updates with the last item in the array after...
- (IBAction)start {
for (NSString *p in self.currentNumber) {
self.showLabel.text = [NSString stringWithFormat:#"%#", p];
sleep(3);
}
}
#end
And the last bit of weirdness, if I use the NSLog, and try to change the .text attribute before the "for" loop is called, the text change is ignored until AFTER the loop completes...
- (IBAction)start {
self.showLabel.text = [NSString stringWithFormat:#"5"];
for (NSString *p in self.currentNumber) {
NSLog(#"%#", p);
sleep(3);
}
}
#end
What am I missing?
(If you want to see the source files, you can get them at https://github.com/lamarrg/iterate
As you've realized, the UI will only update when the main thread is processing events. In a loop, it won't be.
There's a couple ways around this.
The simplest is to perform your loop in a background thread. There's a wrinkle, though: This will allow the user to continue to interact with your UI. And also, the UI can only be updated from the main thread.
You'll want to dispatch your work to the background, then have the background dispatch your work back to the main thread.
This sounds complicated, and it is. Thankfully, Apple added blocks and Grand Central Dispatch to Objective-C. You can use those to break down the chunks of code and make sure they're executed on the correct thread.
- (IBAction)start {
[self disableMyUI];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_NORMAL, 0), ^{
// this code will be executed "later", probably after start has returned.
// (in all cases, later should be considered "soon but not immediately.")
for (NSString *p in self.currentNumber) {
dispatch_async(dispatch_get_main_queue(),^{
// this code will be executed "later" by the main loop.
// You may have already moved on to the next thing, and even
// dispatched the next UI update.
// Don't worry; the main queue does things in order.
self.showLabel.text = [NSString stringWithFormat:#"%#", p];
});
sleep(3); // do your heavy lifting here, but keep in mind:
// you're on a background thread.
}
dispatch_async(dispatch_get_main_queue,^{
// this occurs "later," but after other all other UI events queued
// to the main queue.
[self enableMyUI];
});
}
// this line of code will run before work is complete
}
You'll have to write disableMyUI and enableMyUI; make sure they disable everything (including the back button if you're using navigation, the tab bar if you're using a tab bar controller, etc).
Another way around this is to use a NSTimer. However, if you do this you're still doing your work on the main thread. It'll work if you can split your work into predictable, small pieces, but you're better off doing it on a background thread.
One thing to keep in mind: Although you're not likely to run into problems while developing, doing heavy work on the main thread will lead to user crashes. On iOS there is a process that watches if applications are responding to events, such as drawing updates. If an application isn't responding to events in a timely fashion, it will be terminated. So living with the lack of UI updates isn't an option for you; you need to only do time consuming operations from background thread.
See also:
Programming with Objective-C: Working with Blocks
If you want to update the label periodically, don't use sleep. If you call it on the main thread you'll be blocking the UI, which is not very desirable.
Use a NSTimer instead, making it fire every N seconds.
Something like this will do:
- (void)startUpdatingLabel {
[NSTimer scheduledTimerWithTimeInterval:0 target:self selector:#selector(updateLabelWithIndex:) userInfo:#0 repeats:NO];
}
- (void)updateLabel:(NSTimer *)timer {
NSInteger index = [timer.userInfo integerValue];
if (index >= self.currentNumber.count) {
return;
}
self.showLabel.text = [NSString stringWithFormat:#"%#", self.currentNumber[index]];
[NSTimer scheduledTimerWithTimeInterval:3 target:self selector:#selector(updateLabelWithIndex:) userInfo:#(index+1) repeats:NO];
}
Every time updateLabel: is invoked it schedules a new timer which will call it again in 3 seconds. Each time the index value is increased and passed along.
I am porting my library from Andriod to IOS and I have made a great deal of progress and learned a little about Objective C in the process. I say a little, because there is a lot to learn. The Android library creates a surfaceView and does animation by drawing the images directly to the surfaceView; in Objective C, I am using a UIView with CALayers.
In the UIView I am dynamically adding CALayers loaded with Sprite Sheet images and then moving the CALayers around in the UIView while roating through the Sprite image in the CALayer by moving the contentsRect of the CALayer.
This all works perfectly in a background thread in Android and does not interfere with the host application. In Objective C, I am trying to run it in the background as well using either performSelectorInBackground or the new Grand Central Dispatch. Both seem to work in the background fine, loading images into the CALayers and then I hit the snag: the CALayers are not displaying.
I know the CALayers are there, because when I pause the app in the simulator by clicking on the middle hardware button and then click again to display it the CALayers are there looking great – but static.
Having read posts for days and trying various ways of setting NeedsDisplay and NeedsLayout on the UIView and the individual CALayers, I have given up and I am asking for your help.
So, please take a look at the code below and let me know what I am doing wrong. I am also having problems setting the frame of my CALayers. When I try to set them with [self setFrame: CGRectMake(0,0,spWidth, spHeight)] – both NSIntegers; I get an error saying Sending CGRect to parameter of incompatible type NSInteger. I know this is probably a rookie mistake.
Here are the relevant sections of my code. I will post more, but I am hoping that my error is in the run method.
In my custom UIView
if ((self = [super initWithFrame:CGRectMake(0,0,dw,dh)])){
(initialization and defining variables)
self.bounds = CGRectMake(0, 0, dw, dh);
self.frame = CGRectMake(0,0,dw,dh);
self.backgroundColor = [UIColor whiteColor];
MainDrawLayer = [[CALayer layer]retain];
MainDrawLayer.bounds = CGRectMake(0, 0, dw, dh); //dw=320 dh=50.
MainDrawLayer.frame = CGRectMake(0,0,dw,dh);
[self.layer addSublayer:MainDrawLayer] ;
}
return self;
All of the action is triggered in the run method below which I call from the ViewController.
- (void) run {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
PGM = [[[GadsMonitor alloc] init: #"http://www.javelin21.com/servlets/theloader" :_displaySizeW :_displaySizeH]autorelease];
while(!PGM.adsdone){
[NSThread sleepForTimeInterval:2.0];
NSLog(#"The value of adsdone is %#\n", (PGM.adsdone ? #"YES" : #"NO"));
}
NSLog(#"Into processComponent Number in PGM.PGVarray %d ", PGM.PGVarray.count);
GVarray = [PGM.PGVarray mutableCopy];
NSLog(#"Into RUN after copy GVArray %d ", GVarray.count);
Gad * readyGad = (Gad *) [GVarray objectAtIndex:0];
if (readyGad != nil)
[readyGad getImages];
NSLog(#"The value of the bool is %#\n", (readyGad.gdone ? #"YES" : #"NO"));
while (!readyGad.gdone) {
[NSThread sleepForTimeInterval:2.0];
[readyGad checkImages];
NSLog(#"The value of the readyGad.gdone is %#\n", (readyGad.gdone ? #"YES" : #"NO"));
}
rstop = NO;
while (!rstop) {
NSLog(#"The value of rstop in while is %#\n", (rstop ? #"YES" : #"NO"));
[self rotateGames];
stop = NO;
while (!stop) {
[self gameAction];
[self paint];
dispatch_async(dispatch_get_main_queue(), ^{
[self setNeedsLayout]; //I have also tried self.layer
[self setNeedsDisplay]; //I have also tried self.layer
});
//These are various timers for CALayer action
[NSThread sleepForTimeInterval:stime];
rtime = (rtime +runGad.gslp);
ltime += runGad.gslp;
jtime += runGad.gslp;
vtime += runGad.gslp;
ftime += runGad.gslp;
ttime += runGad.gslp;
Stime += runGad.gslp;
NSLog(#"After Thread Sleep rtime + gslp %f ", rtime);
if ((runTime += runGad.gslp) >= runGad.grun) {
stop = YES;
}
}
stop = NO;
}
});
}
I thank you for your time.
In iOS any UI updating code must be performed in the main thread. You cannot use GCD (unless its running on the mainQueue) or performSelectorInBackground to update the UI.
You can update your data and do IO on background threads, but when you have the data loaded and you want to display it you must switch back to the main thread.
Trying to do otherwise will sometimes work, sometimes not, or crash your app.
The main thread owns UI updates in iOS.
Also when it comes to doing the work on the main thread you have a few options, you can queue a block up with performSelectorOnMainThread
or you can send a block to the main thread GCD queue, sometimes you may need to play with dispatch_async vs dispatch_sync on the main thread if you use GCD they dont both work in every situation. performSelectorInMainthread seems to be more reliable than GCDs main thread queue.
ymmv