NSOutlineView - Get NSTableCellView for a Given Item - macos

My view-based outline view displays a custom context menu (right-click menu) for its rows.
One of the menu items on the menu is "Rename...", and the menu item's representedObject property is set to the object represented by the outline view row:
let menu = NSMenu()
// ...other menu items...
let renameItem = NSMenuItem(
title: "Rename...",
action: #selector(OutlineViewController.rename(_:)),
keyEquivalent: "")
renameItem.representedObject = object
menu.addItem(renameItem)
On the action side, I want to make the text field in the table cell editable, programmatically. The problem is, I am not sure of how to get a reference to the table cell from the represented object alone.
This is my action method:
#IBAction func rename(_ sender: Any) {
guard let menuItem = sender as? NSMenuItem else { return }
guard let item = menuItem.representedObject else { return }
I can get the row for the represented object (Int):
let row = outlineView.row(forItem: item)
...and the row view (NSTableRowView):
let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false)
I can get the column index (Int) and column (NSTableColumn):
let columnIndex = outlineView.column(withIdentifier: "TitleColumn")
let column = outlineView.tableColumns[columnIndex]
...and attempt to get the cell view (NSTableCellView):
guard let cell = outlineView(outlineView, viewFor: column, item: item) as? NSTableCellView else {
return
}
Finally, I try to make the text field editable:
guard let textField = cell.textField else {
return
}
textField.becomeFirstResponder()
All these test pass (I set breakpoints), but nothing happens: The text field does not become editable (unlike when double clicking).
What am I doing wrong?
Edit I have realized that when I call:
outlineView(outlineView, viewFor: column, item: item)
This is basically the NSOutlineViewDelegate method that my view controller is implementing, so instead of giving me the cell already on screen, it is creating a new copy on demand. Kind of like calling
UITableViewController.tableView(_:,cellForRowAt:)
// Data source method, creates or dequeues a new cell to pass
// back to the table for display
...instead of calling:
UITableView.cellForRowAt(_:)
// Method of table view proper; returns existing cell if
// already on screen, or defers to the data source (method
// above) otherwise
So instead of calling a delegate method that creates the cell view anew, I should query the outline view itself. However, the only meaningful method I can see is:
func rowView(atRow row: Int, makeIfNecessary: Bool) -> NSTableRowView?
...which returns an NSTableRowView...

Solved it. Just had to dig a little deeper into the API:
// Get row index
let row = outlineView.row(forItem: item)
// Get view for the whole row:
guard let rowView = outlineView.rowView(atRow: row, makeIfNecessary: false) else {
return
}
// THIS is the missing piece: Get row subview for the given column
// (= cell)
guard let cell = rowView?.view(atColumn: 0) as? NSTableCellView else {
return
}
Now I have a reference to the actual text field already on screen, and it becomes editable:
self.view.window?.makeFirstResponder(cell.textField)

Related

Is there a way to force reloadData on a UICollectionView using RxSwift?

