Tutorial: Creating a navigation bar

Tutorial: Creating a navigation bar

Using Corona, it’s easy enough to create our own top navigation bar — just draw the background, add some title text, and place the buttons as necessary. But if we want to adhere to the “DRY” programming theory (Don’t Repeat Yourself), including an entire set of navigation bar code in every scene of an app is more work than it should be.

About the navigation bar

In the iOS vernacular, the top navigation bar is known as the UI Navigation Bar and it has fairly consistent behavior:

  • a background
  • an optional left button
  • an optional right button
  • an optional title

This tutorial illustrates how to create a standard iOS-style “UI Navigation Bar” widget in which we provide the definitions for two buttons, the functions to handle button interaction, and some basic information to draw the bar.

Setup

Like we did in the Customizing text input tutorial, let’s begin by expanding the core widget library and defining the default parameters:

When creating a top nav bar, there are a few things to consider, and we’ve done so in this code. For example, does the app have a device-specific status bar? If so, the nav bar should be positioned below it, but the area the status bar occupies is still space that we’re responsible for. Because we cannot programmatically detect if the status bar is showing or not, we must pass in a flag that states if we decided to show or hide the status bar. The function will then calculate the status bar height and adjust placement of the nav bar accordingly.

Next, let’s construct the bar itself:

In this code block, we start by creating a container group to hold all of the visual objects. Then, a rectangle is created for the “background” and it’s added to the container group. Next, the passed-in parameters determine how to fill the rectangle — either with a specified image, a solid color, or (if nothing is provided) a solid white fill.

Now, let’s add the “title” of the page to the nav bar, if provided (note that some iOS apps don’t use titles):

Now, let’s set up the buttons. There will be two buttons in this example: a “left” button and a “right” button. To keep things simple, we’ll use the existing widget.newButton() widget — essentially, we’ll pass the parameters to the button constructor:

If images are provided via the defaultFile attribute, the code looks for the parameters associated with image-based widget buttons (see the documentation). If simple text buttons are preferred, we can specify a label, font, fontSize, and labelColor array. Once both buttons are set up, we just return the container to the caller as a reference to the object.

Using the navigation bar widget

To use this new widget, we simply include the above code in main.lua. It will be added to the widget library, so in other modules where you require("widget"), it will be available to you. Then, when we need to display a new navigation bar, we follow three basic steps:

1. Write the button listeners

The buttons on the nav bar will not be very useful unless there are callback listeners associated with tap action upon them. As such, let’s include some basic listener functions in the module where we want to use the nav bar:

2. Declare the navigation bar buttons

Now, let’s configure the buttons specific to this nav bar. In this example, the left button will be a two-image button and the right button will be a simple text button. Notice that we are also including a reference to the callback listeners we just wrote so that our buttons respond to touch.

3. Declare the navigation bar

Finally, we declare the actual navigation bar as follows:

Where from here?

There’s plenty of room for you to expand on this concept. First, this is very iOS-friendly, but it doesn’t build Android-style navigation bars. The Android-style top bar is more like the iOS Toolbar, with a series of buttons on the right side and a “hamburger” icon and graphic title on the left. Secondly, this example could be expanded to support more button styles than 2-image or basic text buttons. Finally, this example doesn’t support the navigation chevrons (less-than and greater-than symbols), but it could be configured easily enough.


Tags:
,
Rob Miracle
[email protected]

Rob is the Developer Relations Manager for Corona Labs. Besides being passionate about helping other developers make great games using Corona, he is also enjoys making games in his spare time. Rob has been coding games since 1979 from personal computers to mainframes. He has over 16 years professional experience in the gaming industry.

