Stop NSView drawRect clearing before drawing? (lockFocus no longer working on macOS 10.14) - calayer

I have an animation using two views where I would call lockFocus and get at the graphics context of the second NSView with [[NSGraphicsContext currentContext] graphicsPort], and draw. That worked fine until macOS 10.14 (Mojave).
Found a reference here:
https://developer.apple.com/videos/play/wwdc2018/209/
At 22:40 they talk about the "legacy" backing store that has changed. The above lockFocus and context pattern was big on the screen, saying that that won't work any more. And that is true. lockFocus() still works, and even gets you the correct NSView, but any drawing via the context does not work any more.
Of course the proper way to draw in a view is via Nsview's drawRect. So I rearranged everything to do just that. That works, but, drawRect has already automatically cleared the "dirty" area for you, prior to calling your drawRect. If you used setNeedsDisplayInRect: it will even clear only those areas for you. But, it will also clear areas made up of more than one dirty rectangle. And, it clears rectangular areas, while I draw roundish objects, so I end up with too much cleared away (black area):
Is there a way to prevent drawRect to clear the background?
If not I will have switch to using the NSView's layer instead, and use updateLayer, or something.
Update: I am playing around with using the layers of NSView, returning TRUE for wantsLayer and wantsUpdateLayer, and implementing:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
That is only called when I do:
- (BOOL) wantsLayer { return YES; }
- (BOOL) wantsUpdateLayer { return NO; }
and use setNeedsDisplayInRect: or setNeedsDisplay:
Which indeed makes drawRect no longer called, but the same automatic background erase has already taken place by the time my drawLayer: is called. So I have not made any progress there. It really is the effect of setNeedsDisplayInRect, and not my drawing. Just calling setNeedsDisplayInRect causes these erases.
If you set:
- (BOOL) wantsLayer { return YES; }
- (BOOL) wantsUpdateLayer { return YES; }
the only thing that is called is:
- (void) updateLayer
which does not provide me with a context to draw.
I had some hope for:
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawNever;
The NSView doc says:
Leave the layer's contents alone. Never mark the layer as needing
display, or draw the view's contents to the layer. This is how
developer created layers (layer-hosting views) are treated.
and it does exactly that. It doesn't notify you, or call delegates.
Is there a way to have complete control over the content of an NSView/layer?
UPDATE 2019-JAN-27:
NSView has a method makeBackingLayer that is not there for nothing, I guess. Implemented that, and it seems to work, basically, but no output shows on screen. Hence the followup question: nsview-makebackinglayer-with-calayer-subclass-displays-no-output

Using NSView lockFocus and unlockFocus, or trying to access the window's graphics contents directly not working in macOS 10.14 anymore.
You can create an NSBitmapImageRep object and update drawing in this context.
Sample code:
- (void)drawRect:(NSRect)rect {
if (self.cachedDrawingRep) {
[self.cachedDrawingRep drawInRect:self.bounds];
}
}
- (void)drawOutsideDrawRect {
NSBitmapImageRep *cmap = self.cachedDrawingRep;
if (!cmap) {
cmap = [self bitmapImageRepForCachingDisplayInRect:self.bounds];
self.cachedDrawingRep = cmap;
}
NSGraphicsContext *ctx = [NSGraphicsContext graphicsContextWithBitmapImageRep:cmap];
NSAssert(ctx, nil);
[NSGraphicsContext setCurrentContext:ctx];
// Draw code here
self.needsDisplay = YES;
}

You can try overriding method isOpaque and return YES from there. This will tell OS that we draw all pixels ourselves and it do not need to draw the views/window at the back of our view.
- (BOOL)isOpaque {
return YES;
}

Related

Why is an empty drawRect interfering with a simple animation?

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.

NSTrackingArea not fully working when not key window

A bit of context first. Essentially I have a window that is covering the desktop. On it I have a few WebKit WebView views which allow user interaction. By deafult, as one would expect, when another application is active it does not receive these events (such as hovering, mouse entered, and clicking). I can make it work by clicking my window first, then moving the mouse, but this is not good for usability. I've also managed to make it activate the window when the cursor enters, but it's far from ideal and rather hacky.
So instead I'm trying to use a tracking area. At the moment on the WebViews superview I have this tracking area:
NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:[self visibleRect]
options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingInVisibleRect | NSTrackingActiveAlways
owner:self
userInfo:nil];
This works as I want it to, I'm receiving the all the mouse events. However, the WebViews don't seem to be responding as intended. JavaScript mouse move events only fire when I hold and drag, not just hover and drag.
I've tried using hitTest to get the correct view, but nothing seems to work. Here's an example method, I'm using the isHandlingMouse boolean because without it an infinite loop seemed to be created for some reason:
- (NSView *)handleTrackedMouseEvent: (NSEvent *)theEvent{
if(isHandlingMouse)
return nil;
isHandlingMouse = true;
NSView *hit = [self hitTest: theEvent.locationInWindow];
if (hit && hit != self) {
return hit;
}
return nil;
}
- (void)mouseMoved:(NSEvent *)theEvent{
NSView *hit = [self handleTrackedMouseEvent: theEvent];
if (hit){
[hit mouseMoved: theEvent];
}
isHandlingMouse = false;
}
The 'hit' view, is a WebHTMLView, which appears to be a private class. Everything seems like it should be working,but perhaps there's something I'm doing that's breaking it, or I'm sending the event to the WebHTMLView incorrectly.
Post a sample Xcode project to make it easier for people to test solutions to this problem.
I was doing something similar and it took a lot of trial and error to find a solution. You will likely need to subclass NSWindow and add - (BOOL)canBecomeKeyWindow { return YES; }, then whenever you detect the mouse is over your window, you might call [window orderFrontRegardless] just so it can properly capture the mouse events.

