NSSplitView resizes the custom NSView contained - cocoa

I've a vertical NSSplitView, the bottom subview contains a custom view (eg NSView) and a NSTextView.
The NSView contains inside it two NSButtons.
When I resize the splitView, making it smaller, the NSView containing the buttons is resized, too.
I don't want this behavior.
To better explain my problem please view the attached image.
Image 1: the window at application startup, everything is ok
Image 2: I've resized making smaller the split view, only a little part of buttons is visible
Image 3: I've enlarged again the split view but as you can see the NSView remains smaller and buttons are no longer visible (if I resize the splitView to bottom the NSView 'disappears')

This is a vicious problem that's based on the legacy workings of Cocoa views. The best solution I've seen is to constrain the minimum dimension of any portion of the split view. If the subviews never collapse, their metrics don't cross into another dimension and they should re-enlarge just fine.
To do this, set up a delegate for your split view, which will implement - splitView:constrainMaxCoordinate:ofSubviewAt:. The split view will call your delegate method hoping it can leave the max divider position at the height of the split view (passing this in as the second argument), but you can simply subtract some quantity from that value (say, 60) to return it as the minimum height for the bottom view.
- (CGFloat)splitView:(NSSplitView *)aSplitView
constrainMaxCoordinate:(CGFloat)proposedMin
ofSubviewAt:(NSInteger)dividerIndex {
return proposedMin - 60;
}
Of course, you'll probably want to do more checking in this method to make sure you're talking about the right split view, and the right subview, to avoid overreaching effects, but this is the basic idea.
(See also this fabulicious article on the subject.)

