iOS UITableView 100% Programmatically in Swift 4.0
In this swift tutorial, you are going to learn how to build a custom UITableView programmatically using Swift 4.0 with STEP by STEP instructions.
Along the way, I am going to be showing you how to create a custom UITableViewCell programmatically from scratch as well.
If you are new to building programmatic UI in Xcode, take a look at this article which covers how to create a simple login screen programmatically.
At the end of this guide, you will be able to build a simple contacts app like the image below.
Sounds interesting! Let’s get started! 🚀

- STEP #1: Create a New Xcode Project
- STEP #2: Get Rid of Storyboard
- STEP #3: Set Root View Controller to the Window Object
- STEP #4: Create Data Model Struct and Class Files [MVC]
- STEP #5: Add UITableView to the ContactViewController
- STEP #6: Add AutoLayout Constraints to the UITableView
- STEP #7: Implement UITableView DataSource Protocol Methods
- STEP #8: Add UINavigationController to the View Controller
- STEP #9: Create Custom UITableViewCell Programmatically
- STEP #10: Implement UITableViewDelegate Protocol Method
STEP #1: Create a New Xcode Project
Create a project by opening Xcode and choose File → New → Project. Then, choose a single view application and name it contactsapp and leave all other settings as is.
Once the project is created, download the asset files in here and unzip it, then drag them into the Assets.Xcassets
file.

As I am going to build this app 100% programmatically, Main.Storyboard
is not needed for this project.
If you want to keep the
Main.Storyboard
, that’s okay, you can still follow along. In that case, you can skip the next two sections and jump right into Step #4: Create a Data Model.
Otherwise, keep reading.
✅Recommended
iOS 13 & Swift 5 – iOS App Development Bootcamp
STEP #2: Get Rid of Storyboard
Delete Main.Storyboard.
Get rid of its reference as well by going to the top level of the project folder → General Tab→ Deployment Info → Main Interface and clear the Main
text.
Delete the ViewController.swift
file as well.

At this stage, the Xcode does not have an entry point, meaning the project does not have a root view controller that it points to, which will normally appear first followed by the splash screen when you run the app.
Recommended
STEP #3: Set Root View Controller to the Window Object
• In Xcode, go to File → New→ CocouTouch Class → name it ContactsViewController and make it a subclass of UIViewController.
Note: This class could be a subclass of UITableViewController instead of UIViewController that will hook everything to the TableView for us. However, doing it manually will help you to understand the steps involved in the same process.
• Go to AppDelegate.swift
file → didFinishLaunchingWithOptions()
method.
• Create a window object by instantiating UIWindow()
constructor and make it the screen size of the device using UIScreen.main.bounds
.
window = UIWindow(frame:UIScreen.main.bounds)
• Once the window
object is created, make it visible by invoking makeKeyAndVisible()
method on it.
window?.makeKeyAndVisible()
• Finally, assign ContactsViewController as a root controller of it.
window?.rootViewController = ContactsViewController()
When adding the root view controller manually, Xcode will set its background colour to black by default. Let’s change it so that it’s working properly.
• Go to ContactsViewController.swift file → ViewDidLoad() method
view.backgroundColor = .red

Let’s follow the MVC [Model-View-Controller] pattern of organizing files for our contacts app.
STEP #4: Create Data Model Struct and Class Files
I am going to create two files as part of the model for our app, which are Contact.swift
and ContactAPI.swift.
Go ahead and create the contact.swift
file and choose the Swift File option as it will be a UI Independent Class.
This file contains a simple Contact Struct that will have a few properties in it based on the final table view cell image below.

