How can I disable the animations in an NSCollectionView - cocoa

I would like to turn off the 'shuffle' animations that happen when you resize an NSCollectionView. Is this possible?

This works, but it's setting a private instance variable so it may no be ok in the Mac App Store.
[collectionView setValue:#(0) forKey:#"_animationDuration"];

kainjow is correct. adding this:
- (id) animationForKey:(NSString *) key
{
return nil;
}
to the prototype view subclass (not the collection view!) disables animations

For 10.6, I was able to disable the animation by subclassing NSView, overriding animationForKey: and returning nil. Then make sure you use that view for the prototype's view.

To disable all the collection view's animations in Swift, do this just before the something animatable happens:
NSAnimationContext.current.duration = 0

I was only able to get this to work if I did the following:
1) Subclass the view that the NSCollectionViewItem used as its view. That subclassed view required a CALayer and I set the view subclass as the delegate of the CALayer.
2) Implement the CALayer delegate method so no animation actions should occur:
override func actionForLayer(layer: CALayer, forKey event: String) -> CAAction? {
return NSNull()
}
3) Finally, in the NSCollectionView data source method:
func collectionView(collectionView: NSCollectionView, itemForRepresentedObjectAtIndexPath indexPath: NSIndexPath) -> NSCollectionViewItem {
// get a new collection view item
....
// disable animations
CATransaction.begin()
CATransaction.setDisableActions(true)
// populate your cell
....
CATransaction.commit()
}

Related

Custom NSTextFieldCell draws text differently

I have view-based NSTableView with 2 text columns.
First column uses custom NSTextFieldCell. Second column uses the default cell.
Here's the custom cell code:
class CustomTextFieldCell: NSTextFieldCell {
// don't do anything, just call the super implementation
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
super.drawInterior(withFrame: cellFrame, in: controlView)
}
}
Somehow the text fields in the custom (first) column look differently (thinner?).
Here's the screenshot (Sierra).
Not sure what's going on, may be custom implementation uses different antialiasing settings.
What am I missing there? Thank you in advance.
edit: mentioned view-based NSTableView
Looks like that bug still exists today. Overriding NSTextField.draw(_ dirtyRect: NSRect) or NSTextFieldCell.draw(withFrame cellFrame: NSRect, in controlView: NSView) and simply calling super is enough to reproduce this behavior.
As a workaround, set textField.drawsBackground = false, place your text field into an NSBox or custom NSView and handle your drawing there.

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);
}

View based NSTableView editing

