Tutorial: Adding a UITableView programmatically

Martin Lasek
9 min readMar 29, 2019

--

In this tutorial I will show you how simple it actually is to abandon storyboard and create your UI completely in code using a UITableView as an example šŸ”„

Watch the video tutorial of this article on youtube here!

Index

1. Create a new project
2. Delete all unnecessary files and references
3. Add root ViewController
4. Add and constrain UITableView
5. Populate UITableView with data

1. Create a new project

Weā€™ll start by creating a new iOS project ā€” thereā€™s no better feeling than creating a fresh new project šŸ˜

Select ā€œSingle View Appā€
Give it a cool name and save it anywhere you wish!

Thatā€™s it for the first chapter ā€” on to the next one!

2. Delete all unnecessary files and references

This is a step I love to do: deleting the Main.storyboard šŸ™ŒšŸ»āœØ
Make sure to ā€œmove to trashā€ when deleting the file. And also delete the reference to it in the project settings in the ā€œGeneralā€ tab at ā€œMain Interfaceā€.

Deleting (moving to trash) the Main.storyboard
Deleting the reference to the Main.storyboard by clearing this input.

3. Add root ViewController

In this chapter we are de-mystifying what the storyboard actually does for us by doing it ourselves ā€” and itā€™s surprisingly simple šŸ’ŖšŸ»

The entry point of our App is the file AppDelegate.swift letā€™s go there and reduce the noise by deleting all functions except the one you see down below:

Delete all functions except for the one displayed here

You can see that the class AppDelegate has an optional property called window. That is actually the window where all our views of all our view controllers will be shown within. An App indeed can have multiple windows but can only have one window active at a time. Most of the Apps are only implementing one window. I honestly donā€™t even know (yet) a use case where you would need multiple windows šŸ˜„.

If youā€™re interested in learning more about UIWindow just put your cursor on it and open the Inspector (right side of Xcode) it will show the documentation to this class. I always find it interesting to read about things there šŸ˜Š!

We will have to (1) instantiate a new UIWindow with the size of our phone screen and assign it to the window property. We then (2) instantiate the view controller we want to display when the app launches and assign it to the rootViewController property of window. Finally we will (3) make the window ā€œkeyā€ and ā€œvisibleā€. Basically telling our app that this is our active window.

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?

func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:[UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// (1)
window = UIWindow(frame: UIScreen.main.bounds)
// (2)
let viewController = ViewController()
window?.rootViewController = viewController
// (3)
window?.makeKeyAndVisible()
return true
}
}

Although you see the code above hereā€™s another screenshot just to be sure šŸ˜Š!

Creating a UIWindow with the screen size of our phone, assigning a rootViewController and making the window ā€œkeyā€ and ā€œvisibleā€.

If you now run our App everything should ā€œjust workā€ and you should see a black screen ā€” that is a good sign!

In case your App crashes try and hit cmd + shift + k to clean the build folder.

4. Add and constraint a UITableView

In our ViewController.swift file we will add a property to the class called tableView:

import UIKit

class ViewController: UIViewController {
let tableView = UITableView()

override func viewDidLoad() {
super.viewDidLoad()
}
}

Next we will setup our UITableView šŸ˜Š!

Letā€™s create an own function that will take care of setting up our UITableView and call that function from within viewDidLoad()

import UIKit

class ViewController: UIViewController {
let tableView = UITableView()

override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
}

func setupTableView() {
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
}

Okay there is a lot happening here. Letā€™s break it down. The first thing we are doing is adding our tableView as a subview to the ViewControllers view property. This is crucial. Because next we are constraining our tableView so basically saying what where it is place and what size it should have.

Before you constraint a UIView make sure to always add it as a subview first!

In order to constraint a UIView (which UITableView is a subclass of) you have to set translatesAutoresizingMaskIntoConstraints to false.

After doing that you can start constraining your view. Here we basically say that the top of our tableView should be set to the top of the view. Same goes for the left, bottom and right side of our tableView. When doing so we also have to ā€œactivateā€ each constrain by setting their isActive property to true.

Now we are able to run the app and see a beautiful tableView with an empty grid that shows where the rows would go if there were any šŸ˜Š!

Empty UITableView

Thereā€™s one last adjustment Iā€™d like to do. Do you see at the bottom of the app the black bar? If you try to scroll up and down that grid you will notice that the grid will go underneath the bar which I donā€™t mind but it will also go under the notch and the status bar with the time and everything. We can easily fix that by staying within a safe area when constraining our tableview!

import UIKit

class ViewController: UIViewController {
let tableView = UITableView()
var safeArea: UILayoutGuide!

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
safeArea = view.layoutMarginsGuide
setupTableView()
}

func setupTableView() {
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: safeArea.topAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
}

