Using the MVC Pattern in Designing a TableView

Hello, Habr! I present to you the translation of the article “iOS Tableview with MVC” , published in October 2016 on Medium.com by the developer Stan Ostrovskiy.


An example of using UITableView in an application

In this article, using a specific example, you can familiarize yourself with the application of the popular MVC pattern when designing one of the most popular UITableView interface elements . Also, this article in a fairly understandable and accessible way provides an opportunity to understand the basic architectural principles when designing your application, and also gives you the opportunity to familiarize yourself with the UITableView element. Considering the fact that a considerable number of developers often neglect any architectural decisions when creating their applications, I believe that this article will be very useful for both beginner developers and programmers with certain experience. The MVC pattern is promoted by Apple itself and is the most popular pattern used when developing for iOS. This does not mean that it is suitable for any tasks and is always the best choice, but, firstly, using MVC it ​​is easiest to get a general understanding of the architecture of your application, and secondly, quite often MVC is really good for solving certain project objectives. This article will help you structure your code, make it convenient, reusable, readable and compact.

If you are developing iOS projects, then you already know that one of the most used components is UITableView . If you are not developing for iOS yet, then in any case you can see that UITableView is used in many modern applications, such as Youtube, Facebook, Twitter, Medium, as well as in the vast majority of instant messengers, etc. Simply put, every time you need to display a variable number of data objects, you use a UITableView .

Another basic component for this purpose is CollectionView, which I personally prefer to use because it is more flexible than TableView.

So you want to add a UITableView to your project.

The most obvious way in which this usually go the UITableViewController , which has just built the UITableView . Its configuration is quite simple, you need to add your data array and create a table cell. It looks simple and works the way we want, except for a few points: firstly, the UITableViewController code becomes huge and secondly, it breaks the whole concept of the MVC pattern.

Even if you don’t want to deal with any design patterns, in any case you will most likely want to split into pieces the UITableViewController code , which consists of several thousand lines.

There are several methods for transferring data between Model and Controller, in this article I will use delegation. This approach provides clear, modular, and reusable code.

Instead of using one UITableViewController , we will break it into several classes:

  • DRHTableViewController: we will make it a subclass of UIViewController , and add a UITableView as its subview
  • DRHTableViewCell: subclass of UITableViewCell
  • DRHTableViewDataModel: this class will deal with API requests, generate data and return it to DRHTableViewController using delegation
  • DRHTableViewDataModelItem: a simple class that contains the data that we will display in the DRHTableViewCell cell

Let's start with UITableViewCell

Part 1: TableViewCell

Create a new project as a “Single View Application”, and delete the standard ViewController.swift and Main.storyboard files. We will create all the files that we need later, step by step.

First, create a subclass of UITableViewCell . If you want to use the XIB file, check the “Also create XIB file” option.



For this example, we use a table cell with the following fields:

  1. Avatar Image (User Image)
  2. Name Label (username)
  3. Date Label
  4. Article Title
  5. Article Preview

You can use Autolayout as you like, because the design of the table cell does not affect anything that we do in this guide. Create an outlet for each subview. Your DRHTableViewCell.swift file should look like this:

class DRHTableViewCell: UITableViewCell {
   @IBOutlet weak var avatarImageView: UIImageView?
   @IBOutlet weak var authorNameLabel: UILabel?
   @IBOutlet weak var postDateLabel: UILabel?
   @IBOutlet weak var titleLabel: UILabel?
   @IBOutlet weak var previewLabel: UILabel?
}

As you can see, I changed all the default values ​​of @IBOutlet with "!" on the "?". Each time you add a UILabel from InterfaceBuilder to your code, a “!” Is automatically added to the variable at the end, which means that the variable is declared as an implicitly retrieval option. This is to ensure compatibility with the Objective-C API, but I prefer not to use forced extraction, so I use regular options instead.

Next, we need to add a method to initialize all the elements of the table cell (labels, pictures, etc.). Instead of using separate variables for each element, let's create a small DRHTableViewDataModelItem class .

class DRHTableViewDataModelItem {
   var avatarImageURL: String?
   var authorName: String?
   var date: String?
   var title: String?
   var previewText: String?
}

