• IOS: How to build a Table View with Multiple cell types
  • Originally written by Stan Ostrovskiy
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: LoneyIsError
  • Proofread by Fengziyin1234

Part 1: How do I not get lost in a lot of code

In a table view with static cells, the number and order of cells is constant. Implementing such a table view is very simple, not much different from implementing a regular UIView.

A table view of dynamic cells with only one content type: the number and order of cells change dynamically, but all cells have the same type of content. Here you can use reusable cells. This is also the most common tableview style.

Table views that contain dynamic cells with different content types: number, order, and Cel L types are dynamic. Implementing this table view is the most interesting and challenging.

Imagine an application where you have to build pages like this:

All data comes from the back end, and we have no control over what data the next request will receive: there may be no “about” information, or the “gallery” section may be empty. In this case, we don’t need to show these cells at all. Finally, we must know which Cell type the user is clicking on and react accordingly.

First, let’s identify the problem.

I’ve often seen this approach in different projects: configure cells according to index in a UITableView.

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

   if indexPath.row == 0 {
        //configure cell type1}else if indexPath.row == 1 {
        //configure cell type2}... }Copy the code

Also use almost the same code in the proxy method didSelectRowAt:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

if indexPath.row == 0 {
        //configure action when tap cell 1
   } else if indexPath.row == 1 {
        //configure action when tap cell 1
   }
   ....
}
Copy the code

Until the moment you want to reorder cells or delete or add new cells in the table view, the code will work as expected. If you change an index, the entire table view structure will be broken and you will need to manually update all indexes in cellForRowAt and didSelectRowAt methods.

In other words, it’s not reusable, it’s not readable, and it doesn’t follow any programming patterns because it mixes views and models.

Is there a better way?

In this project, we will use the MVVM pattern. MVVM stands for “model-view-viewModel,” which is useful when you need additional views between the Model and the View. You can read more about all the major iOS design patterns here.

In the first part of this tutorial series, we will build dynamic table views using JSON as a data source. We will discuss the following topics and concepts: protocols, protocol extensions, property calculations, declaration transformations and more.

In the next tutorial, we’ll take it up a notch by folding sections with a few lines of code.


Part 1: Model

First, create a new project, add the TableView to the default ViewController, bind the TableView to the ViewController, and embed the ViewController into the NavigationController, And make sure the project compiles and runs as expected. This is the basic step and will not be covered here. If you’re having trouble with this section, it’s probably too early for you to delve into this topic.

Your ViewController class should look something like this:

class ViewController: UIViewController {
   @IBOutlet weak var tableView: UITableView?
 
   override func viewDidLoad() {
      super.viewDidLoad()
   }
}
Copy the code

I created a simple JSON data that mimics the server response. You can download it in my Dropbox. Save this file in the project folder and make sure that the project name of the file is the same as the target name in the file inspector:

You’ll also need some pictures, which you can find here. Download the archive, unzip it, and add the images to the resources folder. Do not rename any images.

We need to create a Model that will hold all the data we read from JSON.

class Profile {
   var fullName: String?
   var pictureUrl: String?
   var email: String?
   var about: String?
   var friends = [Friend]()
   var profileAttributes = [Attribute]()
}

class Friend {
   var name: String?
   var pictureUrl: String?
}

class Attribute {
   var key: String?
   var value: String?
}
Copy the code

We’ll add initialization methods to JSON objects so that you can easily map JSON to Model. First, we need a method to extract the content from the.json file and turn it into a Data object:

public func dataFromFile(_ filename: String) -> Data? {
   @objc class TestClass: NSObject { }
   let bundle = Bundle(for: TestClass.self)
   if let path = bundle.path(forResource: filename, ofType: "json") {
      return (try? Data(contentsOf: URL(fileURLWithPath: path)))
   }
   return nil
}
Copy the code

Using the Data object, we can initialize the Profile class. There are many different ways native or third-party libraries can parse JSON in Swift, and you can use whichever one you like. I stick to the standard Swift JSONSerialization library to keep the project lean and don’t use any third party libraries:

class Profile { var fullName: String? var pictureUrl: String? var email: String? var about: String? var friends = [Friend]() var profileAttributes = [Attribute]() init? (data: Data) {do {
         if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], letThe body = json (" data ") as? [String: Any] {self.fullName = body[" fullName "] as? String self.pictureUrl = body[" pictureUrl "] as? String self. About = body[" about "] as? String self.email = body[" email "] as? Stringif letFriends = body [" friends "] the as? [[String: Any]] { self.friends = friends.map { Friend(json:$0)}}if letProfileAttributes = body [" profileAttributes "] the as? [[String: Any]] { self.profileAttributes = profileAttributes.map { Attribute(json:$0) }
            }
         }
      } catch {
         print(" Error deserializing JSON: \ "(Error))returnnil } } } class Friend { var name: String? var pictureUrl: String? init(json: [String: Any]) {self.name = json[" name "] as? String self.pictureUrl = json[" pictureUrl "] as? String} class Attribute {var key: String? var value: String? init(json: [String: String self.value = json[" value "] as? String}}Copy the code

Part 2: View Model

Our Model is ready, so we need to create the ViewModel. It’s going to be responsible for feeding data to our TableView.

We will create 5 different table sections:

  • Full name and Profile Picture
  • About
  • Email
  • Attributes
  • Friends

The first three sections have only one Cell each, and the last two sections can have multiple cells, depending on the contents of our JSON file.

Because our data is dynamic, the number of cells is not fixed, and we use different TableViewCells for each type of data, we need to use the correct ViewModel structure. First, we must differentiate the data types so that we can use the appropriate cells. Enumerations are best used when you need to use multiple types in Swift and can easily switch between them. So let’s start building ViewModel using ViewModelItemType:

enum ProfileViewModelItemType {
   case nameAndPicture
   case about
   case email
   case friend
   case attribute
}
Copy the code

Each enum case represents a different data type required by TableViewCell. However, since we want to use data in the same table view, we need a separate dataModelItem that will determine all attributes. We can do this by using a protocol that will provide property calculations for our item:

protocol ProfileViewModelItem {  

}
Copy the code

The first thing we need to know is the type of item. So we create a type attribute for the protocol. When you create a protocol property, you need to set name, type for the property and specify whether the property is gettable, settable and gettable. You can get more information and examples of protocol properties here. In our case, the type will be ProfileViewModelItemType, we just need to read this property:

protocol ProfileViewModelItem {
   var type: ProfileViewModelItemType { get }
}
Copy the code

The next property we need is rowCount. It’s going to tell us how many rows each section has. Specify type and read-only type for this property:

protocol ProfileViewModelItem {
   var type: ProfileViewModelItemType { get }
   var rowCount: Int { get }
}
Copy the code

It is best to add a sectionTitle property to the protocol. Basically, sectionTitle is also the data associated with TableView. As you know, when using the MVVM structure, we don’t want to create any type of data anywhere but in the viewModel:

protocol ProfileViewModelItem {
   var type: ProfileViewModelItemType { get }
   var rowCount: Int { get }
   var sectionTitle: String  { get }
}
Copy the code

We are now ready to create a ViewModelItem for each data type. Each item needs to comply with the agreement. But before we get started, let’s take one more step toward a clean and orderly project: provide some default values for our protocol. In Swift, we can use protocol extension to provide default values for protocols:

extension ProfileViewModelItem {
   var rowCount: Int {
      return1}}Copy the code

Now, if rowCount is 1, we don’t have to assign rowCount to item, which will save you some redundant code.

Protocol extensions also allow you to generate optional protocol methods without using the @objc protocol. You simply create a protocol extension and implement the default methods in that extension.

Create a ViewModeItem for nameAndPicture Cell.

class ProfileViewModelNameItem: ProfileViewModelItem {
   var type: ProfileViewModelItemType {
      return .nameAndPicture
   }
   
   var sectionTitle: String {
      return"The Main Info}}"Copy the code

As I said earlier, we don’t need to assign rowCount in this case, because we only need the default value of 1.

Now we add other attributes that are unique to this item: pictureUrl and userName. Both are storage properties with no initial value, so we also need to provide init methods for this class:

class ProfileViewModelNameAndPictureItem: ProfileViewModelItem {
   var type: ProfileViewModelItemType {
      return .nameAndPicture
   }
   
   var sectionTitle: String {
      returnVar pictureUrl: String var userName: String init(pictureUrl: String, userName: String) String) { self.pictureUrl = pictureUrl self.userName = userName } }Copy the code

Then we can create the remaining four models:

class ProfileViewModelAboutItem: ProfileViewModelItem {
   var type: ProfileViewModelItemType {
      return .about
   }
   
   var sectionTitle: String {
      return"About"} var About: String init (About: String) {self. The About = About}} class ProfileViewModelEmailItem: ProfileViewModelItem { vartype: ProfileViewModelItemType {
      return .email
   }
   
   var sectionTitle: String {
      return"Email"} var Email: String init (Email: String) {self. Email = Email}} class ProfileViewModelAttributeItem: ProfileViewModelItem { vartype: ProfileViewModelItemType {
      return .attribute
   }
   
   var sectionTitle: String {
      return"Attributes"} var rowCount: Int {return attributes.count
   }
   
   var attributes: [Attribute]
   
   init(attributes: [Attribute]) {
      self.attributes = attributes
   }
}

class ProfileViewModeFriendsItem: ProfileViewModelItem {
   var type: ProfileViewModelItemType {
      return .friend
   }
   
   var sectionTitle: String {
      return"Friends"} var rowCount: Int {return friends.count
   }
   
   var friends: [Friend]
   
   init(friends: [Friend]) {
      self.friends = friends
   }
}
Copy the code

For ProfileViewModeAttributeItem and ProfileViewModeFriendsItem, we may have more than one Cell, so RowCount is the corresponding number of Attributes and the number of Friends.

That’s all you need for the data item. The last step is to create the ViewModel class. This class can be used by any ViewController, which is one of the key ideas behind the MVVM structure: Your ViewModel doesn’t know anything about the View, but it provides all the data that the View might need.

The only property that _ViewModel_ has is the item array, which corresponds to the section array that UITableView contains:

class ProfileViewModel: NSObject {
   var items = [ProfileViewModelItem]()
}
Copy the code

To initialize the ViewModel, we will use the Profile Model. First, we try to parse the.json file as Data:

class ProfileViewModel: NSObject {
   var items = [ProfileViewModelItem]()
   
   override init(profile: Profile) {
      super.init()
      guard let data = dataFromFile("ServerData"), let profile = Profile(data: data) else {
         return
      }
      
      // initialization code will go here
   }
}
Copy the code

Here’s the most interesting part: Based on the Model, we’ll configure the ViewModel that needs to be displayed.

class ProfileViewModel: NSObject {
   var items = [ProfileViewModelItem]()
   
   override init() {
      super.init()
      guard let data = dataFromFile("ServerData"), let profile = Profile(data: data) else {
         return
      }
 
      if let name = profile.fullName, let pictureUrl = profile.pictureUrl {
         let nameAndPictureItem = ProfileViewModelNamePictureItem(name: name, pictureUrl: pictureUrl)
         items.append(nameAndPictureItem)
      }
      
      if let about = profile.about {
         let aboutItem = ProfileViewModelAboutItem(about: about)
         items.append(aboutItem)
      }
      
      if let email = profile.email {
         let dobItem = ProfileViewModelEmailItem(email: email)
         items.append(dobItem)
      }
      
      let attributes = profile.profileAttributes
      // we only need attributes item if attributes not empty
      if! attributes.isEmpty {let attributesItem = ProfileViewModeAttributeItem(attributes: attributes)
         items.append(attributesItem)
      }
      
      let friends = profile.friends
      // we only need friends item if friends not empty
      if! profile.friends.isEmpty {let friendsItem = ProfileViewModeFriendsItem(friends: friends)
         items.append(friendsItem)
      }
   }
}
Copy the code

Now, if you want to reorder, add, or remove items, all you need to do is modify the Item array of this ViewModel. It’s clear, right?

Next, we add the UITableViewDataSource to the ModelView:

extension ViewModel: UITableViewDataSource {
   func numberOfSections(in tableView: UITableView) -> Int {
      return items.count
   }
   
   func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return items[section].rowCount
   }
   
   func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   
   // we will configure the cells here
   
   }
}
Copy the code

Part 3: View

So let’s go back to the ViewController and start preparing the TableView.

First, we create the storage property ProfileViewModel and initialize it. In a real project, you would have to request the data, feed it to the ViewModel, and then reload the TableView when the data is updated (see here how to pass the data in an iOS application).

Next, let’s configure tableViewDataSource:

override func viewDidLoad() { super.viewDidLoad() tableView? .dataSource = viewModel }Copy the code

Now we can start building the UI. We need to create five different types of cells, each corresponding to a ViewModelItems. How to create cells is not covered in this tutorial; you can create your own Cell classes, styles, and layouts. For reference, I’ll show you some simple examples:

If you need some help creating cells, or want some hints, check out one of my previous tutorials on tableViewCells.

Each Cell should have an item property of type ProfileViewModelItem, which we will use to build the Cell UI:

// this assumes you already have all the cell subviews: labels, imagesViews, etc

class NameAndPictureCell: UITableViewCell {  
    var item: ProfileViewModelItem? {  
      didSet {  
         // cast the ProfileViewModelItem to appropriate item type  
         guard let item = item as? ProfileViewModelNamePictureItem  else {  
            return} nameLabel? .text = item.name pictureImageView?.image = UIImage(named: item.pictureUrl) } } } class AboutCell: UITableViewCell { var item: ProfileViewModelItem? { didSet { guardlet item = item as? ProfileViewModelAboutItem else {  
            return} aboutLabel? .text = item.about } } } class EmailCell: UITableViewCell { var item: ProfileViewModelItem? { didSet { guardlet item = item as? ProfileViewModelEmailItem else {  
            return} emailLabel? .text = item.email } } } class FriendCell: UITableViewCell { var item: Friend? { didSet { guardlet item = item else {  
            return  
         }

         if letpictureUrl = item.pictureUrl { pictureImageView? .image = UIImage(named: pictureUrl) } nameLabel? .text = item.name } } } var item: Attribute? { didSet { titleLabel?.text = item?.key valueLabel?.text = item?.value } }Copy the code

You may make a reasonable question: why don’t we create the same for ProfileViewModelAboutItem and ProfileViewModelEmailItem Cell, they are only a label? And the answer is yes, we can use one Cell. But the purpose of this tutorial is to show you how to use different types of cells.

If you want to use them as reusableCells, don’t forget to register cells: UITableView provides ways to register Cell classes and NIB files, depending on how you create cells.

Now it’s time to use cells in TableView. Again, the ViewModel will handle it in a very simple way:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let item = items[indexPath.section]
   switch item.type {
   case .nameAndPicture:
      if let cell = tableView.dequeueReusableCell(withIdentifier: NamePictureCell.identifier, for: indexPath) as? NamePictureCell {
         cell.item = item
         return cell
      }
   case .about:
      if let cell = tableView.dequeueReusableCell(withIdentifier: AboutCell.identifier, for: indexPath) as? AboutCell {
         cell.item = item
         return cell
      }
   case .email:
      if let cell = tableView.dequeueReusableCell(withIdentifier: EmailCell.identifier, for: indexPath) as? EmailCell {
         cell.item = item
         return cell
      }
   case .friend:
      if let cell = tableView.dequeueReusableCell(withIdentifier: FriendCell.identifier, for: indexPath) as? FriendCell {
         cell.item = friends[indexPath.row]
         return cell
      }
   case .attribute:
      if let cell = tableView.dequeueReusableCell(withIdentifier: AttributeCell.identifier, for: indexPath) as? AttributeCell {
         cell.item = attributes[indexPath.row]
         return cell
      }
   }
   
   // return the default cell if none of above succeed
   returnYou can use the same structure to build the didSelectRowAt proxy method: override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch items[indexPath.section].type { //do appropriate action for each type}}Copy the code

Finally, configure headerView:

override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
   return items[section].sectionTitle
}
Copy the code

Build to run your project and enjoy the dynamic table view!

To test the flexibility of this method, you can modify the JSON file: add or remove some Friends data, or remove some data entirely (just don’t break the JSON structure, otherwise you won’t see any data). When you rebuild your project, the tableView will find and work the way it should, without any code changes. To change the Model itself, you simply modify the ViewModel and ViewController: add new properties, or refactor its entire structure. That’s another story, of course.

You can view the full project here:

Stan-Ost/TableViewMVVM

Thanks for reading! If you have any questions or suggestions – feel free to ask!

In the next article, we will update the existing project to add a nice collapse/expand effect to these sections.


Update: See here how to update this tableView dynamically without using the ReloadData method.


I also write for the American Express Engineering blog. inAmericanExpress.ioCheck out my other work and the work of my talented colleagues.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.