I have a UICollectionView bound to an array of entities using BehaviorSubject and all is fine, data is loaded from the network and displayed correctly.
The problem is, based on user action, I'd like to change the CellType used by the UICollectionView and force the collection to re-create all cells, how do I do that?
My bind code looks like:
self.dataSource.bind(to: self.collectionView!.rx.items) {
view, row, data in
let indexPath = IndexPath(row: row, section: 0)
var ret: UICollectionViewCell? = nil
if (self.currentReuseIdentifier == reuseIdentifierA) {
// Dequeue cell type A and bind it to model
ret = cell
} else {
// Dequeue cell type B and bind it to model
ret = cell
}
return ret!
}.disposed(by: disposeBag)
The general way to solve problems in Rx is to think of what you want the output effect to be and what input effects can affect it.
In your case, the output effect is the display of the table view. You have identified two input effects "data is loaded from the network" and "user action". In order to make your observable chain work properly, you will have to combine your two input effects in some way to get the behavior you want. I can't say how that combination should take place without more information, but here is an article explaining most of the combining operators available: https://medium.com/#danielt1263/recipes-for-combining-observables-in-rxswift-ec4f8157265f
As a workaround, you can emit an empty list then an actual data to force the collectionView to reload like so:
dataSource.onNext([])
dataSource.onNext([1,2,3])
I think you can use different data type to create cell
import Foundation
import RxDataSources
enum SettingsSection {
case setting(title: String, items: [SettingsSectionItem])
}
enum SettingsSectionItem {
case bannerItem(viewModel: SettingSwitchCellViewModel)
case nightModeItem(viewModel: SettingSwitchCellViewModel)
case themeItem(viewModel: SettingCellViewModel)
case languageItem(viewModel: SettingCellViewModel)
case contactsItem(viewModel: SettingCellViewModel)
case removeCacheItem(viewModel: SettingCellViewModel)
case acknowledgementsItem(viewModel: SettingCellViewModel)
case whatsNewItem(viewModel: SettingCellViewModel)
case logoutItem(viewModel: SettingCellViewModel)
}
extension SettingsSection: SectionModelType {
typealias Item = SettingsSectionItem
var title: String {
switch self {
case .setting(let title, _): return title
}
}
var items: [SettingsSectionItem] {
switch self {
case .setting(_, let items): return items.map {$0}
}
}
init(original: SettingsSection, items: [Item]) {
switch original {
case .setting(let title, let items): self = .setting(title: title, items: items)
}
}
}
let dataSource = RxTableViewSectionedReloadDataSource<SettingsSection>(configureCell: { dataSource, tableView, indexPath, item in
switch item {
case .bannerItem(let viewModel),
.nightModeItem(let viewModel):
let cell = (tableView.dequeueReusableCell(withIdentifier: switchReuseIdentifier, for: indexPath) as? SettingSwitchCell)!
cell.bind(to: viewModel)
return cell
case .themeItem(let viewModel),
.languageItem(let viewModel),
.contactsItem(let viewModel),
.removeCacheItem(let viewModel),
.acknowledgementsItem(let viewModel),
.whatsNewItem(let viewModel),
.logoutItem(let viewModel):
let cell = (tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as? SettingCell)!
cell.bind(to: viewModel)
return cell
}
}, titleForHeaderInSection: { dataSource, index in
let section = dataSource[index]
return section.title
})
output.items.asObservable()
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: rx.disposeBag)
RxDataSources
swiftHub

Select and return selection of multiple rows in a single column Swift OSX table [duplicate]

I have a NSTableView with one column. I would like to print the row number of the row that the user has clicked on. I am not sure where I should start with this. Is there a method for this?
You can use the selectedRowIndexes property from the tableView in the tableViewSelectionDidChange method in your NSTableView delegate.
In this example, the tableView allows multiple selection.
Swift 3
func tableViewSelectionDidChange(_ notification: Notification) {
if let myTable = notification.object as? NSTableView {
// we create an [Int] array from the index set
let selected = myTable.selectedRowIndexes.map { Int($0) }
print(selected)
}
}
Swift 2
func tableViewSelectionDidChange(notification: NSNotification) {
var mySelectedRows = [Int]()
let myTableViewFromNotification = notification.object as! NSTableView
let indexes = myTableViewFromNotification.selectedRowIndexes
// we iterate over the indexes using `.indexGreaterThanIndex`
var index = indexes.firstIndex
while index != NSNotFound {
mySelectedRows.append(index)
index = indexes.indexGreaterThanIndex(index)
}
print(mySelectedRows)
}
Use -selectedRowIndexes
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSTableView_Class/#//apple_ref/occ/instp/NSTableView/selectedRowIndexes
Then you can use those indexes to grab the data from your dataSource
(typically an array)

How to get custom table cell views into NSTableView?

I have a NSTableView that uses mostly standard NSTextTableCellViews but I want some other cells that contain another UI component in my table in one or two rows. The question is: Where do I define those custom cells so that Cocoa finds them?
I just dropped a custom NSTableCellView into my XIB (same XIB in that the table is) and gave it an identifier. But then using this code it will obviously not find the cell ...
func tableView(tableView:NSTableView, viewForTableColumn tableColumn:NSTableColumn?, row:Int) -> NSView?
{
var view:NSTableCellView?;
if (tableColumn?.identifier == "labelColumn")
{
view = tableView.makeViewWithIdentifier("labelCell", owner: nil) as? NSTableCellView;
view!.textField?.stringValue = _formLabels[row];
}
else if (tableColumn?.identifier == "valueColumn")
{
if (row == 1)
{
view = tableView.makeViewWithIdentifier("customCell", owner: nil) as? NSTableCellView;
println("\(view)"); // nil - Not found!
}
else
{
view = tableView.makeViewWithIdentifier("valueCell", owner: nil) as? NSTableCellView;
view!.textField?.stringValue = _formValues[row];
}
}
return view;
}
All works but the cell with id customCell will not be found. How do I tell Cocoa to find it?
You need to drop the new cell view in the table column that would contain that kind of cell view (the one whose identifier is "valueColumn", in this case).

