From Apple to Raspberry Pi: Living without Storyboards and Interface Builder

appleberryIn iOS, avoiding Interface Builder is nearly impossible. IB is old – as old as the Xcode SDK itself. Interface builder was one of the components of NextStep, and thus one of the reasons Steve Jobs got his old job back, who then developed NextStep into the platform for all Apple products. In more recent versions of Xcode and Cocoa, Storyboards organized IB views into whole apps without a lot of code. Outlets, segues and delegates might sometimes be a headache, but dragging and dropping most of the user interface made life easy for many developers, including me.

Interval Run Calc storyboard
The storyboard for Interval Run Calc, a running calculator I designed and implemented in fall 2013.

Coding a UI in Xcode

There are times even in Xcode where interface builder is not an option. I had one situation like this placing buttons and labels into UIImagePickerController's property cameraOverlayView when I was writing MiPlatform for Scientific Device Laboratory. I had to code the user interface. For example I could set up a label like this:

 
    UILabel *timeLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 60, 40, 40)];
    timeLabel.text = @"Time";
    [self.view addSubview:timeLabel];

I can set up  a button like this:

    //make the on/off button
    onOffButton=[UIButton buttonWithType:UIButtonTypeSystem];
    onOffButton.frame = CGRectMake(10,10, 100, 50);
    [onOffButton setTitle:@"OFF" forState:UIControlStateNormal];
    [onOffButton addTarget:self action:@selector(OnOffButtonPressed) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:onOffButton];

In general, setting up a control in Objective-C is this:

  • Make an instance of a UIButton or UILabel using a CGRect to define the frame of the control.
  • Set attributes for the Label or Button
  • For a button, use the addtarget:action:forControlEvents: method to set up one or more event handlers for the button.
  • Add the control to the view or subview

Reviewing Tkinter Buttons and Labels

Of course in Python with Tkinter, there is no such thing as a storyboard or Interface Builder.  There is a lot of differences between Xcode and Tkinter when it comes to setting up user interfaces, but there are similiarities when working in code.  For comparision, a label would look like this:

time_value.set(2400.0)
time_value = DoubleVar()
time_value_label = Label(frame, textvariable = time_value, justify = LEFT)
time_value_label.grid(row =0, column= 1, rowspan = 1, sticky = W)

A button would look like this:

onOff_button = Button(frame, command = onOffButtonPressed)
onOff_button.grid(row = 2, column = 0, rowspan=2, columnspan = 2,sticky = SW + NE)
onOff_button.configure(textvariable = onOff_button_title)

In the case of Buttons and Labels in Tkinter, the setup for the widget goes like this:

  • Define and initialize your variables
  • Make an instance of a Button or Label
  • Use the grid method to position the widget
  • Set attributes for the Label or Button
  • For a button, use the command attribute to set a event handler for the button.
  • If the button title or label text needs to be dynamic, use the variabletext attribute to bind a defined Tkinter variable

Using Thumbnail Sketches

When we are doing all the work by hand it is nice to plan it out first, otherwise we waste a lot of time adjusting positions in code. I usually just take a plain piece of paper and do a few thumbnail sketches, until I find one version I like. This page took five minutes to do, but from it, I learn most of what I need to know to put the UI together.

IMG_4295
Thumbnail sketches of the Power Panel User interface.

On the bottom left of the thumbnail page, I added a grid to the thumbnail sketch. Now I have an idea of where to place everything neatly.

I could place them in Xcode now by figuring a few points and widths. I often do something like this to start:

    _topLeft.x = CGRectGetMinX(self.view.frame);
    _topLeft.y = CGRectGetMinY(self.view.frame);
    _bottomRight.x = CGRectGetMaxX(self.view.frame);
    _bottomRight.y =CGRectGetMaxY(self.view.frame);

By multiplying by the percentage from the _bottomRight point in x and y, I can then position anything relative to the size of the frame. Like grid in TKinter this  will resize on its own for any screen. This is a technique I use frequently with Sprite Kit as well. A better way in Xcode for UIViews is to use Auto Layout, which you can access from code and IB, but that is another topic — if not a series of blog entries.

Coding the Power Panel in Python

iPhones do not have GPIO pins. That is a Raspberry Pi feature. If this is to become a control panel for a device, it makes a lot more sense to write this code in Python for the Raspberry Pi.
From the thumbnail alone I can often figure out the grid. Notice the bottom right of the photo above. I drew a grid over the UI. We have three rows and two columns.  All the elements fit into this structure, with the button spanning both columns. Though you don’t have to do this, I drew this up  quickly, but a little more formally for clarity:

grid

Let’s make a Tkinter UI. As covered in other posts, I’d start by making a window and frame:

from tkinter import *

#make the window
root=Tk()
root.title('Power Panel #1')

#make the frame
frame = Frame(root)
frame.grid(row=0, column=0)

#start the main loop
mainloop()

After making the frame I can add the two labels for Time and Voltage. These will go in Column 0, rows 0 and 1 respectively:

#render Labels
voltage_label = Label(frame, text = 'Voltage')
voltage_label.grid(row = 1,column = 0, sticky = E)

time_label = Label(frame, text = 'Time')
time_label.grid(row = 0, column = 0, sticky = E)
 

Note the use sticky=E. It’s good style to align the labels to the right side of the grid, and the sticky does this. Now for a dynamic label for the time in row 0, column 1:

time_value = DoubleVar()
time_value.set(2400.0)
time_value_label = Label(frame, textvariable = time_value, justify = LEFT)
time_value_label.grid(row =0, column= 1, rowspan = 1, sticky = W)

To keep this label aligned correctly, we used sticky = W. Finally we add the button spanning the bottom:

