I'm trying to put together my own take on Cocoa's NSPathControl
.
In a bid to figure out the best way to handle the expand-contract animation that you get when you mouse over a component in the control, I put together a very simple sample app. Initially things were going well - mouse into or out of the view containing the path components and you'd get a simple animation using the following code:
// This code belongs to PathView (not PathComponentView)
override func mouseEntered(theEvent: NSEvent) {
NSAnimationContext.runAnimationGroup({ (ctx) -> Void in
self.animateToProportions([CGFloat](arrayLiteral: 0.05, 0.45, 0.45, 0.05))
}, completionHandler: { () -> Void in
//
})
}
// Animating subviews
func animateToProportions(proportions: [CGFloat]) {
let height = self.bounds.height
let width = self.bounds.width
var xOffset: CGFloat = 0
for (i, proportion) in enumerate(proportions) {
let subview = self.subviews[i] as! NSView
let newFrame = CGRect(x: xOffset, y: 0, width: width * proportion, height: height)
subview.animator().frame = newFrame
xOffset = subview.frame.maxX
}
}
As the next step in the control's development, instead of using NSView
instances as my path components I started using my own NSView
subclass:
class PathComponentView: NSView {
override func drawRect(dirtyRect: NSRect) {
super.drawRect(dirtyRect)
}
}
Although essentially identical to NSView
, when I use this class in my animation, the animation no longer does what it's supposed to - the frames I set for the subviews are pretty much ignored, and the whole effect is ugly. If I comment out the drawRect:
method of my subclass things return to normal. Can anyone explain what's going wrong? Why is the presence of drawRect:
interfering with the animation?
I've put a demo project on my GitHub page.
You've broken some rules here, and when you break the rules, behavior becomes undefined.
In a layer backed view, you must not modify the layer directly. See the documentation for
wantsLayer
(which is how you specify that it's layer-backed):You make this call:
That's "interact[ing] with the underlying layer object directly." You're not allowed to do that. When there's no
drawRect
, AppKit skips trying to redraw the view and just uses Core Animation to animate what's there, which gives you what you expect (you're just getting lucky there; it's not promised this will work).But when it sees you actually implemented
drawRect
(it knows you did, but it doesn't know what's inside), then it has to call it to perform custom drawing. It's your responsibility to make sure thatdrawRect
fills in every pixel of the rectangle. You can't rely on the backing layer to do that for you (that's just a cache). You draw nothing, so what you're seeing is the default background gray intermixed with cached color. There's no promise that all of the pixels will be updated correctly once we've gone down this road.If you want to manipulate the layer, you want a "layer-hosting view" not a "layer-backed view." You can do that by replacing AppKit's cache layer with your own custom layer.
While it probably doesn't matter in this case, keep in mind that a layer-backed view and a layer-hosting view have different performance characteristics. In a layer-backed view, AppKit is caching your
drawRect
results into its caching layer, and then applying animations on that fairly automatically (this allows existing AppKit code that was written beforeCALayer
to get some nice opt-in performance improvements without changing much). In a layer-hosting view, the system is more like iOS. You don't get any magical caching. There's just a layer, and you can use Core Animation on it.