0

Here is my current setup:

MainMenuViewController -> SubMenuViewController -> UserInputViewController -> ResultViewController

All of my view controllers contains a tableView.

When a user taps on a cell in MainMenuViewControlle, it will segue to SubMenuViewController. All fine and dandy.

Here is where it gets tricky, in SubMenuViewController, there are cells that needs to instantiate another SubMenuViewController because the sub menu options can be several levels deep.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

        guard let selectedNode = node?.childenNode[indexPath.row] else {
            return
        }

        if selectedNode.isLeaveNode() {
            performSegue(withIdentifier: "userInput", sender: self)
        } else {
            let subMenuViewController = SubMenuViewController(node: selectedNode)
            self.navigationController?.pushViewController(subMenuViewController, animated: true)
        }

When there are no child nodes, then it will segue to UserInputViewController, but when there are more options, it needs to instantiate another SubMenuViewController and the tableView will populate itself based on cell the user had tapped previously until selectedNode.isLeaveNode() is true (which means there won't be any child nodes).

This problem is when this code runs :

    let subMenuViewController = SubMenuViewController(node: selectedNode)
    self.navigationController?.pushViewController(subMenuViewController, animated: true)

it give me the following error:

fatal error: unexpectedly found nil while unwrapping an Optional value

From where I registered my cells which is here:

let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "SubMenuTableViewCell", bundle: bundle)
tableView.register(nib, forCellReuseIdentifier: "SubMenuCell")

All my tableView cells are instantiated using a xib file, and I have registered my cells in viewDidLoad()

Can anybody see the problem?

UPDATE

Here is the rest of my code:

UIViewController

class SubMenuViewController: UIViewController {

    var node: Node?

    init(node: Node) {
        self.node = node
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {

        super.viewDidLoad()

        self.navigationController?.isNavigationBarHidden = false
        self.navigationItem.title = node?.value.rawValue

        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: "SubMenuTableViewCell", bundle: bundle)
        tableView.register(nib, forCellReuseIdentifier: "SubMenuCell")
    }
}

UITableViewDataSource

extension SubMenuViewController: UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return node!.childCount
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        print("called")

        let cell = tableView.dequeueReusableCell(withIdentifier: "SubMenuCell", for: indexPath) as! SubMenuTableViewCell
        let desciptionModule = node?.childenNode[indexPath.row].value

        let description = Modules.description(module: desciptionModule!)

        cell.title.text = description.main
        cell.subtitle.text = description.sub

        return cell

    }
}

UITableViewDelegate

extension SubMenuViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 68
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

        guard let selectedNode = node?.childenNode[indexPath.row] else {
            return
        }

        if selectedNode.isLeaveNode() {
            performSegue(withIdentifier: "userInput", sender: self)
        } else {
            let subMenuViewController = SubMenuViewController(node: selectedNode)
            self.navigationController?.pushViewController(subMenuViewController, animated: true)
        }

    }  
}
Brendon Cheung
  • 995
  • 9
  • 29
  • Is your `subMenuViewController` being instantiated ? What you get if you print it right after your `let` ? – GIJOW Aug 25 '17 at 18:35
  • Add your SubMenuViewController initialization code, please. – Vasilii Muravev Aug 25 '17 at 19:01
  • Please see update, thanks – Brendon Cheung Aug 25 '17 at 19:12
  • @BrendonCheung On which exact lane of code do you catch the crash? Does this outlet `@IBOutlet weak var tableView: UITableView!` connected with storyboard? Do you use `prepareForSegue:` function? – Vasilii Muravev Aug 25 '17 at 19:16
  • @VasiliiMuravev The error was in `tableView.register(nib, forCellReuseIdentifier: "SubMenuCell")`, and yes my `tableView` is connect and no I did not use a `prepareForSegue`. On that thought, we can't use `prepareForSegue` because there is no segue in the storyboard. The `SubMenuViewController` instantiate itself – Brendon Cheung Aug 25 '17 at 19:21
  • If you want to instantiate a view controller like that, you have some more work to do. See https://stackoverflow.com/questions/26131693/instantiate-view-controller-from-storyboard-vs-creating-new-instance#26131792 – ryantxr Aug 25 '17 at 19:23
  • I see, so instead of doing `let subMenuViewController = SubMenuViewController(node: selectedNode) self.navigationController?.pushViewController(subMenuViewController, animated: true)`, I should be using the storyboard to instantiate? – Brendon Cheung Aug 25 '17 at 19:25

2 Answers2

0

In your viewDidLoad() method, make sure to do the following:

Based on the code you've posted, you might be having issues with this line:

let bundle = Bundle(for: type(of: self))

I would recommend replacing it with the following line:

let bundle = Bundle(forClass: self)

or

let bundle = Bundle.main

If you're still having trouble with this, then try modifying the following lines of code:

let nib = UINib(nibName: "SubMenuTableViewCell", bundle: bundle)

to

let nib = UINib(nibName: "SubMenuTableViewCell", bundle: nil)

In your tableView:cellForRowAtIndexPath UITableViewControllerDelegate method, include the following lines:

var cell = tableView.dequeueReusableCellWithIdentifier("SubMenuCell") as? UITableViewCell

if cell == nil {
    tableView.registerNib(UINib(nibName: "SubMenuTableViewCell", bundle: nil), forCellReuseIdentifier: "SubMenuCell")
    cell = tableView.dequeueReusableCellWithIdentifier("SubMenuCell") as SubMenuTableViewCell!
}

cell.configure(data: data[indexPath.row])
tableView.reloadData()
return cell

Note

  1. Make sure that the UITableViewCell's reuseIdentifier is "SubMenuCell"
  2. Make sure that the SubMenuTableViewCell.xib file's owner is SubMenuTableViewCell
  3. Make sure the Module does not say "None" (i.e. the Module should be the name of your Project's Target).
  4. Make sure to call tableView.reloadData() in your viewDidLoad() function.
umarqattan
  • 499
  • 1
  • 6
  • 13
  • Thanks, the strange thing is that when `MainMenuViewController` segues into `SubMenuViewController`, everything works! but as soon as I tried to instantiate the very same controller (`SubMenuViewController`), it fails. I tried your approach and the error still persists – Brendon Cheung Aug 25 '17 at 19:40
  • I added an extra line inside the `tableView:cellForRowAtIndexPath` delegate method that reloads the tableView. Try that out. – umarqattan Aug 25 '17 at 19:56
0

Problem solved

The problem lies in the fact that I instantiated my view controller wrong. The line:

let subMenuViewController = SubMenuViewController(node: selectedNode)

only return a raw object with no outlets, that is why I was getting the optional nil error because there weren't a tableView to start with.

The correct approach would be to instantiate it using your storyboard. Each view controller has a storyboard property as an optional, since my view controller exists in a storyboard, that property will not be nil.

Instead of doing this:

let subMenuViewController = SubMenuViewController(node: selectedNode)
self.navigationController?.pushViewController(subMenuViewController, animated: true)

I actually needed to do this:

let subMenuViewController = storyboard!.instantiateViewController("SubMenuViewController") 

That way I know for sure that my subMenuViewController will contain my tableView property which is an IBOutlet from storyboard.

Brendon Cheung
  • 995
  • 9
  • 29