- Profile Image
- Name
- Job Title
- Country
I will be setting the profile image reference names to the same as names so that I do not have to create a property for the profile image.
struct Contact {
let name:String?
let jobTitle:String?
let country:String?
}
Note: Make all the properties
optional
so that you can safely unwrap them using if let when they arenil
rather than using the force unwrapping operator.(!).
Now, create a second file which is ContactAPI.swift.
In real-world applications, all the API calls code would go there. For simplicity sake, I will be adding some dummy data so that I do not have to make an HTTP request as it’s out of our scope topic.
Create a static method called getContacts(
)
inside the ContactAPI
class which is responsible to get all of the contacts data.
class ContactAPI {
static func getContacts() -> [Contact]{
let contacts = [
Contact(name: "Kelly Goodwin", jobTitle: "Designer", country: "bo"),
Contact(name: "Mohammad Hussain", jobTitle: "SEO Specialist", country: "be"),
Contact(name: "John Young", jobTitle: "Interactive Designer", country: "af"),
Contact(name: "Tamilarasi Mohan", jobTitle: "Architect", country: "al"),
Contact(name: "Kim Yu", jobTitle: "Economist", country: "br"),
Contact(name: "Derek Fowler", jobTitle: "Web Strategist", country: "ar"),
Contact(name: "Shreya Nithin", jobTitle: "Product Designer", country: "az"),
Contact(name: "Emily Adams", jobTitle: "Editor", country: "bo"),
Contact(name: "Aabidah Amal", jobTitle: "Creative Director", country: "au")
]
return contacts
}
}
As you can see in the above code, I have created an array named contacts and added a few contact objects instantiated with some data and returned it.
Create a property called contacts and assign the value, which would be an array of contact objects, by invoking getContacts()
method on ContactAPI, like the code below.
private let contacts = ContactAPI.getContacts() // model
Note: As you can see, I use static keyword in front of the
getContacts()
methods when declaring it so that I do not have to instantiate ContactAPI class to invokegetContacts()
method.
Pretty straight forward!
✅Recommended
The Complete iOS 11 & Swift Developer Course – Build 20 Apps
STEP #5: Add UITableView to the ContactsViewController
Go to contactsViewController.swift
→ Create a property called contactsTableView
assign to a table view object by instantiating UITableView()
constructor.
let contactsTableView = UITableView() // view
Go to ViewDidLoad() method → Add contactsTableView as a subview of the main view.
view.addSubview(contactsTableView)
Note: Always add the views to the view hierarchy before setting auto layout constraints to them.
STEP #6: Add AutoLayout Constraints to the UITableView
In the ViewDidLoad()
method, add the following code, and enable Auto Layout on contactsTableView by setting translatesAutoresizingMaskIntoConstraints to false.
contactsTableView.translatesAutoresizingMaskIntoConstraints = false
Set the topAnchor of contactsTableView equal to the topAnchor of the main view.
contactsTableView.topAnchor.constraint(equalTo:view.topAnchor).isActive = true
This will make sure that contactsTableView will stick to the top of the main view.
Let’s add the code for left, right and bottom Anchors similar to the topAnchor code.
contactsTableView.leftAnchor.constraint(equalTo:view.leftAnchor).isActive = true
contactsTableView.rightAnchor.constraint(equalTo:view.rightAnchor).isActive = true
contactsTableView.bottomAnchor.constraint(equalTo:view.bottomAnchor).isActive = true
Run the app and you will see the table view on the screen!

Time to populate the data to the view.
✅Recommended
iOS 10 & Swift 3: From Beginner to Paid Professional™
STEP #7: Implement UITableViewDataSource Protocol Methods
UITableViewDataSource protocol has two important methods that we MUST implement to populate data on the contactsTableView, they are numberOfRowsInTableView and cellForRowAtIndexPath.
Before implementing these methods:
• Make contactsViewController conform to the UITableViewDataSource protocol.
contactsViewController: UIViewController, UITableViewDataSource {}
• Let the contactsTableView know where it’s data source protocol methods are implemented, in this case, contactsViewController and in other-words self.
contactsTableView.dataSource = self
At this stage, Xcode will show you an error telling that protocol methods are missing.
Type 'ContactsViewController' does not conform to protocol 'UITableViewDataSource'. Do you want to add protocol stubs?
The easiest way to add numberOfRowsInTableView
and cellForRowAtIndexPath
methods are by clicking the fix button on the right side of the error message.
1. numberOfRowsInTableView
The numberOfRowsInTableView()
method determines how many rows (UITableViewCells) it should create and display in the table view, which would be based on the length of the model array contacts.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return contacts.count
}
2. cellForRowAtIndexPath
The CellForRowAtIndexPath()
will be invoked multiple times depending on the length of the contacts array that the numberOfRowInSection method returns.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath)
cell.textLabel?.text = contacts[indexPath.row].name
return cell
}
Inside this method, declare a variable called cell
by invoking the dequeReusableCell()
on the tableView passing identifier string and indexpath. This identifier string will be used to register this cell for contactsTableView later.
Once the cell has been created, by default, it will have a textLabel
that you can set the data to.
Get the name property from the contact object on each iteration using indexPath.row and assign it to the cell.textLable.text.
Then, return the cell.
At this point, contactsTableView does not know about this cell. Let’s fix it by registering it using the identifier string.
Go to ViewDidLoad()
in the contactsViewController
and register the cell to the tableView
.
contactsTableView.register(UITableViewCell.self, forCellReuseIdentifier: "contactCell")
Note: If you change the identifier string insidecellForRowAtIndexPath, you should come back to the code above and change it there as well.
Run the app, if everything goes well, you should be able to see the contact names on the screen.
But the content of tableview is overlapping with the status bar at the top and with the horizontal bar at the bottom which are outside of the safe area.
• To keep the content in a safe area, we need to add safeAreaLayoutGuide
to the contactsTableView auto layout constraints, like the code below:
contactsTableView.topAnchor.constraint(equalTo:view.safeAreaLayoutGuide.topAnchor).isActive = true contactsTableView.leadingAnchor.constraint(equalTo:view.safeAreaLayoutGuide.leadingAnchor).isActive = true contactsTableView.trailingAnchor.constraint(equalTo:view.safeAreaLayoutGuide.trailingAnchor).isActive = true contactsTableView.bottomAnchor.constraint(equalTo:view.safeAreaLayoutGuide.bottomAnchor).isActive = true
As you can see, I have also replaced from leftAnchor
to leadingAnchor
and from rightAnchor
to trailingAnchor
.
Left and right are absolute to the screen, on the other hand, leading and training will be based on the reading direction of the text.
Run it and you can see the content does not go beyond the non-safe areas which are highlighted with red like in the screenshot below.

