How to use NSCollectionView and Outlets properly? - objective-c

I'm desperately trying to connect controls of NSViews which will reside in a NSCollectionView using outlets. The collection view is fed using an NSArrayController.
I created the NSView in a separate NIB file and in the implementation of NSCollectionViewItem I overwrote copyWithZone to load it:
-(id)copyWithZone:(NSZone *)zone
{
id result = [super copyWithZone:zone];
[NSBundle loadNibNamed:#"InputView" owner:result];
return result;
}
I've used this approach according to this instructions.
Unfortunately this is what happening:
The NSView looks like this:
The NSCollectionView resides in a NSScrollView and the scrollbar is set to enable automatically.
But as you can see there's no scrollbar.
I don't really understand what I need to do so the NSCollectionView knows the dimensions of its NSViews.
It has worked before when I didn't have a seperate NIB-file, but then I couldn't make outlet connections from the view to the item :-(

How many item are in the array controller? Your output looks correct for what you've described, assuming there are at least 14 things in the controller (1 view per item). The sizing is just off. It's not clear which problem you're trying to solve.
Perhaps you were looking for a grid, and so need to call setMaximumNumberOfColumns:? Or perhaps your views aren't being resized as you expect (check -maxItemSize and -minItemSize)?

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?

Forwarding drag & drop event to parent view

I have an application where I have one custom view which is derived from NSView.
Within this view, there are several custom subviews, which are also derived from NSView.
I want to implement a drag and drop behavior which allows URLs to be dropped onto the views. Everything is already working for the main view.
So, actually I would have to implement dragging behavior handlers on the child-views and the parent-view class.
The thing is, that I don't want to copy the complete handling code to all the child-views to make them also accept drag events. So I thought that it would be the best way to just let them forward all drag events to the parent view.
Is this possible somehow?? Not sure if I can somehow set this up with the responder-chain maybe?
Any tips are highly appreciated!!
Thanks in advance.
I faced a similar issue where I wanted anything dropped in a view, or any of it's subviews to be processed by the view, but the calls never got there.
After some research I found this answer to be the most helpful: https://stackoverflow.com/a/7389711/327471
Essentially if a view is not registered to receive any drag events it should pass that information up to it's parent view automatically. So in my case I ended up with something like this:
NSArray *subviews = [self.view subviews];
for (NSView *aSubview in subviews) {
[aSubview unregisterDraggedTypes];
}
Of course you can be more precise than that, and make sure to only check subclasses of a certain type or whatever parameters you want. But ultimately the key was unregistering the problem subview from it's dragged types.
I hope this helps.
If the subviews are used for display only and don't require any user interaction, you can override -hitTest: in the parent view like so:
- (NSView *)hitTest:(NSPoint)aPoint
{
NSView* hitView = [super hitTest:aPoint];
if(hitView)
return self;
return nil;
}
This makes the parent view receive all mouse events.
Still works XCode 10.1. Swift 4.2. under 10.14.4 Beta (18E184e).
// MARK: - ViewController lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.view.subviews.forEach { $0.unregisterDraggedTypes() }
}
There's probably a better way, but you could put your dragging protocol implementation in a category, rather than in the view directly, and include that category in each of the views.

How to bind nib custom view to a NSVIew subclass

I have a simple requirement.
On Click of a + button, I am trying to add a custom view to a SplitView.
I have created a class MyCustomView which is a subclass of NSView
In the applications nib file, I have a custom view which contains the buttons etc.
Now How to allocate a new MyCustomView every time ?
Is there an example to do this?
I am hoping something like
MyCustomView *v1 = [[MyCustomView alloc] init];
..
..
[splitView addSubView:v1];
[splitView addSubView:v2];
...
Please help
It's hard to tell exactly what you're describing based on your description but let's see if I understand you. You want to add a "copy" of your custom view assembly into a split view each time "+" is clicked, right?
The absolute best way to do this is to put the custom view assembly that will be copied (the "prototype") in its own xib. For each object you want to represent, you will instantiate a new copy from the xib and give it to some owner then add it to some parent view (a split view in your case ... odd for an unlimited number of views, but I don't have enough detail to say otherwise).
So. In the modern Cocoa world, such a view assembly should likely have its own view controller (NSViewController). This makes things easier for you since the xib's File's Owner will be an instance of your MyCustomViewController, whose -view is connected to the main container view in the xib (your custom view with all its subviews) and whose -representedObject is set to whatever model object your custom view represents. Your app will then maintain a list (an array or a dictionary, perhaps) of all the view controllers for the model objects. See this SO question/answer for a run-down of how to load from nibs/xibs.
This is basically how an NSCollectionView works (though the views must all be the same size - might not work for you). The collection view corresponds to your split view in this case; NSCollectionViewItem corresponds to your MyCustomViewController (and in fact on 10.5 and above NSCollectionViewItem is a subclass of NSViewController); your custom view is the collection view item's main -view. For each model object in its collection, it instantiates an NSCollectionViewItem and loads the view prototype from a xib (ideally, but this is optional), and uses this to set the item's view, then it sets the item's represented object (the model object).
I hope this clarifies things a bit. You've got some reading to do in order to understand enough of the nuts and bolts, but if you're still stuck, you might try editing your question to clarify or opening a new, more specific question.

Custom UISplitViewController?

I want the effect of a UISplitViewController however I am not using the split view template.
Is it relatively easy to achieve without the template? And with using normal UIViewController?
What I want it a customary sized and positioned UITableView which then has a customary sized detail view which then of course goes into a popover and detail view when portrait.
Doing it without Interface Builder, you would create a UIViewController class. In the viewDidLoad method of that class, create a UIView or a UITableView with the frame origin where you want it and a size that you want. (Release it in the viewDidUnload method.) Then set the UIViewController's self.view to point to this new view.
self.view = [[UIView alloc] initWithFrame:...]; // edit - added in response to your question
If you created a UIView, then you will want to put your UITableView inside this new view. (This approach lets you add more items to the container UIView if you need to.)
Make sure your UIViewController adheres to the UITableViewDelegate and UITableViewDataSource protocols. Add the delegate and datasource methods and you should be good to go.
This new view can cover other views, or you can size the other views to fit beside it. You only need to set there frames according to what you want to do with them.
There are some limitations if you use a UITableViewController, so a lot of people recommend using a UIViewController instead. like I described above. You can google for more info on that topic.
Just great a new temporary Xcode-project from that template and judge yourself, if it is complicated for you, to adept your (real) code.
Yes. You can do it quite easily. Just use delegates to pass messages between the left and the right side views (root and detail). For instance the didSelectRowAtIndexPath tableView method could be used along with delegation to pass a message to the right sided detail view. Tap a cell on the left table, show its text as a Label on the right side. Its just a simple example. And yes you can handle the rotations and send left side view into a UIPopoverController as well, thus giving the detail view full screen real estate in Portrait orientation.
Also try MGSplitViewController . It gives you a lot of other customization options on a split view controller.

Is there a way to have varying views in an NSCollectionView?

I am wanting something similar to how iWork has the template selection screen for Pages when you can select different templates, and each view contains different info has difference sizes etc.
I have tried subclassing NSCollectionView and determining which view to display using the newItemForRepresentedObject method (as opposed to using itemPrototype view Interface Builder), but it for some reason doesn't position the views correctly, and it does not show the correct number of views for the number of items present. Here is my code. I was hoping someone may have a better way to do this, or an example of how this is done.
personView and companyView are properties in the subclassed NSCollectionView, that are IBOutlets to views in IB.
-(NSCollectionViewItem *)newItemForRepresentedObject:(id)object{
NSCollectionViewItem *collectionViewItem = [[NSCollectionViewItem alloc] init];
[collectionViewItem setRepresentedObject:object];
if([[object valueForKey:#"company"] boolValue] == YES){
NSView *view = [companyView retain];
[collectionViewItem setView:companyView];
}else{
[collectionViewItem setView:personalView];
}
return collectionViewItem;
}
(It doesn't even seem possible to make an NSCollectionView with differently-sized item views; each size would need to be a multiple or integer divisor of some "main" size, and you'd need to do massive item-checking and -reordering to be sure it's even possible to render them in a grid. Are you sure you're asking the right question?)
Also, I don't see anything like this in iWork: all the views in its template chooser are the same. (Though their NSImageView subviews are of different sizes.) I'd recommend if at all possible using the same view and changing its subviews appropriately. It's easy to, for example, bind text fields' "hidden" property or change the width of an image view. Can't you make a single view that works for both classes, changing itself appropriately depending on the represented object?