GLKit's -drawRect: is not called during screen rotation - ios

I have GLKit-based app with some simple object displayed.
All works fine, except screen rotation, during which GLKView is not updated (-drawRect: is not called). So during rotation projection matrix is not updated according to dynamically changing screen sizes and object looks badly (stretched).

This might be a shot in the dark since I don't have any experience with GLKit, however I do have experience with UIView and -drawRect:. Try changing the contentMode of the view in question to redraw:
view.contentMode = UIViewContentModeRedraw;
This will tell the view that it needs to redraw it's contents when it's boundaries change. Otherwise, UIView will simply attempt to scale it's contents (the default for contentMode is UIViewContentModeScaleToFill). The reason for this is that it's a lot easier to scale what's there than to redraw the contents and UIView is designed to be as efficient as possible.

That's simply not how UIKit animations work. I sort-of explain how half of it works in this answer, but I'll try to summarize the relevant bits:
Everything is a textured rectangle. (There are some exceptions, like perhaps CAShapeLayer, but this is true for the most part.)
UIView's properties are linked to CALayer's "model tree" properties. They change instantaneously.
Animations work by animating the "presentation tree" properties from the starting value to the current model value.
The starting value of the animation is, by default, the previous model value. Specifying UIViewAnimationOptionBeginFromCurrentState makes it use the current presentation value.
There are, of course, a few exceptions (CAShapeLayer seems to be more than a textured rect, UIScrollView does scrolling animations on a timer, UIView transitions are another thing entirely...).
How rotation is supposed to work is that you get a single -setFrame:, everything is laid out (and potentially rerendered), and then the animatable properties are animated. By default, this means things will rerender to the new dimensions but get stretched to fit the old dimensions, and then animate (and unstretch) as the rotation progresses.
That said, GLKView might work differently. If you're using GLKViewController, it might even suspend rendering during the rotation animation. You could try calling -setNeedsDisplay during the rotation, but it won't help much since the frame is already set to its final value.
Probably the easiest way to handle the rotation animation yourself is to force a non-animated relayout to the rotated frame and then do some fudging in the renderer (the black border and status bar animate separately though).
Alternatively, the traditional way is to make your VC portrait-only and handle the device orientation notifications yourself, but this is a big can of worms (you then have to worry about the status bar orientation which determines things like the touch offset and keyboard orientation, and it tends to interacts "interestingly" when transitioning to/from other VCs).

