Posted on by

Of all the widgets included with Corona SDK, probably none provides as much potential utility as widget.newTableView(). Since it was introduced as part of the Widgets 2.0 library a year ago, it has probably generated more questions and additional feature requests in the forums than any other widget.

In this tutorial, we’re going to look more deeply at this powerful tool in the widget repository. Table views, or “list views,” are based on a mobile device paradigm where information is presented to the user in a series of single-column rows. In iOS, apps like Mail, Clock, and Contacts are obvious table views, but they are also used in countless other apps for menus and more.

In Corona, widget.newTableView() is a widget rendered in OpenGL and it has been built to emulate some of the more common features of a native table view. Under the hood, a Corona table view is built on top of a widget.newScrollView() which handles the up/down scrolling, the spring-like behavior, and the momentum-based scrolling. Starting with Graphics 2.0, the table view widget is also “self-masking,” meaning that Corona developers no longer need to create bitmap masks for table views that don’t occupy the entire screen.

Setting Up

In this tutorial example, we’ll create a table view that spans the full width of the screen, positioned just below an upper title bar and spanning down to a widget.newTabView() controller at the bottom. Here’s the basic code:

local navBarHeight = 60
local tabBarHeight = 50
local myList = widget.newTableView {
   top = navBarHeight, 
   width = display.contentWidth, 
   height = display.contentHeight - navBarHeight - tabBarHeight,
   onRowRender = onRowRender,
   onRowTouch = onRowTouch,
   listener = scrollListener
}

This creates an “empty” table view of the described size. Now, we must populate it with rows. This is done using the tableView:insertRow() method which inserts a blank, empty row into the view. To populate the widget in this example, we’ll use a table of items that contain a name and phone number:

local myData = {}
myData[1] = { name="Fred",    phone="555-555-1234" }
myData[2] = { name="Barney",  phone="555-555-1235" }
myData[3] = { name="Wilma",   phone="555-555-1236" }
myData[4] = { name="Betty",   phone="555-555-1237" }
myData[5] = { name="Pebbles", phone="555-555-1238" }
myData[6] = { name="BamBam",  phone="555-555-1239" }
myData[7] = { name="Dino",    phone="555-555-1240" }

for i = 1, #myData do
   myList:insertRow{
      rowHeight = 60,
      isCategory = false,
      rowColor = { 1, 1, 1 },
      lineColor = { 0.90, 0.90, 0.90 }
   }
end

Even with this setup, nothing is visually rendered to the table view at this point. Behind the scenes, the table view widget keeps track of which rows are on screen and draws only those rows when they are needed. This is accomplished by a function that we provide and define as the onRowRender parameter in the table view constructor. When this function is called by the table view, we’re provided with the handle to the appropriate row.

In Corona, each row is actually a separate display.newGroup(). Thus, in the row rendering function, we can create the necessary custom display objects and use the standard group:insert(object) method to insert each into the table view row.

How does the row rendering function know what to render within each row? Let’s inspect a typical example of the row rendering function:

local function onRowRender( event )

   --Set up the localized variables to be passed via the event table

   local row = event.row
   local id = row.index

   row.bg = display.newRect( 0, 0, display.contentWidth, 60 )
   row.bg.anchorX = 0
   row.bg.anchorY = 0
   row.bg:setFillColor( 1, 1, 1 )
   row:insert( row.bg )

   row.nameText = display.newText( myData[id].name, 12, 0, native.systemFontBold, 18 )
   row.nameText.anchorX = 0
   row.nameText.anchorY = 0.5
   row.nameText:setFillColor( 0 )
   row.nameText.y = 20
   row.nameText.x = 42

   row.phoneText = display.newText( myData[id].phone, 12, 0, native.systemFont, 18 )
   row.phoneText.anchorX = 0
   row.phoneText.anchorY = 0.5
   row.phoneText:setFillColor( 0.5 )
   row.phoneText.y = 40
   row.phoneText.x = 42

   row.rightArrow = display.newImageRect( "rightarrow.png", 15 , 40, 40 )
   row.rightArrow.x = display.contentWidth - 20
   row.rightArrow.y = row.height / 2

   row:insert( row.nameText )
   row:insert( row.phoneText )
   row:insert( row.rightArrow )
   return true
