Adding / Removing items from a NSTableView

Hi,

Like in the Music App, I load my playlists and show them in a tree and then when I click a playlist, a TableView shows its content. After that I select some tracks to add it to another TableView at the bottom :

I add the data programmatically but maybe it could be better to use the binding because actually I have a problem to remove the selected items at the bottom. Indeed I have to maintain the datas manually when an item is removed and it costs a lot of work...

Any idea about that ?

Thx.

Accepted Reply

Thanks for feedback. Don't forget to close the thread on the correct answer.

As for your next question:

Since each time I change something in the array used by the NSTableView, I need to reloadData() the NSTableView, I guess it is the same when I want to filter the array, right ?

In essence, yes, but you can reload only the relevant rows (indexPath) after insert / delete.

What is very important is to update dataSource BEFORE you add, remove or reload.

Effectively, to filter, you filter the dataSource. Then you'd better reload the full table, because it will be harder to see who has changed.

Replies

I add the data programmatically but maybe it could be better to use the binding because actually I have a problem to remove the selected items at the bottom

How did you define your dataSource ?

It is very easy to manage table view by maintaining in sync both dataSource and TableView with insert/delete rows.

Please show code.

  • Thx for your answer.

    I initialize the dataSource on the self (like often).

    After that I set the tracks with a setter and I reload the datas : `var tracks: [NSObject]? {

            set {

                datas = newValue

                DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in

                    self!.tracksListTableView.reloadData() // to avoid the init bug...

                }

            }

            get {

                if let tracks: [NSObject] = datas as? [NSObject] {

                    return tracks

                }

                return nil

            }

        }

    My bad but my project has 2 years old and I don't even remember how I plug the datas to the TableView... In the inspector ?

    Thx.

Add a Comment

Thx for your answer.

I initialize the dataSource on the self (like often).

After that I set the tracks with a setter and I reload the datas : `var tracks: [NSObject]? {

        set {

            datas = newValue

            DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in

                self!.tracksListTableView.reloadData() // to avoid the init bug...

            }

        }

        get {

            if let tracks: [NSObject] = datas as? [NSObject] {

                return tracks

            }

            return nil

        }

    }`

My bad but my project has 2 years old and I don't even remember how I plug the datas to the TableView... In the inspector ?

Thx.

That was not the question.

For the tableView, you have a dataSource: var tracks: [NSObject]?

Why NSObject ?

The question is: how do you fill this array (tracks).

Then, in tableView delegates (cellForRow), you must use those data to fill the cell.

That's the codes we would need to look at.

I fill the tracks array with the setter I typed just before (with the reloadData) and then I have the NSTableViewDataSource extension to set the number of lines :

func numberOfRows(in tableView: NSTableView) -> Int {

        if let tracks = tracks {

            print("V&G_FW___numberOfRows : ", self, tracks.count)

            return tracks.count

        }

        return 0

    }

And in the NSTableViewDelegate extension I just get the tracks array for each track :

func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {

        if let theTracks: [ITLibMediaItem] = tracks as? [ITLibMediaItem] {

            let theItem = theTracks[row]

            let cellIdentifier: String = "TracksListCellID"

            var text: String = ""

            

            switch tableColumn?.identifier.rawValue {

            case TableColumnID.trackNumberTableColumnID.rawValue:

                text = String(row + 1)

            case TableColumnID.titleTableColumnID.rawValue:

                text = getTrackTitle(theTrack: theItem)

            case TableColumnID.artistTableColumnID.rawValue:

                text = iTunesModel.getArtistName(theITTrack: theItem)

            case TableColumnID.albumTableColumnID.rawValue:

                text = iTunesModel.getAlbumTitle(theITTrack: theItem)

            case TableColumnID.locationTableColumnID.rawValue:

                text = theItem.location?.path ?? ""

            case TableColumnID.totalTimeTableColumnID.rawValue:

                text = getTotalTime(theTrack: theItem)

            default:

                text = ""

            }

            

            if let cell: NSTableCellView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), owner: nil) as? NSTableCellView {

                cell.textField!.stringValue = text

                return cell

            }

        }

        

        print("V&G_FW___viewFor tableColumn : ", self, "no TitleCellID is present !")

        

        return nil

    }

So classic code.

My problem is I think I need to call the reloadData each time I delete an item in the TableView at the bottom to refresh the view... Right ?

Alright, everything is OK with adding / removing programmatically but now I'd like to filter.

Since each time I change something in the array used by the NSTableView, I need to reloadData() the NSTableView, I guess it is the same when I want to filter the array, right ?

Thx.

Thanks for feedback. Don't forget to close the thread on the correct answer.

As for your next question:

Since each time I change something in the array used by the NSTableView, I need to reloadData() the NSTableView, I guess it is the same when I want to filter the array, right ?

In essence, yes, but you can reload only the relevant rows (indexPath) after insert / delete.

What is very important is to update dataSource BEFORE you add, remove or reload.

Effectively, to filter, you filter the dataSource. Then you'd better reload the full table, because it will be harder to see who has changed.

Thx for your answer.

Another method is also to use a NSArrayController to bind programmatically to the NSTableView, right ?

With that method, is it easier ? Because from the moment I start to add/delete/filter, it becomes more complicated to reload the datas and get it like it is in the TableView, isn't it ?

So I start to change my code for the binding :

tracksListTableView.bind(.content, to: _arrayController, withKeyPath: "arrangedObjects", options: nil)

Like that, I don't have to reload the datas each time I add a track to the TableView. I just do :

_arrayController.add(theTrack)

But now I have to change my code to remove the selected tracks... What is the good way ?

Thx.

What is the good way ?

It is really a question of personal preferences.

I am not fond of bindings (probably because I do not practice a lot !), because I prefer to keep some visibility of what's going on in code. But once again, just a personal choice.

Ok so you never use NSArrayController, right ?

  • Exact. The only time I used was when I started learning Swift, following this series of courses, including one with array binding. h t t p s : / / w w w.youtube.com/watch?v=vOaXyOGL3bw.    But never used in app. Once again, take it as a personal choice.

Add a Comment

Back !

So I have the adding / removing actions. What about filtering ?

I'm using a NSArrayController now which has binding capacities. To bind it to the NSTableView, I simply do :

tracksListTableView.bind(.content, to: arrayController, withKeyPath: "arrangedObjects", options: nil)

Et voila !

I tried to bind the NSSearchField in the same way :

searchField.bind(

            .predicate,

            to: self.arrayController,

            withKeyPath: NSBindingName.filterPredicate.rawValue,

            options: [.predicateFormat: "col CONTAINS[cd] $value"]

But it fails when I start to type in the searchField :

Multiline Error setting value for key path filterPredicate of object <NSArrayController: 0x6000036fc2a0>[object class: NSMutableDictionary, number of selected objects: 1] (from bound object <NSSearchField: 0x14c710d90>): [<ITLibMediaItem 0x600001013580> valueForUndefinedKey:]: this class is not key value coding-compliant for the key col.

How to fix that ?

Thx.