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.
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;
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. I 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.
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:
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
Leave a Reply