end

The “magic” here is how we use the row’s ID number to map against the myData dataset. However, while this works great for simple cases, it won’t work for more advanced table views that contain “category” rows or empty rows for spacing purposes.

Introducing Passable Parameters

This feature has been available for some time but it hasn’t been discussed too much. Essentially, it provides a programmatic way to associate a row and specific data without relying on an ID number. Let’s use the same example but pass in parameters instead:

for i = 1, #myData do
   myList:insertRow{
      rowHeight = 60,
      isCategory = false,
      rowColor = { 1, 1, 1 },
      lineColor = { 0.90, 0.90, 0.90 },
      params = {
         name = myData[i].name,
         phone = myData[i].phone
      }
   }
end

Now we can insert rows that contain data using the params table. Rows that have no data can be inserted without parameters (category rows, for example). Then, in the row rendering function, we can simply test to see if it’s a category row or not and render it accordingly:

local function onRowRender( event )

   --Set up the localized variables to be passed via the event table

   local row = event.row
   local id = row.index
   local params = event.row.params

   row.bg = display.newRect( 0, 0, display.contentWidth, 60 )
   row.bg.anchorX = 0
   row.bg.anchorY = 0
   row.bg:setFillColor( 1, 1, 1 )
   row:insert( row.bg )

   if ( event.row.params ) then    
      row.nameText = display.newText( params.name, 12, 0, native.systemFontBold, 18 )
      row.nameText.anchorX = 0
      row.nameText.anchorY = 0.5
      row.nameText:setFillColor( 0 )
      row.nameText.y = 20
      row.nameText.x = 42

      row.phoneText = display.newText( params.phone, 12, 0, native.systemFont, 18 )
      row.phoneText.anchorX = 0
      row.phoneText.anchorY = 0.5
      row.phoneText:setFillColor( 0.5 )
      row.phoneText.y = 40
      row.phoneText.x = 42

      row.rightArrow = display.newImageRect( "rightarrow.png", 15 , 40, 40 )
      row.rightArrow.x = display.contentWidth - 20
      row.rightArrow.y = row.height / 2

      row:insert( row.nameText )
      row:insert( row.phoneText )
      row:insert( row.rightArrow )
   end
   return true
end

Now the row rendering function doesn’t need to know anything about our data structure. This helps support the concept of Model-View-Controller (MVC). With this method, we can focus on our View and know which data to get without knowledge of how the Model is tracking the data. In most cases, this is a very good way to populate table view rows and it should make it easier to visualize data with the associated row.

Reloading the Table View With Status Bar Tap

This is a frequently-requested feature that’s easy to implement with a small amount of code. How the data is pulled in will depend largely on the app design, and it’s your responsibility to create a reload function that will be called as part of the tap handler. In most cases, the common functionality will be:

  1. Retrieve the updated data (new tweets or RSS feeds, for example).
  2. Flush (clear) the existing table view’s data.
  3. Re-insert the new data into the table view.

To flush/empty the rows as mentioned in step #2, we can call this function:

myList:deleteAllRows()

Now, to build in the “status bar tap” functionality, we can create a transparent rectangle where the status bar resides and add a “tap” event handler on it. This tap function will, not surprisingly, call the custom reload function. It’s that simple!

local reloadBar = display.newRect( display.contentCenterX, display.topStatusBarContentHeight*0.5, display.contentWidth, display.topStatusBarContentHeight )
reloadBar.isVisible = false
reloadBar.isHitTestable = true
reloadBar:addEventListener( "tap", reloadTable )

There’s one important aspect to note: since the rectangle object is invisible (.isVisible = false), we must set .isHitTestable to true so it reacts to tap/touch events.

Implementing “Spring Reloading”

Table view “spring reloading” is the technique of pulling down on the list to have it reload/refresh. This isn’t quite as easy to implement as the status bar tap method, but let’s walk through it.

