MBProgressHUD is not shown on time when main thread is blocked - ios

I'm developing a game that uses cocos2d-x, I'm having a problem on old devices such as ipad 1 where large scene takes a lot of time to load.
so the scene transition can take a few seconds, therefore I tried to implement a "busy" animation between scene transitions while the new scene is being loaded.
I implemented this using MBProgressHUD on IOS and ProgressDialog on android.
I decided that I don't want to start showing this animation immediately, instead I could schedule the animation to start 1-2 seconds after the scene transition starts, so that on newer devices the animation will not be shown at all.
Initially what I did was this:
- (void) showProgressDialog: (int) runWithoutDelay
{
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(showProgressDialogAfterDelay) object:nil];
shouldShow = YES;
if (runWithoutDelay){
[self showProgressDialogAfterDelay];
}
else{
[self performSelector:#selector(showProgressDialogAfterDelay) withObject:nil afterDelay:delay];
}
}
- (void) showProgressDialogAfterDelay
{
if (shouldShow){
isShown = YES;
[progressHUD show:YES];
}
}
and if I pass the scene transition part I would just set shouldShow flag to false and won't start the animation.
The problem is that because cocos2d-x scene transition is done in main\ui thread sometimes instead of calling the show method after 2 seconds it takes up to 6-8 seconds for it to be called and sometimes it even gets called after I set my flag to false.
As I understand it happens because performSelector (and so is NSTimer which I also tried) both run on the same thread by placing the call in the thread run loop queue.
I needed something like performSelectorInBackground that takes delay, so I tried using dispatch_after (even though I still haven't figure out how this could be canceled, as I need to cancel a previous schedule when I create a new one) this looked more accurate according Xcode's logs but even though the logs said that the method was called exactly 2 seconds after being scheduled the time would take 5-8 seconds to show and sometimes would not show at all.
As I understand it, and correct me if I'm wrong, this happens because MBProgressHUD changes to UI must happen on main\UI thread so even though I call [ProgressHUD show:YES] on a background thread the actual update of the UI is scheduled to be executed somehow on the main thread and because of it being stuck on cocos2d-x stuff it only starts to show after that, when the scene transition is completed and its too late.
is there any way around this problem? can I somehow schedule it to start with a delay but have it display right when I want it?
What I don't understand is why if I start it with no delay the animation works smoothly without being stuck even though the main thread is busy with cocos2d-x processing.

I‘ve managed a way around this somehow!
Create a normal method where you will call the progressHUD and call the second
Create the second method where you do the time consuming stuff (loading views)
Perform that method on the main thread
Sample:
-(void)callHUD {
[progressHUD show];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelectorOnMainThread:#selector(loadView) withObject:nil waitUntilDone:YES];
dispatch_async(dispatch_get_main_queue(), ^{
[progressHUD dismiss];
});
});
}
-(void)loadView {
//Perform your segue or transition which needs to load
}
Hope that could help you a little.

Related

Why is that constantly animating UIActivityIndicator won't block main thread?