It is better to store the date as a Date type, but for simplicity, in our example we will store it as a String.

All variables are optional, so you can not worry about their default values. A little later we will write Init (), and now let's go back to DRHTableViewCell.swift and add the following code, which will initialize all the elements of our table cell.

func configureWithItem(item: DRHTableViewDataModelItem) {
   // setImageWithURL(url: item.avatarImageURL)
   authorNameLabel?.text = item.authorName
   postDateLabel?.text = item.date
   titleLabel?.text = item.title
   previewLabel?.text = item.previewText
}

The SetImageWithURL method depends on how you are going to work with image loading in the project, so I will not describe it in this article.

Now that we have the cell ready, we can move on to the TableViewView table.

Part 2: TableView

In this example, we will use the viewController in the Storyboard. First, create a subclass of UIViewController :



In this project, I will use the UIViewController instead of the UITableViewController to expand the control capabilities on the elements. Also, using a UITableView as a subview will allow you to place the table as you like using Autolayout. Next, create a storyboard file and give it the same name DRHTableViewController . Drag the ViewController from the library with objects and write the class name into it:



Add a UITableView, attach it to all four edges of the controller:



And at the end add the tableView outlet to the DRHTableViewController :

class DRHTableViewController: UIViewController {
   @IBOutlet weak var tableView: UITableView?
}

We have already created DRHTableViewDataModelItem , so we can add the following local variable to the class:

fileprivate var dataArray = [DRHTableViewDataModelItem]()

This variable stores the data that we will display in the table.

Note that we do not initialize this array in the ViewController class: it is just an empty array for the data. We will fill it with data later using delegation.

Now set all the basic properties of the tableView in the method of the viewDidLoad . You can adjust the colors and styles as you want, the only property that we will definitely need in this example is registerNib :

tableView?.register(nib: UINib?, forCellReuseIdentifier: String)

Instead of creating a nib before calling this method and entering a long and complex identifier for our cell, we will make both Nib and ReuseIdentifier properties of the DRHTableViewCell class .

Always try to avoid using long and complex identifiers in the project body. If there are no other options, you can make a string variable and assign it this value.

Open DRHTableViewCell and add the following code to the top of the class:

class DRHMainTableViewCell: UITableViewCell {
   class var identifier: String { 
      return String(describing: self)
   }
   class var nib: UINib { 
      return UINib(nibName: identifier, bundle: nil)
   }
   .....
}

Save the changes and return to the DRHTableViewController. Calling the registerNib method will look a lot easier:

tableView?.register(DRHTableViewCell.nib, forCellReuseIdentifier: DRHTableViewCell.identifier)

Remember to set tableViewDataSource and TableViewDelegate to self.

override func viewDidLoad() {
   super.viewDidLoad()
   tableView?.register(DRHTableViewCell.nib, forCellReuseIdentifier:   
   DRHTableViewCell.identifier)
   tableView?.delegate = self
   tableView?.dataSource = self
}

As soon as you do this, the compiler will throw an error: “ Cannot assign value of type DRHTableViewController to type UITableViewDelegate ” (I cannot assign a value of type DRHTableViewController to UITableViewDelegate ).

When you use the UITableViewController subclass, you already have built-in delegate and datasource. If you add a UITableView as a subspecies of the UIViewController, you need to implement the UIViewController to the UITableViewControllerDelegate and UITableViewControllerDataSource protocols yourself.

To get rid of this error, simply add two extensions to the DRHTableViewController class :

extension DRHTableViewController: UITableViewDelegate {
}
extension DRHTableViewController: UITableViewDataSource {
}

After that, another error will appear: “Type DRHTableViewController does not conform to protocol UITableViewDataSource” (Type DRHTableViewController does not conform to the UITableViewDataSource protocol ). This is because there are several required methods that must be implemented in these extensions.

extension DRHTableViewController: UITableViewDataSource {
      func tableView(_ tableView: UITableView, cellForRowAt    
      indexPath: IndexPath) -> UITableViewCell {
      }
      func tableView(_ tableView: UITableView, numberOfRowsInSection 
      section: Int) -> Int {
      }
}

