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"
})
Related
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)
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.
I need to show log outputs in a textview then and there in my app. I tried using performSelector inside my sequence but it did not work as I thought it would. Can someone show me how to do it?
For example, when I click a button, I do lot of operations underneath, and I want to display the logs in the textview then and there, not after the entire operation is done.
Plus can't I call performSelector more than once inside the same sequence?
Below is the sequence inside the button click:
- (IBAction)Write:(id)sender {
//do some action here
DisplayString = #"Seq1 pass"
[self performSelector:#selector(updateviewText) withObject:nil afterDelay:0];
//do some more action
DisplayString = #"Seq2 pass"
[self performSelector:#selector(updateviewText) withObject:nil afterDelay:0];
....
}
This the updateviewText part:
-(void)updateviewText {
dispatch_queue_t queueNew = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(queueNew,^ {
dispatch_async(dispatch_get_main_queue(),^{
[self.txtViewUseCaseLOG setText:[NSString stringWithFormat:#"%#\n%#",
self.txtViewUseCaseLOG.text,DisplayString ]];
});
});
}
The DisplayString is a global variable here.
This code doesn't setText to the textview then and there... But as I asked earlier I need those messages then and there...
You can't update the UI asynchronously. If you're doing task asynchronously and want to update the UI you have to use dispatch_sync.
And why would you when you're doing something asynchronously open another asynchronous task?
Next thing is that if you're only using DisplayString in these two methods you might better add a NSString parameter to updateViewText so you don't need the global variable.
I'm running mathematical computation in a background thread. Attempting to post results in real time in a UITextView. However the results don't show up until the background thread completes. Why not?
I kick off a method in the background,
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^() {
[self v2];
});
The background thread method is of the form,
- (void) v2 {
NSString *result;
// ... loop a bunch of times generating lots of results
for (bunch of stuff to compute) {
// If using dispatch_async, nothing is displayed until this method finishes
// If dispatch_sync then it does display and update
result = [self computeNextValue];
dispatch_async(dispatch_get_main_queue(), ^() {
textView.text = result;
});
} // end computation
}
This actually hadn't been much of a problem until I started trying to scroll the view. Painfully slow. So I created a NSTimer to periodically scroll the UITextView. However, even though the timer popped and the method is run to request the scroll, the UITextView doesn't scroll until the background method completes.
First, make sure that you are using weakSelf rather than self within the block.
__weak MyClass weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^() {
[weakSelf v2];
});
...
__weak MyClass weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^() {
weakSelf.textView.text = result;
});
But that won't cause the delayed update. I am very suspect of the queue's priority, DISPATCH_QUEUE_PRIORITY_LOW. Try using DISPATCH_QUEUE_PRIORITY_DEFAULT instead.
I think I figured it out, at least empirically.
It looks like setting text on a UITextView or programmatically initiating scrolling sets up an animation sequence thats run in another thread. What seems to be happening is that the requests to set the text of the UITextView is coming in faster than the ability of UITextView to setup the animation. When the property changes again, it apparently cancels the earlier animation that it was setting up on another thread. As I continue to flood it with change requests, its never able to finish what it wants to do before the value of 'text' has changed again.
My solution involves multiple steps.
In my original approach I was setting textView.text very rapidly to some new value. I set up a timer to periodically request the UITextView to scroll.
In my modification, I calculate the new value of the result string, but do not set it to the UITextView. Instead the text is set on the textview periodically based on the timer. This allows the text view to catch up.
However, I noticed that this still wasn't reliable. If I happened to set the text again while it was still scrolling, weird effects would occur, such as a very slow scroll. It seems that the scroll animation and repeated settings of text were still causing a problem.
So solve this problem, I created a property to indicate if the view is scrolling. Set the view controller as the UITextField delegate. When I request the view to scroll, I set the flag to indicate its scrolling. Only update the content and request scroll if its not already scrolling. Ends up working great. Doesn't matter how fast I set the timer, it ends up waiting appropriately.
// ViewController.h
#property BOOL isViewScrolling;
// ViewController.m
// initialize property in viewDidLoad
- (void)viewDidLoad {
self.isViewScrolling = FALSE;
textView.delegate = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(scrollIt) userInfo:nil repeats:TRUE];
}
- (void)scrollIt {
NSLog(#"scrollit thread=%d", [[NSThread currentThread]isMainThread]);
if (!self.isViewScrolling) {
textView.text = self.result;
NSRange range = NSMakeRange(textView.text.length - 1, 1);
self.isViewScrolling = TRUE;
[textView scrollRangeToVisible:range];
}
}
// UITextView delegate
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
NSLog(#"Animation stopped");
self.isViewScrolling = FALSE;
}
Try changing the predefined dispatch queue string to end with background.
__weak MyClass weakSelf = self;
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^() {
weakSelf.textView.text = result;
});
Also you should add a UIActivityIndicator and start animating at the beginning of the operation, then stop animating after the textview.text field has been updated.
It's a nice feature to have to show the user there is a process currently being done
Also I would stay away from NSThread as I have read a few forums and docs from apple that emphasize the use of GCD and block operations.
Any reason why my label won't update?
- (void)setLabel:(NSNotification*)notification {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"setLabel");
[self.label setText:#"My Label"];
});
}
I've also tried using performSelectorOnMainThread to no avail.
Note that setLabel is appears on the log.
Additional info:
I also have two other functions which does the same thing but only with a different text. The two other functions doesn't have dispatch_async but they both work. Also, the notification of the two working function was sent by NSURLConnection (method #2 in this post). While the notification of the non working function above, was sent by a call to FBRequestConnection (see this post).
For clarity, my two other working functions is as follows:
- (void)setLabel2:(NSNotification*)notification {
NSLog(#"setLabel2");
[self.label setText:#"My Label 2"];
}
- (void)setLabel3:(NSNotification*)notification {
NSLog(#"setLabel3");
[self.label setText:#"My Label 3"];
}
Yes I did try to remove dispatch_async in my code. In fact, originally there was no dispatch_async because the other two were working.
If you set a break point right after where you set the text and print description of self.label does it show the text has changed? If so it must be getting reset somewhere else after this method fires. If self.label is nil than there's your problem