Edit data in the model according to the user's input in view-based NSTableView

I have a view-based NSTableView where all the cells are editable. I need to refresh the data from the model every time the user modifies a textField from the view.
All the doc I find is related to the cell-based NSTableView.
Does anyone have a clue about this?
EDIT:
I'm using data source to populate this NSTableView.
This is the code of the Controller of the NSTableView
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
#IBOutlet var globalView: NSView!
#IBOutlet var songsTableView: NSTableView!
var tableContents = NSMutableArray()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
for (song) in songManager.songs {
var obj = Dictionary<String,String>()
obj["title"] = song.title
obj["artist"] = song.artist
tableContents.addObject(obj)
}
songsTableView.reloadData()
}
func numberOfRowsInTableView(tableView: NSTableView) -> Int {
return tableContents.count
}
func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView?{
var obj = tableContents[row] as Dictionary<String,String>
let column = tableColumn?.identifier
var cellView = tableView.makeViewWithIdentifier(column!, owner: self) as NSTableCellView
if column == "title" {
cellView.textField?.stringValue = obj["title"]!
}
if column == "artist" {
cellView.textField?.stringValue = obj["artist"]!
}
cellView.textField?.editable = true
return cellView
}
}
And this is the code of the class that manages the data.
var songManager = SongManager()
struct song {
var title = "No name"
var artist = "No artist"
}
class SongManager: NSObject {
var songs = [song]()
func addSong(title: String, artist: String) {
songs.append(song(title: title, artist: artist))
}
}
I have not touched the row that the storyboard creates by default, so I guess it contains a single NSTextField.
I get to display the data, but cannot detect when the user tried to modify a textfield.
Given how things are currently set up, the simplest approach is probably to connect the text field's action selector to an action method on a target, such as your controller. You can do that in IB or in your tableView(_:viewForTableColumn:row:) method.
In that action method, you can call songsTableView.rowForView(sender) to determine which row was edited. Each column's text field would have a different action method, such as changeTitle() or changeArtist(), so that you know which column was edited. (You could also use songsTableView.columnForView(sender), then get the table column by using the index in songsTableView.tableColumns[col], and checking the returned column's identifier. For that, you would assign specific identifiers to the columns rather than letting IB assign them automatically.)
Once you have the row, you look up your dictionary using var obj = tableContents[row] as Dictionary<String,String> and set the value for the key appropriate to the action method (or column identifier) to the sender's stringValue.

How to get selected item of NSOutlineView without using NSTreeController?

How do I get the selected item of an NSOutlineView with using my own data source.
I see I can get selectedRow but it returns a row ID relative to the state of the outline. The only way to do it is to track the expanded collapsed state of the items, but that seems ridiculous.
I was hoping for something like:
array = [outlineViewOutlet selectedItems];
I looked at the other similar questions, they dont seem to answer the question.
NSOutlineView inherits from NSTableView, so you get nice methods such as selectedRow:
id selectedItem = [outlineView itemAtRow:[outlineView selectedRow]];
Swift 5
NSOutlineView has a delegate method outlineViewSelectionDidChange
func outlineViewSelectionDidChange(_ notification: Notification) {
// Get the outline view from notification object
guard let outlineView = notification.object as? NSOutlineView else {return}
// Here you can get your selected item using selectedRow
if let item = outlineView.item(atRow: outlineView.selectedRow) {
}
}
Bonus Tip: You can also get the parent item of the selected item like this:
func outlineViewSelectionDidChange(_ notification: Notification) {
// Get the outline view from notification object
guard let outlineView = notification.object as? NSOutlineView else {return}
// Here you can get your selected item using selectedRow
if let item = outlineView.item(atRow: outlineView.selectedRow) {
// Get the parent item
if let parentItem = outlineView.parent(forItem: item){
}
}
}
#Dave De Long: excellent answer, here is the translation to Swift 3.0
#objc private func onItemClicked() {
if let item = outlineView.item(atRow: outlineView.clickedRow) as? FileSystemItem {
print("selected item url: \(item.fileURL)")
}
}
Shown is a case where item is from class FileSystemItem with a property fileURL.

Resources