Creating view-model for each UITableViewCell

后端 未结 2 1041
陌清茗
陌清茗 2021-01-29 22:55

I\'m stuck on a design decision with creating view-models for table view\'s cells. Data for each cell is provided by a data source class (has an array of Contacts)

2条回答
  •  梦毁少年i
    2021-01-29 23:22

    Let me start with some theory. MVVM is a specialization of the Presentation Model (or Application Model) for Microsoft's Silverlight and WPF. The main ideas behind this UI architectural pattern are:

    • The view part is the only one that depends on the GUI framework. This means that for iOS, the view controller is part of the view.
    • The view can only talk to the view model. Never to the model.
    • The view model holds the state of the view. This state is offered to the view via view model properties. These properties contain not only the value of the labels, but also other view related information like if the save button is enabled or the color for a rating view. But the information of the state must be UI framework independent. So in the case of iOS, the property for the color should be an enum, for example, instead of a UIColor.
    • The view model also provides methods that will take care of the UI actions. This actions will talk to the model, but they never change the state of the view that is data related directly. Instead, it talks to the model and asks for the required changes.
    • The model should be autonomous, i.e. you should be able to use the same code for the model for a command line application and a UI interface. It will take care of all the business logic.
    • The model doesn't know about the view model. So changes to the view model are propagated through an observation mechanism. For iOS and a model with plain NSObject subclasses or even Core Data, KVO can be used for that (also for Swift).
    • Once the view model knows about changes in the model, it should update the state that it holds (if you use value types, then it should create an updated one and replace it).
    • The view model doesn't know about the view. In its original conception it uses data binding, that not available for iOS. So changes in the view model are propagated through an observation mechanism. You can also use KVO here, or as you mention in the question, a simple delegation pattern, even better if combined with Swift property observers, will do. Some people prefer reactive frameworks, like RxSwift, ReactiveCocoa, or even Swift Bond.

    The benefits are as you mentioned:

    • Better separation of concerns.
    • UI independence: easier migration to other UIs.
    • Better testability because of the separation of concerns and the decoupled nature of the code.

    So coming back to your question, the implementation of the UITableViewDataSource protocol belongs to the view part of the architecture, because of its dependencies on the UI framework. Notice that in order to use that protocol in your code, that file must import UIKit. Also methods like tableView(:cellForRowAt:) that returns a view is heavily dependent on UIKit.

    Then, your array of Contacts, that is indeed your model, cannot be operated or queried through the view (data source or otherwise). Instead you pass a view model to your table view controller, that, in the simplest case, has two properties (I recommend that they are stored, not computed properties). One of them is the number of sections and the other one is the number of rows per section:

    var numberOfSections: Int = 0
    var rowsPerSection: [Int] = []

    The view model is initialized with a reference to the model and as the last step in the initialization it sets the value of those two properties.

    The data source in the view controller uses the data of the view model:

    override func numberOfSections(in tableView: UITableView) -> Int {
        return viewModel.numberOfSections
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.rowsPerSection[section]
    }

    Finally you can have a different view model struct for each of the cells:

    struct ContactCellViewModel {
        let name: String
    
        init(contact: Contact) {
            name = contact.name ?? ""
        }
    }

    And the UITableViewCell subclass will know how to use that struct:

    class ContactTableViewCell: UITableViewCell {
        
        var viewModel: ContactCellViewModel!
    
        func configure() {
            textLabel!.text = viewModel.name
        }
    }

    In order to have the corresponding view model for each of the cells, the table view view model will provide a method that generates them, and that can be used to populate the array of view models:

    func viewModelForCell(at index: Int) -> ContactCellViewModel {
        return ContactCellViewModel(contact: contacts[index])
    }

    As you can see the view models here are the only ones talking to the model (your Contacts array), and the views only talk to the view models.

    Hope this helps.

提交回复
热议问题