querying the presentation layer of a UISlider - animation

i am setting up a simple UISlider to animate it's progress:
[UIView animateWithDuration:songLength
delay:0.0
options:UIViewAnimationOptionRepeat|
UIViewAnimationOptionAllowUserInteraction|
UIViewAnimationOptionBeginFromCurrentState
animations:^{
[UIView setAnimationDuration:10.0];
[myUISlider setValue:10.0];
} completion:nil
];
[UIView commitAnimations];
when the user presses a button i want to stop the animation at it's place.
i understand i need to query the presentation layer to figure out the value, however, the presentation layer is of type CALayer and not UISlider. hence, it has layer properties, like it's x/y position on the screen, but not the value of the slider itself.
it makes sense that by design the presentation layer can access all the current animated data of a layer, but i'm not sure how to work that out in code.
any ideas?

i figured out one way of resolving this matter, in case you are dealing with the same issue.
by design there is no apparent way to query an animated thumb of a UISlider. animation querying works well for a layer, so one way is to create your own background and animated a layer that is the thumb.
the way i worked around this is the use of NSTimer class. once i want the progress bar to start animating i set an NSTimer for 0.1s interval and call a function to update the thumb location. the animation before/after the thumb (as it progresses the left part is blue and what's remained is still white) is taken care of for you automatically.
here is the code:
updateTimer = [NSTimer scheduledTimerWithTimeInterval:.01 target:self selector:#selector(updateCurrentTime) userInfo:p repeats:YES];
so every .01 of a second this method is called and it redraws the thumb, thus creating the animation. no need for an animation block.
this works well and i am happy with the results. i am concerned with performance and will measure how resource intensive this implementation is. it maybe a good idea to go with the first option i've mentioned above.

Related

Dynamically resizing views based based on NSOutlineView after expand/collapse animation

I'm using an NSOutlineView to display a hierarchy of tracks in a timeline, very similar to what would be seen in video editing packages. As the user expands/collapses items in the outline view, the corresponding objects in a scroll view alongside it appear and disappear.
Unfortunately, the outlineViewItemDidCollapse: and outlineViewItemDidExpand: delegate methods get called before the animation is complete, and calling frameOfCellAtColumn: on the outline view only gets the frame at that instant, while Cocoa is still animating its [dis]appearance.
As a workaround, I'm using performSelector:withObject:afterDelay: with a delay of 100ms (as is this Dynamically resize NSWindow based on NSOutlineView height person with a similar problem), but this isn't perfect (and it's smells bad). The animation isn't always finished inside 100ms, and any longer delay becomes quite noticeable.
-(void)outlineViewItemDidExpand:(NSNotification *)notification
{
// we need to delay the update to allow the outline view expand animation to
// complete. Perhaps 0.1s isn't long enough in all cases, but any longer seems
// too sluggish
[self performSelector:#selector(updateSubviewsForTimeline:) withObject:timelineView afterDelay:0.1];
}
Is there any way I can hook into the outlineViewDidExpand|Collase: methods after the animation is complete? Alternatively, can I somehow get a handle on the animation itself inside my outlineViewDidExpand|Collase: methods so I can install a callback on that?

NSView performance of wantsLayer

If I create a blank Mac XCode project and layout 500 simple NSView objects side by side in the main window it loads pretty damn fast. If I set wantsLayer=YES on each subview, performance dramatically drops, by several seconds. Why is this the case conceptually? It seems that layers would be faster not slower than regular old NSViews.
You're giving the system more work to do by layer-backing so many views. Layer-backing allows graphic acceleration (for drawing) but it adds a bit of overhead to things like layout, not to mention just creating them and putting them on screen. If used properly, it's not really much of a problem.
Typically, if you had so many "things" to manage on screen at once, you'd have one layer-backed hosting view that manages its own tree of sublayers. "But what about view-based table views?" you ask. Trickery, trickery, I say! Table views don't actually keep all the cell views they manage around; they efficiently reuse them, keeping around only enough to represent what's on screen and/or animating around.
So I'd say this isn't really a problem since it's not a particularly good approach to throw 500+ layer-backed views up for layout and drawing to begin with. :-)
as of 2021, Joshua Nozzi's answer is just the half of the truth.
When you want to utilise that much layers, you should make use of the power of CALayers with Sublayers instead of using NSViews over and over again, each with its own NSCell and possible CALayer backed as you forced it to with -wantsLayer:.
There is nothing wrong with 500 sublayers. Sublayers allow you to be fast when properly structured.
If you want to speed up even more, turn off autoanimation by force on each Layer that does not need it. Yes a lot examples out there show that you can turn off Autoanimation each time you call some property to be changed, slowing down your drawing even more, because it involves even more object messaging.
Keep in mind the following example turns off Auto-animated Keys by their Key name and kicks out the whole action and does not apply onto all Sublayers on purpose. If your structure of Sublayers of Sublayers need it you could call this on according ParentLayer to wipe out the CPU eating Autoanimation. Its much better to let the autoanimation do its job on the layers you actually want it to. Which turns around Apples default paradigm to animate everything. If most action keys are turned off be aware that your layers do show up kind of like a state machine. In the example here #"frame" animations are not wiped out..
void turnOffAutoAnimationsForLayerAndSublayers(CALayer* layer) {
NSNull *no = [NSNull null]; //(id)kCFNull
NSDictionary *dict = [NSDictionary dictionaryWithObjects:#[no,no,no,no,no,no,no,no]
forKeys:#[#"bounds",#"position",#"contents",#"hidden",#"backgroundColor",#"foregroundColor",#"borderWidth",#"borderColor"]];
for (CALayer *layertochange in layer.sublayers ) {
if (layertochange.sublayers.count) {
for (CALayer *sublayertochange in layertochange.sublayers) {
sublayertochange.actions = dict;
}
}
layertochange.actions = dict;
}
}
and use it in your CALayer backed NSView -init like..
CALayer *layer = [CALayer layer];
layer.frame = self.frame;
//...backgroundColor, contents, etc etc..
// here you could even do a for loop adding as much sublayers you want..
for (int y=0; y<100; y++) {
CALayer *sub = [CALayer layer];
//...frame, backgroundColor, contents, of sublayers etc etc..
layer.frame = CGRectMake(0, 0, 10, y*10);
[layer addSublayer:sub];
}
// structure is ready, add it to the base layer
self.layer = layer;
// now tell NSView you really want to make use this layer structure
self.wantsLayer = YES;
// turn off AutoAnimations
turnOffAutoAnimationsForLayerAndSublayers(self.layer);
For layers that are basically all the same there is even a specialised Subclass called CAReplicatorLayer. Allowing you to add even more with less internal draw calls.
You should also know that layers that are hidden are not calculated at all (meaning drawing here). You can still change properties even on hidden Layers. Unhide them, when needed, not earlier. So in example a custom CATextLayer that will change its string property to lets say #"", aka nothing, will still be drawn. But if you Subclass CATextLayer and change its string implementation to lets say..
#interface YourTextAutoHideLayer : CATextLayer
-(void)setString:(id _Nullable )string;
#end
#implementation YourTextAutoHideLayer
-(void)setString:(id _Nullable)string {
self.hidden = [string isEqual:#""];
super.string = string;
}
#end
you speed up drawing even more. A) because less object messaging to hide the layer that has no text anyway and B) once hidden it is not part of the internal draw calls effective speeding up your main CALayers drawing.
On NSTableViews that are NSCell based you usually never use CALayers. There is no need to, NSTableViews manage the visible Cells on purpose to keep in example scrolling smoothly. And still they (NSTableViewCells) have a reuse-mechanism to speed things up. You will very likely not re-invent a tableview with CALayers anyway. But in case you do reinvent, there is a CAScrollLayer class too.
And if that is not enough speed it is worth thinking about a MetalView utilising the power of GPUs.
Edit: code your CALayers not in -drawRect:. -drawRect: is called anytime something changes, in example frame/position on screen or bounds etc.. so try to avoid coding CALayers in there. You can set [self setNeedsLayout:YES]; & [self setNeedsDisplay:YES]; on purpose for a good reason, the reason is you want to avoid too much drawing for basically no change at all. -drawRect: in example was/is such method that is supposed to handle a backing with its own context to draw in. As CALayers have their very own mechanism you can let the drawRect block blank, doing nothing in there, maybe even erasing the method at all if you dont need it.
CALayers are not part of the auto layout system unless you code in -layout.. so again, keep CALayer drawing outside such and you are good. and pssst a lot CALayers properties can be changed even outside the main thread, some definitely not like layer.string.

Efficient drawing in NSView drawRect

I have an application I'm writing in which I'm drawing into an NSView. In mouseDown I am saving the location in my data model and then drawing a graphic at that location within the drawRect method of the view. It all works fine.
At the end of my mouseDown I was calling [self setNeedsDisplay:YES]; to force a redraw. The only thing is that the dirtyRect is always the full size of the view. I wanted to optimize this as much as possible so that I'm not redrawing the entire window for just a few changed pixels.
So now I'm calling [self drawRect:...] instead and specifying the rectangle.
Now in my drawRect I am comparing every graphic I have to see if it falls in the dirtyRect. It seems like I've traded work of drawing for work of bounds checking everything. I'm not sure I've made it any more or less efficient.
So what is the standard practice? Is it common to just redraw everything in the view and ignore the dirtyRect? Is there a nice function I can use as a test to see whether my object is in the dirtyRect?
You should never call -drawRect: yourself if you're trying to draw to the screen. Let AppKit call it for you. What you should do is call -setNeedsDisplayInRect: at the end of your -mouseDown:.
Then, in -drawRect:, only draw stuff contained in the dirtyRect. You can test to see if a point is inside the dirtyRect with NSPointInRect(). There are lots of other useful functions for working with NSRect. See the documentation for the point functions and the rectangle functions.

Core animation code structure/conventions

In learning Core Animation, I learned very quickly that if you don't do it right, you get really weird undefined behavior. To that end, I have a few questions that will help me conceptually understand it better.
My NSView subclass declares the following in it's init. This view is a subview of normal layer backed view.
[self setLayer:[CALayer layer]];
[self setWantsLayer:NO];
After this, when and in what situations should I refer to self as opposed to [self layer]? I have been ONLY manipulating the layer with explicit and implicit animations, staying away from [self setFrame:] etc. and using [[self layer] setPosition] etc.
The problem with this approach is that the actual frame of the view stays in one spot throughout any and all animations applied. What if my view is supposed to recieve mouse events? For example, I have a view that uses core animation and it is dragged around by the mouse. Is there a way I can somehow keep the view frame synced with the current state of the presentation layer so I can receive proper mouse events?
About the presentation layer, apparently it's only available when an actual animation is in progress. Is there any sort of property of the layer that can tell me where it's ACTUALLY visually at even when an animation's not in progress?
I think you need to re-phrase your question a little. It seems there is some underlying misunderstanding, but you're not really expressing it very clearly. You're question title suggests you're looking to understand something more theoretical, but your actual question suggests you're looking for something more concrete. Let me see if I can clarify a few things.
The presentationLayer provides information about the layer's current state while "in-flight".
When there is no animation occurring, the presentationLayer and the layer information will be identical. Query the layer's bounds, frame, or position to find out where it is currently in its parents coordinate space.
NSViews must have layer backing enabled to be able to perform animations.
Make sure you're not just animating with an explicit animation and not actually setting the layer value that you're animating. Animations don't automatically change the properties of the layers they're animating. You have to change the property to the ending value yourself or it will just "snap back" to the starting value.
If you want to animate the view, as opposed to a layer, you can use the animator proxy, like [[view animator] setFrame:newFrame];
Wrap calls to the animator in a CATrasaction to alter things like animation duration.
Let me know if you need some clarification by updating your question. Providing some pertinent code would really help identify the problems you're having trouble solving.
Firstly, you want to use [self setWantsLayer: YES]. Also, it's only important to call -setLayer: before -setWantsLayer: if you want to provide a specific CALayer subclass (such as a CAScrollLayer); if you just want a regular CALayer you just call -setWantsLayer: and it'll be created for you. Even better, just check the 'wants layer' option in Interface Builder.
Secondly, the entire point of using a layer-backed view is that you can continue to use the regular NSView methods and get the free CoreAnimation 'tweening' effects. If you want to use CoreAnimation as your only means of moving items around, then the correct way to do so is to create a layer backed view which contains your pure-CALayer presentation hierarchy.
I've not looked at any freely-available CoreAnimation tutorials, but I can definitely recommend the Pragmatic Programmers' book on the subject. They also have a screencast available by the book's author.

Is there a proper way to handle overlapping NSView siblings?

I'm working on a Cocoa application, and I've run into a situation where I would like to have two NSView objects overlap. I have a parent NSView which contains two subviews (NSView A and NSView B), each of which can have several subviews of their own.
Is there a proper way to handle this kind of overlap? NSView B would always be "above" NSView A, so I want the overlapped portions of NSView A to be masked out.
Chris, the only solution is to use CALayers. That is definitely the one and only solution.
NSViews are plain broken in OSX (Sept 2010): siblings don't work properly. One or the other will randomly appear on top.
Just to repeat, the problem is with siblings.
To test this: using NSViews and/or nsimageviews. Make an app with a view that is one large image (1000x1000 say). In the view, put three or four small images/NSViews here and there. Now put another large 1000x1000 image on top. Build and launch the app repeatedly - you'll see it is plain broken. Often the underneath (small) layers will appear on top of the large covering layer. if you turn on layer-backing on the NSViews, it does not help, no matter what combo you try. So that's the definitive test.
You have to abandon NSViews and use CALayers and that's that.
The only annoyance with CALayers is that you can't use the IB to set up your stuff. You have to set all the layer positions in code,
yy = [CALayer layer];
yy.frame = CGRectMake(300,300, 300,300);
Make one NSView only, who's only purpose is to hold your first CALayer (perhaps called 'rear'), and then just put all your CALayers inside rear.
rear = [CALayer layer];
rear.backgroundColor = CGColorCreateGenericRGB( 0.75, 0.75, 0.75, 1.0 );
[yourOnlyNsView setLayer:rear]; // these two lines must be in this order
[yourOnlyNsView setWantsLayer:YES]; // these two lines must be in this order
[rear addSublayer:rr];
[rear addSublayer:yy];
[yy addSublayer:s1];
[yy addSublayer:s2];
[yy addSublayer:s3];
[yy addSublayer:s4];
[rear addSublayer:tt];
[rear addSublayer:ff];
everything then works utterly perfectly, you can nest and group anything you want and it all works flawlessly with everything properly appearing above/below everything it should appear above/below, no matter how complex your structure. Later you can do anything to the layers, or shuffle things around in the typcial manner,
-(void) shuff
{
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:0.0f]
forKey:kCATransactionAnimationDuration];
if ..
[rear insertSublayer:ff below:yy];
else
[rear insertSublayer:ff above:yy];
[CATransaction commit];
}
(The only reason for the annoying 'in zero seconds' wrapper for everything you do, is to prevent the animation which is given to you for free - unless you want the animation!)
By the way in this quote from Apple,
For performance reasons, Cocoa does
not enforce clipping among sibling
views or guarantee correct
invalidation and drawing behavior when
sibling views overlap.
Their following sentence ...
If you want a view to be drawn in
front of another view, you should make
the front view a subview (or
descendant) of the rear view.
Is largely nonsensical (you can't necessarily replace siblings with subs; and the obvious bug described in the test above still exists).
So it's CALayers! Enjoy!
If your application is 10.5-only, turn on layers for the views and it should just work.
If you're meaning to support 10.4 and below, you'll need to find a way not to have the views overlap, because overlapping sibling views is undefined behavior. As the View Programming Guide says:
For performance reasons, Cocoa does not enforce clipping among sibling views or guarantee correct invalidation and drawing behavior when sibling views overlap. If you want a view to be drawn in front of another view, you should make the front view a subview (or descendant) of the rear view.
I've seen some hacks that can make it kinda work sometimes, but it's not anything you can rely on. You'll need to either make View A a subview of View B or make one giant view that handles both of their duties.
There is a way to do it without using CALayers and an App I have been working on can prove it. Create two windows and use this:
[mainWindow addChildWindow:otherWindow ordered:NSWindowAbove];
To remove "otherWindow" use:
[mainWindow removeChildWindow:otherWindow];
[otherWindow orderOut:nil];
And you will probably want to take the window's title bar with:
[otherWindow setStyleMask:NSBorderlessWindowMask];
As Nate wrote, one can use:
self.addSubview(btn2, positioned: NSWindowOrderingMode.Above, relativeTo: btn1)
However, the ordering of views is not respected as soon as you ask either of the views to redraw it self through the "needDisplay = true" call
Siblings wont get the drawRect call, only the views direct hierarchy will.
Update 1
To solve this problem I had to dig deep, very deep. Probably a week of research and ive spread my findings over a few articles. The final break-through is in this article: http://eon.codes/blog/2015/12/24/The-odd-case-of-luck/
Update 2
Be warned though the concept is hard to understand, but it works, and it works great. Here is the end result and code to support it, links to the github repo etc: http://eon.codes/blog/2015/12/30/Graphic-framework-for-OSX/
In order to ensure that NSView B always overlaps NSView A, make sure that you use the correct NSWindowOrderingMode when you are adding the subview:
[parentView addSubview:B positioned:NSWindowAbove relativeTo:A];
You should also keep in mind that the hidden portions of A will not be asked to redraw if view B is 100% opaque.
If you are moving the subviews, you also need to make sure that you call
-setNeedsDisplayInRect: for for the areas of the view you are uncovering.

Resources