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)")
}
}
Related
With Xcode 9.2, I'm working on a macOS project with two different windows.
In one of the windows, I have a view-based tableView with one column, the header of the column is regularly shown. In another window, there is a similar tableView, but the header (which is visible in IB), at runtime is not shown.
I compare the properties of the two tableViews in IB but they are the same. Also, the Header checkbox in the Table View section (which has a role in this UI element, as described in a reply to this question Hiding NSTableView header?) is checked.
What could the problem depend on?
TableView Properties pane:
Interface Builder:
Runtime:
Edited:
class EditTasksController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
self.actionsListTableView.dataSource = self
self.actionsListTableView.delegate = self
actionsListTableView.backgroundColor = NSColor.clear
actionsListTableView.headerView = nil
func tableViewSelectionDidChange(_ notification: Notification) {
...
This line:
actionsListTableView.headerView = nil
Removes the header from the table. Remove that line of code, and the header will be visible.
I have some quite simple doubt regarding optional binding,global variable & wrapping and unwrapping . Since I am new to SWIFT, its very important to understand the tits & bits of their concepts.
1) In Swift if I declare a global variable, I have 2 options either to make it optional or non optional, so let I am having 2-4 or more optional variables . So is it advisable to optional bind all those variables in
viewDidLoad() method// so that I could use them without any problem of unwrapping and fatal error in my program.
2) Let me make myself more clear by the following example- I have 2 VC in my project VC1 & VC2 . VC2 has a text field in which user enters some value and displays it in a tabelview in VC1.
In Vc1
var namevc1 = NSMutableArray?//holds the input of textfield to be passed from VC2.
As you can see, my VC1 is the first view controller that loads when my project runs and I am using an optional variable to populate my tabke vuew that is
'arr'
So when the app runs for the first time its empty . So it might cause a fatal error while using its value in the code. So what is its solution whether to unbind it in the
viewDidLoad()
method or in all total declare an empty NSMutable array type in place of optional type .
Thanks in advance.
I'll start by repeating the my comment from above.
Possibly you've misunderstanding the concept of global variables in Swift.
If you have a global variable, you won't have to "pass" it between any views/methods/classes etc, because the variable is defined at global scope (accessible everywhere).
Generally global variables is not a good idea, and something that you want to avoid.
Regarding the matter of global variables and swift, you really should include singletons into the discussion. See e.g. the following existing SO thread(s):
Any reason not use use a singleton "variable" in Swift?
(How to create a global variable?)
(Declaring Global Variables in Swift)
Communication between TableViewController and ViewController by means of segues (prepare for & unwind segues)
(This answer ended up being very and probably a bit too thorough, as I didn't know in detail what your current tableview/viewcontroller program state looks like. Sorry for the lengthy answer and any inconvenience it might bring to readers of it).
Now, lets leave global variables and discuss one (among other) viable options for the communication between the two controllers in your example. From your question, I'll summarize your example as follows
VC1: storyboard entry point, a UITableViewController consisting of UITableViewCells, where, in these cells, you display some text, say, via instances of UILabel.
VC2: a UIViewController, accessible from the cells of VC1, containing an UITextField instance. When user enters text into this text field, your want the text to be displayed in the associated cell in VC2 (associated in the sense that it was the cell in VC1 that was used to access VC2).
We'll associate VC1 and VC2 with (cocoa touch) classes TableViewController (TableViewController.swift) and ViewController (ViewController.swift), respectively. The cells in the table view controller will be associated with (cocoa touch) class TableViewCell (TableViewCell.swift). Details for these classes follow below.
For this simple example, note that we will not embed VC1 into a navigation controller (which is otherwise appropriate for table view -> view navigation).
We'll start in the storyboard, adding objects (drag-and-drop from object library) for a Table View Controller and a View Controller. The table view container will also, automatically, contain, in its Table View, a TableViewCell. Continuing in the storyboard:
Add a UILabel object to the TableViewCell container in the Table View Controller (align it as you wish)
In the View Controller, add a Text Field object and a Button object (align them as you wish).
Set the entry point to the Table View Controller.
Thereafter Ctrl-drag a 'Show' segue from the TableViewCell to the View Controller.
Select the Show segue and, from the Attributes inspector, enter an identifier for it, say, ShowDetail.
Finally, with the TableViewCell selected, (as above; from the attribute inspector), enter an identifier for the cell. Here, we'll use simply use identifier TableViewCell.
We now leave the storyboard for now and implement three classes, associated with the Table View Controller, the View Controller and the formers' TableViewCell.
We start with the Table View Controller, and implement our UITableViewController sub-class. Note that here, instead of using an NSMutableArray to hold the texts of the UITextLabel in each cell, we'll simply use a String array.
// TableViewController.swift
Import UIKit
class TableViewController: UITableViewController {
// Properties
var userTextLabels = [String]()
var numberOfCells: Int?
override func viewDidLoad() {
super.viewDidLoad()
numberOfCells = loadSampleTextLabels() // Load sample labels.
}
func loadSampleTextLabels() -> Int {
userTextLabels += ["Label #1", "Label #2", "Label #3"]
return userTextLabels.count
}
// func numberOfSectionsInTableView(tableView: UITableView) ...
// func tableView(tableView: UITableView, numberOfRowsInSection section: Int) ...
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = ("TableViewCell")
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! TableViewCell
// Text for current cell
let cellText = userTextLabels[indexPath.row]
cell.userSuppliedTextLabel.text = cellText
return cell
}
// ... communication?
}
Where the two commented out methods are standard methods used in any UITableViewController, for number of sections (e.g. return 1) and cells (e.g. return (numberOfCells ?? 0)) in the table, respectively. I'll leave fixing these to you.
Now, we associate the TableViewCell object(s) in the table view with instances of a subclass to UITableViewCell. Here, we'll use a very simple class for our cells; each cell just containing a single UILabel instance (created via storyboard Ctrl-drag as an #IBOutlet from the UILabel in the table view cells).
// TableViewCell.swift
import UIKit
class TableViewCell: UITableViewCell {
// Properties
#IBOutlet weak var userSuppliedTextLabel: UILabel!
// Ctrl-drag from UILabel (in TableViewCell) in storyboard
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
Finally, for the view controller that is accessed from the table view cells: use a single #IBOutlet to the UITextField used for user text input, and handle events in this text field using the pre-existing UITextFieldDelegate. E.g.:
// ViewController.swift
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
// Properties
#IBOutlet weak var userSuppliedText: UITextField!
// Ctrl-drag from storyboard...
var cellText: String?
override func viewDidLoad() {
super.viewDidLoad()
userSuppliedText.text = cellText ?? "..."
// Handle the user input in the text field through delegate callbacks
userSuppliedText.delegate = self
}
// UITextFieldDelegate
func textFieldShouldReturn(textField: UITextField) -> Bool {
// User finished typing (hit return): hide the keyboard.
textField.resignFirstResponder()
return true
}
func textFieldDidEndEditing(textField: UITextField) {
cellText = textField.text
}
}
We've also declared a string property (cellText) here, that will as act as container for communication between VC1 and VC2.
We return to the storyboard and---from the Identity inspector---associate the three storyboard objects (Table View Controller, View Controller, TableViewCell) with their associated classes that we've just written above.
We're now almost at our goal; it only remains to specify how to communicate between the two controllers.
We'll begin with communication from VC1 to VC2. In your comment above, you were on the right track (for this specific solution, anyway) by looking at the prepareForSegue(...) method. In the class for the Table View Controller, we add the following method:
// ... add to TableViewController.swift
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
if segue.identifier == "ShowDetail" {
let viewController = segue.destinationViewController as! ViewController
if let selectedCell = sender as? TableViewCell {
let indexPath = tableView.indexPathForCell(selectedCell)!
let currentTextInCell = userTextLabels[indexPath.row]
viewController.cellText = currentTextInCell // <-- note this
}
}
}
Hence, for VC1->VC2 communication, we can (in this example) bring whatever existing text that is currently occupying the UILabel in the sender cell (as is specified by the String array userTextLabels). Look at the viewDidLoad(...) method in the ViewController.swift to see how this value is passed from VC1 and set as default text in the UITextField in VC2.
Now, for communication VC2->VC1, which was the specific communication direction you were asking about, add another method (programmatically), again to TableViewController.swift:
// ... add to TableViewController.swift
#IBAction func unwindToTableView(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? ViewController,
text = sourceViewController.cellText {
// ^ note 2nd clause of if let statement above
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update cell text
userTextLabels[selectedIndexPath.row] = text
tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
}
}
}
Here, we define an unwind action that, when triggered, retrieves the cellText property of the view controller that was the source of the segue, i.e., in our case, the instance of ViewController. But how do we trigger this action?
Return to the storyboard and the View Controller. Note the three little icons in the top of the View Controller object, more specifically, the right-most of these, named Exit. Ctrl-drag an action from your Button to the Exit icon, and select the unwindToTableView Action Segue. When you click your button the view controller, the view unwind (exit) and land at the unwindToTableView method in the TableViewController.
The resulting app should look something like this:
This was way longer than I had expected, but once you get started writing... Anyway, the method above uses, naturally, no global variables, but make use of references to future (prepareForSegue) or historic (unwindToTableView) views to get (generally from current or historic view) or set (generally in current of future view) values by using these references (to future/historic view).
Apple has their own very thorough tutorial on an example app in the tableviewcontroller/viewcontroller context that I would recommend going over. I found it very valuable myself when I started coding Swift.
Start Developing iOS Apps (Swift)
I've got a view-based NSTableView, using Cocoa Bindings to change the values of some labels and images in the cell. It all works great. However, I want to add a button to the cell. I've got the button working, but its action method only has the button as sender, which means I have no idea of the content of the cell that the button is in. Somehow I need to store some extra data on the button - at the very least the row index that the button is in. I subclassed NSButton and used my subclass in the cell, but Interface Builder doesn't know about the extra property so I can't bind to it. If I wanted to bind it in code, I don't know the name of the object or keypath that would be passed to it.
How can I get this to work?
You can use rowForView in your action method to get the row value
- (IBAction)doSomething:(id)sender
{
NSInteger row = [_myTableView rowForView:sender];
}
You can use the Identity field in Interface Builder to associate a table cell view from the nib with an instance in your code:
Additionally you have to implement - tableView:viewForTableColumn:row: in your table view's delegate. (Don't forget to connect the delegate in IB)
- (NSView*)tableView:(NSTableView*)tableView viewForTableColumn:
(NSTableColumn*)tableColumn row:(NSInteger)row
{
SSWButtonTableCellView *result = [tableView makeViewWithIdentifier:#"ButtonView" owner:self];
result.button.title = [self.names objectAtIndex:row][#"name"];
result.representedObject = [self.names objectAtIndex:row];
return result;
}
I added representedObject property in my NSTableCellView subclass, which I set in the above table view delegate method.
Your custom table cell view can later use that object in it's action. e.g.:
- (IBAction)doSomething:(id)sender
{
NSLog(#"Represented Object:%#", self.representedObject);
}
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
}
}
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")
}
}