Search code examples
iosswiftuitableviewdelegation

How to Pass Data Between Two Side-by-Side Instances of a UITableViewController


In interface builder, I embedded two instances of a UITableViewController in container views in a UIStackView. Both TableViewControllers are linked to the same custom class document (see code below). The only difference between them is in the data they display. Both have UITableViews that allow multiple selection – but I also want so that selecting anything in one table causes the deselection of everything in the other table, and vice versa. I tried setting this up with delegation, but I don't know how to reference one instance from the other within UITableViewController, to assign each as the delegate of the other.

I couldn't find anything relevant about delegation or about referencing a view controller by anything other than its subclass name. So in my latest attempt, I tried referring to the other child of the parent object. Here's the relevant code:

protocol TableViewSelectionDelegate: AnyObject {
    func didSelectInTableView(_ tableView: UITableView)
}

class TableViewController: UITableViewController, TableViewSelectionDelegate {

    weak var delegate: TableViewSelectionDelegate?

    @IBOutlet var numbersTableView: UITableView!
    @IBOutlet var lettersTableView: UITableView!

    // Received by segue
    var displayables: [Character] = []

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // (It's too soon to determine parents/children in viewDidLoad())
    override func viewWillAppear(_ animated: Bool) {

        guard let tableViewControllers = parent?.children else {
            print("No tableViewControllers found!")
            return
        }

        switch restorationIdentifier {

        case "NumbersTableViewController":
            for tableViewController in tableViewControllers {
                if tableViewController.restorationIdentifier == "LettersTableViewController" {
                    delegate = tableViewController as? TableViewSelectionDelegate
                }
            }

        case "LettersTableViewController":
            for tableViewController in tableViewControllers {
                if tableViewController.restorationIdentifier == "NumbersTableViewController" {
                    delegate = tableViewController as? TableViewSelectionDelegate
                }
            }

        default: print("Unidentified Table View Controller")
        }
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        delegate?.didSelectInTableView(tableView)
    }

    func didSelectInTableView(_ tableView: UITableView) {

        switch tableView {

        case numbersTableView:
            numbersTableView.indexPathsForSelectedRows?.forEach { indexPath in
                numbersTableView.deselectRow(at: indexPath, animated: false)
            }

        case lettersTableView:
            lettersTableView.indexPathsForSelectedRows?.forEach { indexPath in
                lettersTableView.deselectRow(at: indexPath, animated: false)
            }

        default: print("Unidentified Table View")
        }
    }
}

Running the above and tapping in either table results in "Unidentified Table View" printed to the console, and neither table's selections are cleared by making a selection in the other.

Any insights into how I could get the results that I want would be appreciated. If something here isn't clear, let me know, and I'll make updates.


Solution

  • Passing information between two instances of a UITableViewController through delegation is apparently not as complicated as it at first seemed. The only noteworthy part is the setting of the delegate. Within the custom TableViewController class, when one instance is initialized, it needs to set itself as the delegate of the other instance. That's it!

    In this case, to reference one instance from within another, one can use the tableViewController's parent to get to the other child tableViewController. Although there might be a better way to do this, see the code for my particular solution. Notably, since the parent property is not yet set just after viewDidLoad(), I needed to set things up in viewWillAppear(). Also note that this approach doesn't require using restorationIdentifiers or tags. Rather, it indirectly determines the tableViewController instance through its tableView property.

    The delegated didSelectInTableView() function passes the selectedInTableView that was selected in the other tableViewController instance. Since the delegate needs to clear its own selected rows, the selectedInTableView is not needed for this purpose. That is, for just clearing rows, the function doesn't need to pass anything.

    protocol TableViewSelectionDelegate: AnyObject {
        func didSelectInTableView(_ selectedInTableView: UITableView)
    }
    
    class TableViewController: UITableViewController, TableViewSelectionDelegate {
    
        weak var delegate: TableViewSelectionDelegate?
    
        // Received by segue
        var displayables: [Character] = []
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        // (It's too soon to determine parents/children in viewDidLoad())
        override func viewWillAppear(_ animated: Bool) {
    
            guard let siblingTableViewControllers = parent?.children as? [TableViewController] else { return }
    
            switch tableView {
    
            case siblingTableViewControllers[0].tableView: siblingTableViewControllers[1].delegate = self
            case siblingTableViewControllers[1].tableView: siblingTableViewControllers[0].delegate = self
    
            default: print("Unidentified Table View Controller")
            }
        }
    
        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    
            delegate?.didSelectInTableView(tableView)
        }
    
        func didSelectInTableView(_ selectedInTableView: UITableView) {
    
            // selectedTableView is NOT the one that needs to be cleared
            // The function only makes it available for other purposes
    
            tableView.indexPathsForSelectedRows?.forEach { indexPath in
                tableView.deselectRow(at: indexPath, animated: false)
            }
        }
    }
    

    Please feel free to correct my conceptualization and terminology.