I'm not sure what I'm doing wrong. Since I couldn't find any other questions (or even documentation) about this, it seems do be normally working without problems for other people.
I'm simply trying to get a view based NSTableView to do support editing of it's content. I.e. the app displays a NSTableView with one column and several rows, containing a NSTextField with some content. I want to be able to (double) click on a cell and edit the cell's content. So basically the normal behavior of a cell based NSTableView where the tableView:setObjectValue:forTableColumn:row: method is implemented.
I analyzed the Complex TableView example in the TableViewPlayground sample code from Apple (which is supporting editing of cell content), but I cannot find the setting/code/switch which is enabling the editing.
Here's a simple sample project (Xcode 6.1.1, SDK 10.10, storyboard based):
Header:
#import <Cocoa/Cocoa.h>
#interface ViewController : NSViewController
#property (weak) IBOutlet NSTableView *tableView;
#end
Implementation:
#import "ViewController.h"
#implementation ViewController
{
NSMutableArray* _content;
}
- (void)viewDidLoad {
[super viewDidLoad];
_content = [NSMutableArray array];
for(NSInteger i = 0; i<10; i++) {
[_content addObject:[NSString stringWithFormat:#"Item %ld", i]];
}
}
#pragma mark - NSTableViewDataSource
- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
{
return _content.count;
}
#pragma mark - NSTableViewDelegate
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSTableCellView* cell = [tableView makeViewWithIdentifier:#"CellView" owner:self];
cell.textField.stringValue = _content[row];
return cell;
}
- (IBAction)endEditingText:(id)sender {
NSInteger row = [_tableView rowForView:sender];
if (row != -1) {
_content[row] = [sender stringValue];
}
}
#end
The storyboard file looks like this:
The datasource and delegate of the table view are set to the view controller.
When running this app, the table view displays the 10 test rows, but it is not possible to edit one of the rows.
Why is that? What did I miss here?
I double checked all attributes of the NSTableView (and it's contents) to be the same as in the TableViewPlayground sample from Apple. And after several hours of searching the documentation and internet for helpful hints without any success, I'm kind of frustrated. All you can find on view based NSTableViews are non-editable samples or very vague information on editable content. And of course, there is tons of information, documentation and samples on editable, cell based NSTableViews...
A zip with my sample project is downloadable here:
TableTest.zip
Even though all the pieces for editing a view based NSTableView are present in the question and answer, I still had trouble putting it all together. The following demo is in Swift, using Xcode 6.3.2, but it should be easy to follow for the objective-C cavemen/womens. A full code listing is at the end.
Let's start here:
NSTableViewDataSource Protocol Reference
Setting Values
- tableView:setObjectValue:forTableColumn:row:
Swift:
optional func tableView(_ aTableView: NSTableView,
setObjectValue anObject: AnyObject?,
forTableColumn aTableColumn: NSTableColumn?,
row rowIndex: Int)
Objective-C:
- (void)tableView:(NSTableView *)aTableView
setObjectValue:(id)anObject
forTableColumn:(NSTableColumn *)aTableColumn
row:(NSInteger)rowIndex
Discussion: This method is intended for use with cell-based table views, it must not be used with view-based table views. In
view-based tables, use target/action to set each item in the view
cell.
If you're like me, you searched through the NSTableViewDelegate and NSTableViewDataSource protocols looking for some kind of edit method to use. However, the Discussion in the quote above is telling you that things are much simpler.
1) If you look in the document outline for your TableView in Xcode, you'll see something like this:
A cell in the TableView is represented by a Table Cell View. The cell contains a few items, and by default one of the items is an NSTextField. But where's the NSTextField in the document outline?! Well, the controls in the document outline have an icon that looks like a slider next to their names. Take a look. Inside the cell, you'll see something which has a slider icon next to it. And, if you select that line in the document outline, then open the Identity Inspector, you'll see that it's an NSTextField:
You can consider that just a regular old NSTextField.
When you implement your NSTableViewDataSource protocol methods:
import Cocoa
class MainWindowController: NSWindowController,
NSTableViewDataSource {
...
...
var items: [String] = [] //The data source: an array of String's
...
...
// MARK: NSTableViewDataSource protocol methods
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return items.count
}
func tableView(tableView: NSTableView,
objectValueForTableColumn tableColumn: NSTableColumn?,
row: Int) -> AnyObject? {
return items[row]
}
}
..the TableView takes the value returned by the second method and assigns it to a property named objectValue in the outer Table Cell View--in other words the TableView does not use the returned value to set the NSTextField (i.e. the inner Table View Cell). That means your data source items will not be displayed in the TableView because the NSTextField is what displays an item. In order to set the value of the NSTextField, you need to connect, or bind, the NSTextField's value to the objectValue property. You do that in the Bindings Inspector:
Warning: Make sure you don't check the Bind to checkbox until after you select the object you want to bind to. If you check the
checkbox first, an object will be inserted into your document outline,
and if you don't notice it, you will get errors when you run your
program. If you accidentally check the Bind to checkbox first, make
sure you delete the automatically added object in your document
outline. Xcode creates a separate section for the added object, so it is easy to spot in your document outline.
2) Backtracking for a moment, you are probably familiar with connecting a button to an action method, and thereafter if you click on the button the action method will execute. On the other hand, with an NSTextField you typically declare an IBOutlet, which you then use to get or set the NSTextField's stringValue.
However, an NSTextField can also trigger the execution of an action method. Wha??! But you can't click on an NSTextfield like you can a button! Nevertheless, an NSTextField has a trigger, just like a button click, which will cause the execution of an action method, and the trigger is: done editing the NSTextField. How does the NSTextField know when you are done editing it? There are two ways:
You hit Return.
You click on some other control.
You can choose the trigger in the Attributes Inspector:
3) As #Paul Patterson showed in his answer, the next thing you need to do is set the NSTextField's Behavior to Editable in the Attributes Inspector.
4) Then connect the NSTextField to the action method that you want to execute. If you haven't used the following trick to connect a control to an action method, you should try it some time:
Select your .xib file in the Project Navigator, so that the window and
its controls are displayed. Then click on the Assistant Editor(the
two_interlocking_rings icon at the top of the Xcode window on the far right)--that will display your Controller file(if some other file is shown, then use the jump bar to navigate to your Controller file). Then Control+drag
from the NSTextField (in the document outline) to the spot in your
Controller file where you want to create your action method:
When you release, you'll see this popup window:
If you enter the same information as shown, the following code will be entered in the file:
#IBAction func onEnterInTextField(sender: NSTextField) {
}
And...the connection between the NSTextField and the action method will already have been made. (You also can use those steps to create and connect an IBOutlet.)
5) Inside the action method, you can get the currently selected row, i.e. the one that has just been edited, from the TableView:
#IBAction func onEnterInTextField(sender: NSTextField) {
let selectedRowNumber = tableView.selectedRow //tableView is an IBOutlet connected to the NSTableView
}
Then I got stumped by how to get the text of the selected row in a TableView, and back to the docs I scrambled looking through the TableView and protocol methods. But, we all know how to get the stringValue of an NSTextField, right? The sender is the NSTextField you were editing:
#IBAction func onEnterInTextField(sender: NSTextField) {
let selectedRowNumber = tableView.selectedRow //My Controller has an IBOutlet property named tableView which is connected to the TableView
if selectedRowNumber != -1 { //-1 is returned when no row is selected in the TableView
items[selectedRowNumber] = sender.stringValue //items is the data source, which is an array of Strings to be displayed in the TableView
}
}
If you don't insert the new value in the data source, then the next time the TableView needs to display the rows, the original value will get displayed again, overwriting the edited changes. Remember, the TableView retrieves the values from the data source--not the NSTextField. Then the NSTextField displays whatever value the TableView assigned to the cell's objectValue property.
One last thing: I got a warning that said I couldn't connect the NSTextField to an action inside a class if the class wasn't a delegate of the TableView....so I connected the TableView's delegate outlet to File's Owner:
I had previously set the File's Owner to be my Controller(=MainWindowController), so after I made that connection, the MainWindowController--which contained the action method for the NSTextField--became the delegate of the TableView, and the warning went away.
Random tips:
1) I found the easiest way to start editing an NSTextField is to select a row in the TableView, then hit Return.
2) NSTableView's come with two columns by default. If you select one of the columns in the document outline, then hit Delete on your keyboard, you can make a one column table--however the TableView still shows the column divider, so it looks like there are still two columns. To get rid of the column divider, select the Bordered Scroll View - Table View in the document outline, then drag one of the corners to resize the TableView--the single column will instantly resize itself to take up all the available space.
Credit for steps #1 and #2, and Random tip #2: Cocoa Programming For OS X (5th Edition, 2015).
Full code listing:
//
// MainWindowController.swift
// ToDo
//
//import Foundation
import Cocoa
class MainWindowController: NSWindowController,
NSTableViewDataSource {
//#IBOutlet var window: NSWindow? -- inherited from NSWindowController
#IBOutlet weak var textField: NSTextField!
#IBOutlet weak var tableView: NSTableView!
var items: [String] = [] //The data source: an array of String's
override var windowNibName: String {
return "MainWindow"
}
#IBAction func onclickAddButton(sender: NSButton) {
items.append(textField.stringValue)
tableView.reloadData() //Displays the new item in the TableView
}
#IBAction func onEnterInTextField(sender: NSTextField) {
let selectedRowNumber = tableView.selectedRow
if selectedRowNumber != -1 {
items[selectedRowNumber] = sender.stringValue
}
}
// MARK: NSTableViewDataSource protocol methods
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return items.count
}
func tableView(tableView: NSTableView,
objectValueForTableColumn tableColumn: NSTableColumn?,
row: Int) -> AnyObject? {
return items[row]
}
}
The Connections Inspector showing all the connections for File's Owner (= MainWindowController):
By default, each cell (instance of NSTableCellView) has an NSTextField sitting on it. When you're editing the cell, what you're in fact editing is this text field. Interface Builder makes this text-field non-editable:
All you need to do is set the Behaviour pop-up to Editable. Now you can edit the text-field with a return hit or a single-click.
Just a correction to the accepted answer - you should get the row and column using the tableView.row(for: NSView) and tableView.column(for: NSView. Other methods may not be reliable.
#IBAction func textEdited(_ sender: Any) {
if let textField = sender as? NSTextField {
let row = self.tableView.row(for: sender as! NSView)
let col = self.tableView.column(for: sender as! NSView)
self.data[row][col] = textField.stringValue
print("\(row), \(col), \(textField.stringValue)")
print("\(data)")
}
}

