How a reusable data provider helps you cut code in your iOS app

In mobile applications, tabular screens occupy a significant place in the overall volume of the interface. This is due to their ability to display a large amount of content. But there is also the opposite effect – programming such screens generates a lot of the same type of code.

In our previous articles, we began to solve the problem of boilerplate code and its propagation by introducing a new approach, and also talked about a universal data source for implemented screens. In this text, we will look at another sub-part of our solution – a reusable data provider. We will show in detail and in detail how to implement the View layer, adhering to the principles SOLIDso that it does not depend on the type of data storage.

Regardless of what architecture (MVC, MVVM, VIPER, etc.) you use, the components from this article will help you to reduce the development time, find and fix bugs, and add new functionality.

Section list

Suppose, as the application evolves, displaying data in a flat list is not enough and now needs to be split into groups. As an example, we will use the data from the previous articles and divide them into groups according to the ViewModel type, which is one of the most common scenarios:

let firstSectionObjects = [ 
    TextViewModel(text: "First Cell"), 
    TextViewModel(text: "Cell #2"), 
    TextViewModel(text: "This is also a text cell"),
] 
  
let secondSectionObjects = [ 
    ValueSettingViewModel(parameter: "Size", value: 25), 
    ValueSettingViewModel(parameter: "Opacity", value: 37), 
    ValueSettingViewModel(parameter: "Blur", value: 13),  
] 
  
let thirdSectionObjects = [ 
    SwitchedSettingViewModel(parameter: "Push notifications  enabled", enabled: true), 
    SwitchedSettingViewModel(parameter: "Camera access  enabled", enabled: false), 
] 

The previous flat array can be simply thought of as the sum of the specified arrays:

lazy var plainArray = firstSectionObjects + 
                      secondSectionObjects + 
                      thirdSectionObjects 

You cannot pass three separate arrays instead of one to the ArrayDataProvider implemented in previous articles. In order to work with three arrays, each of which represents its own section, you need to describe a separate data type and implement a new provider that works with this data. Let’s try to do it in accordance with the principles SOLID, which will require preliminary preparation.

In the second cell of FirstViewController, set the text “Section Divided Data”, style – Basic and bind the cell selection segway to the previously created visual representation SimpleArchTableViewController – this is done according to the principle DRY… The visual representation for displaying our data has already been implemented, why repeat it? We set the plainListDataSegue identifier to the segway created in the last article, and sectionDevidedDataSegue to the newly added one.

However, if we run the application and try to tap on the created cell, we will see that the controller will open without division into sections. This happened because the data provider statically created in our FirstTableViewController was not replaced.

By Apple guides it is known that newly opened controllers must be configured in the prepare (for: sender 🙂 function. Let’s create a controller FirstTableViewController, specify it in the storyboard instead of the default UITableViewController and implement the specified function:

class FirstTableViewController: UITableViewController {   
    let dataSourceFabric: FirstDataSourceFibricProtocol = FirstDataSourceFabric()
  
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 
        guard let destinationTableViewController =  
            segue.destination as? ConfigurableTableViewController  
        else { 
            return 
        } 
        switch segue.identifier { 
        case "plainListDataSegue": 
            destinationTableViewController.dataSource =  
                dataSourceFabric.makePlainListDataSource(array: plainArray) 
        case "sectionDevidedDataSegue": 
            destinationTableViewController.dataSource =   
                dataSourceFabric.makeSectionDevidedDataSource(sections: sectionArray) 
        default: 
            break 
        } 
    } 
    
}

This code implements everything that Apple advises – it creates and configures an openable viewController, which is hidden behind the ConfigurableTableViewController protocol. The latter is declared by analogy with the Configurable cells protocol. It only specifies that the tabular controller can be configured by specifying the corresponding tabular data source:

protocol ConfigurableTableViewController where Self: UITableViewController { 
    var dataSource: UITableViewDataSource? { get set } 
} 

Note that although the use of a factory to create openable controllers is hidden behind the FirstDataSourceFabricProtocol protocol, a specific instance of the FirstDataSourceFabric factory is created in the constructors of the controller. This is a gross violation dependency inversion principle, but temporarily leave that out of brackets and return to the topic in future articles.

Factory

Creating two similar controllers, differing only in the way they display data, requires the introduction of a factory. In accordance with the principle of single responsibility, she will precisely deal with the creation and configuration of controllers. The code for it is taken entirely from FirstViewController, slightly modified to avoid code duplication according to the principle DRY, and looks like this:

class FirstDataSourceFabric: FirstDataSourceFibricProtocol {  
    let firstSectionObjects = [ 
        TextViewModel(text: "First Cell"), 
        TextViewModel(text: "Cell #2"), 
        TextViewModel(text: "This is also a text cell"),
    ]
  
