CADisplayLink OpenGL rendering breaks UIScrollView behaviour - iphone

There are a few similar questions out there on SO (links at end), but none of them has allowed me to fix my problem, so here goes:
I'm using OpenGL rendering to make an image tiling and caching library for use in a game project, and I want to hijack the physics of the UIScrollView to allow the user to navigate around the images (since it has nice bounce behaviour, might as well use it). So I have a UIScrollView which I'm using to get the rendering view for my textures, but there's a problem - moving around on the scroll view prevents the CADisplayLink from firing until the user has finished scrolling (which looks horrible). One temporary fix has been to use NSRunLoopCommonModes instead of the default run mode, but unfortunately this breaks some aspects of scroll view behaviour on certain phones I'm testing on (the 3GS and simulator seem to work fine, while the iPhone4 and the 3G don't).
Does anyone know how I could get around this clash between the CADisplayLink and the UIScrollView, or know how to fix the UIScrollView working in other run modes? Thanks in advance :)
Promised links to similar questions:
UIScrollView broken and halts scrolling with OpenGL rendering (related CADisplayLink, NSRunLoop)
Animation in OpenGL ES view freezes when UIScrollView is dragged on iPhone

It's possible that slow updates on the main thread triggered by the CADisplayLink are what's breaking UIScrollView's scrolling behavior here. Your OpenGL ES rendering might be taking long enough for each frame to throw off the timing of a UIScrollView when using NSRunLoopCommonModes for the CADisplayLink.
One way around this is to perform your OpenGL ES rendering actions on a background thread by using a Grand Central Dispatch serial queue. I did this in my recent update to Molecules (source code for which can be found at that link), and in testing with using NSRunLoopCommonModes on my CADisplayLink, I don't see any interruption of the native scrolling behavior of a table view that's onscreen at the same time as the rendering.
For this, you can create a GCD serial dispatch queue and use it for all of your rendering updates to a particular OpenGL ES context to avoid two actions writing to the context at the same time. Then, within your CADisplayLink callback you can use code like the following:
if (dispatch_semaphore_wait(frameRenderingSemaphore, DISPATCH_TIME_NOW) != 0)
{
return;
}
dispatch_async(openGLESContextQueue, ^{
[EAGLContext setCurrentContext:context];
// Render here
dispatch_semaphore_signal(frameRenderingSemaphore);
});
where frameRenderingSemaphore is created earlier as follows:
frameRenderingSemaphore = dispatch_semaphore_create(1);
This code will only add a new frame rendering action onto the queue if one isn't in the middle of executing. That way, the CADisplayLink can fire continuously, but it won't overload the queue with pending rendering actions if a frame takes longer than 1/60th of a second to process.
Again, I tried this on my iPad here and found no disruption to the scrolling action of a table view, just a little slowdown as the OpenGL ES rendering consumed GPU cycles.

My simple solution is to halve the rendering rate when the run loop is in tracking mode. All my UIScrollViews now work smoothly.
Here is the code fragment:
- (void) drawView: (CADisplayLink*) displayLink
{
if (displayLink != nil)
{
self.tickCounter++;
if(( [[ NSRunLoop currentRunLoop ] currentMode ] == UITrackingRunLoopMode ) && ( self.tickCounter & 1 ))
{
return;
}
/*** Rendering code goes here ***/
}
}

