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")
}
}
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.
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.
SideMenuItem
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
}