    let secondSectionObjects = [ 
        ValueSettingViewModel(parameter: "Size", value: 25),  
        ValueSettingViewModel(parameter: "Opacity", value: 37),  
        ValueSettingViewModel(parameter: "Blur", value: 13),  
    ] 
  
    let thirdSectionObjects = [ 
        SwitchedSettingViewModel(parameter: "Push notifications enabled", enabled: true), 
        SwitchedSettingViewModel(parameter: "Camera access  enabled", enabled: false), 
    ] 
    func makePlainListDataSource() -> UITableViewDataSource? {  
        let plainArray = firstSectionObjects +  
                         secondSectionObjects + thirdSectionObjects 
        let dataProvider = ArrayDataProvider(array: plainArray)  
        return makeDataSource(with: dataProvider) 
    } 
    func makeSectionDevidedDataSource() -> UITableViewDataSource? { 
        let sectionArray = [ 
            Section(objects: firstSectionObjects, name: "Text Cells", indexTitle: "T"), 
            Section(objects: secondSectionObjects, name: "Int Cells", indexTitle: "V"), 
            Section(objects: thirdSectionObjects, name: "Bool Cells", indexTitle: "B"),
         ] 
         let dataProvider = SectionDataProvider(sections: sectionArray) 
         return makeDataSource(with: dataProvider) 
     } 
  
     func makeDataSource(with dataProvider: ViewModelDataProvider) -> UITableViewDataSource? { 
         let dataSource = TableViewDataSource(dataProvider: dataProvider) 
         dataSource.registerCell(class: TextTableViewCell.self, 
                            identifier: "TextTableViewCell",
                                   for: TextViewModel.self) 
         dataSource.registerCell(class: DetailedTextTableViewCell.self, 
                            identifier: "DetailedTextTableViewCell", 
                                   for: ValueSettingViewModel.self)
         dataSource.registerCell(class: DetailedTextTableViewCell.self, 
                            identifier: "SwitchedSettingTableViewCell",
                                   for: SwitchedSettingViewModel.self) 
         return dataSource 
     } 
} 

The makePlainListDataSource () and makeSectionDevidedDataSource () functions create the data source for the flat and sectioned lists, respectively.

