NSLayoutConstraint behaving differently when drawing to NSBitmapImageRep - objective-c

I have this control called ITNavigationView on Github.
It's smoothly animating from one NSView to another by caching and adding them to a NSImageView.
When caching the view, a subview centred in the x axis will be pulled exactly 1 pixel to the right.
If I instead add a leading constraint, this doesn't happen.
How can I prevent this from happening?
To cache the view, I'm using this code:
- (NSImage *)imageOfView:(NSView *)view {
[view layoutSubtreeIfNeeded];
[view setNeedsUpdateConstraints:YES];
[view updateConstraintsForSubtreeIfNeeded];
NSBitmapImageRep* rep = [view bitmapImageRepForCachingDisplayInRect:view.bounds];
[view cacheDisplayInRect:view.bounds toBitmapImageRep:rep];
return [[NSImage alloc] initWithCGImage:[rep CGImage] size:view.bounds.size];
}
EDIT
Also worth noting is that this only happens when the superview has an odd width.

Related

NSWindow animate size change new subview with auto layout

I have a NSWindowController containing several NSViewController. The windowController displays the view of the first viewController as a subView of the main window.
On a button click the windowController adds the next viewControllers view as subView, adds some layout constraints and animates them so that the first view moves out and the next view moves in. After the animation the first view is removed from its superView.
[nextController setLeftLayoutConstraint:nextLeft];
// ^^^^^^^^^^^^^^^^^^^^^^^ custom category'd property
[containerView addSubview:nextView];
[containerView addConstraints:#[nextWidth, nextHeight, nextTop, nextLeft]];
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
[context setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[context setDuration:0.5];
[[nextLeft animator] setConstant:[self leftOffset]];
[[[[self currentViewController] view] animator] setAlphaValue:-0.25]; // negative alpha to shift the timing
[[[[self currentViewController] leftLayoutConstraint] animator] setConstant:-NSWidth(containerView.frame)];
// ^^^^^^^^^^^^^^^^^^^^ custom category'd property
} completionHandler:^{
[[[self currentViewController] view] setAlphaValue:1.0];
[[[self currentViewController] view] removeFromSuperview];
[[self currentViewController] removeFromParentViewController];
_currentViewController = nextController;
}];
Now the second view is much taller than the first so it changes the windows hight as well.
Unfortunately this window frame change is not animated and the window pops ugly in the right size.
I tried getting the next views hight first to then animate all constraints or something like this. Unfortunately the views are not in the correct size before the animation is done.
Is there any way to animate the window change as well?

How to add an NSView to NSWindow in a Cocoa app?