onOff_button_title = StringVar()        
onOff_button_title.set('Off')
onOff_button = Button(frame, command = onOffButtonPressed)
onOff_button.grid(row = 2, column = 0, rowspan=2, columnspan = 2,sticky = SW + NE)
onOff_button.configure(textvariable = onOff_button_title)

We will need an event handler as well for the button to work. For this exercise, this handler will toggle the button title:

#event Handlers
def onOffButtonPressed():
    if onOff_button_title.get() == 'Off':
        onOff_button_title.set('On')
    else:
        onOff_button_title.set('Off')

Adding the Entry Widget

In Xcode, we have UITextField for keyboard entry of data. The Python equivalent is Entry. We will set up the desired Voltage as an Entry, which codes like a label with a textvariable:

#render text fields
voltage_value = DoubleVar()
voltage_value.set(120.0)
voltage_value_entry = Entry(frame, textvariable = voltage_value)
voltage_value_entry.grid(row=1, column = 1)

Run and build the application. We should get a window like this — which looks nice and neat.
Screenshot 2014-04-20 07.51.13

When we press the button, it toggles on and off.

User interfaces can get messy. Planning them out on paper certainly helps no matter if you are working on a Raspberry Pi or an iPhone. I often do paper thumbnails even when working in Interface Builder. A lot of my Interface builder Auto Layout headaches disappear if I do that simple paper and pencil exercise first.

Python Code for the Power Panel

Here’s the full Python code. Note a change here. While above I kept variables with the widgets as they were being created I usually group variable declarations and initializations together, as I’ve done below. This is a style preference, and the merits of each style can be debated forever. If you’ve worked with a static-typed language like C, you might have a similar style.

#Power_Panel_01
#code By Steven Lipton 04 19 14

from tkinter import *

#make the window
root=Tk()
root.title('Power Panel #1')

#make the frame
frame = Frame(root)
frame.grid(row=0, column=0)

#make our variables
voltage_value = DoubleVar()
time_value = DoubleVar()
onOff_button_title = StringVar()

#set initial values for the variables
voltage_value.set(120.0)
time_value.set(2400.0)
onOff_button_title.set('Off')

#event Handlers
def onOffButtonPressed():
    if onOff_button_title.get() == 'Off':
        onOff_button_title.set('On')
    else:
        onOff_button_title.set('Off')
 
#render button
onOff_button_title = StringVar()        
onOff_button_title.set('Off')
onOff_button = Button(frame, command = onOffButtonPressed)
onOff_button.grid(row = 2, column = 0, rowspan=2, columnspan = 2,sticky = SW + NE)
onOff_button.configure(textvariable = onOff_button_title)

#render Labels
voltage_label = Label(frame, text = 'Voltage')
voltage_label.grid(row = 1,column = 0, sticky = E)

time_label = Label(frame, text = 'Time')
time_label.grid(row = 0, column = 0, sticky = E)

time_value_label = Label(frame, textvariable = time_value, justify = LEFT)
time_value_label.grid(row =0, column= 1, sticky = W)


#render text fields
voltage_value_entry = Entry(frame, textvariable = voltage_value)
voltage_value_entry.grid(row=1, column = 1)



#start the main loop
mainloop()

A Power Panel  in Objective-C

Just in case you want to see equivalent code in Objective-C, here’s a control panel set up for iPhone. I self-contained this into private and instance variables merely for compactness. In a true Model-View-Controller pattern however, it is likely you would need the UIButton to be a public property, so you can make one class a view and another its view controller.

//
//  SLViewController.m
//  HandCodedUI
//
//  Created by Steven Lipton on 4/19/14.
//  Copyright (c) 2014 Steven Lipton. All rights reserved.
//

#import "SLViewController.h"
@interface SLViewController ()

@end

@implementation SLViewController{
    BOOL isPowerOn;
    UIButton *onOffButton;     
}

-(void)OnOffButtonPressed{
    if (isPowerOn){
        isPowerOn=NO;
        [onOffButton setTitle:@"OFF" forState:UIControlStateNormal];
    }else{
        isPowerOn=YES;
        [onOffButton setTitle:@"ON" forState:UIControlStateNormal];
    }
}
-(void)makeViewsOnController{
    //an example of coding our own buttons, labels and text fields
    //make the  static time label
    UILabel *timeLabel = [[UILabel alloc]initWithFrame:CGRectMake(10, 60, 95, 40)];
    timeLabel.text = @"Time";
    [self.view addSubview:timeLabel];
    //make the  Static voltage label
    UILabel *voltageLabel = [[UILabel alloc]initWithFrame:CGRectMake(10, 100, 95, 40)];
    voltageLabel.text = @"Voltage";
    [self.view addSubview:voltageLabel];
    
    //make the  TIME text field
    UITextField *timeText = [[UITextField alloc]initWithFrame:CGRectMake(105,60,95,40)];
     timeText.placeholder = @"00";
    
    [self.view addSubview:timeText];
    
    //make the  voltage  text field
    UITextField *voltageText = [[UITextField alloc]initWithFrame:CGRectMake(105,100, 95, 40)];
    voltageText.placeholder = @"150";
    [self.view addSubview:voltageText];
    
    //make the on/off button
    onOffButton=[UIButton buttonWithType:UIButtonTypeSystem];
    onOffButton.frame = CGRectMake(10,150, 190, 40);
    [onOffButton setTitle:@"OFF" forState:UIControlStateNormal];
    [onOffButton addTarget:self action:@selector(OnOffButtonPressed) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:onOffButton];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    isPowerOn=NO;
    [self makeViewsOnController];
}

@end

2 thoughts on “From Apple to Raspberry Pi: Living without Storyboards and Interface Builder”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s