Category

Swift NSTable Column Chooser

Swift Language

Swift NSTable Column Chooser

Although I wrote NeXTStep programs back in the 1980s for my dissertation, I haven’t written many Cocoa programs. The extra Apple Developer fee for OSX apps was something I didn’t want to pay in addition to the iOS fee. OK, I’m cheap. But how many bags of cat food would that $99 buy?

At WWDC15, Apple dropped the additional $99 fee. So, I’m guessing that there are some iOS developers out there adding Cocoa to their bag of trix. This is aimed at iOS developers, so if you’re a beginning beginner, some things might be vague. Sorry.

Introduction

Table of Contents

If you’re familiar with the UITable class and its minions, the NSTable should be fairly easy for you. We will make a Cocoa Application project with a storyboard, add an NSTable to the storyboard, and then write the code.

Project setup

Table of Contents

Create a new Cocoa Application project. Open the storyboard, and drag a Table View to the ViewController. This is pretty much like what you do in iOS. The storyboard will put the table view inside a scroll view automatically. Select the table view and the attributes inspector. Set the number of columns to 3 (our struct has 3 fields). Note that the “content mode” is set by default to “View Based”. More on that in a bit.

Optional: one time saver is the “autosave” option a bit lower down on the attribute inspector. Simply check the box and make up a user defaults name and it will remember the user’s preference for column size and order.

For each column in the table, you need to set the identifier in the identity inspector (⌥–⌘-3) and title in the attributes inspector (⌥–⌘-4). The identifiers I use are the names of the fields in the data struct so I don’t confuse myself.

NSTable setup

Table of Contents

I’m going to make the ViewController a table data source and delegate. So it will needs some data. Here’s a simple struct with some data (more in the github example).

Table data

Now, the table protocols. The NSTableViewDataSource will return the data. Sort of. There are a few ways to configure an NSTableView : “Cell Based” and “View Based”. The latter seems to be more flexible and it’s the default setting, so I’ll use that. If you opt for “Cell Based”, then the NSTableViewDataSource will need the objectValueForTableColumn function.

For our View Based table, the delegate will return the data via viewForTableColumn.
We didn’t change anything in the storybaord configuration for the table cells. We just said we wanted 3 of something. That something for a View Based table is an NSTableCellView. You create a reusable cell view via tableView.makeViewWithIdentifier. The I retrieve the appropriate data from the array, and set each view’s text field to that data by looked at the column identifier.

Control-Drag from the table to the view controller (twice) to set the data source and delegate.

At this point, your table should work.

Run the project and see what happens.

Context Menu

Table of Contents

To make the column selection work, I will create a context menu and attach it to the header of the table. I call this from viewDidLoad.

Let’s create the context menu first.

So, I create an NSMenu then iterate over the table columns and create an NSMenuItem via addItemWithTitle. Each item will have its action set to the contextMenuSelected function I’ll talk about next. I check the user defaults to see if a column identified by its identifiers is hidden or not and then set the state appropriately. I show how to save the defaults later.

The action for each NSMenuItem toggles the value of the column’s hidden property and also the state of the menu item to match. If you’ve hidden a column, there is screen real estate that needs to be dealt with. There are two things you can do. Tell the table view to siteToFit or size just the last column. I don’t know which one I like better. Try them both and see what you like.

Finally, the state of the column’s hidden property needs to be save in user defaults. So I call a func to do that. Let’s look at that next.

To save the value of the hidden property of each table column, I create a Dictionary with the column identifier as the key and the hidden property as the value. Then I just save the Dictionary in user defaults. Easy.

Before I knew about the auto column save feature I mentioned, I saved a Dictionary of Dictionaries to save multiple column properties. The auto feature saves some code.

In the AppDelegate, you need to register the user defaults. In real life I’d use a plist. Here I’m just hard coding it.

You can check the user defaults in the Terminal. Use the bundle identifier. I set mine to com.rockhoppertech.TableColumnChooser. Look at the General tab of your project to find yours.

What you should see are the non-default values for the user defaults. If you haven’t hidden any columns, you will see only the column autosave values here. Go ahead, hide a column and come back to check.

Summary

Table of Contents

It really isn’t hard to create a usable NSTableView and off the user the option of hiding certain columns. You just need to install a context NSMenu and set the hidden property of the NSColumn, then save the values in user defaults.

You can bind a special controller to the table view for it to receive its data. I’ll talk about “Cocoa Bindings” next time.

Resources

Table of Contents