Since the template of an OS X app in Xcode seems to be similar to an empty app template, the following is used to add a view and a button (trying not to use Interface builder for now):
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSView *view = [[NSView alloc] initWithFrame:NSMakeRect(100, 100, 100, 100)];
view.layer.backgroundColor = [[NSColor yellowColor] CGColor];
[self.window.contentView addSubview:view];
NSRect frame = NSMakeRect(10, 40, 90, 40);
NSButton* pushButton = [[NSButton alloc] initWithFrame: frame];
pushButton.bezelStyle = NSRoundedBezelStyle;
[self.window.contentView addSubview:pushButton];
NSLog(#"subviews are %#", [self.window.contentView subviews]);
}
Similar code on iOS should have produced a yellow box and a button, but the code above only produce a button, but the view won't show. Is there something wrong with the code above, and how to make it show the view with a yellow background?
Use setWantsLayer: method of NSView class.
NSView *view = [[NSView alloc] initWithFrame:NSMakeRect(100, 100, 100, 100)];
[view setWantsLayer:YES];
view.layer.backgroundColor = [[NSColor yellowColor] CGColor];
[self.window.contentView addSubview:view];
NSRect frame = NSMakeRect(10, 40, 90, 40);
NSButton* pushButton = [[NSButton alloc] initWithFrame: frame];
pushButton.bezelStyle = NSRoundedBezelStyle;
[self.window.contentView addSubview:pushButton];
NSLog(#"subviews are %#", [self.window.contentView subviews]);
To expand on the suggestion by Kevin Ballard, the classic way to do this is to subclass NSView and override the -drawRect: method. NSRectFill is a very convenient function for filling a rectangle without having to create a bezier path:
- (void)drawRect:(NSRect)rect
{
[[NSColor yellowColor] set];
NSRectFill(rect);
}
NSViews in Cocoa are, by default, not layer-backed. I suspect that if you type
NSLog(#"%#", view.layer);
you will see that it is nil.
In iOS, all views have layers. But on OS X, views don't have layers. In addition, there's 2 "modes" of layer-backed views on OS X. There's what's called a "layer-backed views" and a "layer-hosting view". A layer-backed view uses a CoreAnimation layer to cache drawn data, but you are not allowed to interact with the layer in any way. A layer-hosting view uses a CALayer that you explicitly provide, and you may mess with that layer all you want. However, with a layer-hosting view you may not add any subviews, or use the built-in NSView drawing mechanism. A layer-hosting view must only be used as the root of a CoreAnimation layer hierarchy.
Given all this, you should probably avoid using CoreAnimation at all for your view.
It's possible that an NSBox will do what you want. You can certainly set a fill color there, turn off the border, and set the style to custom. I'm just not 100% certain it will draw as a simple filled rectangle of color. Alternatively you can define your own NSView subclass that draws a color in -drawRect:.

OS X version of bringSubviewToFront:?

I need to replicate the function of bringSubviewToFront: on the iPhone, but I am programming on the Mac. How can this be done?
Haven't actually tried this out - and there may be better ways to do it - but this should work:
NSView* superview = [view superview];
[view removeFromSuperview];
[superview addSubview:view];
This will move 'view' to the front of its siblings
Pete Rossi's answer didn't work for me because I needed to pop the the view to front when dragging it with the mouse. However, along the same lines the following did work without killing the mouse:
CALayer* superlayer = [[view layer] superlayer];
[[view layer] removeFromSuperlayer];
[superlayer addSublayer:[view layer]];
Also, the following placed in a NSView subclass or category is pretty handy:
- (void) bringToFront {
CALayer* superlayer = [[self layer] superlayer];
[[self layer] removeFromSuperlayer];
[superlayer addSublayer:[self layer]];
}
also be sure to enable the layer for quartz rendering:
...
NSImageView *anImage = [[NSImageView alloc] initWithFrame:NSRectMake(0,0,512,512)];
[anImageView setWantsLayer:YES];
...
otherwise, your layer cannot be rendered correctly.
Pete Rossi's answer works, but remember to retain the view when you remove it from the superview.
You can add this in a category on NSView :
-(void)bringSubviewToFront:(NSView*)view
{
[view retain];
[view removeFromSuperview];
[self addSubview:view];
[view release];
}
Sibling views that overlap can be hard to make work right in AppKitā€”it was completely unsupported for a long time. Consider making them CALayers instead. As a bonus, you may be able to reuse this code in your iOS version.
This is Swift 3.0 solution:
extension NSView {
public func bringToFront() {
let superlayer = self.layer?.superlayer
self.layer?.removeFromSuperlayer()
superlayer?.addSublayer(self.layer!)
}
}

Making UITableView with cell-sized images smooth scrolled

EVERYTHING WRITTEN HERE ACTUALLY WORKS RIGHT
EXEPT FOR [UIImage imageNamed:] METHOD USAGE
Implementation
I am using model in witch you have a custom UITableViewCell with one custom UIView set up as Cell's backgroundView.
Custom UIView contains two Cell-sized images (320x80 px), one of which is 100% transparent to half of the view. All elements are set to be Opaque and have 1.0 Alpha property.
I don't reuse Cells because I failed to make them loading different images. Cell's reused one-by-one up to 9 cells overall. So I have 9 reusable Cells in memory.
Cell initWithStyle:reuseIdentifier method part:
CGRect viewFrame = CGRectMake(0.0f, 0.0f, 320.0f, 80.0f);
customCellView = [[CustomCellView alloc] initWithFrame:viewFrame];
customCellView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self setBackgroundView:customCellView];
CustomCellView's initialization method:
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
self.opaque = YES;
self.backgroundColor = [UIColor UICustomColor];
}
return self;
}
Images are being pre-loaded to NSMutableArray as UIImage objects from PNG files with UIImage's imageNamed: method.
They are being set in UITableViewDelegate's method tableView:cellForRowAtIndexPath: and passed through UITableViewCell with custom method to UIView.
And then drawn in UIView's drawRect: overridden method:
- (void)drawRect:(CGRect)rect {
CGRect contentRect = self.bounds;
if (!self.editing) {
CGFloat boundsX = contentRect.origin.x;
CGFloat boundsY = contentRect.origin.y;
CGPoint point;
point = CGPointMake(boundsX, boundsY);
if (firstImage) { [firstImage drawInRect:contentRect blendMode:kCGBlendModeNormal alpha:1.0f]; }
if (secondImage) { [secondImage drawInRect:contentRect blendMode:kCGBlendModeNormal alpha:1.0f]; }
}
}
As you see images are being drawn with drawInRect:blendMode:alpha: method.
Problem
Well, UITableView can't be scrolled at all, it's being struck on every cell, it's chunky and creepy.
Thoughts
Well digging sample code, stackoverflow and forums gave me thought to use OpenGL ES to pre-render images, but, really, is it that hard to make a smooth scrolling?
What's wrong with just using UIImageViews? Are they not fast enough? (They should be if you're preloading the UIImages).
One thing to note is that [UIImage imageNamed:] won't explicitly load the image data into memory. It'll give you a reference which is backed by the data on disk. You can get around this by making a call to [yourImage CGImage].

