I build web, mobile and desktop apps, produce screencasts, write ebooks, and provide pairing and training

Working with NSTableView

Aug 18, 2015 - Elliott Draper

This article is a look at how to use NSTableView when building a Mac app using RubyMotion, and is taken from the Data chapter in my book, Building Mac OS X apps with RubyMotion. If you like what you read here, please take a look at my book - in the book we progress further with the following app code to look at Core Data, and then integrate with iCloud. You can also get a free 20+ page Getting Started guide here.

A table view can be a good way to start with presenting data in your app, before you customize further or even replace it later if needed. It’s a no-frills, easy to use way to get something up and running. We’ll start by building a simple note taking app, that will just keep the notes in memory and display them in the table view. Create the app using the OS X template as we’ve done before:

motion create --template=osx Noteworthy
cd Noteworthy

First up, let’s add in our favorite user interface gem, motion-kit:

gem 'motion-kit'

Then run the following:

bundle install

Then we’ll create our layout file to setup our user interface - here is the code for app/main_layout.rb:

class MainLayout < MotionKit::Layout
  FORM_HEIGHT = 100
  TITLE_HEIGHT = 30
  CONTENT_HEIGHT = 70
  BUTTON_WIDTH = 70
  BUTTON_HEIGHT = 30

  def layout
    add NSView, :form do
      constraints do
        width.equals(:superview)
        height.is(FORM_HEIGHT)

        min_left.is 0
        min_top.is 0
      end

      add NSTextField, :title_text do
        placeholderString "Enter note title"

        constraints do
          width.equals(:superview)
          height.is(TITLE_HEIGHT)

          min_left.is 0
          min_top.is 0
        end
      end

      add NSTextField, :content_text do
        placeholderString "Enter note content"

        constraints do
          width.equals(:superview)
          height.is(CONTENT_HEIGHT)

          min_left.is 0
          min_top.is TITLE_HEIGHT
        end
      end

      add NSButton, :save_button do
        title "Save"

        constraints do
          width.equals(BUTTON_WIDTH)
          height.is(BUTTON_HEIGHT)

          min_right.is 0
          min_bottom.is 0
        end
      end
    end

    add NSScrollView, :scroll_view do
      has_vertical_scroller true

      constraints do
        width.equals(:superview)
        height.equals(:superview).minus(FORM_HEIGHT)

        min_top.is FORM_HEIGHT
      end

      table_view = add(NSTableView, :table_view) do
        row_height 25

        add_column "title" do
          title "Title"
          min_width 150
        end

        add_column "content" do
          title "Content"
        end
      end
      document_view table_view
    end
  end
end

Most of this should be fairly straightforward if you’ve previously read through my previous post on motion-kit - we’re just setting up a window with the top section containing form fields for creating a new note, with a button to save it, and the bottom section is a table view inside a scroll view, so that when there is more content in the table than there is room in the window, you can scroll through it easily. We’re setting up two columns in the table view, title and content.

After that, we’ll update our AppDelegate, adding the following to the end of our applicationDidFinishLaunching:

    @title_text = @layout.get(:title_text)
    @content_text = @layout.get(:content_text)

    @notes = []
    @table_view = @layout.get(:table_view)
    @table_view.delegate = self
    @table_view.dataSource = self

    @save_button = @layout.get(:save_button)
    @save_button.target = self
    @save_button.action = 'note_save:'

We’re grabbing references to the controls we need to interact with, and then we’re following that up by ensuring that the AppDelegate itself acts as both the delegate, and data source for our table view - we’ll come back to that in a minute. Then we’re setting up the save button to call a method on the AppDelegate, called note_save. That method looks like this:

  def note_save(sender)
    title = @title_text.stringValue
    content = @content_text.stringValue
    return if title.nil? || title.empty? ||
content.nil? || content.empty?

    @notes << {:title => title, :content => content}
    @table_view.reloadData
    @title_text.stringValue = ""
    @content_text.stringValue = ""
  end

Nothing too tricky here - we’re validating the inputs first of all, then adding a note to our @notes array as a simple hash with :title and :content keys. We’ll reload the table view so that it updates to show the new note, and then we’ll clear out the text fields ready for the next note. All that remains now is to implement a couple of methods we need to handle the table view itself - specifically, we’re going to be making sure we conform to the base necessities required for the NSTableViewDataSource and NSTableViewDelegate protocols. For the former, we need to add the following very simple method:

  def numberOfRowsInTableView(table_view)
    @notes.length
  end

This just ensures that we’re telling the table view how many rows we have based on our notes in our @notes array. The next method we need, and final bit of code we have to add to see all of this working, is a little bit more involved:

  def tableView(table_view, viewForTableColumn: column,
row: row)
    result = table_view.makeViewWithIdentifier(column.identifier, owner: self)
    if result.nil?
      result = NSTextField.alloc.initWithFrame([[0, 0],
[column.width, 0]])
      result.identifier = column.identifier
      result.editable = false
    end
    result.stringValue =
@notes[row][column.identifier.to_sym]
    result
  end

This is called for each column, in each row - i.e. for every one of our configured columns in our layout (we have two, title and content), and then for however many rows we have based on the previous method we defined that looks at the length of the @notes array. We’re calling makeViewWithIdentifier to see if we already have a view we can use in the pool, utilizing the built-in table view recycling to avoid creating too many controls. This is important, especially if you’re building an app that might ultimately have hundreds or thousands of rows perhaps, and/or a lot of columns.

If we can’t find a view to use, we’ll set one up. We don’t need to worry about any frame dimensions besides the width which we can ascertain from the configured column width, and likewise we set the identifier to match the column too. In this case we don’t want the text fields editable. Lastly, regardless of how we got our view, the final thing to do is to set the string value contents of the cell - we’re effectively indexing into our @notes array by both the row (to find the right note), and then using the column identifier to find the right key in our note hash to get the contents. We just have to return our control, and we’re done!

If you fire up the app now, you’ll see that we have the form, and an empty table. Entering a note without filling in both fields doesn’t save, and filling in both fields and hitting save then clears the fields, and adds the note to our table below. You can keep adding as many notes as you’d like!

NSTableView displaying notes, with form fields for adding a note above

This is a fairly simple table view implementation, and of course you can customize an awful lot about the table view to make it more useful, to make it look nicer, and to make it better suit your purposes.

If you quit the app and restart it though, you’ll see all of our precious notes have disappeared. That’s where Core Data would come in handy!

You can see the code for this section here.




Available now in early access: Building Mac OS X apps with RubyMotion!


Unlock the power of Ruby in your Mac OS X apps to build everything from utility and productivity apps, to developer tools and helpers, to fully fledged desktop user interfaces. You'll integrate with web APIs, with core system functions, learn powerful ways to build user interfaces, and more. You'll learn how to best structure your apps and to take advantage of the Ruby syntax to make your development more efficient than building the app in Objective-C.


Learn more or purchase now.

blog comments powered by Disqus
Back to blog
 

Building Mac OS X apps with RubyMotion

Learn how to build Mac apps with using Ruby with this ebook, currently in early access, and with the finished version coming soon.


Purchase