Control layout of sublayers of NSView with transforms applied - core-animation

I'm attempting to create an NSView subclass which can give me access to a layer for which I can control the normally-off-limits parameters like anchorPoint and transform, as well as apply animations.
My plan was to create a layer-hosting view, and then add a sublayer to its root layer. This works perfectly, with the exception that I'm having a very difficult time positioning and sizing this sublayer. I feel like I'm missing something obvious, but I cannot figure it out.
My first attempt was to set the layoutManager of the root layer to CAConstraintLayoutManager. This works great unless I set the sublayer's transform property. CAConstraintLayoutManager updates the layer's frame directly and this causes issue with rotation transformations. Took me forever to figure this out, but I eventually did find the documentation that explains this.
Ok, so next, I made a custom CALayoutManager that just adjusts sublayer's bounds and position, instead of changing the frame. This appears to work, but corrupts layout if my view has subviews. This surprised me. I guess it is messing with NSView's internal layout system when other layers are involved?
So, I moved on using the root layer delegate method func layoutSublayers(of layer: CALayer). This also corrupts layout, as I didn't realize that NSView itself expects to be the root layer's delegate and uses that method specifically.
So, at this point, I'm at a loss. I cannot figure out how to control layout and also apply a transform to a sublayer. Does anyone have any suggestions?

It turns out there's an obvious answer to this question. NSView has a layout method that is perfect for this. I experimented with this early on, but my code was in a bad state and things weren't working for other reasons. Once I removed all the experiments/half-commented code, things worked great!

Related

Layer hosting NSView within NSOutlineView

