Search code examples
iosswiftmvvmweak

swift tableView in custom view programatically - losing reference to controllers delegate and data source


I am trying to learn MVVM pattern and writing all my views programatically using Snapkit. I am creating hamburger menu which consist of simple tableView and I have a problem, that my tableView in cusom view is losing delegate and data source references on the view controller. I also tried using UITableViewController, but result is the same, here is my code:

ViewModel:

class SideMenuViewModel {

    let cellId = "SideMenuCellId"
    weak var delegate: SideMenuViewModelDelegate?
    private let cells: [SideMenuItemStruct] = [SideMenuItemStruct(type: .allDogs, title: "ALL DOGOS"),
                                           SideMenuItemStruct(type: .randomDog, title: "RANDOM DOGO")]

    init(delegate: SideMenuViewModelDelegate) {
        self.delegate = delegate
    }

    var numberOfRows: Int {
        return cells.count
    }

    func selectedMenuItem(indexPath: IndexPath) {
        switch SideMenuItemsEnum(rawValue: indexPath.row) {
        case .allDogs?:
            delegate?.selectedMenuItem(selectedItem:        SideMenuItemsEnum.allDogs)
        case .randomDog?:
            delegate?.selectedMenuItem(selectedItem: SideMenuItemsEnum.randomDog)
        default:
            print("error when choosing menu item")
        }
    }

    func cellForRow(_ tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as? SideMenuCell else {
            fatalError("could not deque Side menu cell")
        }

        cell.selectionStyle = .none
        cell.setUpCell(sideMenuItem: cells[indexPath.row])
        return cell
        }
}

View:

class SideMenuView: UIView {

    var sideMenuTableView = UITableView()

    let sideMenuButton = UIButton(type: .system)

    weak var delegate: UITableViewDelegate? {
        get {
            return sideMenuTableView.delegate
        }
        set {
            sideMenuTableView.delegate = newValue
        }
    }

