Cannot display a game centre leaderboard from OpenGL game loop - ios

I have an OpenGL ios application which I have built on top of the framework used in the tutorials on 71squared.com
All the views, states, controls are state managed within the application, and there is only one UIViewController.
The basic game loop structure is as follows;
GameAppDelegate - this holds the UIView glView, and launches it in applicationDidFinishLaunching as follows;
[glView performSelectorOnMainThread:#selector(mainGameLoop) withObject:nil waitUntilDone:NO];
GLView contains the mainGameLoop method, and is derived from UIView
The main game loop does the following, as well as creating then releasing an auto release pool and managing a frame counter;
while(true)
{
[gameController updateScene];
[self drawView];
}
gameController is a UIViewController derived class.
drawView does the following;
[EAGLContext setCurrentContext:context];
glBindFrameBufferOES(GL_FRAMEBUFFER_OES, viewFrameBuffer);
[gameController renderScene];
glBindRenderBufferOES(GL_RENDERBUFFER_OES, viewRenderBuffer);
[context presentRenderBuffer:GL_RENDERBUFFER_OES];
OKAY... this is all fine.
However, I am trying to display a game centre leaderboard, which just doesnt do anything. I have a feeling I may need to manage the state and do something a little different in the render loop when the leaderboard is presented. The following code is my UIViewController gameController;
GKGameCenterViewController* gameCenterViewController = [[GKGameCenterViewControler alloc] init];
gameCenterController.viewState = GKGameCenterViewControllerStateLeaderboards;
[self presentViewController:gameCenterController animated:NO completion:nil];
The code runs, but doesn't display anything, the game loop just continues. Why not?
EDIT:
Tried triggering the main game loop via both NSTimer and CADisplayLink with no success, presentViewController still does not appear.

Ok, your problem is that main thread is always busy with while(true).
I want suggest you, next time, to use GLKit. It's much simple and template in Xcode has everything what you need for simple application.
In current situation you need create loop for drawing and it has 2 different ways:
1. create CADisplayLink - which is better, here you can grab code
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(mainGameLoop)];
[displayLink setFrameInterval:2];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
2. simple create NSTimer with interval, which is will call mainGameLoop each iteration, but it's not so perfect solution
Here you can find some interesting, Jeff Lamarche has a lot of good tutorials and explanations
https://github.com/jlamarche/iOS-OpenGLES-Stuff
http://iphonedevelopment.blogspot.ie/

Related

cocos2d-js game iOS Crash when enter the game at the second time