8 thoughts on “Swift NSTable Column Chooser”

  1. Hi

    I have found your tutorial very interesting but as a self-taught coder I am slightly stuck as I need to save the data array. Can you please advise how to save and retrieve the data below to default preferences. I have tried various combinations after the as! Including these:-
    let preferences: UserDefaults = UserDefaults.standard

    preferences.set(dataArray, forKey: “dataArray_Prefs”) as! [Person[String:Any]]
    dataArray = preferences.array(forKey: “dataArray_Prefs”) as! [Person[String:Any]]

    From your tutorial:-
    // create some people
    dataArray.append(Person(givenName: “Noah”, familyName: “Vale”, age: 72))
    dataArray.append(Person(givenName: “Sarah”, familyName: “Yayvo”, age: 29))
    ……….

    1. 1) Your Person needs to conform to NSCoding.
      2) Encode it:

      3) save the encoded data to user defaults

      1. Thanks for the quick response.

        I tried adding the suggested line of code to your tutorial and Xcode gave errors so I let it auto correct and ended up with :-

        dataArray.append(Person(givenName: “Woody”, familyName: “Forrest”, age: 62))
        dataArray.append(Person(givenName: “X.”, familyName: “Benedict”, age: 88))
        let encodedData = NSKeyedArchiver.archivedData(withRootObject: Person(givenName: , familyName: , age: ))

        but compiler still complaining thus :-
        TableColumnChooser-master/TableColumnChooser/ViewController.swift:75:90: Editor placeholder in source file

        I then typed strings and number into code and that cleared errors but don’t think this is what I need. I want to save to Default preferences ALL the rows of data not just one person.

        Can you offer any more help please?

        1. You used autocomplete and Xcode inserted a “helpful” placeholder. It has a darker background. You replace this with your actual code.

          1. Sorry I don’t understand how your code snippet works, if I add your line:-
            let encodedData = NSKeyedArchiver.archivedData(withRootObject: person)

            Firstly is ‘person’ a typo and it should be ‘Person’? I assume so but this line compiler prompts me to add self or highlighted placeholder is “Add arguments after the type to construct a value of the type” which gives me:-
            let encodedData = NSKeyedArchiver.archivedData(withRootObject: Person())

            then it prompts again and creates this:-
            let encodedData = NSKeyedArchiver.archivedData(withRootObject: Person(givenName: ))
            which it then repeats for each variable.

            If I except self option:-
            let encodedData = NSKeyedArchiver.archivedData(withRootObject: Person.self)
            it compiles without errors but when app runs tableview is completely empty.

            Could I please ask if you can send a snippet from you tutorial with this feature to save the dataArray (and how I get it later in my app) included. Alternatively as I understand a bit about data arrays and use this to load example data on first run:-
            var storedDataArray:[[String:Any]] = []
            storedDataArray.append([“MyString”: “Some words”, “FileName”: “TestFile”, “version”: 1, “saved”: false])
            storedDataArray.append([“MyString “: “More words”, “FileName”: “TestFileTwo”, “version”: 3, “saved”: true]). I think I only need your code data structure to use the “sort columns” feature, so how can I modify the code from your tutorial to read data from my storedDataArray? I don’t understand how column sorting works and what “let l = lhs.familyName.characters” etc is actually doing

            func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
            if sortDescriptor.key == “familyName” {
            if sortDescriptor.ascending {
            self.dataArray = dataArray.sorted {lhs, rhs in
            let l = lhs.familyName.characters
            let r = rhs.familyName.characters
            return l.lexicographicallyPrecedes(r)
            }
            } else {
            self.dataArray = dataArray.sorted {lhs, rhs in
            let l = lhs.familyName.characters
            let r = rhs.familyName.characters
            return !l.lexicographicallyPrecedes(r)
            }
            }
            }

            if sortDescriptor.key == “givenName” {
            ……………….

            I hope you understand me as it’s difficult to explain.

            Andrew

  2. Hi Gene

    Can you offer any help please to use storedDataArray format :-
    “var storedDataArray = [[String: Any]]()
    storedDataArray.append([“MyString”: “Some words”, “FileName”: “TestFile”, “version”: 1, “saved”: false])
    storedDataArray.append([“MyString “: “More words”, “FileName”: “TestFileTwo”, “version”: 3, “saved”: true])”

    to place data in your column sorting code:-
    func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
    if sortDescriptor.key == “familyName” {
    if sortDescriptor.ascending {
    self.dataArray = dataArray.sorted {lhs, rhs in
    let l = lhs.familyName.characters
    let r = rhs.familyName.characters
    return l.lexicographicallyPrecedes(r)
    }
    } else {
    self.dataArray = dataArray.sorted {lhs, rhs in
    let l = lhs.familyName.characters
    let r = rhs.familyName.characters
    return !l.lexicographicallyPrecedes(r)
    }
    }
    }

    Thanks Andrew

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.