Search code examples
iosdelegation

What is the best design solution for this situation in iOS?


I have UITableView with two static cells. Each cell has custom class and independently validate account name, when I fill text field in the cell. (This part of code I got as is and I am not allowed to rewrite it). The cell delegates about changes if validation is correct to delegate (SocialFeedSelectCellDelegate). Originally, this tableView appeared in SignUpViewController: UITableViewController, UITableViewDataSource, UITableViewDelegate, SocialFeedSelectCellDelegate only.

Problem : The same UITableView should appear in two different places (SignUpViewController and SettingsViewController). Also SignUpViewController and SettingsViewController should know about success or fail of account validation.

What I tried : I created SocialFeedTableViewController: UITableViewController, SocialFeedSelectCellDelegate for the tableView with two cells. Set view in SocialFeedTableViewController as container view for SignUpViewController and SettingsViewController. I used second delegation (from SocialFeedTVC to SignUp and Settings) to notify SignUp and Settings about validation changes. I think it is bad idea, because of double delegation. Teammate said me that it is hard to understand.

Question: What is the best and simple design solution for the problem?enter image description here


Solution

  • Why is the double delegation a problem? As far as I see it you have 2 table views, 1 for each controller. Then each controller sets the delegate to each of the table view as self. Even if not it is quite common to change the delegate of the object in runtime. It is also normal to have 2 delegate properties with the same protocol simply to be able to forward the message to 2 objects or more.

    There are many alternatives as well. You may use the default notification center and be able to forward the messages this way. The only bad thing about it is you need to explicitly resign the notification listener from the notification center.

    Another more interesting procedure in your case is creating a model (a class) that holds the data from the table view and also implements the protocol from the cells. The model should then be forwarded to the new view controller as a property. If the view controller still needs to refresh beyond the table view then the model should include another protocol for the view controller itself.

    Take something like this for example:

    protocol ModelProtocol: NSObjectProtocol {
        func cellDidUpdateText(cell: DelegateSystem.Model.MyCell, text: String?)
    }
    
    class DelegateSystem {
        class Model: NSObject, UITableViewDelegate, UITableViewDataSource, ModelProtocol {
    
            // My custom cell class
            class MyCell: UITableViewCell {
                weak var modelDelegate: ModelProtocol?
                var indexPath: NSIndexPath?
    
                func onTextChanged(field: UITextField) { // just an example
                    modelDelegate?.cellDidUpdateText(self, text: field.text) // call the cell delegate
                }
            }
    
            // some model values
            var firstTextInput: String?
            var secondTextInput: String?
    
            // a delegate method from a custom protocol
            func cellDidUpdateText(cell: DelegateSystem.Model.MyCell, text: String?) {
                // update the appropriate text
                if cell.indexPath?.row == 0 {
                    self.firstTextInput = text
                } else {
                    self.secondTextInput = text
                }
            }
    
            // table view data source
            func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
                return 2
            }
            func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
                let cell = MyCell() // create custom cell
                cell.indexPath = indexPath // We want to keep track of the cell index path
                // assign from appropriate text
                if cell.indexPath?.row == 0 {
                    cell.textLabel?.text = self.firstTextInput
                } else {
                    cell.textLabel?.text = self.secondTextInput
                }
                cell.modelDelegate = self // set the delegate
                return cell
            }
        }
    
        // The first view controller class
        class FirstViewController: UIViewController {
            var tableView: UITableView? // most likely from storyboard
            let model = Model() // generate the new model
    
            override func viewDidLoad() {
                super.viewDidLoad()
                refresh() // refresh when first loaded
            }
            override func viewDidAppear(animated: Bool) {
                super.viewDidAppear(animated)
                refresh() // Refresh each time the view appears. This will include when second view controller is popped
            }
    
            func refresh() {
                if let tableView = self.tableView {
                    tableView.delegate = model // use the model as a delegate
                    tableView.dataSource = model // use the model as a data source
                    tableView.reloadData() // refresh the view
                }
            }
    
            // probably from some button or keyboard done pressed
            func presentSecondController() {
                let controller = SecondViewController() // create the controller
                controller.model = model // assign the same model
                self.navigationController?.pushViewController(controller, animated: true) // push it
            }
        }
    
        // The second view controller class
        class SecondViewController: UIViewController {
            var tableView: UITableView? // most likely from storyboard
            var model: Model? // the model assigned from the previous view controller
    
            override func viewDidLoad() {
                super.viewDidLoad()
                refresh() // refresh when first loaded
            }
            override func viewDidAppear(animated: Bool) {
                super.viewDidAppear(animated)
                refresh() // Refresh each time the view appears. This will include when third view controller is popped
            }
    
            func refresh() {
                if let tableView = self.tableView {
                    tableView.delegate = model // use the model as a delegate
                    tableView.dataSource = model // use the model as a data source
                    tableView.reloadData() // refresh the view
                }
            }
    
            // from back button for instance
            func goBack() {
                self.navigationController?.popViewControllerAnimated(true)
            }
        }
    }
    

    Here the 2 view controllers will communicate with the same object which also implements the table view protocols. I do not suggest you to put all of this into a single file but as you can see both of the view controllers are extremely clean and the model takes over all the heavy work. The model may have another delegate which is then used by the view controllers themselves to forward additional info. The controllers should then "steal" the delegate slot from the model when view did appear.

    I hope this helps you understand the delegates are not so one-dimensional and a lot can be done with them.