Today we will implement iOS TableView UITableView class in our application without using Storyboard.
We have implemented a UITableView using Storyboards in Objective-C here. We’ll use Swift 3 in this tutorial.
iOS TableView UITableView
There are lots of major code changes in Swift 3 over Swift 2.3. For starters, this is just another language that we’ll be using in all our future iOS Tutorials. But if you haven’t yet migrated over to Swift 3, I’ll be highlighting the significant changes along the length of this tutorial.
iOS TableView Getting Started
First things first, create a New Project and choose the template as Single View Application for now. In the following screen enter the relevant Application and Bundle Identifier names. A sample is shown below.
Once your project is ready, select the Main.storyboard file and delete it(Move to trash).
Having done that let’s run the empty project on the simulator to see if all’s good.
Watching the logs in the bottom right sidebar would display an error as: Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Could not find a storyboard named 'Main' in bundle NSBundle
.
We need to remove the storyboard references from our Project Configuration.
For that go to Info.plist
and set an empty value in the field Main storyboard file base name.
We’re now ready to code our application without the need for Storyboards. Let’s select ViewController.swift
and start coding.
iOS UITableView Example Code
Now that we’ve deleted the Main.storyboard the ViewController.swift is unlinked. We need to find a way to make it the root view controller of the application without creating another storyboard.
The AppDelegate.swift file comes to our rescue. The AppDelegate class ensures that your app interacts properly with the system and with other apps.
We’ve overridden the function application
in the following way:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window!.backgroundColor = UIColor.white
window!.rootViewController = ViewController()
window!.makeKeyAndVisible()
// Override point for customization after application launch.
return true
}
This makes ViewController the root view controller and the first ViewController to be launched in our application. Let’s jump onto the ViewController.swift file where we would be displaying and customising a UITableView programmatically.
Let’s begin by implementing the UITableViewDelegate, UITableViewDataSource protocols in our ViewController.swift class as shown below:
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
}
}
The above three functions require a mandatory implementation else you’ll get a compile-time error.
Swift 3 Note: The function parameters now consistently include labels for the first parameters. In essence, the last part of the function’s name has been moved to be the name of the first parameter.
For example:
//Swift 2.3
override func numberOfSectionsInTableView(tableView: UITableView) -> Int
//Swift 3
override func numberOfSections(in tableView: UITableView) -> Int
Besides Swift 3 omits the needless words from the functions since they are self evident.
Back to the code, we need to initialize and add the UITableView in our view.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView = UITableView()
var tableData = ["Beach", "Clubs", "Chill", "Dance"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tableView = UITableView(frame: self.view.bounds, style: UITableViewStyle.plain)
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = UIColor.white
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "my")
view.addSubview(tableView)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "my", for: indexPath)
cell.textLabel?.text = "This is row (tableData[indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
}
The rows of the TableView are populated from the tableData array.
The TableView style can be also set as UITableViewStyle.grouped
. The output when the above code is run is shown below.
Few places of improvement:
We need to add a padding between the view and the status bar. Also we have to remove the unnecessary row dividers from the empty cells.
To add a padding add the following line in viewDidLoad method.
tableView.contentInset.top = 20
To remove the empty cells and dividers we need to wrap the height of the TableView till the number of non-empty rows and add a footer at the bottom.
The ViewController.swift code after including the above two implementations is given below.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView = UITableView()
var tableData = ["Beach", "Clubs", "Chill", "Dance"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tableView = UITableView(frame: self.view.bounds, style: UITableViewStyle.plain)
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = UIColor.white
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "my")
tableView.contentInset.top = 20
let contentSize = self.tableView.contentSize
let footer = UIView(frame: CGRect(x: self.tableView.frame.origin.x,
y: self.tableView.frame.origin.y + contentSize.height,
width: self.tableView.frame.size.width,
height: self.tableView.frame.height - self.tableView.contentSize.height))
self.tableView.tableFooterView = footer
view.addSubview(tableView)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "my", for: indexPath)
cell.textLabel?.text = "This is row (tableData[indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
}
The output from the above ViewController implementation is given below.
There’s a scrolling issue in the above output. The TableView on scrolling overlaps with the status bar instead of going under it. Let’s fix this by setting bounds for the TableView as given below.
let barHeight: CGFloat = UIApplication.shared.statusBarFrame.size.height
let displayWidth: CGFloat = self.view.frame.width
let displayHeight: CGFloat = self.view.frame.height
tableView.frame = CGRect(x: 0, y: barHeight, width: displayWidth, height: displayHeight - barHeight)
Adding this in the ViewController.swift file would produce the below code and it’s output.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView = UITableView()
var tableData = ["Beach", "Clubs", "Chill", "Dance"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tableView = UITableView(frame: self.view.bounds, style: UITableViewStyle.plain)
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = UIColor.white
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "my")
let barHeight: CGFloat = UIApplication.shared.statusBarFrame.size.height
let displayWidth: CGFloat = self.view.frame.width
let displayHeight: CGFloat = self.view.frame.height
tableView.contentInset.top = 20
tableView.frame = CGRect(x: 0, y: barHeight, width: displayWidth, height: displayHeight - barHeight)
let contentSize = self.tableView.contentSize
let footer = UIView(frame: CGRect(x: self.tableView.frame.origin.x,
y: self.tableView.frame.origin.y + contentSize.height,
width: self.tableView.frame.size.width,
height: self.tableView.frame.height - self.tableView.contentSize.height))
self.tableView.tableFooterView = footer
view.addSubview(tableView)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "my", for: indexPath)
cell.textLabel?.text = "This is row (tableData[indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
}
Let’s add a functionality such that selecting any row would show an alert dialog. We need to override the function didSelectRowAt
to make each selectable.
The code for ViewController.swift with the above implementation is given below.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView = UITableView()
var tableData = ["Beach", "Clubs", "Chill", "Dance"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tableView = UITableView(frame: self.view.bounds, style: UITableViewStyle.plain)
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = UIColor.white
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "my")
let barHeight: CGFloat = UIApplication.shared.statusBarFrame.size.height
let displayWidth: CGFloat = self.view.frame.width
let displayHeight: CGFloat = self.view.frame.height
tableView.contentInset.top = 20
tableView.frame = CGRect(x: 0, y: barHeight, width: displayWidth, height: displayHeight - barHeight)
let contentSize = self.tableView.contentSize
let footer = UIView(frame: CGRect(x: self.tableView.frame.origin.x,
y: self.tableView.frame.origin.y + contentSize.height,
width: self.tableView.frame.size.width,
height: self.tableView.frame.height - self.tableView.contentSize.height))
self.tableView.tableFooterView = footer
view.addSubview(tableView)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "my", for: indexPath)
cell.textLabel?.text = "This is row (tableData[indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableData.count
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let currentCell = tableView.cellForRow(at: indexPath)! as UITableViewCell
showDialog(text: (currentCell.textLabel?.text)!)
}
func showDialog(text : String)
{
let alert = UIAlertController(title: "Alert", message: text, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
tableView.deselectRow(at: indexPath, animated: true)
is used to remove the highlight from the selected row when it’s no longer pressed.
UITableView With Multiple Sections
Let’s customise the UITableView such that it would contain multiple sections.
We need to override two more functions shown below.
func numberOfSections(in tableView: UITableView) -> Int {
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
}
The ViewController.swift with multiple sections is given below.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView = UITableView()
var tableData = ["Beach", "Clubs", "Chill", "Dance"]
let data = [["0,0", "0,1", "0,2"], ["1,0", "1,1", "1,2"]]
let headerTitles = ["Some Data 1", "Some Data 2"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tableView = UITableView(frame: self.view.bounds, style: UITableViewStyle.plain)
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = UIColor.white
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "my")
let barHeight: CGFloat = UIApplication.shared.statusBarFrame.size.height
let displayWidth: CGFloat = self.view.frame.width
let displayHeight: CGFloat = self.view.frame.height
tableView.contentInset.top = 20
tableView.frame = CGRect(x: 0, y: barHeight, width: displayWidth, height: displayHeight - barHeight)
let contentSize = self.tableView.contentSize
let footer = UIView(frame: CGRect(x: self.tableView.frame.origin.x,
y: self.tableView.frame.origin.y + contentSize.height,
width: self.tableView.frame.size.width,
height: self.tableView.frame.height - self.tableView.contentSize.height))
self.tableView.tableFooterView = footer
view.addSubview(tableView)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "my", for: indexPath)
//cell.textLabel?.text = "This is row (tableData[indexPath.row])"
cell.textLabel?.text = "This is row (data[indexPath.section][indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//return tableData.count
return data[section].count
}
func numberOfSections(in tableView: UITableView) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section < headerTitles.count {
return headerTitles[section]
}
return nil
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let currentCell = tableView.cellForRow(at: indexPath)! as UITableViewCell
showDialog(text: (currentCell.textLabel?.text)!)
}
func showDialog(text : String)
{
let alert = UIAlertController(title: "Alert", message: text, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
The output of the above code is given below.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var tableView = UITableView()
var tableData = ["Beach", "Clubs", "Chill", "Dance"]
let data = [["0,0", "0,1", "0,2"], ["1,0", "1,1", "1,2"]]
let headerTitles = ["Some Data 1", "Some Data 2"]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tableView = UITableView(frame: self.view.bounds, style: UITableViewStyle.plain)
tableView.dataSource = self
tableView.delegate = self
tableView.backgroundColor = UIColor.white
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "my")
let barHeight: CGFloat = UIApplication.shared.statusBarFrame.size.height
let displayWidth: CGFloat = self.view.frame.width
let displayHeight: CGFloat = self.view.frame.height
tableView.contentInset.top = 20
tableView.frame = CGRect(x: 0, y: barHeight, width: displayWidth, height: displayHeight - barHeight)
let contentSize = self.tableView.contentSize
let footer = UIView(frame: CGRect(x: self.tableView.frame.origin.x,
y: self.tableView.frame.origin.y + contentSize.height,
width: self.tableView.frame.size.width,
height: self.tableView.frame.height - self.tableView.contentSize.height))
self.tableView.tableFooterView = footer
view.addSubview(tableView)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "my", for: indexPath)
//cell.textLabel?.text = "This is row (tableData[indexPath.row])"
cell.textLabel?.text = "This is row (data[indexPath.section][indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//return tableData.count
return data[section].count
}
func numberOfSections(in tableView: UITableView) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section < headerTitles.count {
return headerTitles[section]
}
return nil
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let currentCell = tableView.cellForRow(at: indexPath)! as UITableViewCell
showDialog(text: (currentCell.textLabel?.text)!)
}
func showDialog(text : String)
{
let alert = UIAlertController(title: "Alert", message: text, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let sectionHeight: CGFloat = 80
if scrollView.contentOffset.y = sectionHeight {
scrollView.contentInset = UIEdgeInsetsMake(-sectionHeight, 0, 0, 0)
}
}
}
The output of the above application is given below.
This brings an end to iOS UITableView tutorial. You can download the iOS UITableViewNoStoryboard Project from the link below.
Reference: Official Documentation