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):
In a layer-backed view, any drawing done by the view is cached to the underlying layer object. This cached content can then be manipulated in ways that are more performant than redrawing the view contents explicitly. AppKit automatically creates the underlying layer object (using the makeBackingLayer method) and handles the caching of the view’s content. If the wantsUpdateLayer method returns NO, you should not interact with the underlying layer object directly. Instead, use the methods of this class to make any changes to the view and its layer. If wantsUpdateLayer returns YES, it is acceptable (and appropriate) to modify the layer in the view’s updateLayer method.
You make this call:
subview.layer?.backgroundColor = color.CGColor
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 that drawRect
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.
let subview = showMeTheProblem ? PathComponentView() : NSView()
addSubview(subview)
subview.layer = CALayer() // Use our layer, not AppKit's caching layer.
subview.layer?.backgroundColor = color.CGColor
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 before CALayer
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.