NSTableHeaderView prevents auto layout from resizing scroll view in a split view - cocoa

The question: Why having a header view prevents scroll view from being resized by auto layout?
I'm trying to embed my custom view in a scroll view, which in turn is enclosed in a split view. I've created the following view hierarchy using Interface Builder in Xcode 4.5 DP 4, but the same problem seems to happen also in Xcode 4.4.
NSWindow
NSView (content view of the window)
NSSplitView
NSView (split view panel)
NSView (split view panel)
NSScrollView
TestView (my custom view)
Now, if TestView provides a NSTableHeaderView (via -headerView) property the split view divider cannot be dragged all the way to bottom (or right) to hide the TestView but stops to the boundary of the initial width or height of the TestView. If the -headerView property returns nil, the divider can be dragged freely.
This can be reproduced every time, just by creating a fresh Cocoa application project, adding the views and running the project. The steps:
Create a new Cocoa Application project
Create TestView class with headerView property which returns a NSTableHeaderView instance.
Edit MainMenu.xib and add a split view
Add custom view and make it TestView
Choose Editor -> Embed in -> Scroll view
Run the project
(No constraints or other Interface Builder menus touched)
TestView.m:
#implementation TestView {
NSTableHeaderView *_header;
}
- (NSTableHeaderView *)headerView
{
if (!_header) {
_header = [[NSTableHeaderView alloc]
initWithFrame:NSMakeRect(0.0, 0.0, 100.0, 17.0)];
}
return _header;
}
#end
Any pointers, what should I do to get the split view divider moving again?

Implement this NSSplitViewProtocol method in a convenient class:
- (BOOL)splitView:(NSSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)subview {return TRUE;}
Make sure to connect the split view's delegate output the class object.
The split view can now be adjusted to any size.

My solution was to manually remove the autoresizing constraints of the table header:
NSTableHeaderView *headerView = outlineView.headerView;
NSView *headerViewSuperview = headerView.superview;
[headerViewSuperview removeFromSuperview];
headerView.superview.translatesAutoresizingMaskIntoConstraints = NO;
[scrollView addSubview:headerViewSuperview];

Related

NSViewController disable resize when presented as Sheet?

So I am presenting an NSViewController as a sheet of a window that has resize disabled.
The view controller that is presented as a sheet can still be resized.
How do I disable resizing of a NSViewController?
Swift 4:
override func viewDidAppear() {
// any additional code
view.window!.styleMask.remove(.resizable)
}
By the way you can do this without writing code, here is how:
Drag a Window Controller element to the Storyboard from the Object
Library.
Connect the Window Controller to the specific View Controller which you want to disable resize.
On the Window Controller's Attributes uncheck Resize option.
After some more trying I found out this did the trick in viewDidLoad:
self.preferredContentSize = NSMakeSize(self.view.frame.size.width, self.view.frame.size.height);
If you add these methods, the issue will be fixed.
- (void)updateViewConstraints NS_AVAILABLE_MAC(10_10) {
[super updateViewConstraints];
}
- (void)viewWillLayout NS_AVAILABLE_MAC(10_10) {
self.preferredContentSize = NSMakeSize(self.view.frame.size.width, self.view.frame.size.height);
}

Can UIPopoverPresentationController be forced to reposition popover instead of resizing it?