All methods in the UITableViewDelegate are optional, so there will be no errors if you do not override them. Click the Command button on the UITableViewDelegate to see which methods are available. The most commonly used are methods for selecting table cells, setting the height of a table cell, and configuring the top and bottom table headers.

As you can see, the two methods mentioned above should return a value, so you again see the error “Missing return type” (Missing return value). Let's fix it. First, set the number of columns in the section: we have already declared the data array dataArray , so we can just take its number of elements:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return dataArray.count
}

Some might have noticed that I did not override another method: numberOfSectionsInTableView , which is commonly used in the UITableViewController . This method is optional and it returns a default value of one. In this example, we have only one section in the tableView , so there is no need to override this method.

The final step in configuring the UITableViewDataSource is to set up the table cell in the cellForRowAtIndexPath method :

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   if let cell = tableView.dequeueReusableCell(withIdentifier: 
   DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell   
   {
      return cell 
   }
   return UITableViewCell()
}

Let's look at it line by line.

In order to create a table cell, we call the dequeueReusableCell method with the identifier DRHTableViewCell . It returns a UITableViewCell , and accordingly, we use the optional cast from UITableViewCell to DRHTableViewCell :

let cell = tableView.dequeueReusableCell(withIdentifier: 
   DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell

Next, we safely remove the optional and return the cell if successful:

if let cell = tableView.dequeueReusableCell(withIdentifier: 
   DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell   
   {
      return cell 
   } 

If it was not possible to extract the value, then return the default cell to UITableViewCell :

if let cell = tableView.dequeueReusableCell(withIdentifier: 
   DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell   
   {
      return cell 
   }
return UITableViewCell()

Maybe we still forgot something? Yes, we need to initialize the cell with data:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   if let cell = tableView.dequeueReusableCell(withIdentifier: 
   DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell 
   { 
      cell.configureWithItem(item: dataArray[indexPath.item])
      return cell
   }
   return UITableViewCell()
}

Now we are ready for the final part: we need to create and connect a DataSource to our TableView.

Part 3: DataModel

Create the DRHDataModel class .

Inside this class, we request data either from a JSON file, or using an HTTP
request, or simply from a local data file. This is not what I would like to focus on in this article, therefore, therefore, I will assume that we have already made an API request and it returned to us an optional array of type AnyObject and an optional Error error:

class DRHTableViewDataModel {
   func requestData() {
      // code to request data from API or local JSON file will go   
         here
      // this two vars were returned from wherever:
      // var response: [AnyObject]?
      // var error: Error?
      if let error = error {
          // handle error
      } else if let response = response {
          // parse response to [DRHTableViewDataModelItem]
          setDataWithResponse(response: response)
      }
   }
}

In the setDataWithResponse method , we will populate the array from DRHTableViewDataModelItem using the array received in the request. Add the following code below requestData :

private func setDataWithResponse(response: [AnyObject]) {
   var data = [DRHTableViewDataModelItem]()
   for item in response {
     // create DRHTableViewDataModelItem out of AnyObject
   }
}

As you recall, we have not yet created any initializer for the DRHTableViewDataModel . So let's go back to the DRHTableViewDataModel class and add a method to initialize. In this case, we will use the optional initializer with the dictionary [String: String]? ..

init?(data: [String: String]?) {
if let data = data, let avatar = data[“avatarImageURL”], let name = data[“authorName”], let date = data[“date”], let title = data[“title”], let previewText = data[“previewText”] {
self.avatarImageURL = avatar
self.authorName = name
self.date = date
self.title = title
self.previewText = previewText
} else {
   return nil
}
}

If any field is absent in the dictionary, or the dictionary itself is nil, initialization will fail (it will return nil).

Having this initializer, we can create the setDataWithResponse method in the DRHTableViewDataModel class :

private func setDataWithResponse(response: [AnyObject]) {
   var data = [DRHTableViewDataModelItem]()
   for item in response {
     if let drhTableViewDataModelItem =   
     DRHTableViewDataModelItem(data: item as? [String: String]) {
        data.append(drhTableViewDataModelItem)
     }
   }
}

After completing the for loop, we will have a ready-filled array of DRHTableViewDataModelItem . How now to transfer this array to TableView ?

Part 4: Delegate

First, create the DRHTableViewDataModelDelegate delegate protocol in the DRHTableViewDataModel.swift file immediately above the DRHTableViewDataModel class declaration :

protocol DRHTableViewDataModelDelegate: class {
}

Inside this protocol, we will also create two methods:

protocol DRHTableViewDataModelDelegate: class {
   func didRecieveDataUpdate(data: [DRHTableViewDataModelItem])
   func didFailDataUpdateWithError(error: Error)
}

The keyword “class” in the protocol limits the applicability of the protocol to class types (excluding structures and enumerations). This is important if we are going to use a weak delegate link. We must be sure that we will not create a loop of strong links between the delegate and delegated objects, so we use a weak link (see below)

Next, add the optional weak variable to the DRHTableViewDataModel class :

weak var delegate: DRHTableViewDataModelDelegate?

Now we need to add the delegate method. In this example, we need to pass the Error error, if the data request did not pass, in case of a successful request we will create an array of data. The error handler method is inside the requestData method

class DRHTableViewDataModel {
func requestData() {
      // code to request data from API or local JSON file will go   
         here
      // this two vars were returned from wherever:
      // var response: [AnyObject]?
      // var error: Error?
      if let error = error {
          delegate?.didFailDataUpdateWithError(error: error)
     } else if let response = response {
          // parse response to [DRHTableViewDataModelItem]
          setDataWithResponse(response: response)
     }
   }
}

Finally, add the second delegate method at the end of the setDataWithResponse method :

private func setDataWithResponse(response: [AnyObject]) {
   var data = [DRHTableViewDataModelItem]()
   for item in response {
      if let drhTableViewDataModelItem =  
      DRHTableViewDataModelItem(data: item as? [String: String]) {
         data.append(drhTableViewDataModelItem)
      }
   }
   delegate?.didRecieveDataUpdate(data: data)
}

Now we are ready to pass the data to the tableView .

Part 5: Displaying data

Using DRHTableViewDataModel we can populate our tableView with data. First, we need to create a link to the dataModel inside the DRHTableViewController :

private let dataSource = DRHTableViewDataModel()

Next, we need to query the data. I will do this inside ViewWillAppear so that the data is updated every time the page opens.

override func viewWillAppear(_ animated: Bool) {
   super.viewWillAppear(true)
   dataSource.requestData()
}

This is a simple example, so I am querying the data in viewWillAppear. In a real application, this will depend on many factors, such as the time of data caching, the use of the API, and the logic of the application.

Next, set the delegate to self, in the ViewDidLoad method :

dataSource.delegate = self

You will see the error again, because the DRHTableViewController does not yet implement the DRHTableViewDataModelDelegate functions . Fix this by adding the following code at the end of the file:

extension DRHTableViewController: DRHTableViewDataModelDelegate {
   func didFailDataUpdateWithError(error: Error) {
   }
   func didRecieveDataUpdate(data: [DRHTableViewDataModelItem]) {
   }
} 

Finally, we need to handle the didFailDataUpdateWithError and didRecieveDataUpdate events :

extension DRHTableViewController: DRHTableViewDataModelDelegate {
   func didFailDataUpdateWithError(error: Error) {
       // handle error case appropriately (display alert, log an error, etc.)
   }
   func didRecieveDataUpdate(data: [DRHTableViewDataModelItem]) {    
       dataArray = data
   }
}

Once we initialize our local dataArray with data , we are ready to update the table. But instead of doing this in the didRecieveDataUpdate method , we use the dataArray property browser :

fileprivate var dataArray = [DRHTableViewDataModelItem]() {
   didSet {
      tableView?.reloadData()
   }
}

The code inside didSet will execute immediately after the dataArray is initialized , that is, exactly when we need it.

That's all! Now you have a working prototype of tableView , with an individually configured table cell and data initialized. And you don't have any tableViewController 's classes with a few thousand lines of code. Each block of code that you created is reusable and can be reused anywhere in the project, which gives undeniable advantages.

For your convenience, you can read the full project code at the following link on Github.

Also popular now: