Living Without Storyboards or Interface Builder: Grids and a Grid Class

Using Grids

As Steve Jobs liked to say… “Oh, and one more thing more thing.”
In my last post Living without Storyboards or Interface Builder, I mentioned scribbling on a piece of paper.

IMG_4295

I do that often, but it is helpful to go one step further: use a preset grid.

The web building world, at least the graphic designers there, love these things. Here’s two examples one for a iPhone 4 and 5;

iphone 4 grid iphone 5 grid

The white areas are content, the color areas are space called padding between the content. I then use them to plot our elements on a screen. I can for example plot something out like I have at the right. iphone 4 grid with buttonsI get to know exactly where everything is and everything aligns perfectly.

So let’s think this out:

Grids are can be thought of as cells. This is the way web development Tkinter and other systems thinks of content. Both CSS and Tkinter thinks of padding as an internal margin of the cell, margins are external to the object.

Programming Grids

Usually I’m trying to translate from Xcode to Python, but this post is a reverse situation. Tkinter can take my paper grids and translate them into code relatively easy with .grid method. If working in Interface builder, this is a small problem. Using auto layout , one can make sense of a grid, if one is careful. Get into Objective-C and Xcode with some programmatic UIViews, and you are stuck with some very messy CGRect frames. I really want something more like Tkinter’s .grid()method. I want to draw a 6×9 grid with 10 point padding on paper and say something equivalent to

.grid(row =6, column = 0, rowspan = 3 columnspan =4)

With that code I get the button on any device in the same spot, stretched correctly for the device.

So I made a class for Objective-C to do exactly that. Let’s look at it.Screenshot 2014-04-24 07.21.20

Let’s start with the illustration of a content cell that is 168px by 168px. Of the 168px, we take 12pixels on each side as padding, leaving us with 144px for actual content. This math works for paints as well a pixels, so it works for setting up a class to calculate the size of a cell anywhere.

There are the following possibilities for making a grid:

  • A completely flexible grid: give a number of columns and rows in the grid. Scale the dimensions so all cells fit in the grid. This will be rectangles, not squares. It will always fit a complete grid in the view.
  • A row grid of squares : Given the number of rows,  calculate a grid size based on squares of that size. You may have to crop squares on the bottom.
  • A column grid of squares: Given the number of columns, calculate a grid based on squares of that size. Again you may have partial squares on one side.

There are other possibilities  to make a grid. For the two which leave partial squares, we could add the partial square to the margin.

I’ll do the first case here. The others I’ll leave to others or I’ll do them later.

Coding Grids

What I’m calling the margin is  either an external margin of the cell, or a padding of the superview of the cell, which contains the view we want to position. We will set a public property for the padding

@property CGFloat padding;

If we think of margins as a padding of a superview, with one method we can do two steps with one method. Given a frame of a certain size, we find the space we can put content into due to the padding. This work for our margins too.

//makes a frame given a size and padding.
-(CGRect)makeContentRectFromSize:(CGSize)size {
    CGRect frame;
    frame.size.width = size.width  -  (2 * self.padding);
    frame.size.height = size.height - (2 * self.padding);
    frame.origin.x = self.padding;
    frame.origin.y = self.padding;
    return frame;
}

Once we know that, we can set the size of the grid. We’ll use manually set grids not automatically proportional like .grid(). I really debate about the how to name properties for column and row size of the grid. I could make this a CGSize, or two separate values. I started with separate properties to parallel Tkinter, and I’ll probably make some convenience methods later to accept CGSize as well. We have as public methods in our .h:

@property int totalRows;
@property int totalColumns;

Next we figure out the size of the cell by finding the usable area in a frame, and dividing it up to get a height and width for a cell.

-(CGSize)cellSize{
    CGRect frame = [self makeContentRectFromSize: self.viewFrame.size];
    CGSize cellFrameSize;
    cellFrameSize.width = frame.size.width/self.totalColumns;
    cellFrameSize.height = frame.size.height/self.totalRows;
    return cellFrameSize;
}

We’ll need a few more public properties: row and column origin and size:

@property int row;
@property int column;
@property int rowspan;
@property int columnspan;

We can now use everything to make a frame to put our content. First find the origin position of the CGRect in the coordinate system of its parent. We need to put a few things together:

  • The size of the cell times its row or column location,
  • A bit more for its padding
  • A bit more for the margin

For the size we have two parts:

  • The size of the cell times the span of the cell
  • Remove a bit for the padding.

