Search code examples
iosswiftuiviewcontrolleruinavigationcontrollerwebkit

Fatal error when attempting to call a view controller function from an external modal view controller


Summary

I am currently faced with an issue that always worked fine in Objective-C, but I can't seem to get it to work in Swift (specifically v3). This is all built using Storyboards. I have two separate view controllers:

  1. ViewController: WKWebView & UIButtonItems
  2. SideMenuTableView: UITableView & UIButtons

Currently, the SideMenu pops over, modally, on top of the ViewController and lists a set of actions via UIButtons. I guess I'll just go off of the most simplistic example, just to get the idea down. What I am trying to accomplish in this example is reloading the WKWebView (ViewController) from a UIButton tap (SideMenuTableView). Below is a more clear picture of what I am trying to get at if things are still unclear:

Screenshots.png

Currently, I can call the SideMenu with a simple Storyboard segue (Kind: Present Modally). Furthermore, I can dismiss the SideMenu which was implemented into a simple close() function. However, when attempting to call the refresh function, I receive the following error message:

fatal error: unexpectedly found nil while unwrapping an Optional value

Code

import UIKit
import WebKit

class ViewController: UIViewController, UINavigationControllerDelegate, WKNavigationDelegate {

    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let webURL = URL(string: "https://google.ca")
        let webRequest = URLRequest(url: webURL!)
        webView.load(webRequest)

    func refresh() {
        webView.reload()
    }
}



import Foundation    

class SideMenuTableView: UITableViewController {

    @IBAction fileprivate func close() {
        self.dismiss(animated: true, completion: nil)
    }

    @IBAction func refresh(sender: AnyObject!) {
        ViewController().refresh()
        close()
    }
}

EDIT: Updated segue code.

// MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destinationViewController.
        // Pass the selected object to the new view controller.
        if let vc = segue.destination as? SideMenuTableView {
            vc.delegate = self
        }
    }
}

Solution

  • You are initializing a new UIViewController with ViewController().refresh(). A delegate pattern is a nicer way to accomplish what you are trying:

    Create a protocol:

    protocol MainViewControllerDelegate /* Can be called whatever you like */ {
        func refresh()
    }
    

    And make your ViewController conform to it.

    In your SideMenuTableView create a variable to hold the reference to it's delegates.

    weak var delegate: MainViewControllerDelegate? 
    

    In your prepareForSegue() function of ViewController. Check to see if the destination segue is your SideMenuTableView:

    if let vc = destination.vc as? SideMenuTableView {
        vc.delegate = self
    }
    

    Now in your @IBAction button tap in SideMenuTableView call the delegate method.

    @IBAction func refresh(sender: AnyObject!) {
            delegate?.refresh()
            close()
    }
    

    Because your ViewController conforms to this protocol (it has to have a refresh function in it's class), the refresh function will be executed.