preferredContentSize does not shrink

I use UIPopoverController to popover a window. I use preferredContentSize to set the size. Then, I use Push a new view controller with larger size. When the child view pop up, I like to recover the window to original size. But seems not work.
I already put the self.preferredContentSize = myWindowSize; But it seems can not recover.
In iOS6 or before, I set the contentSizeForViewInPopover to CGSizeZero, then set the proper size. But on iOS7, it will make the popup disappear.
This solved it for me:
override func viewDidAppear(animated: Bool)
{
navigationController?.preferredContentSize = preferredContentSize
super.viewDidAppear(animated)
}
The only solution I can think of is to dismiss your popover window and automatically present it again with the new size you want. I'm also having trouble for iOS 8 where the content size won't shrink in height. The width shrinks, but the height can only increase for me.
Have a look at
https://developer.apple.com/library/ios/documentation/uikit/reference/UIPopoverControllerDelegate_protocol/Reference/Reference.html#//apple_ref/occ/intfm/UIPopoverControllerDelegate/popoverController:willRepositionPopoverToRect:inView:
It might help you reposition the rect with the delegate protocol
Solution:
Ok, here's my solution. From the view controller where you present the popover controller, you can set up a delegate called something like "resetContentSize:(CGSize)size". e.g. in your presenting view controller's header file:
#protocol PopoverDelegate <NSObject>
- (void)resetContentSize:(CGSize)size
#end
in the implementation file where you present the popover controller, you can set the delegate
self.popoverController.delegate = self;
From your popover controller, you can add the PopoverDelegate in the header file
#property (nonatomic, weak) id<PopoverDelegate> popoverDelegate;
and call the delegate method in your implementation file:
[popoverDelegate resetContentSize:CGSizeMake(320.0f,205.0f)];
Had the same issue. The solution for me was to set preferredContentSize on the navigationController, instead of self.
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.preferredContentSize = myControllerSize
}