I am using auto layout with Storyboard. I present a popoverPresentationController from a cell rect:
NumberController * viewController = [self.storyboard instantiateViewControllerWithIdentifier:#"NumberController"];
UIPopoverPresentationController *pc = [viewController popoverPresentationController];
pc.delegate = self;
pc.permittedArrowDirections = UIPopoverArrowDirectionAny;
pc.sourceView = tableView;
pc.sourceRect = [tableView rectForRowAtIndexPath:indexPath];
[self.navigationController presentViewController:viewController animated:animated completion:nil];
The popover presents on an iPad in portrait mode with the arrow up.
I rotate the iPad to landscape mode. The popoverPresentationController keeps the same sourceView/sourceRect and properly points to the cell. It also keeps the up arrow.
But it is now at the bottom of the view, so the popover resizes to a shorter height. This is not desired behavior.
If the popover were simply to move to a new position and change the arrow direction, it would not need to resize at all. This is the desired behavior.
I thought the following method might permit me to make changes, but it is not called since the sourceView rect does not change:
- (void)popoverController:(UIPopoverController *)popoverController
willRepositionPopoverToRect:(inout CGRect *)rect
inView:(inout UIView **)view {
}
I have tried to reset the permittedArrowDirections (in preferredContentSize, because this seemed like the most logical place). This does not work (the popover still resizes):
- (CGSize) preferredContentSize {
[super preferredContentSize];
self.popoverPresentationController.permittedArrowDirections = UIPopoverArrowDirectionUnknown;
return CGSizeMake(DEFAULT_POPOVER_WIDTH,DEFAULT_POPOVER_HEIGHT);
}
I simply cannot find a way to force the popoverPresentationController to change arrow direction and reposition the popover instead of resizing the popover. I am beginning to think it is not even possible - but I still hold out hope that I am just missing something.
EDIT: In the meantime, it has occurred to me that maybe a popover is not the best way to present this view if I don't want it resized in iPad. I am going to try it with UIModalPresentationFormSheet presentation. But I would still like to find an answer to this question.
I just ran into the problem where
- (void)popoverController:(UIPopoverController *)popoverController
willRepositionPopoverToRect:(inout CGRect *)rect
inView:(inout UIView **)view {
was not being called because my view controller was detached. There may be a view in your view hierarchy whose view controller has not been added as a child view controller.
I thought the following method might permit me to make changes, but it is not called since the sourceView rect does not change
The sourceView rect does not have to change, just the interface orientation. From the UIPopoverControllerDelegate documentation:
For popovers that were presented using the presentPopoverFromRect:inView:permittedArrowDirections:animated: method, the popover controller calls this method when the interface orientation changes.

Custom NSView in NSTableView not showing all subviews

I'm trying to create a custom NSView to display in a column in a view-based NSTableView. The view contains 2 subviews, an NSTextField and an NSButton. I want the button to stay the width set by the constraints, and the textfield to resize when the NSView is resized. Below is a small animation showing the NSView and its subviews, and the constraints I created.
As you can see, resizing the NSView works as expected.
Now, when displaying this custom NSView in an NSTableView, it looks as if the button just disappears, and resizing the column makes the textfield resize with it (the 'Category' column).
The coded used to create the NSView in tableView:viewForTableColumn:row:
let identifier = tableColumn!.identifier
if identifier == "Category" {
var view = tableView.makeViewWithIdentifier(identifier, owner: self) as? TableCategoryView
if view == nil {
view = TableCategoryView(frame: tableView.frame)
view!.identifier = identifier
}
return view
}
The strange thing is, when there are no constraints on the 2 views, the button and textfield are both happily displayed inside the column, but they then of course don't resize with the table column width.
What am I doing wrong?
EDIT: It looks like something else is wrong. The NSView itself isn't resizing at all with the table column.
I think TableCategoryView is your new class and it is subclassed from NSView and shall replace the NSTableCellView you get when creating (in IB) a view based NSTableView. If you really want to create your own TableCellView it should be a direct subclass of NSTableCellView not NSView.
But in your case (add a button to the TableCellView) you do not need to create a new class. The existing TableCellView object already has a TextField (a property) with the name textField. Then simply drag (means: add) a button into the existing TableCellView (resize it and set the constraints) and drag a link from the button to a corresponding method in the delegate of the TableView. In the "corresponding method" you can ask for the clicked row and column and identify the click button. I did so for a TableView and for me it works well.

How to customize disclosure cell in view-based NSOutlineView

I'm trying to customize the disclosure arrow appearance in my view-based NSOutlineView. I saw that it's recommended to use
- (void)outlineView:(NSOutlineView *)outlineView willDisplayOutlineCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
delegate method to achieve it. The problem is that this method is not called for some reason. I have 2 custom cell views - one for item and second for header item. May be this method is not called for view-based outline views? May be something became broken in Lion?
Please shed some light.
Solution 1:
Subclass NSOutlineView and override makeViewWithIdentifier:owner:
- (id)makeViewWithIdentifier:(NSString *)identifier owner:(id)owner {
id view = [super makeViewWithIdentifier:identifier owner:owner];
if ([identifier isEqualToString:NSOutlineViewDisclosureButtonKey]) {
// Do your customization
}
return view;
}
For Source Lists use NSOutlineViewShowHideButtonKey.
Solution 2:
Interface Builder
The button is added to the column and the identifier set to NSOutlineViewDisclosureButtonKey.
Official documentation from NSOutlineView.h
/* The following NSOutlineView*Keys are used by the View Based NSOutlineView to create the "disclosure button" used to collapse and expand items. The NSOutlineView creates these buttons by calling [self makeViewWithIdentifier:owner:] passing in the key as the identifier and the delegate as the owner. Custom NSButtons (or subclasses thereof) can be provided for NSOutlineView to use in the following two ways:
1. makeViewWithIdentifier:owner: can be overridden, and if the identifier is (for instance) NSOutlineViewDisclosureButtonKey, a custom NSButton can be configured and returned. Be sure to set the button.identifier to be NSOutlineViewDisclosureButtonKey.
2. At design time, a button can be added to the outlineview which has this identifier, and it will be unarchived and used as needed.
When a custom button is used, it is important to properly set up the target/action to do something (probably expand or collapse the rowForView: that the sender is located in). Or, one can call super to get the default button, and copy its target/action to get the normal default behavior.
NOTE: These keys are backwards compatible to 10.7, however, the symbol is not exported prior to 10.9 and the regular string value must be used (i.e.: #"NSOutlineViewDisclosureButtonKey").
*/
APPKIT_EXTERN NSString *const NSOutlineViewDisclosureButtonKey NS_AVAILABLE_MAC(10_9); // The normal triangle disclosure button
APPKIT_EXTERN NSString *const NSOutlineViewShowHideButtonKey NS_AVAILABLE_MAC(10_9); // The show/hide button used in "Source Lists"
This answer is written with OS X 10.7 in mind, for newer versions of OS X/macOS, refer to WetFish's answer
That method does not get called because it is only relevant for cell based outline views.
In a view based outline view, the disclosure triangle is a regular button in the row view of expandable rows. I don't know where it gets added, but it does, and NSView's didAddSubview: method handles exactly that situation of a view being added somewhere else.
Hence, subclass NSTableRowView, and override didAddSubview:, like this:
-(void)didAddSubview:(NSView *)subview
{
// As noted in the comments, don't forget to call super:
[super didAddSubview:subview];
if ( [subview isKindOfClass:[NSButton class]] ) {
// This is (presumably) the button holding the
// outline triangle button.
// We set our own images here.
[(NSButton *)subview setImage:[NSImage imageNamed:#"disclosure-closed"]];
[(NSButton *)subview setAlternateImage:[NSImage imageNamed:#"disclosure-open"]];
}
}
Of course, your outline view's delegate will have to implement outlineView:rowViewForItem: to return the new row view.
Despite the name, frameOfOutlineCellAtRow: of NSOutlineView still gets called for view based outline views, so for the positioning of your triangle, you might want to subclass the outline view and override that method, too.
For Swift 4.2 macOS 10.14, #WetFish's answer can be implemented as follows:
class SidebarView: NSOutlineView {
override func makeView(withIdentifier identifier: NSUserInterfaceItemIdentifier, owner: Any?) -> NSView? {
let view = super.makeView(withIdentifier: identifier, owner: owner)
if identifier == NSOutlineView.disclosureButtonIdentifier {
if let btnView = view as? NSButton {
btnView.image = NSImage(named: "RightArrow")
btnView.alternateImage = NSImage(named: "DownArrow")
// can set properties of the image like the size
btnView.image?.size = NSSize(width: 15.0, height: 15.0)
btnView.alternateImage?.size = NSSize(width: 15.0, height: 15.0)
}
}
return view
}
}
Looks quite nice!
Swift2 version of #Monolo's answer:
override func didAddSubview(subview: NSView) {
super.didAddSubview(subview)
if let sv = subview as? NSButton {
sv.image = NSImage(named:"icnArwRight")
sv.alternateImage = NSImage(named:"icnArwDown")
}
}

Idiom for Initializing UINavigationItem

Since UIViewController navigationItem outlets are deprecated (yes, I'm a bit behind), what is the correct idiom specifying the elements of a UINavigationItem?
I've seen several approaches suggested, but none are entirely clear to me:
"Embed the navigation item in the view controller" in the view controller's XIB.
Initialize the navigation item's properties in code, with from-scratch values.
Create an outlet for the navigation controller in the view controller, wire up a navigation item in the view controller's XIB, and use its properties to initialize the (actual) navigation item's properties in code.
It isn't clear to me how to "embed" the navigation item (simply adding it as a child of the view controller in IB has no effect); and it isn't clear to me which of these approaches is better, or, for that matter, where (in what method) to do 2 or 3.
1) If controller is created in XIB you can drop UINavigationItem on it and tweak this item - it will work. For example when you're defining UINavigationControler in XIB you can put some controller inside as root view controller. So you can have UINavigationItem for this controller in XIB.
2) If controller loads it's view from XIB (it was created by alloc and then init or initWithNibName:bundle:) it's presented in XIB only as File's Owner which doesn't support UINavigationItem in it. In this case you should configure navigation item in code (I do it in viewDidLoad usually). There is no need to create it, it's already there in navigtionItem property of your controller
self.navigationItem.title = #"Price List";
Maybe it's possible to make outlet for navigation item but I wouldn't recommend this. Apple declared such outlet obsolete for a reason. I remember discussing this with co-worker one day, but I forgot what it was (it was obvious then).
On our project we have a requirement to make UI be as customizable from IB as possible.
So, I add UINavigationItem to xib, configure it, link it as outlet to my custom UIViewController subclass, and then copy all properties at runtime with method added to UIViewController using category:
- (void)setNavigationItemPropertiesFromOtherItem:(UINavigationItem *)navItem
{
// WORKAROUND: we can't link UINavigationItem to UIViewController from IB, and navigationItem property in UIViewController is readonly
self.navigationItem.title = navItem.title;
self.navigationItem.prompt = navItem.prompt;
self.navigationItem.hidesBackButton = navItem.hidesBackButton;
if (navItem.backBarButtonItem != nil)
{
self.navigationItem.backBarButtonItem = navItem.backBarButtonItem;
}
if (navItem.leftBarButtonItem != nil)
{
self.navigationItem.leftBarButtonItem = navItem.leftBarButtonItem;
}
if (navItem.rightBarButtonItem != nil)
{
self.navigationItem.rightBarButtonItem = navItem.rightBarButtonItem;
}
if (navItem.titleView != nil)
{
self.navigationItem.titleView = navItem.titleView;
}
}
This workaround also allows to link bar button items to UINavigationItem using IB.

Resources