We all show activity indicator while some lengthy operation is happening in background. Though the activity indicator shows a constantly rotating wheel it won't burden the main thread, because other UIComponents in the same screen still react to the touches.
What I think I know:
I know all touch events are handled by main thread, and main Queue is being used to queue the events. Considering main queue is Serialized queue and only one task at a time can run at any given point in time, alley touch events should get queued up in main queue, while my main thread is busy in refreshing the screen/calling drawrect of UIActivityIndicator.
Study:
I have looked into the code of third party activity indicators. Most of them use CABasicAnimation and call repeat always on animation. While few work directly use NSTimer to repeatedly call drawrect with a small delay. Their code works because there is a small delay in calling drawrect and the method drawrect in itself is light weight.
None of it won't take the loads off the main thread but rather they carefully place load on main thread enough to keep their animation going yet keeping main thread free to handle touch events
What I want to know:
1 - Is this strategy to implement activity indicator is correct? or statement like this
self.timer =[NSTimer timerWithTimeInterval:0.1 target:self
selector:#selector(setNeedsDisplay) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
in one of the third party activity indicator that I saw has any special effect?
2 - If I run CABasicAnimation/transaction and repeat the animation forever will it have any special effects on the load of main thread compared to repeatedly calling setNeedsDispaly/drawrect manually?
I'm not sure whether it will help to implement your own activity indicator, but the system one UIActivitiyIndicatorView is just a UIImageView with an array of 12 images that replace each other over time.
Apple made a pretty neat trick by making their spinner discrete. It allowed them to have a simple implementation that doesn't create any computational load on CPU.
UPD
Returning to the things you want to know:
1 - It's not, because implementing manual frame drawing in drawRect is fully done by CPU. And 2 - I can't say for sure, but if one believes what Apple says in documentation and videos about Core Animation it is heavily optimised and runs on Metal or at least OpenGL underneath, so leverages power of GPU.

Long-running task in application:didFinishLaunchesWithOptions:

I have some long-running startup tasks (like loading Parse objects from local data storage) in my app. Those tasks should be finished before the interface starts to appear. The app was originally created using storyboards, so the interface starts to appear automatically after application:didFinishLaunchesWithOptions: method finishes. I can't block main thread because Parse SDK fires all it's callbacks on main thread (so blocking results in deadlock). I also need to delay return from application:didFinishLaunchesWithOptions: to finish setup. So what I did is:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Dispatch long-running tasks
dispatch_group_t startup_group = dispatch_group_create();
dispatch_group_async(startup_group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Perform some long-running setup here
});
// Run main run loop until startup tasks finished (is it OK to do so?)
while (dispatch_group_wait(startup_group, DISPATCH_TIME_NOW))
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
return YES;
}
Is it the proper usage of NSRunLoop? Are there any potential caveats? Can you propose more elegant (preferably GCD) solution?
UPDATE:
Loading Parse object from local data storage (i.e. loading tiny file from SSD) is not so long operation as loading something from the web. The delay is barely noticeable, but long enough to trigger warnBlockingOperationOnMainThread, so UX is not an issue.
The real issue is (infrequent) crashes caused by spinning another main runloop above regular main runloop, which sometimes leads to reentering UIApplication delegate methods, which apparently is not thread-safe.
Of cause introduction of splash screen is an obvious solution, but I was looking for a something more simple and elegant. I have a feeling it exist.
While this technically works depending on what your setup is doing (for example web service calls) the application may never launch or launch with unexpected results.
The more user friendly method would be to add a "loading" view controller to your storyboard which would be your landing view. Perform your long running setup here while providing the user with information/status/whatever is appropriate, then push your original view controller.
Imho I think it's better to do your stuff in your main View Controller and show something to user like a custom spinner: in this way you can download your data and the user know what happens.
It's never a good idea take so long time to launch an app with a blank screen or just a launch screen with no info for the user.
This is creative but a really bad idea. You should make your UI load in a failsafe state - in other words, assume the network is not available/slow, and show a view that won't explode if it doesn't get data before being rendered. Then you can safely load data on a background queue and either update the view or transition to the data-centric view once it is done loading.
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
//Load your data here
//Dispatch back to the main queue once your data is loaded.
dispatch_async(dispatch_get_main_queue(), ^{
//Update your UI here, or transition to the data centric view controller. The data loaded above is available at the time this block runs
});
});
Lagging the return of didFinishLaunchingWithOptions: is really a bad idea.This will give the bad impression of your app and its a bad design.
Instead you could land in the view controller and show a overlay view that shows the progress or activity you are doing when completed you could dismiss it.This is the right approach.
Here is a link to Third party library that eases showing/hiding the overlay view and setting some text on the view.
You can find so many libraries like this just google iOS HUD.HUD means Heads Up Display or you can design the custom thing yourself using UIAnimation easily.
Generally speaking,
I have to appreciate that code you have written in the sense you have stopped the current flow or run loop using While and NSRunloop.
May be you can alter your code little bit and use in some other place of your program to stop the current flow or runloop and wait for something.
NSDate *runloopUntill = [NSDate dateWithTimeIntervalSinceNow:0.1];
while (YourBoolFlag && [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode runloopUntill])
The following apple documentation will give you deep insights in NSRunloop 1 2

UIViewController didn't present immediately