20 Comments
  • Kerem
    Posted at 13:20h, 31 December

    Great idea and tutorial! Thank you very much Rob. Happy New Year!!!

  • Mark
    Posted at 20:07h, 01 January

    Just curious. Why not add this to the core widget library instead of extending it?

    • Rob Miracle
      Posted at 09:54h, 02 January

      Our engineering staff is pretty booked with features and bugs that they work on. This particular feature isn’t a high-demand one and most people already doing this in some form or fashion. Part of what we blog about on our Tutorial posts are sometimes to inspire you and show you ways to do things. Hopefully at the end of the day the readers would say “Oh, that’s how that’s done” and have something new to play with.

      Rob

  • Dan R.
    Posted at 09:26h, 03 January

    Rob,
    Thanks for a great tutorial post! As a hobbyist these tutorials help tremendously to move me along…

    Two quick questions:
    1. When I run the NavBar demo in my simulator I get a message that the project uses premium graphics features for Pro (or higher) subscribers. After working over your code several times I’m not sure what these are or what to remove to avoid this. Thoughts? I’m using the starter version 2013.2100 on OSX 10.9

    2. What is the best way to adapt the demo to a Storyboard scene to allow device rotation that recreates that Nav Bar at the correct width after the device is rotated? Here’s what I’m trying, and it seems to work, but perhaps I’ve missed a more obvious solution:
    – I’ve put the code from the blog demo in the create scene function.
    – I’ve added the navBar to the createScene’s group.
    – In createScene I’ve added an orientation function (onOrientationChange) that calls storyboard.purgeScene on the current scene and then calls storyboard.gotoScene on the current scene to force recreation of the nav bar elements.
    – at the end of the createScene function I include runtime event listener for orientation: Runtime:addEventListener( “orientation”, onOrientationChange )
    – In both the exitScene and destroyScene functions I’ve included: Runtime:removeEventListener( “orientation”, onOrientationChange ). I’m not sure this needs to be in both of these places.

    So, is there a better way to manage this? What I’ve written seem to work, but is having a scene purge itself and then goto itself ok, or will there be unintended consequences?

    Thanks for your thoughts…
    {and if this topic needs to be moved to a forum discussion please feel free to relocate it…}

    • Rob Miracle
      Posted at 12:05h, 03 January

      The whole creation of the bar is based around Graphics 2.0 methods. That could be easily re-written to not create a rectangle and fill it with a pattern, but use display.newImageRect() instead of display.newRect(). The iOS 6 gradient is also using G2.0 fills. That’s likely what is happening.

      You probably only need the remove listener in destoryScene. I don’t like purging the on screen scene, but this maybe a valid use of that.

  • tarun
    Posted at 05:13h, 06 January

    Hi Rob,

    I am running into this exception:

    stack traceback:
    [C]: in function ‘setFillColor’
    …corona/examples/project/widget_newNavBar.lua:59: in function ‘newNavigationBar’
    …top/android/corona/examples/project/main.lua:40: in main chunk

    Could you confirm if the example code is bug free or not.

    • tarun
      Posted at 05:14h, 06 January

      File: …corona/examples/project/widget_newNavBar.lua
      Line: 59

      Attempt to call method ‘setFillColor’ (a nil value)

    • Rob Miracle
      Posted at 14:49h, 06 January

      What version of Corona SDK are you using?
      What level of subscription are you?

      If line 59 is the color on the title, you are likely using a pre-Graphics 2.0 version of Corona SDK. You can safely change that to :setTextColor() instead, though setting the background of the navBar is very dependent on being Graphics 2.0.

      • Dan R,
        Posted at 08:41h, 07 January

        Taurn – if you aren’t worried about the iOS6 gradient fill and only want an iOS7 style bar, this simplified version seems to work with the Starter edition. It’s not as robust as Rob’s example.
        in function widget.newNavigationBar( options ) make these changes:
        [lua] opt.backgroundColor = customOptions.backgroundColor or {1,1,1} –defaults to white background for iOS7 bar[/lua]

        and
        [lua] local barContainer = display.newGroup()
        local background = display.newRect(barContainer, opt.x, opt.y, opt.width, opt.height + statusBarPad )
        background:setFillColor(unpack(opt.backgroundColor))
        [/lua]

  • Archer
    Posted at 07:38h, 15 January

    Hi there,

    At the end of the tutorial you mention that it is very iOS friendly, but not Android. Are you inferring that this shouldn’t be used for Android or with some minor design changes could it be used cross platform?

    Secondly, the tabbar UI corona offers doesn’t have scaling capability, does this have scaling capability across all platforms?

    Lastly, can the buttons, like the tabBar UI, reflect storyboard scenes in the space below the navigation bar?

    Thanks,
    Archer

    • Rob Miracle
      Posted at 17:48h, 15 January

      On Android, their top bars have what’s known as a Hamburger icon on the left edge and a left aligned graphics Branding icon, like the GMail logo. Then on the right they have a series of buttons based on the app needs. This is more like the iOS Toolbar UI widget and less like the navBar wigets.

      You could easily expand this to take a list of buttons and an icon to work for Android. You can still use this on Android and I think people would be okay with it, but you generally want your UI to behave like the OS wants.

      You have to keep in mind that tutorials have limited space. While I could have written a fully functional cross-platform widget, it would have been too long and too complicated for a tutorial. Also part of the tutorials are to inspire you to expand on the ideas in the tutorial and not be the code solution.

      For your second question, if you’re using a 320px wide content area it will be 320 on all devices so there is no scaling issues. The height may be variable..

      If your tabBar buttons are transluscent the storyboard scenes underneath should show through.

      Rob

      • Archer
        Posted at 08:55h, 16 January

        Okay, thanks for answering those questions Rob. I really appreciate the tutorial, really great stuff, was just to be sure as a I’m a few days into the platform and still getting a feeling for it. I had an issue with passing info through the onhandle event listener for the buttons in main. The demo code does not pass the information, print(“button pressed”). I submitted this issue below before seeing your reply. I’ve been banging my head over why its not doing what I think it should and can’t come up with a reason.

    • Archer
      Posted at 08:45h, 16 January

      The handLeftButton (event) function in main does not pass information. I tried tp print button pressed to screen and got nothing. Was the same in my project as in the downloaded demo code. Recommendations?

      • Rob Miracle
        Posted at 15:58h, 16 January

        I would suggest you open a post in the forums to ask the question about the left button. The blog comments can’t handle posting code very well. When you do, can you post your code you’re using?

        Rob

      • phlo
        Posted at 07:52h, 02 April

        i had the same problem with the left button missing an onEvent, just add it to widget_newNavBar.lua after line 71 i think
        onEvent = opt.leftButton.onEvent

  • Sanghyun Lee
    Posted at 11:03h, 24 December

    ———————————————————————–
    navbar.lua
    —————-code start—————————————–
    local widget = require( “widget” )
    function widget.newNavigationBar( options )
    local customOptions = options or {}
    local opt = {}
    opt.left = customOptions.left or nil
    opt.top = customOptions.top or nil
    opt.width = customOptions.width or display.contentWidth
    opt.height = customOptions.height or 50
    if ( customOptions.includeStatusBar == nil ) then
    opt.includeStatusBar = true — assume status bars for business apps
    else
    opt.includeStatusBar = customOptions.includeStatusBar
    end
    — Determine the amount of space to adjust for the presense of a status bar
    local statusBarPad = 0
    if ( opt.includeStatusBar ) then
    statusBarPad = display.topStatusBarContentHeight
    end
    opt.x = customOptions.x or display.contentCenterX
    opt.y = customOptions.y or (opt.height + statusBarPad) * 0.5
    opt.id = customOptions.id
    opt.isTransluscent = customOptions.isTransluscent or true
    opt.background = customOptions.background
    opt.backgroundColor = customOptions.backgroundColor
    opt.title = customOptions.title or “”
    opt.titleColor = customOptions.titleColor or { 0, 0, 0 }
    opt.font = customOptions.font or native.systemFontBold
    opt.fontSize = customOptions.fontSize or 18
    opt.leftButton = customOptions.leftButton or nil
    opt.rightButton = customOptions.rightButton or nil
    — If “left” and “top” parameters are passed, calculate the X and Y
    if ( opt.left ) then
    opt.x = opt.left + opt.width * 0.5
    end
    if ( opt.top ) then
    opt.y = opt.top + (opt.height + statusBarPad) * 0.5
    end

    local barContainer = display.newGroup()
    local background = display.newRect( barContainer, opt.x, opt.y, opt.width, opt.height + statusBarPad )
    if ( opt.background ) then
    background.fill = { type=”image”, filename=opt.background }
    elseif ( opt.backgroundColor ) then
    background.fill = opt.backgroundColor
    else
    background.fill = { 1, 1, 1 }
    end

    if ( opt.title ) then
    local title = display.newText( opt.title, background.x, background.y + statusBarPad * 0.5, opt.font, opt.fontSize )
    title:setFillColor( unpack(opt.titleColor) )
    barContainer:insert( title )
    end

    local leftButton
    if ( opt.leftButton ) then
    if ( opt.leftButton.defaultFile ) then — construct an image button
    leftButton = widget.newButton({
    id = opt.leftButton.id,
    width = opt.leftButton.width,
    height = opt.leftButton.height,
    baseDir = opt.leftButton.baseDir,
    defaultFile = opt.leftButton.defaultFile,
    overFile = opt.leftButton.overFile,
    onEvent = opt.leftButton.onEvent
    })
    else — else, construct a text button
    leftButton = widget.newButton({
    id = opt.leftButton.id,
    label = opt.leftButton.label,
    onEvent = opt.leftButton.onEvent,
    font = opt.leftButton.font or opt.font,
    fontSize = opt.fontSize,
    labelColor = opt.leftButton.labelColor or { default={ 1, 1, 1 }, over={ 0, 0, 0, 0.5 } },
    labelAlign = “left”,
    })
    end
    leftButton.x = 15 + leftButton.width * 0.5
    leftButton.y = title.y
    barContainer:insert( leftButton ) — insert button into container group
    end
    local rightButton
    if ( opt.rightButton ) then
    if ( opt.rightButton.defaultFile ) then — construct an image button
    rightButton = widget.newButton({
    id = opt.rightButton.id,
    width = opt.rightButton.width,
    height = opt.rightButton.height,
    baseDir = opt.rightButton.baseDir,
    defaultFile = opt.rightButton.defaultFile,
    overFile = opt.rightButton.overFile,
    onEvent = opt.rightButton.onEvent
    })
    else — else, construct a text button
    rightButton = widget.newButton({
    id = opt.rightButton.id,
    label = opt.rightButton.label or “Default”,
    onEvent = opt.rightButton.onEvent,
    font = opt.leftButton.font or opt.font,
    fontSize = opt.fontSize,
    labelColor = opt.rightButton.labelColor or { default={ 1, 1, 1 }, over={ 0, 0, 0, 0.5 } },
    labelAlign = “right”,
    })
    end
    rightButton.x = display.contentWidth – ( 15 + rightButton.width * 0.5 )
    rightButton.y = title.y
    barContainer:insert( rightButton ) — insert button into container group
    end
    return barContainer
    end

    —————————code end——————————–

    In main.lua file, I added

    local navbar = require( “navbar” )

    then, declared listener and buttons

    ———————– code start——————-

    local function handleLeftButton( event )
    if ( event.phase == “ended” ) then
    — do stuff
    end
    return true
    end
    local function handleRightButton( event )
    if ( event.phase == “ended” ) then
    — do stuff
    end
    return true
    end

    local leftButton = {
    onEvent = handleLeftButton,
    width = 60,
    height = 34,
    defaultFile = “icon.png”,
    –overFile = “images/backbutton_over.png”
    }

    local rightButton = {
    onEvent = handleRightButton,
    label = “Add”,
    labelColor = { default = {1, 1, 1}, over = { 0.5, 0.5, 0.5} },
    font = “HelveticaNeue-Light”,
    isBackButton = false
    }

    local navBar = widget.newNavigationBar({
    title = “Pickle”,
    backgroundColor = { 0.96, 0.62, 0.34 },
    titleColor = {1, 1, 1},
    leftButton = leftButton,
    rightButton = rightButton,
    includeStatusBar = true
    })

    ——————–code end——————————–

    error message is

    navbar.lua:79: attempt to index global ‘title'(a nil value) stack tracebak:

    so I removed 79 line which is

    leftButton.y = title.y

    and

    rightButton.y = title.y

    it works, but y position is not right.

    how can I make button’s Y position the same with Title’s Y position?

    • Rob Miracle
      Posted at 11:31h, 25 December

      Please ask in the forums. It’s really hard to work with code in the comments.

  • Yang
    Posted at 17:51h, 07 March

    Hey Rob,
    Great tutorial, I was able to implement this in my app with ease, the only problem(?) I have is I cant manage to change the font size of either buttons or title of the navBar, and the buttons seem to be way bigger than the actual label on the button(if its a label button), so you can tap the title and it will actually read as if you tapped the right button (I tried changing the width of the buttons and it didn’t work, I’m not sure if it was intended to work this way.

    Thank you,
    -Yang L.

    • Rob Miracle
      Posted at 18:48h, 07 March

      I noticed that recently. I don’t have a solution yet. But then anyone can look at the code in the tutorial and make sure the size is getting passed through.

      • Yang
        Posted at 18:29h, 08 March

        What I did to fix it was add
        width = opt.rightButton.width (same goes for left button)
        to the text button when adding the navBar into the widgets libraby.
        I should’ve caught this before posting a comment here, hope this helps anyone who might have had the same problem!

        -Yang L.