First of all, ensure that at some point early in your application lifecycle, enable device orientation changes.
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
Then when your view is instantiated register for a notification like so.
[[NSNotificationCenter defaultCenter] addObserver: self selector: #selector(deviceOrientationDidChange:) name: UIDeviceOrientationDidChangeNotification object: nil];
Then implement -deviceOrientationDidChange: in your view controller and call -setNeedsDisplay

Related

Complex layout in UIView that animates its content

I'm having this general question about a problem that arises very often when I'm designing complex UIViews that require a special layout and animations at the same time.
What I'm currently building is the same kind of view that the Mail app uses on iOS to present a list of recipients when writing an email (the blue badges container) :
So I understand and know how I would build such a view, but I always have this question when facing such a case : How do you handle the complex layout (layouting all the blue rounded rectangles) and their animations at the same time (when adding or removing a recipient) ?
1. Normally I would reimplement :
- (void)layoutSubviews
to reflect the state of the view at the layout moment (ie layout each blue rounded UIButton side by side) according to my current bounds and just add and animate a UIButton when someone adds a new person to the list.
But what would happen to the animation if a layout pass is already running? (this may be a dumb question since everything is supposed to happen on the main thread and so no concurrency is involved here, but I'm not sure on how to handle this)
I think those two things (layout and animations) are not supposed to happen at the same time, since the main runloop "enqueues work and dequeues work" one "block" at a time (but maybe an animation is just hundreds of blocks enqueued that draw different things overtime, which could easily be compromised by a layout block in between??)
2. Also, would this solution be acceptable ?
Reimplement layoutSubviews the exact same way to handle the correct layout of my subviews
When someone adds or deletes a person, just call this to re-position everything animated
[UIView animateWithDuration:0.25f animations:^{
[self setNeedsLayout];
[self layoutIfNeeded];
}];
As you can see, lots of questions here, I don't know exactly how to handle this gracefully.
Both of these solutions have been tested and approved but the thing is which one is the best ?
Do you have a better solution ?
Thanks for your help!
As I understood, you mean animations applied to a view for which layoutSubviews method is implemented, e.g. animated change of its frame. If something is changed in your view while animation is ongoing that causes the view to re-layout, then layoutSubviews method will be called.
Basically, such kind of animations is nothing more than following:
change view's frame (actually, frame of a view's layer) by some small value (e.g. increase origin's Y coordinate by 1 pixel)
call layoutSubviews, if needed
redraw view (i.e. its layer)
change view's frame by some small value
call layoutSubviews, if needed
redraw view
...
repeat steps above until view's properties reach target values
All those steps are performed on the main thread, so steps above are consecutive and are not concurrent.
Generally speaking, the layoutSubviews method should define coordinates and sizes of subviews basing on its own bounds. Animations change the view's frame and correspondingly its bounds, so your implementation of the layoutSubviews method is supposed to handle those changes correctly.
That is, the answer for your question is implement correctly layoutSubviews method.
P.S. This answer has a great explanation of how animations are implemented.
Update
It seems that previously I understood the question incorrectly. My understanding was:
What happens if layoutSubviews method is called for a view being animated?
That is, I assumed that there is some view which e.g. changes its y coordinate with animation, the layoutSubviews method is called for this view during animation, and some subview of the view changes its position in the layoutSubviews.
After clarification from #Nicolas, my understanding is as follows:
Let's have some view ('Parent') with a subview ('Child'); let's animate this subview ('Child'); let's call layoutSubviews method of the 'Parent' view and change 'Child' subview's frame in this layoutSubviews method while animation is ongoing. What will happen?
Background Theory:
UIView itself doesn't render any content; it's done by Core Animation layers. Each view has an associated CALayer which holds a bitmap snapshot of a view. In fact, UIView is just a thin component above Core Animation Layer which performs actual drawing using view's bitmap snapshots. Such mechanism optimizes drawing: rasterized bitmap can be rendered quickly by graphics hardware; bitmap snapshots are unchanged if a view structure is not changed, that is they are 'cahced'.
Core Animation Layers hierarchy matches UIView's hierarchy; that is, if some UIView has a subview, then a Core Animation layer corresponding to the container UIView has a sublayer corresponding to the subview.
Well... In fact, each UIView has even more than 1 corresponding CALayer. UIView hierarchy produces 3 matching Core Animation Layers trees:
Layer Tree - these are layers we used to use through the UIView's layer property
Presentation Tree - layers containing the in-flight values for any running animations. Whereas the layer tree objects contain the target values for an animation, the objects in the presentation tree reflect the current values as they appear onscreen. You should never modify the objects in this tree. Instead, you use these objects to read current animation values, perhaps to create a new animation starting at those values.
Objects in the render tree perform the actual animations and are private to Core Animation.
Change of UIView's properties such as frame is actually change of CALayer's property. That is, UIView's property is a wrapper around corresponding CALayer property.
Animation of UIView's frame is actually change of CALayer's frame; frame of the layer from Layer Tree is set to the target value immediately whereas change of frame value of layer from presentation tree is stretched in time. The following call:
[UIView animateWithDuration:5 animations:^{
CGRect frame = self.label.frame;
frame.origin.y = 527;
self.label.frame = frame;
}];
doesn't mean that self.label's drawRect: method will be called multiple times during next 5 seconds; it means that y-coordinate of the presentation tree's CALayer corresponding to the self.label will change incrementally from initial to target value during these 5 seconds, and self.label's bitmap snapshot stored in this CALayer will be redrawn multiple times according to changes of its y-coordinate.
Answer:
Given this background, now we can answer the original question.
So, we have ongoing animation for a child view, and layoutSubviews method gets called for a parent view; in this method, child view's frame gets changed. It means that frame of a layer assiciated with the child view will be immediately set to the new value. At the same time, layer from the presentation tree has some intermidiate values (according to ongoing animation); setting new frame just changes target value for presentation tree layer, so that animation will continue to the new target.
That is, result of situation described in the original question is a 'jumping' animation. Please see demonstration in the GitHub sample project.
Solution for such complex cases
In your layoutSubviews method, if an animation is ongoing, you need to use in-flight animation coordinates. You can obtain them with the presentationLayer method of CALayer associated with a view. That is, if a view being animated is called aView, then presentation layer for this view can be accessed using [aView.layer presentationLayer].

GLKViewController orientation animation

I have a layout, where the screen is divided into 2 parts: UIView at the top and GLKViewController at the bottom. The problem is that when the screen orientation changes the
GLKViewController part is rotated and gradually stretched out until the animation finishes, at which point a new unstretched frame will be drawn.
Is there a way to disable the automatic rotation animation for the GLKViewController, so I could animate it manually by manipulating the modelview-projection matrix?
If you're just looking to deal with the stretching effect, you don't need to replace the whole orientation/rotation system. Depending on how much you're making use of device orientation in you're app's UI, you're likely to cause yourself more maintenance headaches by reimplementing orientation and rotation. (If you need to do more than just work around the stretching effect, the other answers are still good.)
Your view is drawing itself during the rotation animation, so all you need to do is ensure that it's drawing itself in a way that matches its intermediate size during the animation. The rotation animation is handled by Core Animation, so you use its presentationLayer method to access the transitory state during the animation. For example:
CALayer *presentationLayer = [self.view.layer presentationLayer];
CGSize layerSize = presentationLayer.bounds.size;
float aspect = fabsf(layerSize.width / layerSize.height);
GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(
GLKMathDegreesToRadians(65.0f), aspect, 0.1f, 100.0f);
Run this snippet as part of your update/draw loop — note this might mean moving calculation of your projection matrix into the update/draw loop if it isn't there already. (You might also want to make sure this snippet only runs every frame if an orientation change is in progress.)
To see the animation in progress and make sure it's working right, use the "Toggle Slow Animations" command in the iOS Simulator.
(Credit where due: code is from this answer.)
GLKViewController is a subclass of UIViewController. UIViewController has a method willAnimateRotationToInterfaceOrientation:duration: You can override it and configure the animation of view.
You can find more details in the documentation: UIViewController Class Reference under the Responding to View Rotation Events section.
Depending on what you want to achieve, you would do different things to disable automatic rotation.
If you do not need rotation at all, disable it at project level. In Xcode, use the navigator to select the project (top item of the file list) and visit the "General" section of the target app. For device orientations, untick everything but "Portrait":
If you need to disable it for some "screens" and not for others, you need to disable rotation at the UIViewController level. UIViewController was initially designed to take up the whole screen area, so if you are using a GLKViewController that manages GLKView that covers the screen just partially, it is most probably inside another view controller. You need to subclass the parent view controller and add these methods:
// From iOS 6
- (BOOL)shouldAutorotate
{
return NO;
}
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
// iOS 5
- (BOOL)shouldRotateToOrientation:(UIInterfaceOrientation)orientation
{
return NO;
}
These will prevent the parent view controller from being rotated.
Finally, if you need to allow rotation of some elements on the screen, but not others, then you will have to determine which method to use:
Allow the screen to rotate while rotating back those elements you want to remain static. I would not recommend it.
Block the screen from rotating as mentioned above, and manually rotating those elements that you want rotated by setting their transform property. This is what I would go for.

Notification when visible area of CALayer changes?

I have a CALayer for which I provide content for only the visible area (somewhat similar to CATiledLayer). The problem is there does not seem to be a way to receive notification when the visible area of the CALayer changes so that displayLayer is called. I currently subclass and hook setPosition, setBounds, and setTransform, but this doesn't catch the cases where a superview/layer changes (for example, UIScrollView scrolls by changing the scroll views origin ). I'm left hooking parent views and sprinkling setNeedsDisplay all over the code.
Is there a better way?
The currently visible rect is [CALayer visibleRect]. This is set by the scroll view (layer) and is what you're expected to base drawing on in scroll views.
You probably want to override -needsDisplayOnBoundsChange to return YES. That's typically how you handle most of what you're describing.
If you want things like position to force a redraw (that's unusual, but possible), then you can override +needsDisplayForKey: to return YES for any key changes that you want to force a redraw.
If you want to make sure you're only drawing what you need to draw, then you should be checking your clipping box using CGContextGetClipBoundingBox() during your drawing code.

How to delay flipping a UIView containing an updating UIButton having a gradient background

I have two views that flip based on a button press within the view. The view has two CAGradientLayers as sublayers. If I flip immediately after the action method fires, the button is in the process of changing the opacity of the gradients and so you see stuttering (the UIVIew flip animation is having to accommodate the button that is itself changing.)
I can hack a solution by doing a performWithSelection:withObject:afterDelay:0.1f, but it feels like such a hack. I tried setting the layer's needsDisplay property and testing for when it was clears, but probably this is insufficient to tell me the screen itself has redrawn.
Dav
In the end this has no solution. I have since found that when animating a view there are severe performance implication if you also try to animate the super views. You can see this in many iOS apps where the app animates an image in a scrolling list - the scrolling stumbles.
So what I learned is either animate something, or it's view, but not both!

Main view that should not rotate but subviews that should, including UIPopoverController

I would appreciate some help before spending any more time on trial and error.
Imagine the following: I'm just starting to create something for the iPad that will look something like a dashboard with a number on dials on it. When rotating the iPad (portrait, landscapeLeft etc) the background should not rotate, the dials position should remain but the inside of the dials should rotate to correct position. So, main view should not rotate, but the subviews (inside of the dials) should. I have done this on the iPhone before by telling the viewController to only be in portrait and then checking UIDeviceOrientation, so I thought this was gonna be easy. But my headache starts when displaying a UIPopoverController. Since I'm not changing the UIInterfaceOrientation, the UIPopoverController will always be in portrait.
Ideal solution would be to have the main view (self.view from the viewController) not observe changes in rotation, but allowing the subviews to do it, but from what I understand that is not possible. Only other solution I could think of is to not animate the change in rotation, and jut move the subviews (dials) into their new position. Animating them (subviews) make the dance all over the place. But I have not found any good solution on how to do that.
Any thoughts anyone?
You are correct in thinking that if the main view does not rotate, having the subviews automatically rotate is not possible.
A workaround that springs to mind is this: What is animated when you rotate the view is a change in the view's transform. I am pretty sure that you could register to receive device orientation changes and when you get the change, animate a transform change to a container view that contains all the subviews you want to rotate.
Edit: just read about you having a popover controller.
As far as the popover is concerned, the way the API manages autorotation is to hide the popover and then show it again at the end of the rotation. It shouldn't be too hard to implement similar behavior.
Another thing that occurs to me is this: Is what you want to not rotate just a background? Would it work to just have two backgrounds, one for portrait and one for landscape that you could switch between? It might not be the most pretty looking, but it would probably be easier than recreating autorotation behavior yourself.

Resources