We now use those parts to write the following method:

 
-(CGRect)gridLayoutRect{
//step 1 - find the size of the space inside the margin
    contentFrame = [self makeContentRectFromSize:self.viewFrame.size];
//step 2 - find the size of the cell
    CGSize cellFrameSize = [self cellSize];
//Step 3 - find the coordinates of the origin taking into account padding and margin
    subviewFrame.origin.x = (cellFrameSize.width * self.column) + self.padding + contentFrame.origin.x; 
    subviewFrame.origin.y = (cellFrameSize.height * self.row) + self.padding + contentFrame.origin.y; 
//Step4 - find the size of the rect
    subviewFrame.size.height = cellFrameSize.height * self.rowspan - self.padding;
    subviewFrame.size.width = cellFrameSize.width * self.columnspan - self.padding;
//Step 5 — return it
    return subviewFrame;
}

I made a few overrides for this method which will help in its use:

-(CGRect)gridLayoutRect:(CGPoint)origin size:(CGSize)size{
    self.row = origin.y;
    self.column = origin.x;
    self.columnspan = size.width;
    self.rowspan = size.height;
    return [self gridLayoutRect];
}

-(CGRect)gridLayoutRect:(CGRect)grid{
    self.row = grid.origin.y;
    self.column = grid.origin.x;
    self.columnspan = grid.size.width;
    self.rowspan = grid.size.height;
    return [self gridLayoutRect];
}

We need some initializers as well:

-(void) initWithFrame:(CGRect)frame rows:(int)totalRows columns:(int)totalColumns{
    _viewFrame = frame;
    _totalRows = totalRows;
    _totalColumns = totalColumns;
//set some default values
    _rowspan =1;
    _columnspan =1;
    _padding = 10;
    _row = 0;
    _column = 0;
    
}

-(void) initWithFrame:(CGRect)frame{
    _viewFrame = frame;
//set some default values
    _totalRows = 6;
    _totalColumns  = 9;
    _rowspan =1;
    _columnspan =1;
    _padding = 10;
    _row = 0;
    _column = 0;

}

Import that into the view controller and initialize the object in ViewDidLoad.

Using our Class

Now I can use this like this in a text field for our power panel:

//make the  TIME text field
    timeText = [[UITextField alloc]initWithFrame:[myGridLayout gridLayoutRectRow:1 column:2 rowSpan:1 columnspan:2]];
    timeText.placeholder = @"00";
    [self.view addSubview:timeText];

One cool use is to make a quick series of buttons:

    //make a series of buttons with a for loop
    for(int i=0;i<5;i++){
        //make the button
        UIButton *seriesButton=[UIButton buttonWithType:UIButtonTypeSystem];
        
        //set the grid location
        myGridLayout.row = 3;
        myGridLayout.column = i;
        myGridLayout.columnspan = 1;
        myGridLayout.rowspan = 1;
        seriesButton.frame = [myGridLayout gridLayoutRect];
        
        //properties for the button
        seriesButton.backgroundColor = [UIColor blueColor ];
        [seriesButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [seriesButton setTitle:[NSString stringWithFormat:@"%i",i] forState:UIControlStateNormal];
        //set the target action.
        [seriesButton addTarget:self action:@selector(sequenceButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
        //add to the view
        [self.view addSubview:seriesButton];
    }

When we put everything together, build and run we get this:
iOS Simulator Screen shot Apr 25, 2014, 4.12.03 PM

Using Selectors for UIButtons and Other Things

There is one line here which needs comment.

 [seriesButton addTarget:self action:@selector(sequenceButtonPressed:) forControlEvents:UIControlEventTouchUpInside];

The selector uses SquenceButtonPressed: that points to this method:

-(void)sequenceButtonPressed:(UIButton *)sender{
timeText.text = sender.titleLabel.text;
}

I didn’t point it out before but thought I might now. You do not spell out the parameter in the selector. But you do put a colon to tell the system it is there. So just like a target action for IB, you can set up target actions (or use ones that already exist) that refer to their button.

There is a lot that can be done with this, but this is a basic framework for setting up grids in Xcode. While IB would seem to make this unnecessary, there are places it becomes very important. I already mentioned UIImagePickerController’s camera overlay. One I haven’t talked about here in any detail is the lack of IB for Sprite Kit. There are no buttons in Sprite kit, but there are time buttons are very necessary. That issue we’ll take up in an upcoming installment of the SlippyFlippy Challenge.

The full code can be found at : https://github.com/MakeAppPie/SGNoStoryBoardGrid

4 Replies to “Living Without Storyboards or Interface Builder: Grids and a Grid Class”

    1. Follow the example where I make a row of buttons. If you don’t want the root view, instantiate the view you do want, and use that instead of self.view in line 20. Enclose all that in another for loop. In line 7 of the example, change the 3 to the variable used in the new for loop for the row position. That’s pretty much it.

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