I'm using GLKit's GLKViewController/GLKView to do some basic OpenGL drawing.
I'd like to setup the ViewPort in the ViewDidLoad method. After reading the GLKView reference, I thought I'd be able to do it like this:
- (void)viewDidLoad
{
[super viewDidLoad];
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
if (!self.context) {
NSLog(#"Failed to create ES context");
}
GLKView *view = (GLKView *)self.view;
view.context = self.context;
glViewport( 0, 0, view.DrawableWidth, view.DrawableHeight );
}
The problem is that both DrawableWidth and DrawableHeight properties are zero. Why is that? When the GLKView calls DrawInRect, they're set and their values are what I would expect.
The GLKView Class Reference says this:
After this, the view automatically creates or updates the framebuffer object whenever the view must be redrawn.
The framebuffer hasn't been created by the time you receive viewDidLoad, because the view hasn't needed to be drawn yet. So the dimensions of the framebuffer don't exist either.
It would be inappropriate for the system to create the framebuffer by this time. The view hasn't been added to the on-screen view hierarchy yet. After it's added to the on-screen view hierarchy, its size might be adjusted, which would require throwing away the old framebuffer and creating a new one.
The view waits until it is actually asked to draw itself to create the framebuffer so it doesn't waste time creating a framebuffer just to throw away later.
You must call first [view bindDrawable];
Then drawableWidth and height will be correct. It is because after setup frame buffer is un-binded and drawableWidth / height reflects framebuffer properties.
Related
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;
}
I'm attempting to reproduce the iTunes 11 behavior of navigable views within a popover. I can't seem to find a way to get my animation to happen at the same time as the popover's contentSize change happens, though.
The basic setup I have is a custom view subclass MyPopoverNavigationView with two subviews: the old and new views that I want the popover to navigate between. The popover's contentViewController has a MyPopoverNavigationView instance as its view. I do this:
// Configure constraints how I want them to show the new popover view
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *ctx) {
[ctx setDuration:0.25];
[ctx setAllowsImplicitAnimation:YES];
[self layoutSubtreeIfNeeded];
} completionHandler:nil];
As far as I can tell from the Auto Layout WWDC 2012 videos, this is the recommended way to animate changes to views' frames as a result of constraint changes. It works, but the animation happens in two phases:
First, the popover's contentSize will change to accommodate the new view that I'm moving to (before that view becomes visible, so it partially obscures the existing content).
Second, the views animate as I expect, so that the constraints system I installed is satisfied.
From setting some breakpoints, it looks like -layoutSubtreeIfNeeded eventually calls a private method on the popover called _fromConstraintsSetWindowFrame:, which does the popover size animation outside my animation group. My context's duration isn't respected, and my animations don't happen until the popover's size change is complete.
How can I get my views to animate together with the popover's size change?
Turns out the trick is to explicitly set the popover's contentSize property outside of the animation and completion blocks. The relevant snippet from the sample GitHub project I put together to figure it out looks like:
// Configure constraints for post-navigation view layout
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *ctx) {
[ctx setDuration:0.25];
[ctx setAllowsImplicitAnimation:YES];
[self layoutSubtreeIfNeeded];
} completionHandler:^{
// Tear down some leftover constraints from before the transition
}];
// Explicitly set popover's contentSize so its animation happens simultaneously
containingPopover.contentSize = postTransitionView.frame.size;
This works fine for me on Sierra:
let deltaHeight = 8
let contentSize = popover.contentSize
NSAnimationContext.runAnimationGroup({ (context) -> Void in
context.allowsImplicitAnimation = true
popover.contentSize = NSSize(width: contentSize.width, height: contentSize.height+deltaHeight)
})
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;
}
This has been asked before, but I cannot find a definitive answer.
I would like to design a custom UIView class. I would like to do the layout in XCode 4 in a XIB file. Ideally:
I have the files MyView.h, MyView.m and MyView.xib.
The code defines the behavior and the XIB file defines the layout.
The code may have outlets into the XIB file to reference the layout elements.
The loader of the view could be different objects.
I'd like to be able to load the view by:
MyView *v = [MyView myView];
I've tried lots of different methods of setting the File's Owner and loading the XIB via NSBundle, but I keep getting key value coding problems.
Can anyone share the basic method to do this?
I have a repo of my current code here. As you can see, it generates a key value coding error.
Oh, I seem to have figured it out.
Keys are the following:
The XIB file UIView only needs to be a generic UIView. No need to set it to your subclass.
The File's Owner needs to be your subclass.
Outlets go to the File's Owner.
In your custom UIView, load the NIB as follows:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
UINib *nib = [UINib nibWithNibName:NSStringFromClass([self class]) bundle:nil];
UIView *v = [[nib instantiateWithOwner:self options:nil] lastObject];
v.frame = self.frame;
[self addSubview:v];
}
return self;
}
How exactly do you instantiate it? In my "container" class I have an outlet for my custom view as a property, but when I try to do self.customView = [[CustomView alloc] init];
the view does not come on screen, even though I know that it's correctly initialized because I can see the NSLog()s I put in my custom view's initWithFrame: method.
From within my main view controller, BEFORE instatiating the new class, the view is as I've defined it in the storyboard: <CustomView: 0x6a83bb0; frame = (38 153; 244 153); autoresize = RM+BM; layer = <CALayer: 0x6a83c60>>
However, inside the custom view's initWithFrame:, the frame is {{0, 0}, {0, 0}} and the view is <UIView: 0x68ca160; frame = (0 0; 0 0); autoresize = W+H; layer = <CALayer: 0x68d1800>>.
After the initialization has taken place, the main view's property for the custom view is barely (null), which baffles me.
I'm sure I'm missing something very simple here. Are you even supposed to define the custom view's within the main view in the storyboard at all?
Thanks!
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).