We are using cocos2d-js to develop an iOS App which can launch different games. So I add an button in the native app viewcontroller and start the game by clicking the button, just like this:
-(void)didClickGame2Btn:(id)sender
{
//加载游戏
cocos2d::Application *app = cocos2d::Application::getInstance();
// Initialize the GLView attributes
app->initGLContextAttrs();
cocos2d::GLViewImpl::convertAttrs();
// Use RootViewController to manage CCEAGLView
RootViewController *rootViewController = [[RootViewController alloc] init];
rootViewController.wantsFullScreenLayout = YES;
[self.navigationController presentViewController:rootViewController animated:YES completion:^{
// IMPORTANT: Setting the GLView should be done after creating the RootViewController
cocos2d::GLView *glview = cocos2d::GLViewImpl::createWithEAGLView((__bridge void *)rootViewController.view);
cocos2d::Director::getInstance()->setOpenGLView(glview);
NSString *documentDir = [SEGetDirectories dirDoc];
NSString *wPath = [NSString stringWithFormat:#"%#/GameData/Game2",documentDir];
NSLog(#"document------:%#",documentDir);
std::vector<std::string> searchPathList;
searchPathList.push_back([wPath UTF8String]);
cocos2d::FileUtils::getInstance()->setSearchPaths(searchPathList);
//run the cocos2d-x game scene
app->run();
}];
[[UIApplication sharedApplication] setStatusBarHidden:true];
}
the rootViewController contains the game view. And then we add an button in the game, which is used to exit the game. The click event code of the exit game button likes:
//exit the game and close the view controller
gameEndCallBack:function(sender){
cc.log("director end............");
cc.director.end();
var ojb = jsb.reflection.callStaticMethod("ViewControllerUtils", "dismissCurrentVC");
}
We use the reflection to dismiss the rootViewController:
+(void)dismissCurrentVC
{
UIViewController *currentVC = [ViewControllerUtils getCurrentVC]; //这里获取最顶层的viewcontroller
[currentVC dismissViewControllerAnimated:YES completion:^{
NSLog(#"xxx");
}];
}
Everything is ok when the first time to enter the game, but after dismissing the rootViewController, we try to enter the game again, it crash.
The crash line is in the ScriptingCore::runScript metod and executing the code:
evaluatedOK = JS_ExecuteScript(cx, global, *script, &rval);
And the crash info is "exc_bad_access".
It is much the same problem as this topic, but the approaches in it did not solve the problem.
http://discuss.cocos2d-x.org/t/how-to-destroy-a-cocos-game-on-ios-completely/23805
This problem has been confusing me serveral days, I have no solution for this. Can anyone give me some help?
You can make the app to support multiple games with in the app.
All you have done is required but in addition to that please follow the below instructions.
First of all, cocos provide a singleton instance of cocos2d::Application that can not be restarted again especially in iOS. So the approach of ending the Director cc.director.end(); won't help you.
You should start the application only once by using the function call cocos2d::Application::getInstance()->run(); and next time if you want to start the game layer, you should not call this method again.
Instead, just pause cocos2d::Director::getInstance()->pause(); and resume cocos2d::Director::getInstance()->resume(); the director when you want to stop the game.
In this approach, if you dismiss/dealloc the view-controller instance then you should create the glview cocos2d::GLView instance again without calling the run method.
One more problem is, take care of the delay in loading the new scene. GLView will display the previous game scene for a while. Do a work around that will show blank screen while the new scene is ready.
Hope this will help you.

Can't show the scene second time in Cocos3D

I have a viewController with cocos scene which I push in my navigation controller. In this view controller I have this methods:
-(void) viewDidLoad
{
[super viewDidLoad];
[_cc3FrameView addSubview: [self createGLView]];
CC3Backgrounder.sharedBackgrounder.shouldRunTasksOnRequestingThread = YES;
}
- (void) viewWillAppear:(BOOL)animated
{
if (!sceneInitialized) {
sceneInitialized = YES;
[CCDirector.sharedDirector runWithScene: [[self makePanoramaScene] asCCScene]];
} else {
[CCDirector.sharedDirector resume];
}
[CCDirector.sharedDirector startAnimation];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[self runningPanoramaScene] sceneWillShow];
});
}
-(AxPanoramaScene *)runningPanoramaScene
{
CCScene *scene = [CCDirector.sharedDirector runningScene];
AxPanoramaLayer *panoramaLayer = [scene.children lastObject];
AxPanoramaScene *panoramaScene = (AxPanoramaScene *)panoramaLayer.cc3Scene;
return panoramaScene;
}
-(void)viewWillDisappear:(BOOL)animated
{
[CCDirector.sharedDirector pause];
}
While I push this controller - everything is working, but when I pop this controller and push it again - I got the bright pink screen and continuos messages in log:
2014-12-12 19:30:06.447 UniversalMapExample[2262:258353] cocos2d: animation started with frame interval: 60.00
2014-12-12 19:30:06.452 UniversalMapExample[2262:258353] cocos2d: surface size: 768x973
OpenGL error GL_INVALID_OPERATION detected at -[CCES2Renderer resizeFromLayer:] 161
2014-12-12 19:30:06.452 UniversalMapExample[2262:258353] Failed to make complete framebuffer object 0x8219
2014-12-12 19:30:06.453 UniversalMapExample[2262:258353] cocos2d: surface size: 768x973
OpenGL error GL_INVALID_OPERATION detected at -[CCES2Renderer resizeFromLayer:] 161
2014-12-12 19:30:06.453 UniversalMapExample[2262:258353] Failed to make complete framebuffer object 0x8219
OpenGL error GL_INVALID_OPERATION detected at -[CCRenderer(NoARCPrivate) setRenderState:] 232
OpenGL error GL_INVALID_OPERATION detected at -[CCRenderer(NoARCPrivate) setRenderState:] 232
OpenGL error GL_INVALID_OPERATION detected at -[CCRenderer(NoARCPrivate) setRenderState:] 232
OpenGL error GL_INVALID_OPERATION detected at -[CCRenderer(NoARCPrivate) setRenderState:] 232
[***GL ERROR***] GL_INVALID_VALUE: Numeric argument is out of range from glUseProgram(15).
[***GL ERROR***] GL_INVALID_OPERATION: Operation not allowed in current state from glUniform3fv(7, 4, (0.329, 0.944, 0.000)) setting u_cc3LightSpotDirectionModel.
[***GL ERROR***] GL_INVALID_OPERATION: Operation not allowed in current state from glUniformMatrix4fv(12, 1, GL_FALSE,
[0.021050, -0.007339, -0.000000, 0.210503
0.000000, -0.000000, 0.017596, -0.977556
0.005937, 0.017031, 0.000000, -0.660065
0.005926, 0.016997, 0.000000, 1.339256]) setting u_cc3MatrixModelViewProj.
How to correctly push the controller with cocos scene several times? I changed the example from cocos sources to this code. What am I doing wrong here? Please, note - that my controller is not CCDirector - it just contains a view with Cocos scene - the realization is like CC3DemoMultiScene. Thanks!
Remember that the CCDirector is a UIViewController, but it is also a singleton, which give it some unique nuances.
For example, you seem to be invoking the createGLView method every time you want to replace your controller. If it follows the CC3DemoMultiScene design, this will try to recreate another CCGLView before the old one has been released from the CCDirector singleton.
It's generally best to treat the CCDirector and the CCGLView that you create for it as a self-contained reusable unit. As you pop the containing controller, leave everything as is, and simply add and remove the CCGLView from the view hierarchy each time.
...Bill
This is a copy/paste from my blog where I covered a similar issue. The only difference is that I wanted full UIKit integration. I did have the problem where the 2nd time through. Perhaps it will help you.
http://www.notthepainter.com/full-cocos2d-uikit-integration/
I was working on a cocos2d based tapping game and I wasn’t able to run my game twice. It was clear that I wasn’t shutting the 1st game down correctly or building the 2nd game correctly, or both!
There are a lot of tutorials on the web out there teaching you how to add UIKit buttons to your Cocos2D app, or how to launch Cocos2D from your UIKit based app. But I needed to do both. I wanted a UIViewController under my game and UIKit widgets on top of my game. I spent a lot of time reading and this is what I came up with.
First, building the Xcode project was a nightmare. I eventually used the cocos2d/box2d template and then ripped out the files I didn’t needed, and added all my original files back in. The AppDelegate.m file looks just like a non-cocos2d app should look. This goes against the grain of many of the tutorials which advise you to build your cocos2d environment in the AppDelegate. I struggled with that, didn’t have luck for most of a Friday and then on Monday I put in a Cocos2DSingleton and it pretty much ran first time.
Here is my GameViewController’s viewDidLoad method:
- (void)viewDidLoad
{
[super viewDidLoad];
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:NO];
TTCocos2DSingleton *shared = [TTCocos2DSingleton sharedCocos2D];
CCGLView *glView = [shared currentGLView];
[self.view insertSubview:glView atIndex:1];
}
There are a view things to note. GameViewController has game UIButtons, score UILabels, and other game type of UI widgets. This lets me do a lot of the game controls in Interface Builder, not laying them out by hand. Notice I hide the status bar since the game is full-screen.
I get my cocos2d instance via the singleton, get its glView and insert this into the GameViewController’s view at index 1. This puts it below all the game controls. I’ll show you the sharedCocos2D method later, lets look at viewWillAppear.
- (void) viewWillAppear:(BOOL)animated
{
if(![[CCDirector sharedDirector] runningScene]){
CCScene *scene = [MyGameLayer scene];
myGame = [MyGameLayer node];
myGame.delegate = self;
[scene addChild: myGame];
[[CCDirector sharedDirector] runWithScene:scene];
} else {
// we have a scene already, replace the original to get a new game
[[CCDirector sharedDirector] startAnimation];
CCScene *scene = [MyGameLayer scene];
myGame = [MyGameLayer node];
myGame.delegate = self;
[scene addChild: myGame];
[[CCDirector sharedDirector] replaceScene:scene];
}
}
Notice how we treat the first run differently from the second run. For the second, and subsequent runs, we replace the scene with a new one. This avoids all “restarting” problems. Also notice that I set a delegate. I use a delegate protocol to communicate between my game layer and my UIViewController.
My singleton pattern comes from the Duck Rowing blog which I must admit is a pretty awesome name for a blog. I’m not going to show all the singleton code here, this blog is about cocos2d, but here is how I build my cocos2d environment.
+ (TTCocos2DSingleton *) sharedCocos2D;
{
static dispatch_once_t onceQueue;
dispatch_once(&onceQueue, ^{
if (sharedInstance) {
return;
}
sharedInstance = [[TTCocos2DSingleton alloc]init];
// Create an CCGLView with a RGB565 color buffer, and a depth buffer of 0-bits
sharedInstance->glView = [CCGLView viewWithFrame:[[UIScreen mainScreen] bounds]
pixelFormat:kEAGLColorFormatRGB565 //kEAGLColorFormatRGBA8
depthFormat:0 //GL_DEPTH_COMPONENT24_OES
preserveBackbuffer:NO
sharegroup:nil
multiSampling:NO
numberOfSamples:0];
[sharedInstance->glView setMultipleTouchEnabled:YES];
[sharedInstance setupDirector];
});
return sharedInstance;
}
The singleton sets up the CCGLView, enables multi-touch and then sets up the director. (I put that in another method since I thought, erroneously, that I’d need to call it elsewhere. Turns out I didn’t need to.)
- (void)setupDirector
{
CCDirectorIOS *director = (CCDirectorIOS*) [CCDirector sharedDirector];
[director setView:glView];
[director enableRetinaDisplay:YES];
director.wantsFullScreenLayout = YES;
[director setDisplayStats:NO];
[director setAnimationInterval:1.0/60];
}
And in setupDirector we set the usual suspects needed for a cocos2d app. Now the game can be played multiple times, I have a full UIViewController/UINavController underneath it, and I have UIKit widgets on top of my game. Nirvana.

