iOS UITableView 100% Programmatically in Swift 4.0

iOS Swift

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! 🚀

uitableview-programmatically-in-swift

uitableview-programmatically-in-swift

STEP #1: Create a New Xcode Project

Create a project by opening Xcode and choose FileNew → 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.

add-assets-to-xcassets

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.

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.

get-rid-of-storyboard

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.

STEP #3: Set Root View Controller to the Window Object

• In Xcode, go to FileNewCocouTouch 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.

  1. Profile Image
  2. Name
  3. Job Title
  4. 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 are nil 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 invoke getContacts() method.

Pretty straightforward!

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.

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.

Nice…

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 out 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 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 everytime 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.

custom-uitableviewcell-programmatically

custom-uitableviewcell-programmatically

As you can see, we need to adjust the height of each cell using UITableViewDelegate protocol t0 avoid 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.

uitableview-programmatically-in-swift

uitableview-programmatically-in-swift

Comments Count: 0 2