Accessing controls of views of NSCollectionView

I'm using an NSCollectionView to display various objects. The whole things works rather well, except for one annoying thing. I cannot figure out how to access the various controls on the view used to represent each object in the collection.
Here's the setup:
I have dragged an NSCollectionView into my view in IB.
I made a custom subclass of NSCollectionViewItem. Mapped my class in IB.
I made a custom subclass of NSBox to act as the view for each object in the collection. Also mapped this class in IB and connected it to the view property of my NSCollectionViewItem subclass.
I made all the bindings in IB to display the correct information for each object.
The view:
The resulting collection view:
Reasoning that that my subclass of NSCollectionViewItem is basically a controller for each view in the collection, I made referencing outlets of the various controls in the view in my controller subclass:
#interface SourceCollectionViewItem : NSCollectionViewItem
#property (weak) IBOutlet NSTextField *nameTextField;
#property (weak) IBOutlet NSTextField *typeTextField;
#property (weak) IBOutlet RSLabelView *labelView;
#property (weak) IBOutlet NSButton *viewButton;
#end
When I inspect any instance of SourceCollectionViewItem in the debugger, all the properties show up as nil despite the fact that I can actually see them on my screen and that everything is displayed as it should be.
My setup was inspired by Apple's sample app IconCollection.
I am obviously missing something. What?
EDIT: I found various posts hinting at a similar issue:
CocoaBuilder.com and this question on SO.
EDIT: Just to be complete: this post deals with the subject as well and delivers a solution based on a combination of the options mentioned in the accepted answer.
Outlets are set during nib loading, and only the prototype item is loaded from nib and has its outlets assigned. All other ViewItems and their Views are cloned from the prototype, in that case outlets are just instance variables that are never initialized.
Here are the options I could come up with:
Override newItemForRepresentedObject: of collection view and reload nib instead of cloning the prototype. But this will probably hurt the performance greatly.
Override copyWithZone of collection view item and assign outlets manually using viewWithTag: to find them.
Give up and try to provide data via bindings only.
I found that overriding NSCollectionViewItem's -setRepresentedObject: could also be a good choice, as it is called on the new Item when all IBOutlet seem to be ready. After the call to super you can do whatever is needed:
- (void)setRepresentedObject:(id)representedObject
{
if (representedObject) {
[super setRepresentedObject:representedObject];
[self.anOutlet bind:#"property" toObject:self.representedObject withKeyPath:#"representeProperty" options:nil];
}
}
I used this method to bind a custom property of an interface object. The check is there to avoid useless calls, when the representedObject is not yet ready. The project uses a separate xib for the ViewItem, as explained in the links in the original edits.
Great question. Like #hamstergene suggests, you can use copyWithZone, it will be much more efficient compared to newItemForRepresentedObject. However viewWithTag is not always an option, first, because not everything can be tagged (easily), and, second, using tag for this purpose is a little wrong. Here's a cool approach with performance in mind, in Swift.
import AppKit
class MyViewController: NSCollectionItemView
{
// Here you are cloning the original item loaded from the storyboard, which has
// outlets available, but as you've seen the default implementation doesn't take
// care of them. Each view has a unique identifiers, which you can use to find it
// in sublayers. What's really cool about this, is that you don't need to assign
// any tags or do anything else while having advantage of better performance using
// cached nib object.
override func copyWithZone(zone: NSZone) -> AnyObject {
let copy: NSCollectionItemView = super.copyWithZone(zone) as! NSCollectionItemView
let oldView: RecordingView = self.view as! MyView
let newView: RecordingView = copy.view as! MyView
newView.foo = newView.viewWithIdentifier(oldView.foo.identifier!) as! NSTextfield
newView.bar = newView.viewWithIdentifier(oldView.bar.identifier!) as! NSImageView
return copy
}
}
#IBDesignable class MyView: View
{
// Custom collection view item view. Lets assume inside of it you have two subviews which you want
// to access in your code.
#IBOutlet weak var foo: NSTextfield!
#IBOutlet weak var bar: NSImageView!
}
extension NSView
{
// Similar to viewWithTag, finds views with the given identifier.
func viewWithIdentifier(identifier: String) -> NSView? {
for subview in self.subviews {
if subview.identifier == identifier {
return subview
} else if subview.subviews.count > 0, let subview: NSView = subview.viewWithIdentifier(identifier) {
return subview
}
}
return nil
}
}

Resources