    weak var dataSource: UITableViewDataSource? {
        get {
            return sideMenuTableView.dataSource
        }
        set {
            sideMenuTableView.dataSource = newValue
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        initUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

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

    private func initUI() {
        addSubview(sideMenuButton)
        addSubview(sideMenuTableView)

        setUpSideMenuButton()
        setUpSideMenuTableView()
    }

    private func setUpSideMenuButton() {
        sideMenuButton.setTitle("DELEGATE", for: .normal)
        sideMenuButton.addTarget(self, action: #selector(buttonPrint), for: .touchUpInside)
        sideMenuButton.snp.makeConstraints { (make) in
            make.top.equalTo(self)
            make.centerX.equalTo(self)
        }
    }

    @objc func buttonPrint() {
        print("delegate: \(String(describing: sideMenuTableView.delegate)), data source: \(String(describing: sideMenuTableView.dataSource))")
    }

    private func setUpSideMenuTableView() {
        sideMenuTableView.snp.makeConstraints { (make) in
            make.top.equalTo(sideMenuButton.snp.bottom)
            make.bottom.equalTo(self)
            make.left.equalTo(self)
            make.right.equalTo(self)
        }
    }

}

And my View Controller:

class SideMenuController: UIViewController {

    fileprivate let viewModel: SideMenuViewModel

    fileprivate var sideMenuView: SideMenuView {
        return view as! SideMenuView
    }

    init(viewModel: SideMenuViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }

    override func loadView() {
        let sideMenuView = SideMenuView()
        sideMenuView.sideMenuTableView.delegate = self
        sideMenuView.sideMenuTableView.dataSource = self
        view = sideMenuView
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        sideMenuView.sideMenuTableView.register(SideMenuCell.self, forCellReuseIdentifier: viewModel.cellId)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

extension SideMenuController: UITableViewDelegate, UITableViewDataSource {

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

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return viewModel.cellForRow(tableView, indexPath: indexPath)
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        viewModel.selectedMenuItem(indexPath: indexPath)
        print("awd")
    }

}

Simulater after init

Simulator after scroll

DELEGATE button tapped result

I am learning from few tutorials and they didn't had this problem, but they were all using Interface builders, which I want to avoid. Please, let me know, if I am doing something really wrong, thanks.

SOLUTION

I found out, I made a really huge mistake outside of this showed code, I initialized SideMenuController in a function and didn't keep reference to it, so naturaly it was automaticly deinitialized after end of a function. It was a really bad mistake. Thanks for all answers, code here is working, but I refactored it according to answer.


Solution

  • I guess you have been hacking on this for a while and it looks like code has ended up a bit all over the place.

    If you are going to follow MVVM then you need to think about the role of each component.

    • Model - An array of SideMenuItem
    • ViewModel - In this case it is the same as your Model so you can dispense with the Model and just use the ViewModel. In more complex examples, the ViewModel maps back to the Model, exposing on the data required by the view and performing any required translations
    • View - The actual visual elements; In this case just a tableview (although you also have a button for debugging)

    • Finally, you still have the View Controller that brings it all together

    ViewModel

    struct SideMenuViewModel {
        let items = [SideMenuItemStruct(type: .allDogs, title: "ALL DOGOS"),                                           
                     SideMenuItemStruct(type: .randomDog, title: "RANDOM DOGO")]
    }
    

    View

    class SideMenuView: UIView { 
    
        weak var viewModel: SideMenuViewModel?
        weak var delegate: SideMenuViewDelegate? // Was SideMenuViewModelDelegate
    
        private let sideMenuButton = UIButton(type: .system)
        private var sideMenuTableView = UITableView()
        private let cellId = "YourCellID"
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            initUI()
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func awakeFromNib() {
            super.awakeFromNib()
        }
    
        private func initUI() {
            addSubview(sideMenuButton)
            addSubview(sideMenuTableView)
    
            setUpSideMenuButton()
            setUpSideMenuTableView()
        }
    
        private func setUpSideMenuButton() {
            sideMenuButton.setTitle("DELEGATE", for: .normal)
            sideMenuButton.addTarget(self, action: #selector(buttonPrint), for: .touchUpInside)
            sideMenuButton.snp.makeConstraints { (make) in
                make.top.equalTo(self)
                make.centerX.equalTo(self)
            }
        }
    
        @objc func buttonPrint() {
            print("delegate: \(String(describing: sideMenuTableView.delegate)), data source: \(String(describing: sideMenuTableView.dataSource))")
        }
    
        private func setUpSideMenuTableView() {
            sideMenuTableView.snp.makeConstraints { (make) in
                make.top.equalTo(sideMenuButton.snp.bottom)
                make.bottom.equalTo(self)
                make.left.equalTo(self)
                make.right.equalTo(self)
            }
            sideMenuTableView.datasource = self
            sideMenuTableView.delegate = self
            sideMenuTableView.register(SideMenuCell.self, forCellReuseIdentifier: cellId)
        }
    
    }
    
    extension SideMenuView: UITableViewDelegate, UITableViewDataSource {
    
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return viewModel?.numberOfRows ?? 0
        }
    
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as? SideMenuCell else {
                fatalError("could not deque Side menu cell")
            }
    
            cell.selectionStyle = .none
            cell.setUpCell(sideMenuItem: self.viewModel!.items[indexPath.row])
            return cell
    
        }
    
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            let menuItem = self.viewModel!.items[indexPath.row]
            self.delegate?.didSelect(menuItem)
        }
    }
    

    ViewController

    class SideMenuController: UIViewController {
    
        fileprivate let viewModel: SideMenuViewModel
    
        fileprivate var sideMenuView: SideMenuView {
            return view as! SideMenuView
        }
    
        override func loadView() {
            let sideMenuView = SideMenuView()
            sideMenuView.delegate = self
            sideMenuView.viewModel = viewModel
            view = sideMenuView
        }
    
        init(viewModel: SideMenuViewModel) {
            self.viewModel = viewModel
            super.init(nibName: nil, bundle: nil)
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
    }
    
    extension SideMenuController: SideMenuViewDelegate {
    
    
        // TODO: Implement delegate method for menu selection
    
    
    }