There are some prerequisites to consider as part of the User Interface and User Experience (UI/UX) for spring reloading. When a user pulls down on a table view, it reveals the “background” of the app. In some cases, you may want to place something behind the table view, like a solid gray block. Also, most spring reload systems have an animated graphic that shows when the user has pulled down far enough, prompting them to release their touch. In iOS7, this is usually a spinner, which you may consider imitating via the Corona spinner widget. Ultimately, though, it’s your decision about how to handle the UI/UX for your app.

Before implementing spring reloading, let’s further examine the table view event system. Here’s our example table view constructor again:

local myList = widget.newTableView {
   top = navBarHeight, 
   width = display.contentWidth, 
   height = display.contentHeight - navBarHeight - tabBarHeight,
   onRowRender = onRowRender,
   onRowTouch = onRowTouch,
   listener = scrollListener
}

As we’ve seen so far, the onRowRender function handles rendering and display of the actual table rows. Below that, the onRowTouch function handles “tap” and “touch” events on the individual rows (see the documentation for widget.newTableView() for usage examples). The final function, assigned to the listener property, is the function that will listen for scroll-related events on the table — and those events are of particular interest in spring reloading. Using this listener, we’ll be able to detect when the table view starts to move, when it’s still moving under momentum, and when it stops moving. We’ll also get an event.phase = nil when the table view reaches the limits of its scrolling, and it’s this event phase which indicates that we should reload the table view. Let’s look at the example code:

local springStart = 0
local needToReload = false

local function scrollListener( event )
   if ( event.phase == "began" ) then
      springStart = event.target.parent.parent:getContentPosition()
      needToReload = false
   elseif ( event.phase == "moved" ) then
      if ( event.target.parent.parent:getContentPosition() > springStart + 60 ) then
         needToReload = true
      end
   elseif ( event.limitReached == true and event.phase == nil and event.direction == "down" and needToReload == true ) then
      --print( "Reloading Table!" )
      needToReload = false
      reloadTable()
   end
   return true
end

Notice how these conditional checks are used for different processes within the spring reloading:

  • In the “began” phase, we store the current content position of the table view. This way, when the user pulls down on the list, accidental reloads are not triggered if the user pulls down just slightly. We also set the flag variable needToReload to false in this phase.  If you wish to have an animation start to indicate they are beginning a spring reload, this would be a good time to start it.
  • In the “moved” phase, if the table view position changes by a set amount more than it started at, in this example 60 pixels, we set the needToReload flag to true. Since the user could still be dragging the table view at this time, we do not trigger the reload yet.  If you have a graphic or animation showing the user it’s now time to let go or they have pulled down enough, you would change the graphic/animation here as well.
  • In the “ended” phase, you can stop any animations you started in the “began” or “moved” phases.
  • In the final conditional check, we don’t want to reload only when the movement ends. If we did, the table view would reload every time the scrolling stopped on the table view. Instead, we’ll check for a series of conditions, starting with the event.limitReached event equal to true. This event has no phase (nil), so we also check for the absence of event.phase. Next, we check that the movement direction is “down” (typically, reloads don’t occur after scrolling up) and we also confirm that the needToReload flag is true (i.e. the user has pulled the view down enough distance). If all of these conditions are met, we call the reloadTable() function to start the table view reload.

In Conclusion

Hopefully this tutorial has shown you how to add some creative and useful features to the table view widget without touching one line of the open-source widget library. With the ability to use parameters to pass data to the row rendering function, plus these easy-to-implement reloading features, you can now supercharge your table views.


Posted by . Thanks for reading...

