Tutorial: Adding a UITableView programmatically
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 š
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ā.
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:
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 š!
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 š!
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.
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!