Let’s get rid of the red color as we do not need it anymore by removing the code from your viewDidLoad().
view.backgroundColor = .red
Have a look at the Apple Documentation for more information about UITableViewDataSource protocol methods.
STEP #8: Add UINavigationController to the ContactsViewController
Go to AppDelegate.swift file → didFinishLaunchingWithOptions() method.
Set ContactsViewController as a root view controller of UINavigationController.
window?.rootViewController = UINavigationController(rootViewController: ContactsViewController())
Let’s add the title of UINavigationBar at the top as well as changing its background color and the title text color.
Create a method called setUpNavigation() inside contactsViewController file and invoke it inside the viewDidLoad()
method.
func setUpNavigation() {
navigationItem.title = "Contacts"
self.navigationController?.navigationBar.barTintColor = colorLiteral(red: 0.2431372549, green: 0.7647058824, blue: 0.8392156863, alpha: 1)
self.navigationController?.navigationBar.isTranslucent = false
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor:colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)]
}

STEP #9: Create Custom UITableViewCell Programmatically
Rather than using the default UITableViewCell, I am going to create a custom cell to match the final output design like the image below.

• Create a new file called ContactTableViewCell and make it as a subclass of UITableViewCell.
• Get rid of all the code inside this class.
• Add these two methods to the ContactTableViewCell class.
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
• Create a property called profileImageView inside the ContactTableViewCell class which will be on the left side in the ContactTableViewCell as shown in the picture above. This property is instantiated with UIImageView inside closure aka anonymous function.
let profileImageView:UIImageView = {
let img = UIImageView()
img.contentMode = .scaleAspectFill // image will never be strecthed vertially or horizontally
img.translatesAutoresizingMaskIntoConstraints = false // enable autolayout
img.layer.cornerRadius = 35
img.clipsToBounds = true
return img
}()
• Create a nameLabel property which is responsible for showing the title at the top of each cell.
let nameLabel:UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 20)
label.textColor = colorLiteral(red: 0.2549019754, green: 0.2745098174, blue: 0.3019607961, alpha: 1)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
• Declare another property called jobTitleDetailedLabel, which will be showing the job title under the nameLabel.
let jobTitleDetailedLabel:UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 14)
label.textColor = colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
label.backgroundColor = colorLiteral(red: 0.2431372549, green: 0.7647058824, blue: 0.8392156863, alpha: 1)
label.layer.cornerRadius = 5
label.clipsToBounds = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
• Define containerView property, which will be a container for nameLabel as well as jobTitleDetailedLabel.
let containerView:UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.clipsToBounds = true // this will make sure its children do not go out of the boundary
return view
}()
The main reason for containerView is to add both labels to it and to center them vertically against the content view of the cell.
• Finally, create a countryImageView property which will show the flag image on the right.
let countryImageView:UIImageView = {
let img = UIImageView()
img.contentMode = .scaleAspectFill // without this your image will shrink and looks ugly
img.translatesAutoresizingMaskIntoConstraints = false
img.layer.cornerRadius = 13
img.clipsToBounds = true
return img
}()
• Add the views inside init()
method of contactTableView class. All the views should be added inside the contentView which is the top-level view in the contactTableView class.
self.contentView.addSubview(profileImageView)
containerView.addSubview(nameLabel)
containerView.addSubview(jobTitleDetailedLabel)
self.contentView.addSubview(containerView)
self.contentView.addSubview(countryImageView)
Next, set up auto layout constraints for each view.
profileImageView auto layout constraints.
profileImageView.centerYAnchor.constraint(equalTo:self.contentView.centerYAnchor).isActive = true profileImageView.leadingAnchor.constraint(equalTo:self.contentView.leadingAnchor, constant:10).isActive = true profileImageView.widthAnchor.constraint(equalToConstant:70).isActive = true profileImageView.heightAnchor.constraint(equalToConstant:70).isActive = true
In the first line, profileImageView will be set vertically centered using centerYAnchor against its parent view self.contentView.
Height and Width of the profile image are set to 70 and it’s cornerRadius should be half of the height size so that you will have the image in the circle.
containerView auto layout constraints.
containerView.centerYAnchor.constraint(equalTo:self.contentView.centerYAnchor).isActive = true containerView.leadingAnchor.constraint(equalTo:self.profileImageView.trailingAnchor, constant:10).isActive = true containerView.trailingAnchor.constraint(equalTo:self.contentView.trailingAnchor, constant:-10).isActive = true containerView.heightAnchor.constraint(equalToConstant:40).isActive = true
• nameLabel auto layout constraints.
nameLabel.topAnchor.constraint(equalTo:self.containerView.topAnchor).isActive = true nameLabel.leadingAnchor.constraint(equalTo:self.containerView.leadingAnchor).isActive = true nameLabel.trailingAnchor.constraint(equalTo:self.containerView.trailingAnchor).isActive = true
• jobTitleDetailedLabel auto layout constraints.
jobTitleDetailedLabel.topAnchor.constraint(equalTo:self.nameLabel.bottomAnchor).isActive = true jobTitleDetailedLabel.leadingAnchor.constraint(equalTo:self.containerView.leadingAnchor).isActive = true jobTitleDetailedLabel.topAnchor.constraint(equalTo:self.nameLabel.bottomAnchor).isActive = true
• countryImageView auto layout constraints.
countryImageView.widthAnchor.constraint(equalToConstant:26).isActive = true countryImageView.heightAnchor.constraint(equalToConstant:26).isActive = true countryImageView.trailingAnchor.constraint(equalTo:self.contentView.trailingAnchor, constant:-20).isActive = true countryImageView.centerYAnchor.constraint(equalTo:self.contentView.centerYAnchor).isActive = true
All the auto layout constraints are set. Now we need to let the contactsTableView know about the ContactsTableViewCell class.
Change the default UITableViewCell name to ContactsTableViewCell when registering a cell.
Replace from
contactsTableView.register(UITableViewCell.self, forCellReuseIdentifier: "contactCell")
To
contactsTableView.register(ContactTableViewCell.self, forCellReuseIdentifier: "contactCell")
• Cast cell as a ContactTableViewCell when creating a cell inside cellForRowAtIndexPath()
method.
Replace from
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath)
To
let cell = tableView.dequeueReusableCell(withIdentifier: "contactCell", for: indexPath) as! ContactTableViewCell
• Inside the cellForRowAtIndexPath()
method, I am getting the contact object on each iteration and setting it to the property called contact
which will be inside ContactsTableViewCell
Class.
cell.contact = contacts[indexPath.row]
I could directly set all the labels and image view inside cellForAtIndexPath() method, instead, passing the data to the view and letting it present it would be a clean approach in my opinion.
• Create contact property inside ContactsTableViewCell class. As you can see, I have created a computed property with an observer called didSet. The code inside didSet observer will be executed every time contact property is set.
var contact:Contact? {
didSet {
guard let contactItem = contact else {return}
if let name = contactItem.name {
profileImageView.image = UIImage(named: name)
nameLabel.text = name
}
if let jobTitle = contactItem.jobTitle {
jobTitleDetailedLabel.text = " \(jobTitle) "
}
if let country = contactItem.country {
countryImageView.image = UIImage(named: country)
}
}
}
I use if let to safely unwrap data and set it to the view.
Let’s run the app.

As you can see, we need to adjust the height of each cell using UITableViewDelegate protocol to avoid the cells bumping each other.
STEP #10: Implement UITableViewDelegate Protocol Method
Let’s change the height of the cells.
Set ContactsViewController conform to the UITableViewDelegate protocol.
class ContactsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate{ ... }
Then, set it’s delegate to self, like so:
contactsTableView.delegate = self
Implement heightForRowAtIndexPath() method by returning your desired height as a CGFloat.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
Take a look at the Apple documentation to know more about UITableViewDelegate Protocol methods here.
There you have it! 🙂
Final Source Code can be found here.

Recommended