23 Responses to “Tutorial: Advanced TableView Tactics”

  1. JCH_APPLE

    + 1 for the great quality of this tutorial.

    List often use remote images. In this case, onRowRender must be called once image is loaded.

    Is there an easy way to show a placeholder for an image with a spinner and update rows one image is loaded (inspite of waiting image to be loaded).

    Reply
    • Rob Miracle

      Loading Remote images is something I’ve done in tableView’s quite a bit. It’s tricky, but you can have a place holder and then call your display.loadRemoteImage() and in it’s call back, remove the placeholder and draw the image in it’s place. It’s helpful to have all code for this inside of the onRowRender() function for scoping purposes.

      Reply
      • JCH_APPLE

        Thank you Rob, I will try this approach. As of today I load the image and when loading is complete I insert row.
        It works but can result in rows not displayed in the good order (if product 2 has a smaller image than product 1, it loads faster and it’s displayed before product one).
        Your idea will avoid such behavior.

        Reply
        • Rob Miracle

          The way I do it it ties the download to the row itself and not to the order they are inserted. Since all the work goes on in onRowRender, you store the placeholder in the row:

          row.icon = dipslay.newImageRect(“placeholder.png”, 60, 60)
          row:insert(icon)

          So not only is the object held with the row, it gets inserted into the display group as well. Then call display.loadRemoteImage(). In the listener, if you get the image, remove row.icon and save the image to row.icon and insert it into the group.

          Reply
  2. M

    Thanks for the write up. Does the Config file still need to be set to 320×480? Last time I tried tableview on an app using a 640×960 config it did not work.

    Reply
    • Rob Miracle

      I’m not sure of any reason why tableViews wouldn’t work on 640×960 except that the scroll bar won’t scale up. But there are other widgets that really work better keeping your app at 320×480.

      Rob

      Reply
    • Rob Miracle

      You can get the Business App sample from our github repository and add the above to it. That’s what I used to test these features in. I just don’t have it in a pushable state at the moment.

      Reply
  3. Dave Baxter

    Just adding this to my app and there is a problem with the second onRowRender code block at the bottom, it’s cut off and the word is sitting there.

    Works fine by the way, not implemented the springy thing am refreshing on a button but am using the params and reading in a XML document and displaying it no problems.

    Dave

    Reply
  4. Abdulaziz

    Thanks Rob for the great tutorial..

    In the Spring Reloading,, normally in some apps like Twitter for example, when user pulls down the list, the refresh starts but I noticed the content stayed down in the last place it reaches when user pulled it. When the refresh is over the content goes back to the initial height..

    do you have an idea how to achieve that,

    Thanks
    Abdul

    Reply
    • Rob Miracle

      You could put the refresh request inside a timer so that the main function returns immediately and then when the timer function finishes, it will update. The timer wouldn’t need to be very long, say a couple of milliseconds. The idea is to get it so that you’re not waiting on it to finish.

      Rob

      Reply
    • Rob Miracle

      Many people wanted to have tableViews that would fit many different devices without having to have dozens of mask files and trying to figure out which mask file to use. To do this, we have to use containers and they self-mask. You could always download the open source versions from our github.com repository and change it from a container back to a group and provide your own masking system.

      Rob

      Reply
  5. roddy

    Hi Rob

    Is it possible to add an invisible button on top of the Status Bar inside a Composer Scene?

    Roddy

    Reply
      • roddy

        Thanks Rob, really appreciate your prompt reply.

        So, from your article above… “Now, to build in the “status bar tap” functionality, we can create a transparent rectangle where the status bar resides and add a “tap” event handler on it.” Would that transparent rectangle have to be created in main.lua?

        Cheers

        Roddy

        Reply
        • Rob Miracle

          It works in the simulator and if you have the device status bar turned off. While we can’t beat the iOS status bar being there, you can still provide that functionality (I.e. tapping on the nav bar) if you hide the status bar.

          Rob

          Reply
          • roddy

            Thanks, Rob! I’ll have a play and see what works best.

            Many thanks for your replies! :)

            Roddy

  6. Mark

    Is there any way to get the x,y positions of the event within onTableRowTouch? I’d like to treat press/release events where the user accidentally moves their finger a pixel or two as a tap still. Otherwise, you have to be super precise with your touch for the event.phase to be a tap.

    Reply
    • Rob Miracle

      Not that I’m aware of without getting the Open Source version from github and updating it to do so. The widgets are pure Lua and we’ve made the code available for people who want to have custom features like this.

      Rob

      Reply

Leave a Reply

  • (Will Not Be Published)