Drawing a view hierarchy into a specific context in Cocoa

For part of my application I have a need to create an image of a certain view and all of its subviews.
To do this I'm creating a context that wraps a bitmap with the same-size as the view, but I'm unsure how to draw the view hierarchy into it. I can draw a single view just be setting the context and explicitly calling drawRect, but this does not deal with all of the subviews.
I can't see anything in the NSView interface that could help with this so I suspect the solution may lie at a higher level.
I found that writing the drawing code myself was the best way to:
deal with potential transparency issues (some of the other options do add a white background to the whole image)
performance was much better
The code below is not perfect, because it does not deal with scaling issues when going from bounds to frames, but it does take into account the isFlipped state, and works very well for what I used it for. Note that it only draws the subviews (and the subsubviews,... recursively), but getting it to also draw itself is very easy, just add a [self drawRect:[self bounds]] in the implementation of imageWithSubviews.
- (void)drawSubviews
{
BOOL flipped = [self isFlipped];
for ( NSView *subview in [self subviews] ) {
// changes the coordinate system so that the local coordinates of the subview (bounds) become the coordinates of the superview (frame)
// the transform assumes bounds and frame have the same size, and bounds origin is (0,0)
// handling of 'isFlipped' also probably unreliable
NSAffineTransform *transform = [NSAffineTransform transform];
if ( flipped ) {
[transform translateXBy:subview.frame.origin.x yBy:NSMaxY(subview.frame)];
[transform scaleXBy:+1.0 yBy:-1.0];
} else
[transform translateXBy:subview.frame.origin.x yBy:subview.frame.origin.y];
[transform concat];
// recursively draw the subview and sub-subviews
[subview drawRect:[subview bounds]];
[subview drawSubviews];
// reset the transform to get back a clean graphic contexts for the rest of the drawing
[transform invert];
[transform concat];
}
}
- (NSImage *)imageWithSubviews
{
NSImage *image = [[[NSImage alloc] initWithSize:[self bounds].size] autorelease];
[image lockFocus];
// it seems NSImage cannot use flipped coordinates the way NSView does (the method 'setFlipped:' does not seem to help)
// Use instead an NSAffineTransform
if ( [self isFlipped] ) {
NSAffineTransform *transform = [NSAffineTransform transform];
[transform translateXBy:0 yBy:NSMaxY(self.bounds)];
[transform scaleXBy:+1.0 yBy:-1.0];
[transform concat];
}
[self drawSubviews];
[image unlockFocus];
return image;
}
You can use -[NSView dataWithPDFInsideRect:] to render the entire hierarchy of the view you send it to into a PDF, returned as an NSData object. You can then do whatever you wish with that, including render it into a bitmap.
Are you sure you want a bitmap representation though? After all, that PDF could be (at least in theory) resolution-independent.
You can use -[NSBitmapImageRep initWithFocusedViewRect:] after locking focus on a view to have the view render itself (and its subviews) into the given rectangle.
What you want to do is available explicitly already. See the section "NSView Drawing Redirection API" in the 10.4 AppKit release notes.
Make an NSBitmapImageRep for caching and clear it:
NSGraphicsContext *bitmapGraphicsContext = [NSGraphicsContext graphicsContextWithBitmapImageRep:cacheBitmapImageRep];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:bitmapGraphicsContext];
[[NSColor clearColor] set];
NSRectFill(NSMakeRect(0, 0, [cacheBitmapImageRep size].width, [cacheBitmapImageRep size].height));
[NSGraphicsContext restoreGraphicsState];
Cache to it:
-[NSView cacheDisplayInRect:toBitmapImageRep:]
If you want to more generally draw into a specified context handling view recursion and transparency correctly,
-[NSView displayRectIgnoringOpacity:inContext:]