Blog

UITableView Application without Storyboards

Create UITableView Application with no Storyboards

In this tutorial we will be making a real world application using a UITableView with a custom designed cell view without storyboards or nib files.

What this app does is that it makes an API call to reddit’s top 50 entries and displays them on a TableView. Once all 50 entries are loaded on our TableView, we will then be able to tap on any individual entry to view it in more details. We will use UIWebView to display the details of the entry once it’s tapped.

So, let’s get started.

UITableViewController and AppDelegate

Fire up your Xcode and create a new iOS Single View Application project. Name your project anything you like, click next and create it.

On the Project Navigator panel, click on the project name then select your project under Targets. On the Deployment Info section remove Main from Main Interface field. This will tell Xcode that we are not using storyboards.

Back to Project Navigator panel, delete Main.Storyboard and move it to trash. We can also delete ViewController file. Create a new file and name it Top50Entries. This file will inherit from UITableViewController. UITableViewController is essentially a UIViewController that inherits from UITableViewDelegate and UITableViewDataSource, the major difference is that UITableViewController creates a table view with correct dimensions and autoresizing; it sets delegate and datasource to itself.

Next step, override to loadView method and call its super.loadView. Since we are not using storyboards, it’s better to use loadView method rather than viewDidLoad. We can set the title of our app as Top 50.

There are 2 methods we have to implement in order to satisfy UITableViewDataSource protocol, which are numberOfRowsInSection and cellForRowAt indexPath.
Return self.entries.count for numberOfRowsInSection and for cellForRowAt indexPath we can return an empty UITableViewCell() object for now to suppress the warning.

Open your AppDelegate file and remove all methods except didFinishLaunchingWithOptions. Instantiate the window object defined as a property of AppDelegate class inside didFinishLaunchingWithOptions method. We will pass UIScreen.main.bounds as our window frame.

We can now instantiate our table view. Create a constant called top50 and assign our Top50ViewController with style using plain table view style.

Next, we will create a navigation controller and set its root view controller to top50.

We will call window.makeKeyAndVisible() method to show the current window and position it in front front of all other windows and then assign the rootViewController property of the window object to navigationController we just created.

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        window = UIWindow(frame: UIScreen.main.bounds)

        let top50 = Top50EntriesTableViewController(style: UITableViewStyle.plain)
        let navigationController = UINavigationController(rootViewController: top50)
        
        window?.makeKeyAndVisible()
        window?.rootViewController = navigationController
        return true
    }
}

Run your project and you should see an empty tableview with 50 rows.

Create a new file called Entry, this file will be our Entry model. Create a new folder called Models and put Entry file inside Models folder. Open Entry file and create a new struct called Entry. This model will have 5 properties. we will have title:String ,author:String, created:Date, thumbnalString:String and entry url:URL.

import Foundation
struct Entry{
    var title:String
    var author:String
    var created:Date
    var thumbnailString:String
    var url:URL
}

Go back Top50TableViewController and create a private method called _loadEntries which will have a escaping closure that we will use once our request to reddit api has been completed.

Inside our _loadEntries method, we will create a URLSession with default configuration. URLSession object coordinates network data transfer tasks, in this case data transfer between our app and the API that we are making a request. And the reason we are using default configuration is because we can obtain data incrementally using a delegate. Other configuration objects are shared sessions which is a singleton object and it’s not as customizable. Ephemeral sessions are similar to default sessions but they don’t write caches, cookies or credentials to disk. The last session is background sessions which lets you perform uploads and downloads of content in the background while your app isn’t running.

Then we will define the URL to prepare for the API call. To make the call we have to create a dataTask for URLSession.
We will feed the dataTask with our url and we will use a completion handler block when transfer of data is completed.
Let’s make sure our request is returned with 200 status code. And let’s make sure we received data.

We will use do{} catch{} block to serialize our json object. At this point we have the data returned from the api in our buffer in memory.
To do that we need to use JSONSerialization of the data object with options. use mutableContainers as the option. The data returned is in [String:AnyObject]

We then need to pull data from our JSON object. The first is data node inside the JSON object then we need to extract children object. Children object is an array of entries.

private func _loadEntries(completionHandler: @escaping (_ entries:[Entry])->()){
        let defaultSession:URLSession = URLSession(configuration: URLSessionConfiguration.default)
        
        let redditApiURLString = "https://www.reddit.com/top.json?limit=50"
        let urlReditApi = URL(string: redditApiURLString)!
        
        let dataTask = defaultSession.dataTask(with: urlReditApi) { (data, response, error) in
            guard let httpResponse = response as? HTTPURLResponse else{
                print("Invalid response")
                return
            }
            
            let statusCode = httpResponse.statusCode
            if statusCode != 200 {
                print("Invalid status: \(statusCode)")
                return
            }
            
            guard let data = data else {
                print("Invalid data")
                return
            }
            
            do{
                guard let dictionary = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? [String:AnyObject] else {
                    print("Couldn't parse data")
                    return
                }

                guard let dataDictionary = dictionary["data"] as? [String:AnyObject],
                      let children = dataDictionary["children"] as? [[String:AnyObject]] else {
                        return
                }
                
                
                let entries = children.compactMap({ (entry) -> Entry in
                    
                    let dataEntry = entry["data"] as? [String:AnyObject] ?? [String:AnyObject]()
                    
                    let title = dataEntry["title"] as! String
                    let author = dataEntry["author"] as! String
                    let created = dataEntry["created_utc"] as! Double
                    let thumbnailString = dataEntry["thumbnail"] as! String
                    let urlString = dataEntry["url"] as! String
                    
                    let entry = Entry(title: title, author: author, created: Date(timeIntervalSince1970:created), thumbnailString: thumbnailString, url:URL(string: urlString)!)
                
                    return entry
                })
                completionHandler(entries)
            } catch (let error){
                print("Error: \(error.localizedDescription)")
            }
        }
        dataTask.resume()
    }

Mapping Objects in Swift

We can map the children object and create an array of Entry objects and return the array to entries constant.

After mapping the children object we can pass it to our completionHandler()
In our loadEntries method, assign Top50EntriesViewController entries property to our completionHandler entries, then reload the tableview by calling tableView.reloadData()

Going back to cellForRowAtIndexPath, we can now use our own custom TableViewCell . Create a constant called cell and use dequeReusableCell and pass the identifier and indexPath. We need to coerce this as our custom cell view type which is EntryTableViewCell.

Create another constant called entry which will hold an entry object. Assign it to self.entries[indexPath.row]

We can now use custom label by calling cell.labelTitle.text = entry.title

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.entries.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Entry Cell", for: indexPath) as! EntryTableViewCell
        cell.selectionStyle = .none
        cell.accessoryType = .disclosureIndicator
        
        let entry = self.entries[indexPath.row]
        loadEntryImage(url: entry.url) { (image) in
            cell.imageViewThumbnail.image = image
        }
        cell.imageViewThumbnail.image = UIImage()
        cell.labelAuthor.text = entry.author
        cell.labelDate.text = "\(entry.created)"
        cell.labelTitle.text = entry.title
        return cell
    }
    
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let entry = self.entries[indexPath.row]
        let detailsViewController = DetailsViewController()
        detailsViewController.url = entry.url
        navigationController?.pushViewController(detailsViewController, animated: true)
    }
Emrah UsarUITableView Application without Storyboards