We just add another property and call it safeArea and when the loadView() function gets called we assign the views layoutMarginsGuide to it. This layoutMarginsGuide is the whole view but with the consideration of margins. That way we donā€™t have to deal ourselves ā€œhow muchā€ spacing we want from the top to not go under the notch or even if you donā€™t want to go under that black bar on the bottom you could go and use safeArea.bottomAnchor āœŒšŸ»šŸ˜Š

5. Populate UITableView with data

Populating a tableview is really simple and only requires three steps. The first step is to define an array containing our data we want to display:

import UIKit

class ViewController: UIViewController {
let tableView = UITableView()
var safeArea: UILayoutGuide!
var characters = ["Link", "Zelda", "Ganondorf", "Midna"]

override func viewDidLoad() {
...
}

func setupTableView() {
...
}
}

The second step is to register a UITableViewCell class that we want to have an instance of for each row. And we extend our ViewController by the protocol UITableViewDataSource so we can afterwards tell our tableview that itā€™s actually our ViewController whoā€™s storing the data for all rows. How to tell the tableview about it is step three. But first letā€™s continue with step two:

import UIKit

class ViewController: UIViewController {
let tableView = UITableView()
var safeArea: UILayoutGuide!
var characters = ["Link", "Zelda", "Ganondorf", "Midna"]

override func viewDidLoad() {
...
}

func setupTableView() {
...
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
}

extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return characters.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = characters[indexPath.row]
return cell
}
}

Registering a UITableViewCell with an identifier is important so that the tableview knows what class to use to create a cell instance of. The power of registering a UITableViewCell shines the most when you are creating a custom cell that inherits from UITableViewCell because in that case youā€™d register your custom cell here. Regarding the identifier ā€” this one has to be a unique string. Unique compared to all cell classes you are registering. Well we register only one cell class which means we can be uncreative here hahah: ā€œcellā€ šŸ˜‹

Registering a UITableViewCell with an identifier is important

Extending the protocol UITableViewDataSource gives us two functions we have to implement. Both functions will be called by our tableview. How that works is explained in step three. Back to step two. The first function returns the number of rows. Well, simply the count of our array is the number of rows we want right? This function is called once when the tableview loads. The second function is called for each row the tableview has. Well, itā€™s the count of our array right? Inside the second function we are using the tableview to dequeue a reusable cell. What does that mean?

You have to know that if we have for example an array with 50 elements that our tableview can only display about for example 21 rows at once simply because our screen isnā€™t big enough to display all 50.

That means it will only create about 21 instances of our cell class and as soon as you scroll down and the first few cells are leaving the screen the new cells that are coming into the screen are actually the exact instances of cells that left the screen. Literally. They even still have all the values if we donā€™t go and re-assign for example the textLabel of a cell instance.

Visualization of re-used cells when scrolling to display further array elements

So calling tableview for a re-usable cell in this line of code here:

let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

will always return a cell. It will return a fresh instance of a cell if there is no re-usable cell. This is the case for the first ~21 cells right? When this line is executed for the first row, well at that time there is no cell yet to be re-used. There is no cells at all yet. And so in that case it will create a new instance based on the class we have registered and return that. That line of code does two jobs and thatā€™s super convenient! Thanks dequeueReusableCell šŸ‘ŠšŸ»šŸ˜Š!

Cells that are still visible in the screen arenā€™t ā€œfreeā€ to be returned as re-usable cells

Next to this line of code (assigning a value to textLabel):

cell.textLabel?.text = characters[indexPath.row]

Since our tableview has the same amount of rows as our array has elements and since the tableview calls our function for each row and also passes the indexPath of the current row its at ā€” we can use that indexPath to access the correct element of our array šŸ™ŒšŸ»āœØ

Finally we return the cell and can run the app and see the result! You made it!

Now donā€™t forget to set the dataSource of your tableView to let it know which class is going to give him the information about the data that it should display:

import UIKit

class ViewController: UIViewController {
let tableView = UITableView()
var safeArea: UILayoutGuide!
var characters = ["Link", "Zelda", "Ganondorf", "Midna"]

override func viewDidLoad() {
...
}

func setupTableView() {
...
tableView.dataSource = self
}
}

extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return characters.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = characters[indexPath.row]
return cell
}
}

You successfully implemented a tableview programmatically šŸŽ‰!

Watch the video tutorial of this article on youtube here!

I am really happy you read my article! If you have any suggestions or improvements of any kind let me know! Iā€™d love to hear from you! šŸ˜Š

--

--

Martin Lasek
Martin Lasek

Written by Martin Lasek

I'm an always optimistic, open minded and knowledge seeking fullstack developer passionate about UI/UX and changing things for the better :)

Responses (8)