The answer at the following post works very well for me (it appears to be quite similar to Till's answer):
UIScrollView pauses NSTimer until scrolling finishes
To summarize: disable the CADisplayLink or GLKViewController render loop when the UIScrollView appears and start a NSTimer to perform the update/render loop at the desired framerate. When the UIScrollView is dismissed/removed from the view hierarchy, re-enable the displayLink/GLKViewController loop.
In the GLKViewController subclass I use the following code
on appear of UIScrollView:
// disable GLKViewController update/render loop, it will be interrupted
// by the UIScrollView of the MPMediaPicker
self.paused = YES;
updateAndRenderTimer = [NSTimer timerWithTimeInterval:1.0f/60.0f target:self selector:#selector(updateAndRender) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:updateAndRenderTimer forMode:NSRunLoopCommonModes];
on dismiss of UIScrollView:
// enable the GLKViewController update/render loop and cancel our own.
// UIScrollView wont interrupt us anymore
self.paused = NO;
[updateAndRenderTimer invalidate];
updateAndRenderTimer = nil;
Simple and effective. I'm not sure if this could cause artifacts/tearing of some sort since the rendering is decoupled from screen refreshes, but using CADisplayLink with NSRunLoopCommonModes totally breaks the UIScrollView in our case. Using NSTimer looks just fine for our app and definitely a whole lot better than no rendering.

Even though this is not the perfect solution, it still might work as a workaround;
You could ignore the display link availability and use NSTimer for updating your GL-layer instead.

Related

Is CCScene replace during CCTransition possible?

Calling replaceScene: with a CCScene on CCDirector which is during a CCTransition results in dealloc not being called on any scene used, moreover, after this operation any scene is not being displayed.
Here is a link to the sample project
Shortest way to obtain this behavior is such method (I mean reproducing the problem):
SceneOne *destScene = [SceneOne node];
CCTransitionFade *transition = [[CCTransitionFade alloc]initWithDuration:2 scene:destScene];
[[CCDirector sharedDirector]replaceScene:transition];
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[[CCDirector sharedDirector]replaceScene:[SceneTwo node]];
});
My question is: is there any feasible way to replace scenes when the CCTransition is being performed by the CCDirector?
I have implemented a delegate callback from CCDirector informing me about ending scene replacement, but this is never called if I push the iPhone home button during CCTransition.
If the answer is no, is there a cocos2d-iphone way to achieve goal described below?
Originally, this problem arised when I wanted to add "loading scene" when applicationDidEnterBackground (as a background task) or applicationWillEnterForeground, but I realized it has nothing to do with background execution. My ultimate goal is to provide seamless user experience while a game is waiting for Game Center authentication handler being called, and a "loading scene" being displayed from the very start of the app awaked from background would be sufficient (not only preventing user interaction which can be done many ways, but not showing previous game UI to the user). However, this solution is susceptible to the problem described above - if user taps the home button during scene transition he is going to get a very strange screen after bringing the game from background.
EDIT: After some more research I didn't find any satisfactory solution to replacing scene during CCTransition, however, problem described above was solved by not calling replaceScene but by adding a "loading" CCLayer to the visible CCScene, as #LearnCocos2D (thanks Steffen) suggested. This is not perfect, since adding a child during scene transition still has some narrow window (in the sense of running time) when strange results occur, but it is much better than replacing the scene. I would like to mention that it only concerns me when testing my game on 3GS, since newer devices are significantly faster and it is very difficult to reproduce the problem of "home button" clicking during CCTransition on something faster than 3GS.

How to update ui in background without losing ui responsiveness? [duplicate]

I am creating a application which displays 8 thumbnails per page and it can have n pages. Each of these thumbnails are UIViews and are added to UIScrollView. However i have implemented Paging using the Apple sample code.
The prob:
Each thumbnail(UIView) takes 150
millisecs to be created and added to
scroll view
Hence for 3 pages it takes awful
huge time to be created and added to
the UI Scrollview.
At this point the scroll view is not very respsonsive and it is very jerky and gives a bad user experience
How can i create the thumbnails and add them to UIScrollview without affecting the touch responsiveness? I want them to run independent of the main thread which is resposible for handling touch events (i suppose).
Also i would like to mention that when a thumbnail is created i trigger a Async download of the image and the delegate method is called when download is complete.
Let me know the the options i have to make this more responsive and update UI without affecting the touch operations. The page control works fine with lazy loading of the grid of thumbnails.
TIA,
Praveen S
Grand Central Dispatch is easy to use for background loading. But GCD is only for after iOS4. If you have to support iOS3, performSelectorInBackground/performSelectorOnMainThread or NSOperationQueue are helpful.
And, be careful almost UIKit classes are not thread-safe except drawing to a graphics context. For example, UIScrollView is not thread-safe, UIImage imageNamed: is not thread-safe, but UIImage imageWithContentsOfFile: is thread-safe.
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_queue_t concurrentQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
dispatch_apply([thumbnails count], concurrentQueue, ^(size_t index) {
Thumbnail *thumbnail = [thumbnails objectAtIndex:index];
thumbnail.image = [UIImage imageWithContentsOfFile:thumbnail.url];
dispatch_sync(mainQueue, ^{
/* update UIScrollView using thumbnail. It is safe because this block is on main thread. */
});
}
/* dispatch_apply waits until all blocks are done */
dispatch_async(mainQueue, ^{
/* do for all done. */
});
}
I was having a similar problem.
What i did was at an instance i kept only 3 pages in the memory and cleared remaining all.
If suppose there are 3 screens s1, s2, s3. And the user is viewing s2. Whenever he scrolls to s3 i will remove s1 and load a new page s4.
So that the users will have a better experience. And less memory will be occupied.
Whether you are using a subview or a separate ViewController for each "page" or element of the Scrollview, the jerkiness or poor performance can be helped by changing the location of your code.
Specifically the apple sample code for a scrollview with pagecontrol has something like this:
[self loadScrollViewWithPage:page - 1];
[self loadScrollViewWithPage:page];
[self loadScrollViewWithPage:page + 1];
However, that code appears in their sample in the method "scrollViewDidScroll". It's trying to do multiple heavy lifting by both scrolling and loading at the same time. Even if your images are local this is nasty.
If you move this and related code including a reference to the current page to "scrollViewDidEndDecelerating" the jerkiness of the interface is resolved because the loading happens while the scrollview is no longer moving.