Constraining the divider position did not help in my case, as I'm animating the subviews and subviews can be collapsed.
I managed to achieve an acceptable solution by implementing the splitView delegate method -splitviewWillResizeSubviews: (means, you have to connect the delegate property from the split view to your controller in IB or in code) to maintain a minimum width by setting the subview to hidden instead of shrinking it to zero:
- (void)splitViewWillResizeSubviews:(NSNotification *)notification {
NSUInteger divider = [[[notification userInfo] valueForKey:#"NSSplitViewDividerIndex"] intValue];
NSView *subview = nil;
if(divider == SPLITVIEW_DIVIDER_SIDEBAR) {
subview = (NSView*)[self.splitView.subviews objectAtIndex:SPLITVIEW_SIDEBAR_INDEX];
}
if(subview) {
if(subview.frame.size.width < SPLITVIEW_MINIMUM_SIDEBAR_WIDTH) {
CGRect correctedFrame = subview.frame;
correctedFrame.size.width = SPLITVIEW_MINIMUM_SIDEBAR_WIDTH;
subview.frame = correctedFrame;
subview.hidden = YES;
} else {
subview.hidden = NO;
}
}
}

Related

NSTextField is blurry in NSTableView when image is inserted in a row

In my view-based NSTableView, each view has (among other stuff) an NSTextField, and an NSImageView right below the text field.
Sometimes, when I insert a new row at the top of the table, and this row has an image in its NSImageView, the text in the other rows becomes blurry/degraded.
The text becomes normal again after scrolling several times.
Not ok:
Ok:
An example where the text is only blurred in the rows after the one with the image:
This really makes me think it's a problem coming from .insertRows or noteHeightOfRows.
All elements have autolayout constraints set in IB.
The scroll view has a CoreAnimation layer set in IB. I also use the layer when preparing the cell:
cell.layer?.isOpaque = true
cell.textLabel.layer?.isOpaque = true
and sometimes
cell.layer?.borderColor = someColor
cell.layer?.borderWidth = someWidth
Rows are inserted with:
// update the model, then:
NSAnimationContext.runAnimationGroup({ (context) in
context.allowsImplicitAnimation = true
self.table.insertRows(at: index, withAnimation: [.effectGap])
})
Updating the row with the image:
// in tableViewCell, while populating the table
cell.postImage.image = img
cell.postImage.needsDisplay = true
// once the image is downloaded
table.reloadData(forRowIndexes: ..., columnIndexes: ...)
table.noteHeightOfRows(withIndexesChanged: ...)
How to avoid this issue? It's hard to debug because it doesn't always happen and I can't see what are the reasons for it to happen when it does.
Due to the fact that the text got blurry only when an image was inserted, my first thought is that the NSTextField's frame is not "integral" (its pixel values are not integers). Which is a well known reason for getting blurry text.
You are using an Auto Layout feature of Cocoa so my suggestion is to override the void layout() method inside your view-based table view cell.
To call it's super method (to let Cocoa to do all the needed layout calculations) and just to set the frame of the textFiled to be the same frame, but integral.
Addendum: while the textField itself may have an integral frame - it still can be layed out on a "half pixel" (in real screen coordinates) which will still lead to the blurriness.
TextField is a subView of the cell so itss frame is not in the screen coordinates but in the cell coordinates.
Example: let's imagine that the cell has a frame of (0.5, 0.5 , 100, 100) which is not integral, and the textField has an integral frame of (10,10, 50, 50) which is integral by all means!
BUT when the textField will be layed out on the screen it will have the following frame: (10.5, 10.5, 50, 50) which is not integral - and will lead to drawing the textField on a "half pixel" (not integral) which leads to blurry text.
So in your case the cell itself must be layed out on an integral frame as well as the textField to ensure that the textField is on integral frame in screen coordinates.
In your tableViewCell subclass:
void layout()
{
[super layout];
//**** We have to ensure that the cell is also layed on an integral frame ****//
[self setFrame:NSIntegralRect(self.frame)];
//At this point after calling the [super layout]; myTextField will have a valid (autolayoutwise) frame so all you have to do is to ensure that it is indeed an integral frame doing so by calling NSIntegralRect(NSRect rect); method of Cocoa framework.
[myTetxField setFrame:NSIntegralRect(myTextField.frame)];
//Maybe optionally you will want to set everything to the integral rect :-)
/*
for(NSView * view in self.subViews)
{
[view setFrame:NSIntegralRect(view.frame)];
}
*/
}

Automatically wrap NSTextField using Auto Layout

How does one go about having auto-layout automatically wrap an NSTextField to multiple lines as the width of the NSTextField changes?
I have numerous NSTextFields displaying static text (i.e.: labels) in an inspector pane. As the inspector pane is resized by the user, I would like the right hand side labels to reflow to multiple lines if need be.
(The Finder's Get Info panel does this.)
But I haven't been able to figure out the proper combination of auto layout constraints to allow this behavior. In all case, the NSTextFields on the right refuse to wrap. (Unless I explicitly add a height constraint that would allow it to.)
The view hierarchy is such that each gray band is a view containing two NSTextFields, the property name on the left and the property value on the right. As the user resizes the inspector pane, I would like the property value label to auto-resize it's height as need-be.
Current situation:
What I would like to have happen:
(Note that this behavior is different than most Stack Overflow questions I came across regarding NSTextFields and auto layout. Those questions wanted the text field to grow while the user is typing. In this situation, the text is static and the NSTextField is configured to look like a label.)
Update 1.0
Taking #hamstergene's suggestion, I subclassed NSTextField and made a little sample application. For the most part, it now works, but there's now a small layout issue that I suspect is a result of the NSTextField's frame not being entirely in sync with what auto-layout expects it to be. In the screenshot below, the right-hand side labels are all vertically spaced with a top constraint. As the window is resized, the Where field is getting properly resized and wrapped. However, the Kind text field does not get pushed down until I resize the window "one more pixel".
Example: If I resize the window to just the right width that the Where textfield does it's first wrap, then I get the results in the middle image. If I resize the window one more pixel, then the Kind field's vertical location is properly set.
I suspect that's because auto-layout is doing it's pass and then the frames are getting explicitly set. I imagine auto-layout doesn't see that on that pass but does it it on the next pass, and updates the positions accordingly.
Assuming that's the issue, how do I inform auto-layout of these changes I'm doing in setFrameSize so that it can run the layout again. (And, most importantly, not get caught in recursive state of layout-setFrameSize-layout-etc...)
Solution
I've come up with a solution that appears to work exactly how I was hoping. Instead of subclassing NSTextField, I just override layout in the superview of the NSTextField in question. Within layout, I set the preferredMaxLayoutWidth on the text field and then trigger a layout pass. That appears to be enough to get it mostly working, but it leaves the annoying issue of the layout being briefly "wrong". (See note above).
The solution to that appears to be to call setNeedsDisplay and then everything Just Works.
- (void)layout {
NSTextField *textField = ...;
NSRect oldTextFieldFrame = textField.frame;
[textField setPreferredMaxLayoutWidth:NSWidth(self.bounds) - NSMinX(textField.frame) - 12.0];
[super layout];
NSRect newTextFieldFrame = textField.frame;
if (oldTextFieldFrame.size.height != newTextFieldFrame.size.height) {
[self setNeedsDisplay:YES];
}
}
The simplest way to get this working, assuming you're using an NSViewController-based solution is this:
- (void)viewDidLayout {
[super viewDidLayout];
self.aTextField.preferredMaxLayoutWidth = self.aTextField.frame.size.width;
[self.view layoutSubtreeIfNeeded];
}
This simply lets the constraint system solve for the width (height will be unsolvable on this run so will be what ever you initially set it to), then you apply that width as the max layout width and do another constraint based layout pass.
No subclassing, no mucking with a view's layout methods, no notifications. If you aren't using NSViewController you can tweak this solution so that it works in most cases (subclassing textfield, in a custom view, etc.).
Most of this came from the swell http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html (look at the Intrinsic Content Size of Multi-Line Text section).
If inspector pane width will never change, just check "First Runtime Layout Width" in IB (note it's 10.8+ feature).
But allowing inspector to have variable width at the same time is not possible to achieve with constraints alone. There is a weak point somewhere in AutoLayout regarding this.
I was able to achieve reliable behaviour by subclassing the text field like this:
- (NSSize) intrinsicContentSize;
{
const CGFloat magic = -4;
NSSize rv;
if ([[self cell] wraps] && self.frame.size.height > 1)
rv = [[self cell] cellSizeForBounds:NSMakeRect(0, 0, self.bounds.size.width + magic, 20000)];
else
rv = [super intrinsicContentSize];
return rv;
}
- (void) layout;
{
[super layout];
[self invalidateWordWrappedContentSizeIfNeeded];
}
- (void) setFrameSize:(NSSize)newSize;
{
[super setFrameSize:newSize];
[self invalidateWordWrappedContentSizeIfNeeded];
}
- (void) invalidateWordWrappedContentSizeIfNeeded;
{
NSSize a = m_previousIntrinsicContentSize;
NSSize b = self.intrinsicContentSize;
if (!NSEqualSizes(a, b))
{
[self invalidateIntrinsicContentSize];
}
m_previousIntrinsicContentSize = b;
}
In either case, the constraints must be set the obvious way (you have probably already tried it): high vertical hugging priority, low horizontal, pin all four edges to superview and/or sibling views.
Set in the size inspector tab in section Text Field Preferred Width to "First Runtime layout Width"
This works for me and is a bit more elegant. Additionally i've made a little sample project on Github
public class DynamicTextField: NSTextField {
public override var intrinsicContentSize: NSSize {
if cell!.wraps {
let fictionalBounds = NSRect(x: bounds.minX, y: bounds.minY, width: bounds.width, height: CGFloat.greatestFiniteMagnitude)
return cell!.cellSize(forBounds: fictionalBounds)
} else {
return super.intrinsicContentSize
}
}
public override func textDidChange(_ notification: Notification) {
super.textDidChange(notification)
if cell!.wraps {
validatingEditing()
invalidateIntrinsicContentSize()
}
}
}

UICollectionView fix paging position after rotation

I have a UICollectionView with the frame of
[UIScreen mainScreen].bounds
and these attributes:
_collectionView.pagingEnabled = YES;
_collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
A cell has also the size of the collection view. When I rotate, the contentOffset of the collection view does not fit to the new orientation. It has still the same offset as before the rotation.
To fix this, I changed the contentOffset manually in the didRotate method.
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
_collectionView.contentOffset = CGPointMake(_newContentOffsetX, _collectionView.contentOffset.y);
}
This works, but it looks terrible. I also tried to scroll to the current IndexPath, but it hast the same ugly behaviour:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
[_collectionView scrollToItemAtIndexPath:_currentIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
}
I need a clean transition and behaviour of updating the collection view's content offset when rotating the device.
Its better To use the
willAnimateRotationToInterfaceOrientation:duration:
When this method is called the bound of your view controllers view are already updated to the current device orientation. It seems that this method gets called inside of the rotation animation block. This means all the positions you will set inside of willAnimateRotationToInterfaceOrientation:duration: are animated.
This helps me a lot and I use it to update my view Controllers view content insets on rotation. Works like a charm! :-)
Instead of using didRotateFromInterfaceOrientation, try updating contentOffset in willRotateToInterfaceOrientation.
You'll need to make sure to compensate for using the other width/height dimension that you'll be rotating to since it won't have done the rotation yet.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
_collectionView.contentOffset = CGPointMake(_newContentOffsetX, _collectionView.contentOffset.y);
}

Finder-Style floating group rows in view-based NSOutlineView

I have implemented a view-based NSOutlineView in my project. I am using floating group rows. Now, I would like to have this NSOutlineView look basically like the Finder list-view (CMD-2) when it is in the "arranged-by" layout (e.g. "by kind": CTRL-CMD-2). That means, the top-most group row should display the column titles and as soon as the next lower group row is starting to nudge the previous one out of the view, the column titles fade in on the second group row (I hope this makes sense).
Is there any out-of-the-box way to achieve this? So far I have successfully subclassed NSTableCellView to show the columns' titles, however, I cannot get the fade-in to work as I cannot seem to find out the position of the group row in relation to the floating one above it.
Regards,
Michael
I've found a possible way to achieve what I want. In my custom NSTableCellView's drawRect: method, it's of course possibly in a nasty way to find out the view's position relative to the enclosing NSClipView. The relevant code:
- (void)drawRect:(NSRect)dirtyRect {
// _isGroupView is a member variable which has to be set previously
// (usually in setObjectValue:) in order for us to know if we're a
// group row or not.
if (_isGroupView) {
// This is the nasty party:
NSClipView *clipView = (NSClipView*)self.superview.superview.superview;
if (![clipView isKindOfClass:[NSClipView class]]) {
NSLog(#"Error: something is wrong with the scrollbar view hierarchy.");
return;
}
NSRect clipRect = [clipView documentVisibleRect];
CGFloat distanceToTop = self.superview.frame.origin.y - clipRect.origin.y;
if (self.superview.frame.origin.y - clipRect.origin.y < self.frame.size.height) {
// This means, that this NSTableCellView is currently pushing the
// view above it out of the frame.
CGFloat alpha = distanceToTop / self.frame.size.height;
NSColor *blendColor = [[NSColor blackColor] blendedColorWithFraction:alpha ofColor:[NSColor whiteColor]];
// ...
// do stuff with blendColor
// ...
// blendColor should now be the appropriate color for the wanted
// "fade in" effect.
//
}
}
}
I hope this makes sense ;-). Any tips are still appreciated!
Cheers,
Michael

NSTableView content view inset

I am looking to inset the contents of an NSTableView so that there is a gap between the top of the table view and the first cell.
On iOS this is easy with UITableView - achieved by using setContentInset:.
Turn headers back on and substitute the header view with your own subclass. Override its -drawRect: to draw only your background color. Also override -headerRectOfColumn: to prevent any of the column headers from being drawn. I'm not sure if this prevents column dragging or sorting but I'll bet it does.
The question asked how to adjust content insets similar to iOS. The currently selected answer shows how to move the first row down, but that's not quite the same thing. Adjusting the content insets will also move the start of the scrollbar to the inset position, just like iOS. This is useful when placing content underneath a "vibrant" or transparent toolbar.
An NSTableView itself does not have content insets. On macOS content insets are usually part of NSScrollView. To get access to the scroll view of NSTableView's view controller you can use the enclosingScrollview method of NSView, disable automatic adjustment and set the insets like this:
(warning old school Obj-C here)
self.enclosingScrollView.automaticallyAdjustsContentInsets = NO;
self.enclosingScrollView.contentInsets = NSEdgeInsetsMake(50.f,0.f,0.f,0.f);
Calling these from viewDidLoad is usually fine, however some types of table views will override your values with their own.
NSOutlineView set to source-list mode comes with lots of default values overridden to make the view look like the Finder sidebar.
There is no "clean" way to set the content-insets of these views. They stubbornly override your values, I've found that if you subclass NSOutlineView and overload setFrameSize: it will do the trick. So like this (inside the NSOutlineView subclass):
- (void)setFrameSize:(NSSize)newSize {
[super setFrameSize:newSize];
self.enclosingScrollView.automaticallyAdjustsContentInsets = NO;
self.enclosingScrollView.contentInsets = NSEdgeInsetsMake(100.f,0.f,0.f,0.f);
}
This will do the trick, but the initial scroll position will be strange. Calling scrollToBeginningOfDocument: from the initWithCoder: method of the subclass will scroll it to the correct initial position.
It's not very clean but you can achieve that by having the first row higher than the rest. Implement heightOfRow table delegate method:
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
{
if (row == 0) {
return normalRowHeight + topPadding;
} else {
return normalRowHeight;
}
}
The drawback is that you would also need to implement custom highlighting and custom cell drawing to take into account the extra space for the first row.
scrollView.automaticallyAdjustsContentInsets = false
scrollView.contentInsets = NSEdgeInsets(top: 40, left: 0, bottom: 0, right: 0)

Resources