New 'hideBarsOnSwipe' IOS 8 method affects other view objects

I have been playing around with the new navigationcontroller.hideBarsOnSwipe method and it is really awesome! However, the big downside seems to be that it affects ALL UIView objects in the visible view. To be more presice: when I have a label overlapping my scrollvie/tableview, it recognizes the 'Swipe' gesture and moves the entire view up - would anybody have an idea how to only make this gesture affect the underlying tableview? Thanks!
So the way to fix this in a easy manner, is simply disabling the swipe gesture for certain objects by looking at the recognizer of the object using the following:
- (BOOL)gestureRecognizer:(UISwipeGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if( [gestureRecognizer view] == self.(your view object) {
return NO;
}
return YES;
}
additionally, you can turn the gesture to false or true depending on what popups are open.

Can I use CALayer to speed up view rendering?

I am making a custom NSView object that has some content that changes often, and some that changes much more infrequently. As it would turn out, the parts that change less often take the most time to draw. What I would like to do is render these two parts in different layers, so that I can update one or the other separately, thus sparing my user a sluggish user interface.
How might I go about doing this? I have not found many good tutorials on this sort of thing, and none that talk about rendering NSBezierPaths on a CALayer. Ideas anyone?
Your hunch is right, this is actually an excellent way to optimise drawing. I've done it myself where I had some large static backgrounds that I wanted to avoid redrawing when elements moved on top.
All you need to do is add CALayer objects for each of the content items in your view. To draw the layers, you should set your view as the delegate for each layer and then implement the drawLayer:inContext: method.
In that method you just draw the content of each layer:
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx
{
if(layer == yourBackgroundLayer)
{
//draw your background content in the context
//you can either use Quartz drawing directly in the CGContextRef,
//or if you want to use the Cocoa drawing objects you can do this:
NSGraphicsContext* drawingContext = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:YES];
NSGraphicsContext* previousContext = [NSGraphicsContext currentContext];
[NSGraphicsContext setCurrentContext:drawingContext];
[NSGraphicsContext saveGraphicsState];
//draw some stuff with NSBezierPath etc
[NSGraphicsContext restoreGraphicsState];
[NSGraphicsContext setCurrentContext:previousContext];
}
else if (layer == someOtherLayer)
{
//draw other layer
}
//etc etc
}
When you want to update the content of one of the layers, just call [yourLayer setNeedsDisplay]. This will then call the delegate method above to provide the updated content of the layer.
Note that by default, when you change the layer content, Core Animation provides a nice fade transition for the new content. However, if you're handling the drawing yourself you probably don't want this, so in order to prevent the default fade in animation when the layer content changes, you also have to implement the actionForLayer:forKey: delegate method and prevent the animation by returning a null action:
- (id<CAAction>)actionForLayer:(CALayer*)layer forKey:(NSString*)key
{
if(layer == someLayer)
{
//we don't want to animate new content in and out
if([key isEqualToString:#"contents"])
{
return (id<CAAction>)[NSNull null];
}
}
//the default action for everything else
return nil;
}

How to draw to an NSView graphics context from inside a CVDisplayLink callback?

I have a simple game that renders 2D graphics to a frame buffer (not using any OpenGL). I was going to use a CVDisplayLink to get a clean framerate, however most examples on the web deal with OpenGL or QuickTime.
So far I have a sub class of NSView:
#interface GameView : NSView {
#private
CVDisplayLinkRef displayLink;
}
- (CVReturn)getFrameForTime:(const CVTimeStamp*)outputTime;
#end
And I set up the CVDisplayLink callback:
CVDisplayLinkSetOutputCallback(displayLink, MyDisplayLinkCallback, self);
And I have the callback function:
CVReturn MyDisplayLinkCallback (CVDisplayLinkRef displayLink,
const CVTimeStamp *inNow,
const CVTimeStamp *inOutputTime,
CVOptionFlags flagsIn,
CVOptionFlags *flagsOut,
void *displayLinkContext)
{
CVReturn error = [(GameView*)displayLinkContext getFrameForTime:inOutputTime];
return error;
}
The part where I'm stuck is what to do in getFrameForTime: to draw to the graphics context in the GameView. My first guess was to do the drawing the same way you would in drawRect:
- (CVReturn)getFrameForTime:(const CVTimeStamp*)outputTime
{
CGContextRef ctxCurrent = [[NSGraphicsContext currentContext] graphicsPort];
//.. Drawing code follows
}
But ctxCurrent is nil which I think I understand - normally there is some setup that happens before the drawRect: that makes your view the current context. I think this is the part I'm missing. How do I get the context for my view?
Or am I going about this in all the wrong ways?
You could leave your drawing code in ‑drawRect: and then in your MyDisplayLinkCallback() set an ivar to the current time and call ‑display on your view. This will force your view to immediately redraw itself.
In your ‑drawRect: method, just use the value of the time ivar to do whatever drawing is necessary to update the view appropriately for the current animation frame.
You should create a separate -draw: method, and call that from your MyDisplayLinkCallback() as well as from -drawRect:.
I found Rob Keniger's response to my liking and tried it out; sadly, if you call -display from your display link callback your app may hang in a deadlock when you terminate the app, leaving you to force-quit the app (for me it happened more often than not).

Resources