I am trying to create a custom NSView that hosts a CALayer hierarchy to perform efficient display. This NSView is then embedded within a NSTableCellView that is displayed by a View-Based NSOutlineView.
The problem is that whenever I expand or collapse an item, all rows are being moved, but the layer's content remains displayed at the position it was before changing the outline.
Scrolling the NSOutlineView seems to refresh the layers and they resync with their rows at that point.
I have debugged this behavior using Instruments and it seems that the scrolling provokes a layout operation which updates the layers with a setPosition: call that should have occured when expanding or collapsing items.
Here is some sample code for a simple layer hosting NSView subclass.
#interface TestView : NSView
#end
#implementation TestView
- (instancetype)initWithFrame:(NSRect)frameRect
{
self = [super initWithFrame:frameRect];
CAShapeLayer* layer = [CAShapeLayer layer];
layer.bounds = self.bounds;
layer.position = CGPointMake(NSMidX(self.bounds), NSMidY(self.bounds));
layer.path = [NSBezierPath bezierPathWithOvalInRect:self.bounds].CGPath;
layer.fillColor = [NSColor redColor].CGColor;
layer.delegate = self;
self.layer = layer;
self.wantsLayer = YES;
return self;
}
#end
I have tried a lot of potential solutions to this problem but I couldn't find any interesting method that gets called on the NSView instance that could be overriden to call [self.layer setNeedsDisplay] or [self.layer setNeedsLayout]. I also tried various setters on the CALayer itself such as :
layer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
layer.needsDisplayOnBoundsChange = YES;
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay;
Can anyone help me figure out how to make this layer display properly inside a NSOutlineView?
I ended up answering my question. The problem wasn't in the way my TestView was implemented. I simply missed one of the steps for enabling CoreAnimation support within the application. The relevant reference is within the Core Animation Programming Guide.
Basically, in iOS Core Animation and layer-backing is always enabled by default. On OS X, it has to be enabled this way :
Link against the QuartzCore framework
Enable layer support for one or more of your NSView objects by doing one of the following
In your nib files, use the View Effects inspector to enable layer support for your views. The inspector displays checkboxes for the selected view and its subviews. It is recommended that you enable layer support in the content view of your window whenever possible
For views you create programmatically, call the view’s setWantsLayer: method and pass a value of YES to indicate that the view should use layers.
Once I enable layer support on any of the NSOutlineView's parents, the various glitches are solved.
It is difficult to read the NSOutlineView reference documents and find the information about cell reuse that is likely giving you fits here.
You may have looked at outlineViewItemDidCollapse: but it's kind of a useless for our issue, because it doesn't have a pointer to an NSView, and that's because it's older than view-based outline views.
Perhaps the one helpful mention, buried within the NSOutlineViewDelegate protocol, down in the section on view-based NSOutlineView methods, there is a single mention within outlineView:didRemoveRowView:forRow: that:
The removed rowView may be reused by the table, so any additionally inserted views should be removed at this point.
In other words, when you call the outline view's makeViewWithIdentifier:owner:, for a cellView or rowView with a particular ID you often get a recycled view. Especially often because of collapse. Incidentally, that method is from the NSTableView superclass, and in that reference, there's also this comment:
This method may also return a reused view with the same identifier that is no longer available on screen. If a view with the specified identifier can’t be instantiated from the nib file or found in the reuse queue, this method returns nil.
So you have the option of altering the view hierarchy or niling properties in didRemoveRowView:forRow. However, buried within a third cocoa reference, that for NSView, there is within the commentary on prepareForReuse, this comment:
This method offers a way to reset a view to some initial state so that it can be reused. For example, the NSTableView class uses it to prepare views for reuse and thereby avoid the expense of creating new views as they scroll into view. If you implement a view-reuse system in your own code, you can call this method from your own code prior to reusing them.
So, TL;DR, you need to implement prepareForReuse.
The pertinent references are (mostly) the superclasses of both NSOutlineView and NSTableCellView.
And, FWIW, there was a similar question here, where the questioner seems to indicate things are even worse than I think, in that NSOutlineView is more creative behind the scenes than NSTableView.
In my own work with outline views and embedded NSTextViews, I've seen wildly terrible rendering hiccups relating to expand/collapse/scroll that I seem to have managed in just the NSOutlineViewDelegate methods. On iOS they did everyone the favor of renaming makeViewWithIdentifier to the more explicit dequeueReusableCellViewWithIdentifier.
You shouldn't have to enable layer backing for any of the ancestor views (like the outline view).
In my experience, the layer immediately assigned to a view (as opposed to sublayers) doesn't need its bounds, position, or autoresizing mask to be set. It is automatically made to track the bounds of the view. In fact, I would avoid setting those properties, just in case that breaks the automatic synchronization with the view's bounds rect.
So, the question is: how are you arranging for the view to move or resize with its superview? Are you using auto layout? If so, did you turn off its translatesAutoresizingMaskIntoConstraints? If yes to both, what constraints are you setting on the view? If no to either, how did you position the view within its superview? What frame did you set? Also, is the superview configured to autoresize its subviews (probably yes, since that's the default)? What is your view's autoresizingMask?
You could also override -setFrameOrigin: and -setFrameSize: in your custom view class and call through to super. Also, add logging to show when that's happening and what the new frame rect is. Is your view being moved as you expect when you expand or collapse rows?

Hook on UIView's frame change from third party library

I am building a 3rd party library that I plan to integrate on different projects, so I need this to be as modular and have a non-destructive nature as possible. I need to "hook on" frame changes of UIViews. I am building a category and from that category method, I need to hook on that view's (it can be any regular UIView or subclass) frame change, and perform my additional logic. I've looked at KVO but as seen here https://stackoverflow.com/a/19701380/811405 it's not a safe approach.
How can I hook on frame change of a UIView? I can override setFrame: in my category, but I know that it is the single worst thing an Objective-C programmer can make: overriding a default method in a category. How do I achieve this?
UPDATE: I'm currently working on a really ugly (but working) solution:
I've created an invisible (empty drawRect:) UIView subclass.
I'm instantiating and adding it to the view of which I want to be notified of frame changes.
I'm setting that view's (the "superview") autoresizesSubviews to YES.
I'm setting my "invisible" view's autoresizing mask to both flexible width and height.
I'm overriding my "invisible" view's setFrame: to notify the appropriate object via delegation.
Because the superview will set frame of all subviews with flexible autoresizing mask when autoresizesSubviews is YES, this works, but it involves adding a hidden view inside a view, which is a bit hacky and may create problems in some scenarios (though most are corner cases). I'm still searching for a more suitable/less hacky solution.

NSPopover padding content on one side

I have two NSPopovers, both of which are set up exactly the same (just linking a custom NSView from IB). Both pop up just fine, but one appears to offset the content by about 20px.
This NSPopover is (properly) not padding the content...
but this one adds about 20px from the ride side.
Here are the two views laid out in IB
As you can see, the search bar should be pinned to the right side like it is the left, but for some reason it is not. At first I thought it was a contraints issue, but after messing around with them for a while I can confirm it is not.
Any clue whats up?
EDIT: Decided to subclass the view and fill its rect, got some very strange results! The view appears to be offset. I have no clue why this is...
From here, this caught my eye (emphasis mine):
The principle circumstance in which you should not call
setTranslatesAutoresizingMaskIntoConstraints: is when you are not the
person who specifies a view’s relation to its container. For example,
an NSTableRowView instance is placed by NSTableView. It might do this
by allowing the autoresizing mask to be translated into constraints,
or it might not. This is a private implementation detail. Other views
on which you should not call
setTranslatesAutoresizingMaskIntoConstraints: include an
NSTableCellView, a subview of NSSplitView, an NSTabViewItem’s view, or
the content view of an NSPopover, NSWindow, or NSBox. For those
familiar with classic Cocoa layout: If you would not call
setAutoresizingMask: on a view in classic style, you should not call
setTranslatesAutoresizingMaskIntoConstraints: under autolayout.
Does it apply to you?
if you have an outlet to your content view, you should be able to force it into place with:
[contentView setFrame:[[contentView superview]bounds]];
or more modernly I guess...
contentView.frame = contentView.superview.bounds;
Proplem solved, but I still don't exactly know why it required solving. The NSPopovers content view (or at least mine) requires an origin point of X: 13, Y: 13. For some reason, only one of my views was getting this value.
To solve this, I subclassed NSView and overrode the setFrame method forcing its x and y to always be 13, 13
-(void)setFrame:(NSRect)frameRect {
[super setFrame:NSMakeRect(13, 13, frameRect.size.width, frameRect.size.height)];
}

layoutSubviews being called repeatedly on ios6 after CATransaction

I inherited an overly complicated project (so I don't know all of the inner workings), and I'm running into a bug. Certain parts of my app have some long animations done with CATransaction, and it seems to be causing layoutSubviews to be called repeatedly while the animations are active. This doesn't happen on ios5 and everything looks correct, but on ios6 it gets called nonstop and interferes with a lot of the layout of the view. The stack trace is all hidden/grayed out, but it does seem to begin with CA::Transaction::commit()
Did anything with CATransaction change between ios versions to cause something like this?
See this post: UIView/CALayer: Transform triggers layoutSubviews in superview
Apple answered me via TSI:
why am I seeing this behavior?
is this inconsistency or am I misunderstanding some core concepts?
A view will be flagged for layout whenever the system feels something has changed that requires the view to re-calculate the frames of its subviews. This may occur more often than you'd expect and exactly when the system chooses to flag a view as requiring layout is an implementation detail.
why does it cascade upwards the view hierarchy?
Generally, changing a geometric property of a view (or layer) will trigger a cascade of layout invalidations up the view hierarchy because parent views may have Auto Layout constraints involving the modified child. Note that Auto Layout is active in some form regardless of whether you have explicitly enabled it.
how can I avoid superview to layoutSubviews every time I'm changing transform?
There is no way to bypass this behavior. It's part of UIKit's internal bookkeeping that is required to keep the view hierarchy consistent.
Sounds like an Autolayout issue. Does the view or any of its subviews use Autolayout? Autolayout is nice but doesn't seem very fast and efficient so may cause issues when animating.
Of course it may be necessary for the subviews to be layed out each step in the animation if the size one shape of the view is changing in a way that affects subview placement or size. Consider the animation and what effects it has.

Any "fundamentals-oriented" example of NSScroller out there?

I'm looking for some kind of a basic, straightforward example of how to work with a pair of NSScrollers in an NSScrollView with a custom NSView.
There are sporadic examples out there, largely consisting of contrived examples using programatically created interfaces, or based on the assumption that the developer is working with a typical ImageView or TextView. Even the Sketch example is based on an NSView that uses the Page Setup in the Print dialog for the bounds, and everything is managed by Cocoa. So there's no real discussion or examples anywhere of how make it all work using a custom Model (though that may be part of the problem, because what does one base the Model on?). Even Apple's own documentation is dodgy here.
Essentially, I have a sub-classed NSView enbedded in an NSScrollView (per the Scoll View Guide), that a user can click in the view to create, edit and delete objects, much like an illustration program. The Model is those objects that are just data wrappers that simply record their position for drawRect: to use. The height and width are based on custom values that are being translated into pixels as needed.
My problem is that all of the examples I have found are based on either a text editor, an image viewer, or uses the standard document sizes in the Page Setup dialog. Because these are common document types, Cocoa basically manages for the developer, so the interaction code is more or less hidden (or I'm just not seeing it for what it is). My project doesn't fit any of those needs, and I have no need for printing. Thrusting my Model into the documentView property wouldn't work.
I'm just looking for a simple example on how to initialize the NSScrollers with a custom, object-oriented Model (the documentView), and handle scrolling and updating based on user action, such as when the user drags a smattering of objects off to the left or down or the window gets resized. I think I'm close to getting it all together, but I'm missing the jumping off point that ties the controls to document.
(Not that it matters in a Cocoa question, but when I did this in REALbasic, I would simply calculate and apply the MaxX, MaxY to a ScrollBar's Maximum value based on user actions, watch the position in the ScrollBar when the user clicks, and draw as needed. NSScrollers in the NSScrollView context aren't nearly as obvious to me, it seems.)
I appreciate the time taken by everyone, but I'm updating with more information in the hopes of getting an answer I can use. I'm sorry, but none of this is making sense, Apple's documents are obtuse, but perhaps I'm missing something painfully obvious here...
I have an array of objects sitting in a subclassed NSDocument which are data holders that tell drawRect what and where to draw. This is straight from the Sketch example. The Sketch example uses the document sizes in the Page Setup dialog for the size, so there's nothing to show here. I'm cool with Cocoa handling the state of the scroll bars, but how do I link up the ScrollView to see the initial editor's state held in the NSDocument and updates to those objects and the editor? Do I calculate my own NSRect and pass that to the NSScrollView? Where and how? Am I doing this in my custom NSView which has been embedded in the NSScrollView or my NSDocument in init? The NSScrollView isn't created programmatically (there's no easy way of doing that), so it's all sitting in Interface Builder waiting to be hooked up. I'm missing the hook up bit.
Perhaps I'm wearing my "I don't get it" cap this week, but this can't be this difficult. Illustration apps, MIDI Editors, and countless other similar custom apps do this all the time.
SOLVED (mostly):
I think I have this sorted out now, though it's probably not the best implementation.
My document class now has a NSRect DocumentRect property that looks at all of its objects and gives back a new NSRect based on their location. I call it in my subclassed NSView's mouse events with
[self setFrame:[[self EditorDocument] DocumentRect]];
This updates the size of the View based on user interaction, and the window now handles the scrolling where it didn't before. At this point I'm figuring out how to get the frame to expand while dragging, but at least I now have the fundamental concept I was missing.
The answer given pointed me in the direction I needed to go here (documentView requiring a view, which translated to looking at the NSView class), so Peter gets the credit. Thanks so much for help.
The document view isn't a model, it's a view. That's why it's called the document view.
The reason there are so few examples on working with NSScrollers directly is because you normally don't. You work with NSScrollView and let it handle the scrollers for you.
All you need to do is make a view big enough to show the entire model, then set that as the document view of the scroll view. From there, it should Just Work. You don't need to manage any of the scrolling-related numbers yourself; Cocoa handles them for you.
For details, see the Scroll View Programming Guide.