I have
[self presentViewController:viewController animated:YES completion:^{}];
And view controller presents after 5-6 seconds after i click the button first time only. After first time presenting latter works ok.
What seems to be the problem?
Thanks
I guess there is some url download process has been started in ViewController viewDidLoad method, which is eventually blocking till the download has been completed. Though it works second time fine, because of cache, and it downloads faster than first time.
I suggest you to use dispatch block like below
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//Download Code
[self download];
});
or, alternatively you can create a function say
-(void)download{
}
and call it using
[self performSelector:#selector(download) withObject:nil afterDelay:.1];
all in viewDidLoad.
All will work without any problem.
And too you can choose to add UIActivityIndicator to show user an activity for download.
Hope it helps.
Cheers.
Not only the awakeFromNib issue said by #rckoenes, but also check for any heavy load processes in viewDidLoad of presentingViewController as those processes will run on main thread. The heavy load processes may be
1. Downloading an image and fixing it to image,
2. Drawing any high clarity image,
3. Downloading the data from internet (Ex: [NSData dataWithContentsOfURL:url])etc.
This can be avoided by using threads.
If this is not the case, please let me know what is the process you wanted to run in viewDidLoad of presentingViewController.
Are you sure this is happening on the main thread?
You can confirm as follows:
if(! [NSThread isMainThread] )
{
dispatch_async( dispatch_get_main_queue(),
^{
// Continue here
} );
}

MFMessageComposeViewController - how can I find out when this has been fully presented?

The Problem - Preparing and showing an MFMessageComposeViewController is trivial as per the Apple docs. What I need to know is when this has been fully presented.
Explanation - Showing the MFMessageViewComposeController with a completion block is easy, but doesn't solve my problem:
[self presentViewController:messageController animated:YES completion:^(void){
//Controller has been shown. But not really....
}];
The problem is more obvious for messages to larger groups of recipients (say 50 people). The completion block gets called, but the phone's screen remains black. Several seconds later, the messaging window appears. Several seconds later, the recipient list becomes active with a flashing cursor. Basically, there's a lot of loading and processing that goes on after the controller has supposedly been presented.
What I'd like - To figure out when the interface has been fully loaded. I don't expect a simple answer, and I've already spent quite a bit of time on it - definitely bounty worthy. If you can post a working answer with code I'll award maximum bounty for it.
Just check MFMailComposeViewController view's frame. Once it achieves top of the screen handle the appearance.
[self presentViewController:messageController animated:YES completion:^
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^
{
while(messageController.view.frame.origin.y > 0)
{
}
dispatch_async(dispatch_get_main_queue(), ^
{
// Handle appearance of MFMailComposeViewController
});
});
}];
The other way is to wait 0.3 seconds using dispatch_after() method. But this time interval could be changed next versions of iOS.

MKMapView on iOS 5 inhibits timers when user do drag and/or zoom

I wrote an application that use MKMapView. This application use a timer to update some information on screen. Actually, when user touch the map and start the drag, the timer isn't fired anymore until the user release the touch. I notice that with the new iOS 6, this problem disappears. However I need to support also iOS 5. I haven't figure out if only timers aren't fired or if no events are processed at all. Any idea?
Ok I found the solution here: UIScrollView pauses NSTimer until scrolling finishes
Basically you have to put the NSTimer in it's own run loop.
Hmm, that would suggest that the timers and the touch processing code are being handled by the same runloop, or possibly that the touches are blocking so when the timer completion code tries to run, it can't. Try using asynchronous blocks with completion handlers to run your timers.
- (void)startTimerInBackground {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
//Start timer here, set completion method to be called
NSTimer *t = [NSTimer scheduledTimerWithTimeInterval: 2.0
target: self
selector:#selector(timerCompletionMethod:)
userInfo: nil repeats:NO];
});
}
- (void)timerCompletionMethod:(NSTimer *)timer {
//Switch back to main thread here for completion code
dispatch_async(dispatch_get_main_queue(), ^(void) {
});
}
See if that helps, do note though that timers are not reliable, and if you need very accurate timing you should probably look at alternatives, there is some very good info here:
How do I use NSTimer?

Resources