Handle events in subviews in MVVM in Swift

Viewed 1209

I am trying to get into MVVM in Swift and I am wondering how to handle events in subviews in MVVM, and how these events can travel up the chain of views/viewmodels. I'm talking about pure Swift for now (no SwiftRx etc.).

Example

Say I have a TableViewController with a TableViewModel. The view model holds an array of objects and creates a TableCellViewModel for each one, since each cell represents one of these objects. The TableViewController gets the number of rows to display from its model and also the view model for each cell, so it can pass it along to the cell.

We then have a TableCell and each cell has a TableCellViewModel. The TableCell queries its model for things like user-facing strings etc.

Now let's say TableCell also has a delete button that delete's that row. I'm wondering how to handle that: Usually, the cell would forward the button press to its view model, but this is not where we need it - we eventually need to know about the button press in either TableViewController or TableViewModel, so we can remove the row from the table view.

So the question is:
How does the button event get from a TableCell upwards in the view chain in MVVM?

Code

As requested in the comments, code that goes with the example:

class TableViewController: UIViewController, UITableViewDataSource {

    var viewModel: TableViewModel = TableViewModel()

    // setup and such

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

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableCell
        cell.viewModel = self.viewModel.cellViewModel(at: indexPath.item)
        return cell
    }

}

class TableViewModel {

    // setup, get data from somewhere, ...

    var count: Int {
        return self.modelObjects.count
    }

    func cellViewModel(at index: Int) -> TableCellViewModel {
        let modelObject = self.modelObjects[index]
        let cellViewModel = TableCellViewModel(modelObject: modelObject)
        return cellViewModel
    }

}

class TableCell {

    var viewModel: TableCellViewModel!

    // setup UI, do what a cell does

    func viewModelChanged() {
        self.titleLabel.text = self.viewModel.title()
    }

    func deleteButtonPressed(_ sender: UIButton) {
        // Oh, what to do, what to do?
    }

}

class TableCellViewModel {

    private var modelObject: ModelObject

    init(modelObject: ModelObject) {
        self.modelObject = modelObject
    }

    func title() -> String {
        return self.modelObject.title
    }

}
2 Answers

TableViewModel is the source of truth, so all global operations should be performed in there. Pressing a button is completely UI operation and viewModel shouldn't handle this in direct way.

So, for now we know two facts:

  1. TableViewModel should delete the cell from array and then viewController should handle the deletion animation process;
  2. Button press shouldn't be handled in child viewModel.

According to this you can achieve it by:

  1. Pass button pressed event up to viewController (use callback or delegate pattern);
  2. Call TableViewModel method to delete specific cell:

    viewModel.deleteCell(at: indexPath)

  3. Properly handle deletion animation in viewController.

may be you can use nextResponder util nextResponder is VC, and VC responder to delegate (eg:CellEventDelegate) that handle delete data and cell

UIResponder *nextResponder = pressedCell.nextResponder;
        while (nextResponder) {
            if ([nextResponder conformsToProtocol:@protocol(CellEventDelegate)]) {
                if ([nextResponder respondsToSelector:@selector(onCatchEvent:)]) {
                    [((id<CellEventDelegate>)nextResponder) onCatchEvent:event];
                }
                break;
            }
            nextResponder = nextResponder.nextResponder;
        }
Related