Best Practices - sequence execution of commands in ObjC

What's the best way to execute a series of commands in a certain order? I keep getting frustrated by the darn concurrency of execution in ObjC. This is one of these cases where I realize that I'm a designer, not a "real" coder.
I'm experimenting with SpriteKit on iOS, and I want a sequence of things to occur when an energy gauge reaches <=0.
Call a method to create an explosion at the final contact. The method takes arguments about position and size of explosion.
Call another method afterwards that calls up a new scene, a results screen.
My problem occurs when the new scene gets called before I get a chance to see the last explosion.
Here's the relevant code:
- (void) doGameOver
{
damageIndicator.progress = 0;
energyLeft.text = #"Energy:0%";
GameOver *newScene = [[GameOver alloc]initWithSize:self.size];
newScene.timeElapsed = [started timeIntervalSinceNow];
[self.view presentScene:newScene transition:[SKTransition fadeWithColor:[SKColor whiteColor] duration:1]];
[damageIndicator removeFromSuperview];
}
- (void) makeExplosionWithSize:(float)myBoomSize inPosition:(CGPoint)boomPosition
{
NSString *myFile = [[NSBundle mainBundle] pathForResource:#"explosion" ofType:#"sks"];
SKEmitterNode *boom = [NSKeyedUnarchiver unarchiveObjectWithFile:myFile];
boom.position = boomPosition;
boom.particleSize = CGSizeMake(myBoomSize, myBoomSize);
[self addChild:boom];
[self runAction:self.playMySound];
}
- (void)adjustScoreWithDamage:(float)hitDamage atPosition:(CGPoint)pos
{
_damage = _damage -(hitDamage);
if (_damage < 0) {
//these are the two things I need to execute sequentially
[self makeExplosionWithSize:500 inPosition:pos];
[self doGameOver]
}
}
I've tried schemes using bools (gameOver = YES), but think I may need to create a completion handler, which just makes my head spin.
Can anyone suggest the easiest way to accomplish this?
Thank you in advance.
Easiest (not best) probably would be to replace
[self doGameOver];
with
[self performSelector:#selector(doGameOver) withObject:nil afterDelay:2.0];
I may misunderstand what you're going for, but it sounds like what should happen is:
The explosion begins.
There's a pause of [n] seconds.
The Game Over screen is presented.
To accomplish that, you might want to just fire "doGameOver" with an NSTimer rather than worry about having it fire immediately after the explosion completes.
Here's an example with a 3 second delay:
NSTimer *gameOverTimer = [NSTimer timerWithTimeInterval:3.0 target:self selector:#selector(doGameOver:) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:gameOverTimer forMode:NSDefaultRunLoopMode];

iOS - How to display a large number of images in sequence

I am working on an app that needs to flip through 300 or so images in sequence, with a 2 second delay between images.
These images are barcodes that are generated on the fly during the display. They are not stored images.
The display is in a navigation controller and I would like the user to be able to click the 'back' button without the app crashing from a selector being sent to an instance that no longer exists.
I know that it is possible to animate a UIImageView, but I don't want to create an large array of images because of memory issues.
I'd like to do this in a loop where I generate the barcode, display the image, delay 2 seconds, and then repeat with the next image.
The following code works, but crashes if you click the 'back' button, with a 'message sent to deallocated instance' error.
NSSet *dataSet = [self fetchDataSet];
for (MyBarCode *data in dataSet) {
// display barcode in UIImageView
[self updateBarCodeImage:data ];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow: 2.0]];
}
There doesn't seem to be a way to cancel this timer, or I could do that in viewWillDisapear.
What's the right way to do this?
Please don't just point me to the animationImages examples. I've already seen all of them and -- again -- I don't want to have to hold all these images in memory during the animation. If there's a way to generate the images on the fly during animation, now that would be interesting.
I think something like this should work:
__weak ViewController *bSelf = self;
NSSet *dataSet = [self fetchDataSet];
dispatch_queue_t myQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(myQueue, ^{
__strong ViewController *sSelf = bSelf;
for (BoardingPass *data in dataSet) {
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[sSelf updateBarCodeImage:data]
});
}
});
UPDATED:
Well I am not sure what exactly you are looking for. If you are talking about a simple animation than yes, Cocos 2D would be overkill. In that case I suggest doing this tutorial which gave me a lot of ideas on a project I was working on some time back:
http://www.raywenderlich.com/2454/how-to-use-uiview-animation-tutorial

