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.

    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
    
    
    }