iOS Training from beginner to advanced
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.
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:
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.
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:
For the size we have two parts:
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
.
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:
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
Pingback: Living Without Storyboards: Make a Clock with Sprite Kit | Making App Pie
Pingback: Make a Clock in Sprite Kit: Adding Animation to the Clock | Making App Pie
How can I make a grid of UIButtons on top of a UIView using this?
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 anotherfor
loop. In line 7 of the example, change the3
to the variable used in the newfor
loop for the row position. That’s pretty much it.