A private instance of the data provider is initialized and passed to the makeDataSource (with dataProvider 🙂 function, which completes the creation of the data source.

Note that for the sake of simplification of this example, constant data for these functions is set directly in the factory. In future articles, we will definitely show you the correct work with data in accordance with the principles layered or layered architectures

The SectionInfo protocol is set completely by analogy with the system NSFetchedResultsSectionInfo, serves to describe the data section, its header and the elements it contains, and looks like this:

protocol SectionInfo { 
    var numberOfObjects: Int { get } 
    var objects: [ItemViewModel]? { get } 
    var name: String { get } 
    var indexTitle: String? { get } 
} 

And accordingly, the structure that implements this protocol and is used to describe the data section looks like this:

struct Section: SectionInfo { 
    var numberOfObjects: Int { return objects!.count }  
    var objects: [ItemViewModel]? 
    var name: String 
    var indexTitle: String? 
} 

Data provider

Let’s start implementing a new data provider:

class SectionDataProvider { 
    let sections: [SectionInfo] 
    
    public init(sections: [SectionInfo]) { 
        self.sections = sections 
    } 
    
    func numberOfRows(inSection section: Int) -> Int {       
        let section = sections[section]
        return section.numberOfObjects 
    } 
    
    func itemForRow(atIndexPath indexPath: IndexPath) -> ItemViewModel? {
        let section = sections[indexPath.section]
        return section.objects![indexPath.row]
    }
}

This provider works with an array of sections, each of which contains the number of elements in it, the elements themselves, the title and the index displayed on the right in the table and used to quickly navigate between the sections of the table.

The dataSource property in SimpleArchTableViewController is no longer lazy and is moved to the parent TableViewController class that implements the ConfigurableTableViewController protocol. All configurable table controllers must inherit from this class in the same way as all cells or viewModels` and must implement the corresponding protocols.

class TableViewController: UITableViewController,  
ConfigurableTableViewController { 
  
    var dataSource: UITableViewDataSource? { 
         didSet { 
             guard isViewLoaded else { return } 
             tableView.dataSource = dataSource 
             tableView.reloadData() 
         } 
     } 
     
     override func viewDidLoad() {
         super.viewDidLoad()
         tableView.dataSource = datSource
     }
}

The above class simply stores the given datasource and proxies it to its table.

Note that in this case it was impossible to do with the default implementation of the extension, since it is necessary to redefine the viewDidLoad function, which must set the dataSource after loading the controller plate, which would have been impossible with the default implementation of the protocol.

As a result of all the changes, SimpleArchTableViewController remained completely empty, but now it inherits from TableViewController, therefore, you can completely get rid of it by removing the source code and replace it with the base class in the storyboard. Thus, we got the opportunity to implement various views of table controllers without any inheritance. Both cells open a controller of the same TableViewController base class, with the same view described in the storyboard, but they display the data differently. Let’s launch the application, open the controller hidden behind the Section Divided Data cell, and look at the result:

The screenshot shows that the controller looks exactly the same as the SimpleArchTableViewController, their appearance is exactly the same. Let’s figure out why this happened.

Section headers

Let’s explain the reason. A sectional list was implemented, and the cells in this list are now actually in different sections, and not in one, as it was in the previous article. However, nothing has been done for the section headers, so they are not displayed and our section list looks flat.

To fix this, you need to extend the TableViewDataSource from the first article with a couple more methods, which does not contradict the principle of openness-closedness SOLID:

    func tableView(_ tableView: UITableView,  
titleForHeaderInSection section: Int) -> String? {  
         return dataProvider.title(forSection: section)  
    } 
 
    func sectionIndexTitles(for tableView: UITableView) -> [String]? { 
        return dataProvider.sectionIndexTitles() 
    } 

Since the functions require the data provider to have a method that returns the title for the specified section and an array of strings for the indexes displayed on the right side of the table, you need to extend the ViewModelDataProvider data provider protocol as follows:

protocol ViewModelDataProvider { 
    ... 
    func title(forSection section: Int) -> String?  
    func sectionIndexTitles() -> [String]? 
} 

Already at compile time, it will become clear that the ArrayDataProvider and SectionDataProvider classes do not fully conform to the newly extended protocol. Let’s implement the newly added methods:

extension ArrayDataProvider: ViewModelDataProvider {  
    ...  
    func title(forSection section: Int) -> String? {  
        return sectionTitle 
    } 
    func sectionIndexTitles() -> [String]? { 
        guard 
            let count = sectionTitle?.count, 
            count > 0, 
            let substring = sectionTitle?.prefix(1)  
        else { 
            return nil 
        } 
        return [String(substring)] 
    } 
} 
extension SectionDataProvider: ViewModelDataProvider { 
    ... 
    func title(forSection section: Int) -> String? {  
        let section = sections[section] 
        return section.name 
    } 
    
    func sectionIndexTitles() -> [String]? {
        return section.compactMap { $0.indexTitle }
    }
}

We also extend the ArrayDataProvider initializer and add a property:

class ArrayDataProvider<T: ItemViewModel> { 
    let array: [T] 
    let sectionTitle: String? 
  
    init(array: [T], sectionTitle: String? = nil) {
        self.array = array 
        self.sectionTitle = sectionTitle 
    } 
} 

It would be possible to implement the default implementation of the ViewModelDataProvider protocol in its extension, so that all entities that implement this protocol immediately receive this functionality. However, in this particular case, there are only two classes that implement this protocol, and both have different implementations. When run, I get the following output:

Section titles and their indices on the right are displayed. At the same time, the appearance of the controller implemented in the last article has not changed, but we got additional functionality: for flat lists, we can now also set the title of a single section, if we want.

It is worth noting that by highlighting a separate entity of the data provider, we were able to completely separate the display logic from the data type and convert the data type to the display type. In the example described above, a flat table can be displayed with both a flat data provider and a partitioned data provider, which fully complies with the Liskov principles of dependency inversion and substitution.

Conclusion

We represent the above architectural solutions graphically:

We see that:

  • there was a factory that creates data sources used to configure the opened controllers;

  • the system controller UITableViewController has been replaced with a base implementation of a configurable TableViewController.

In this article, we have shown how to implement a View layer adhering to the principles of SOLID. In particular, we have implemented a basic configurable table controller, TableViewController, whose data display logic is independent of the data provider. We also implemented two data providers, ArrayDataProvider and SectionDataProvider, to display flat arrays and partitioned data, respectively. At the same time, no other architecture classes had to be changed, the views in the storyboard or NIB file were not changed. In accordance with the principle of openness-closedness in SOLID we have not changed any of the classes implemented in the last article.

Of the minuses, there is still a tight connection between FirstViewController and the FirstDataSourceFabric factory, but in one of the next articles we will definitely figure out what to do in such cases. In the next article, we will try to implement a reusable abstract delegate implementation.

Similar Posts

Leave a Reply Cancel reply