Search code examples
iosswiftuitableviewswift33dtouch

3D Touch, peek and pop is crashing with a fatal error : unexpectedly found nil while unwrapping an Optional value


Edit:

I have a tableView with 2 cells, I'm trying to implement the Peek & Pop with the 3DTouch.

  • The 2 cells redirects to 2 different View Controller via a modalView
  • When the user taps on the cell on the 1st View Controller, I send data to the 2nd View Controller
  • And when the user selects a cell in the 2nd View Controller, the modal view is dismissed and Data is sent back to the 1st View Controller

I managed to make each individual cell to Peek and elevate it compared to the other cells, but as soon as I want to Pop it and get redirected to to other View Controller, my app crashes

Here is my code for the 1stVC: I register for Previewing in the viewDidLoad :

if( traitCollection.forceTouchCapability == .available){
  registerForPreviewing(with: self, sourceView: self.tableView)
}

I then conform to the protocol UIViewControllerPreviewingDelegate by implementing the 2 methods:

func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {

    guard let indexPath = tableView?.indexPathForRow(at: location) else { return nil }
    previewingContext.sourceRect = tableTest.rectForRow(at: indexPath) 

    let sb = UIStoryboard(name: "Main", bundle: nil)
    guard let detailVC = sb.instantiateViewController(withIdentifier: "DestinationViewController") as? DestinationViewController else { return nil }

    return detailVC

} 

func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit :UIViewController) {
    show(viewControllerToCommit, sender: self)
}

in func tableView(...didSelectRowAt):

if (indexPath.row == 0) {
       guard let controller = storyboard?.instantiateViewController(withIdentifier: "DestinationViewController") as? DestinationViewController else { return }
       controller.titlePassed = cell?.textLabel?.text
       controller.variableIn2ndVC = theVariabletoSend
       controller.anotherVarIn2nd = theVariabletoSend2
       ...
       ...
       navigationController?.present(controller, animated: true, completion: nil)
}

My Code for the 2nd View Controller:

var titlePassed: String?
var variableIn2ndVC: String?
var anotherVarIn2nd: String?
... 
...
@IBAction func cancelButton(_ sender: AnyObject) {
  self.dismiss(animated: true, completion: nil)
}

...
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   if section == 0 && titlePassed == "FROM" {
      return 1
   } else {
      return Stations.count
   }
}

public func numberOfSections(in tableView: UITableView) -> Int {
  if titlePassed == "FROM" {
    return 2
  } else {
    return 1
   }
}

In didSelectRowAt, I have some callback to send some data to the 1st Controller

When the user taps on the cell, I grab the text in textLabel and send it to 1st VC using the callBack

public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  if indexPath.section == 1 && titlePassed == "FROM" {
     let stringToSendBack = cell?.textLabel!.text

     // put str in callBack  in order to access it in the 1st VC
     callBack?(str!)
     dismiss(animated: true, completion: nil)
}

Solution

  • I think you've already solved it, but I'll just summarize what you've already concluded, just for the record.

    Let's ignore peek and pop for the moment. Then when you normally go from the master controller to the detail controller (DestinationViewController) in response to the user's tap on a cell, you do it like this (code abbreviated):

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
       guard let controller = storyboard?.instantiateViewController(withIdentifier: "DestinationViewController") as? DestinationViewController else { return }
       controller.titlePassed = cell?.textLabel?.text
       controller.variableIn2ndVC = theVariabletoSend
       controller.anotherVarIn2nd = theVariabletoSend2
       navigationController?.present(controller, animated: true, completion: nil)
    }
    

    So far, so good. Now let's bring peek and pop into the picture. This is a completely different situation. You create the navigation controller like this (again, abbreviating):

    func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
        let sb = UIStoryboard(name: "Main", bundle: nil)
        guard let detailVC = sb.instantiateViewController(withIdentifier: "DestinationViewController") as? DestinationViewController else { return nil }
        return detailVC
    } 
    

    See the difference?

    • In the first case, you are creating the DestinationViewController and configuring it.

    • In the second case, you are creating the DestinationViewController but you have completely neglected to configure it. Thus, it is unconfigured and all its properties are nil.

    That fact somehow catches up with you later, and causes the crash.

    If you want to peek and then pop, that is, to present the DestinationViewController after 3D touch just as you would if the user had merely tapped the cell, you must do everything that would you have done if the user had merely tapped the cell. You can do this in either of the two UIViewControllerPreviewingDelegate methods but you must certainly do it in the second one if you didn't do it in the first one, because that is the one that actually presents the view controller. Otherwise you will be presenting a broken view controller (as you already know).

    So, to sum up, your problems are caused by an assumption that tableView(_:didSelectRowAt) and the two UIViewControllerPreviewingDelegate methods are somehow intertwined. They are not. These are two totally separate situations — the user tapped, or the user 3D-pressed. You need complete implementations of the creation and configuration of the view controller for each of them.