Animation in OpenGL ES view freezes when UIScrollView is dragged on iPhone

I have an animated transparent OpenGL ES subview (a modification of Apple's template EAGLView class) which draws a rotating sphere. Just like Apple's example, CADisplayLink is used on devices where available.
On the same screen, there is a UIScrollView containing UIButtons that can be selected. When the user scrolls the UIScrollView, the animation of my EAGLView freezes. This behavior is reproduced on iOS Simulator 4.2 and on iPhone OS 3.1.3 on an iPhone 2G device.
Any ideas on what to do to prevent pause of the EAGLView, apart from coding my own scroll view?
Whether CADisplayLink fires during scrolls depends on the mode with which you add it to the run loop. Probably you have this, somewhere:
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
UIApplication adds a run loop mode, UITrackingRunLoopMode, for 'tracking in controls', which includes when a scrollview is scrolling. So at that point the runloop is switched out of the default mode and hence your display link (and also any timers, NSURLConnections, etc, added in the default mode) will fail to fire until default mode is restored.
Quick fix: change your code to:
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
UITrackingRunLoopMode is considered one of the common modes.
Spending too much time interrupting UIKit can lead to some very poor control responsiveness, so you need to be careful. It'd be to diverge from the topic massively, but although OpenGL is modal and therefore not particularly threading friendly, you can use an EAGLSharegroup to do rendering on a separate thread and then push it onto the main thread.
An example in (2016) Swift3...
let d = CADisplayLink(target: self, selector: #selector(ThisClassName.updateAlpha))
d.add(to: RunLoop.current, forMode: RunLoopMode.commonModes)
//and then, for example...
func updateAlpha() {
let a = leader.layer.presentation()?.value(forKey: "opacity") as! CGFloat
follower.alpha = a
}

CADisplayLink stops updating when UIScrollView scrolled

Title is quite self explanatory, but I have some animation being done in a loop triggered by CADisplayLink. However, as soon as I scroll a UIScrollView I have added to my view hierarchy, the animation stops immediately, only to return again when scrolling has completely stopped and come to a standstill....
Anyway to cancel this behaviour?
You can also mitigate the effects of this issue by using NSRunLoopCommonModes instead of NSDefaultRunLoopModes:
[displayLink addToRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
Run the display link (using -addToRunLoop:forMode:) on another thread with another run loop. So create a new thread, create a run loop on that thread, and run the CADisplayLink on that thread/run loop.
Use UITrackingRunLoopMode. It's specifically designed for scrolling stuffs.
Otherwise, just call render & present routine at -scrollViewDidScroll.
UIScrollView broken and halts scrolling with OpenGL rendering (related CADisplayLink, NSRunLoop)
Here you can find a better (and more complex) solution:
Animation in OpenGL ES view freezes when UIScrollView is dragged on iPhone
that allows you to use 'NSRunLoopCommonModes' and avoids OpenGL freezing when holding a finger without scrolling.
This is related to what Doug found (setting the frame interval of CADisplayLink to 2 instead of 1 fixing UIScrollView).
Actually CADisplayLink support multiple RunloopMode.
try this:
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:UITrackingRunLoopMode];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
NSRunLoopCommonModes seems to mess with the bounciness and continuous nature of the uiscrollview.
I found that if I set the frame interval to 2 instead of 1 (so 30 frames a second) everything works fine. So what I'm doing is setting it to 2 when my popover comes up and resetting it to 1 when it dismisses.
We fixed this same issue by changing the frameInterval from 1, to 2. This essentially halves the rendering rate of your OpenGL scene, but it still may render sufficiently for your needs.
frameInterval
The number of frames that must pass before the display
link notifies the target again.
#property(nonatomic) NSInteger frameInterval Discussion The default
value is 1, which results in your application being notified at the
refresh rate of the display. If the value is set to a value larger
than 1, the display link notifies your application at a fraction of
the native refresh rate. For example, setting the interval to 2 causes
the display link to fire every other frame, providing half the frame
rate.
Setting this value to less than 1 results in undefined behavior and is
a programmer error.

Iphone Invisible Keyboard

I'm working on an app right now that was working fine until I started implementing some threading for background loading of images. Now theres no crashes but the keyboard does not display. That is to say its invisible (I can still type, I just cant see the actual keyboard).
The only thing I've done was implement threading so I'm wondering if I'm somehow messing with the thread the keyboard runs on or something?
The threads I'm calling are like:
[NSThread detachNewThreadSelector:#selector(loadWebView:)
toTarget:self withObject:[NSNumber numberWithInt:pageNum]];
and
[scrollView performSelectorOnMainThread:#selector(addSubview:)
withObject:curWebView waitUntilDone:NO];
Thanks in advance.
UIWebViews (and all UI elements) cannot be used on background threads safely. If you wish to load images in the background, use NSURLConnection's asynchronous loading methods.