Is there a way to make drawRect work right NOW?

If you are an advanced user of drawRect, you will know that of course drawRect will not actually run until "all processing is finished."
setNeedsDisplay flags a view as invalidated and the OS, and basically waits until all processing is done. This can be infuriating in the common situation where you want to have:
a view controller 1
starts some function 2
which incrementally 3
creates a more and more complicated artwork and 4
at each step, you setNeedsDisplay (wrong!) 5
until all the work is done 6
Of course, when you do the above 1-6, all that happens is that drawRect is run once only after step 6.
Your goal is for the view to be refreshed at point 5. What to do?
If I understand your question correctly, there is a simple solution to this. During your long-running routine you need to tell the current runloop to process for a single iteration (or more, of the runloop) at certain points in your own processing. e.g, when you want to update the display. Any views with dirty update regions will have their drawRect: methods called when you run the runloop.
To tell the current runloop to process for one iteration (and then return to you...):
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
Here's an example of an (inefficient) long running routine with a corresponding drawRect - each in the context of a custom UIView:
- (void) longRunningRoutine:(id)sender
{
srand( time( NULL ) );
CGFloat x = 0;
CGFloat y = 0;
[_path moveToPoint: CGPointMake(0, 0)];
for ( int j = 0 ; j < 1000 ; j++ )
{
x = 0;
y = (CGFloat)(rand() % (int)self.bounds.size.height);
[_path addLineToPoint: CGPointMake( x, y)];
y = 0;
x = (CGFloat)(rand() % (int)self.bounds.size.width);
[_path addLineToPoint: CGPointMake( x, y)];
x = self.bounds.size.width;
y = (CGFloat)(rand() % (int)self.bounds.size.height);
[_path addLineToPoint: CGPointMake( x, y)];
y = self.bounds.size.height;
x = (CGFloat)(rand() % (int)self.bounds.size.width);
[_path addLineToPoint: CGPointMake( x, y)];
[self setNeedsDisplay];
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
}
[_path removeAllPoints];
}
- (void) drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor( ctx, [UIColor blueColor].CGColor );
CGContextFillRect( ctx, rect);
CGContextSetStrokeColorWithColor( ctx, [UIColor whiteColor].CGColor );
[_path stroke];
}
And here is a fully working sample demonstrating this technique.
With some tweaking you can probably adjust this to make the rest of the UI (i.e. user-input) responsive as well.
Update (caveat for using this technique)
I just want to say that I agree with much of the feedback from others here saying this solution (calling runMode: to force a call to drawRect:) isn't necessarily a great idea. I've answered this question with what I feel is a factual "here's how" answer to the stated question, and I am not intending to promote this as "correct" architecture. Also, I'm not saying there might not be other (better?) ways to achieve the same effect - certainly there may be other approaches that I wasn't aware of.
Update (response to the Joe's sample code and performance question)
The performance slowdown you're seeing is the overhead of running the runloop on each iteration of your drawing code, which includes rendering the layer to the screen as well as all of the other processing the runloop does such as input gathering and processing.
One option might be to invoke the runloop less frequently.
Another option might be to optimize your drawing code. As it stands (and I don't know if this is your actual app, or just your sample...) there are a handful of things you could do to make it faster. The first thing I would do is move all the UIGraphicsGet/Save/Restore code outside the loop.
From an architectural standpoint however, I would highly recommend considering some of the other approaches mentioned here. I see no reason why you can't structure your drawing to happen on a background thread (algorithm unchanged), and use a timer or other mechanism to signal the main thread to update it's UI on some frequency until the drawing is complete. I think most of the folks who've participated in the discussion would agree that this would be the "correct" approach.
Updates to the user interface happen at the end of the current pass through the run loop. These updates are performed on the main thread, so anything that runs for a long time in the main thread (lengthy calculations, etc.) will prevent the interface updates from being started. Additionally, anything that runs for a while on the main thread will also cause your touch handling to be unresponsive.
This means that there is no way to "force" a UI refresh to occur from some other point in a process running on the main thread. The previous statement is not entirely correct, as Tom's answer shows. You can allow the run loop to come to completion in the middle of operations performed on the main thread. However, this still may reduce the responsiveness of your application.
In general, it is recommended that you move anything that takes a while to perform to a background thread so that the user interface can remain responsive. However, any updates you wish to perform to the UI need to be done back on the main thread.
Perhaps the easiest way to do this under Snow Leopard and iOS 4.0+ is to use blocks, like in the following rudimentary sample:
dispatch_queue_t main_queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// Do some work
dispatch_async(main_queue, ^{
// Update the UI
});
});
The Do some work part of the above could be a lengthy calculation, or an operation that loops over multiple values. In this example, the UI is only updated at the end of the operation, but if you wanted continuous progress tracking in your UI, you could place the dispatch to the main queue where ever you needed a UI update to be performed.
For older OS versions, you can break off a background thread manually or through an NSOperation. For manual background threading, you can use
[NSThread detachNewThreadSelector:#selector(doWork) toTarget:self withObject:nil];
or
[self performSelectorInBackground:#selector(doWork) withObject:nil];
and then to update the UI you can use
[self performSelectorOnMainThread:#selector(updateProgress) withObject:nil waitUntilDone:NO];
Note that I've found the NO argument in the previous method to be needed to get constant UI updates while dealing with a continuous progress bar.
This sample application I created for my class illustrates how to use both NSOperations and queues for performing background work and then updating the UI when done. Also, my Molecules application uses background threads for processing new structures, with a status bar that is updated as this progresses. You can download the source code to see how I achieved this.
You can do this repeatedly in a loop and it'll work fine, no threads, no messing with the runloop, etc.
[CATransaction begin];
// modify view or views
[view setNeedsDisplay];
[CATransaction commit];
If there is an implicit transaction already in place prior to the loop you need to commit that with [CATransaction commit] before this will work.
In order to get drawRect called the soonest (which is not necessarily immediately, as the OS may still wait until, for instance, the next hardware display refresh, etc.), an app should idle it's UI run loop as soon as possible, by exiting any and all methods in the UI thread, and for a non-zero amount of time.
You can either do this in the main thread by chopping any processing that takes more than an animation frame time into shorter chunks and scheduling continuing work only after a short delay (so drawRect might run in the gaps), or by doing the processing in a background thread, with a periodic call to performSelectorOnMainThread to do a setNeedsDisplay at some reasonable animation frame rate.
A non-OpenGL method to update the display near immediately (which means at the very next hardware display refresh or three) is by swapping visible CALayer contents with an image or CGBitmap that you have drawn into. An app can do Quartz drawing into a Core Graphics bitmap at pretty much at any time.
New added answer:
Please see Brad Larson's comments below and Christopher Lloyd's comment on another answer here as the hint leading towards this solution.
[ CATransaction flush ];
will cause drawRect to be called on views on which a setNeedsDisplay request has been done, even if the flush is done from inside a method that is blocking the UI run loop.
Note that, when blocking the UI thread, a Core Animation flush is required to update changing CALayer contents as well. So, for animating graphic content to show progress, these may both end up being forms of the same thing.
New added note to new added answer above:
Do not flush faster than your drawRect or animation drawing can complete, as this might queue up flushes, causing weird animation effects.
Without questioning the wisdom of this (which you ought to do), you can do:
[myView setNeedsDisplay];
[[myView layer] displayIfNeeded];
-setNeedsDisplay will mark the view as needing to be redrawn.
-displayIfNeeded will force the view's backing layer to redraw, but only if it has been marked as needing to be displayed.
I will emphasize, however, that your question is indicative of an architecture that could use some re-working. In all but exceptionally rare cases, you should never need to or want to force a view to redraw immediately. UIKit with not built with that use-case in mind, and if it works, consider yourself lucky.
Have you tried doing the heavy processing on a secondary thread and calling back to the main thread to schedule view updates? NSOperationQueue makes this sort of thing pretty easy.
Sample code that takes an array of NSURLs as input and asynchronously downloads them all, notifying the main thread as each of them is finished and saved.
- (void)fetchImageWithURLs:(NSArray *)urlArray {
[self.retriveAvatarQueue cancelAllOperations];
self.retriveAvatarQueue = nil;
NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];
for (NSUInteger i=0; i<[urlArray count]; i++) {
NSURL *url = [urlArray objectAtIndex:i];
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:#selector(cacheImageWithIndex:andURL:)]];
[inv setTarget:self];
[inv setSelector:#selector(cacheImageWithIndex:andURL:)];
[inv setArgument:&i atIndex:2];
[inv setArgument:&url atIndex:3];
NSInvocationOperation *invOp = [[NSInvocationOperation alloc] initWithInvocation:inv];
[opQueue addOperation:invOp];
[invOp release];
}
self.retriveAvatarQueue = opQueue;
[opQueue release];
}
- (void)cacheImageWithIndex:(NSUInteger)index andURL:(NSURL *)url {
NSData *imageData = [NSData dataWithContentsOfURL:url];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *filePath = PATH_FOR_IMG_AT_INDEX(index);
NSError *error = nil;
// Save the file
if (![fileManager createFileAtPath:filePath contents:imageData attributes:nil]) {
DLog(#"Error saving file at %#", filePath);
}
// Notifiy the main thread that our file is saved.
[self performSelectorOnMainThread:#selector(imageLoadedAtPath:) withObject:filePath waitUntilDone:NO];
}
I think, the most complete answer comes from the Jeffrey Sambell's blog post 'Asynchronous Operations in iOS with Grand Central Dispatch' and it worked for me!
It's basically the same solution as proposed by Brad above but fully explained in terms of OSX/IOS concurrency model.
The dispatch_get_current_queue function will return the current queue
from which the block is dispatched and the dispatch_get_main_queue
function will return the main queue where your UI is running.
The dispatch_get_main_queue function is very useful for updating the
iOS app’s UI as UIKit methods are not thread safe (with a few
exceptions) so any calls you make to update UI elements must always be
done from the main queue.
A typical GCD call would look something like this:
// Doing something on the main thread
dispatch_queue_t myQueue = dispatch_queue_create("My Queue",NULL);
dispatch_async(myQueue, ^{
// Perform long running process
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI
});
});
// Continue doing other stuff on the
// main thread while process is running.
And here goes my working example (iOS 6+). It displays frames of a stored video using the AVAssetReader class:
//...prepare the AVAssetReader* asset_reader earlier and start reading frames now:
[asset_reader startReading];
dispatch_queue_t readerQueue = dispatch_queue_create("Reader Queue", NULL);
dispatch_async(readerQueue, ^{
CMSampleBufferRef buffer;
while ( [asset_reader status]==AVAssetReaderStatusReading )
{
buffer = [asset_reader_output copyNextSampleBuffer];
if (buffer!=nil)
{
//The point is here: to use the main queue for actual UI operations
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI using the AVCaptureVideoDataOutputSampleBufferDelegate style function
[self captureOutput:nil didOutputSampleBuffer:buffer fromConnection:nil];
CFRelease (buffer);
});
}
}
});
The first part of this sample may be found here in Damian's answer.
I'd like to offer a clean solution to the given problem.
I agree with other posters that in an ideal situation all the heavy lifting should be done in a background thread, however there are times when this simply isn't possible because the time consuming part requires lots of accessing to non thread-safe methods such as those offered by UIKit. In my case, initialising my UI is time consuming and there's nothing I can run in the background, so my best option is to update a progress bar during the init.
However, once we think in terms of the ideal GCD approach, the solution is actually a simple. We do all the work in a background thread, dividing it into chucks that are called synchronously on the main thread. The run loop will be run for each chuck, updating the UI and any progress bars etc.
- (void)myInit
{
// Start the work in a background thread.
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// Back to the main thread for a chunk of code
dispatch_sync(dispatch_get_main_queue(), ^{
...
// Update progress bar
self.progressIndicator.progress = ...:
});
// Next chunk
dispatch_sync(dispatch_get_main_queue(), ^{
...
// Update progress bar
self.progressIndicator.progress = ...:
});
...
});
}
Of course, this is essentially the same as Brad's technique, but his answer doesn't quite address the issue at hand - that of running a lot of non thread safe code while updating the UI periodically.
Joe -- if you are willing to set it up so that your lengthy processing all happens inside of drawRect, you can make it work. I just wrote a test project. It works. See code below.
LengthyComputationTestAppDelegate.h:
#import <UIKit/UIKit.h>
#interface LengthyComputationTestAppDelegate : NSObject <UIApplicationDelegate> {
UIWindow *window;
}
#property (nonatomic, retain) IBOutlet UIWindow *window;
#end
LengthComputationTestAppDelegate.m:
#import "LengthyComputationTestAppDelegate.h"
#import "Incrementer.h"
#import "IncrementerProgressView.h"
#implementation LengthyComputationTestAppDelegate
#synthesize window;
#pragma mark -
#pragma mark Application lifecycle
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
IncrementerProgressView *ipv = [[IncrementerProgressView alloc]initWithFrame:self.window.bounds];
[self.window addSubview:ipv];
[ipv release];
[self.window makeKeyAndVisible];
return YES;
}
Incrementer.h:
#import <Foundation/Foundation.h>
//singleton object
#interface Incrementer : NSObject {
NSUInteger theInteger_;
}
#property (nonatomic) NSUInteger theInteger;
+(Incrementer *) sharedIncrementer;
-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval;
-(BOOL) finishedIncrementing;
incrementer.m:
#import "Incrementer.h"
#implementation Incrementer
#synthesize theInteger = theInteger_;
static Incrementer *inc = nil;
-(void) increment {
theInteger_++;
}
-(BOOL) finishedIncrementing {
return (theInteger_>=100000000);
}
-(NSUInteger) incrementForTimeInterval: (NSTimeInterval) timeInterval {
NSTimeInterval negativeTimeInterval = -1*timeInterval;
NSDate *startDate = [NSDate date];
while (!([self finishedIncrementing]) && [startDate timeIntervalSinceNow] > negativeTimeInterval)
[self increment];
return self.theInteger;
}
-(id) init {
if (self = [super init]) {
self.theInteger = 0;
}
return self;
}
#pragma mark --
#pragma mark singleton object methods
+ (Incrementer *) sharedIncrementer {
#synchronized(self) {
if (inc == nil) {
inc = [[Incrementer alloc]init];
}
}
return inc;
}
+ (id)allocWithZone:(NSZone *)zone {
#synchronized(self) {
if (inc == nil) {
inc = [super allocWithZone:zone];
return inc; // assignment and return on first allocation
}
}
return nil; // on subsequent allocation attempts return nil
}
- (id)copyWithZone:(NSZone *)zone
{
return self;
}
- (id)retain {
return self;
}
- (unsigned)retainCount {
return UINT_MAX; // denotes an object that cannot be released
}
- (void)release {
//do nothing
}
- (id)autorelease {
return self;
}
#end
IncrementerProgressView.m:
#import "IncrementerProgressView.h"
#implementation IncrementerProgressView
#synthesize progressLabel = progressLabel_;
#synthesize nextUpdateTimer = nextUpdateTimer_;
-(id) initWithFrame:(CGRect)frame {
if (self = [super initWithFrame: frame]) {
progressLabel_ = [[UILabel alloc]initWithFrame:CGRectMake(20, 40, 300, 30)];
progressLabel_.font = [UIFont systemFontOfSize:26];
progressLabel_.adjustsFontSizeToFitWidth = YES;
progressLabel_.textColor = [UIColor blackColor];
[self addSubview:progressLabel_];
}
return self;
}
-(void) drawRect:(CGRect)rect {
[self.nextUpdateTimer invalidate];
Incrementer *shared = [Incrementer sharedIncrementer];
NSUInteger progress = [shared incrementForTimeInterval: 0.1];
self.progressLabel.text = [NSString stringWithFormat:#"Increments performed: %d", progress];
if (![shared finishedIncrementing])
self.nextUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:0. target:self selector:(#selector(setNeedsDisplay)) userInfo:nil repeats:NO];
}
- (void)dealloc {
[super dealloc];
}
#end
Regarding the original issue:
In a word, you can (A) background the large painting, and call to the foreground for UI updates or (B) arguably controversially there are four 'immediate' methods suggested that do not use a background process. For the result of what works, run the demo program. It has #defines for all five methods.
Alternately per Tom Swift
Tom Swift has explained the amazing idea of quite simply manipulating the run loop. Here's how you trigger the run loop:
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
This is a truly amazing piece of engineering. Of course one should be extremely careful when manipulating the run loop and as many pointed out this approach is strictly for experts.
However, a bizarre problem arises ...
Even though a number of the methods work, they don't actually "work" because there is a bizarre progressive-slow-down artifact you will see clearly in the demo.
Scroll to the 'answer' I pasted in below, showing the console output - you can see how it progressively slows.
Here's the new SO question:
Mysterious "progressive slowing" problem in run loop / drawRect
Here is V2 of the demo app...
http://www.fileswap.com/dl/p8lU3gAi/stepwiseDrawingV2.zip.html
You will see it tests all five methods,
#ifdef TOMSWIFTMETHOD
[self setNeedsDisplay];
[[NSRunLoop currentRunLoop]
runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
#endif
#ifdef HOTPAW
[self setNeedsDisplay];
[CATransaction flush];
#endif
#ifdef LLOYDMETHOD
[CATransaction begin];
[self setNeedsDisplay];
[CATransaction commit];
#endif
#ifdef DDLONG
[self setNeedsDisplay];
[[self layer] displayIfNeeded];
#endif
#ifdef BACKGROUNDMETHOD
// here, the painting is being done in the bg, we have been
// called here in the foreground to inval
[self setNeedsDisplay];
#endif
You can see for yourself which methods work and which do not.
you can see the bizarre "progressive-slow-down". Why does it happen?
you can see with the controversial TOMSWIFT method, there is actually no problem at all with responsiveness. tap for response at any time (but still the bizarre "progressive-slow-down" problem)
So the overwhelming thing is this weird "progressive-slow-down": on each iteration, for unknown reasons, the time taken for a loop decreases. Note that this applies to both doing it "properly" (background look) or using one of the 'immediate' methods.
Practical solutions?
For anyone reading in the future, if you are actually unable to get this to work in production code because of the "mystery progressive slowdown", Felz and Void have each presented astounding solutions in the other specific question.

Resources