Swift WKWebView.evaluateJavaScript 出现严重错误:在强制解包可选值时意外发现 nil

huangapple go评论65阅读模式
英文:

Swift WKwebView.evaluateJavaScript Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value

问题

我想要在外部屏幕的 Web 视图中执行 JavaScript。在我的主视图中,我尝试像这样调用 External View 中的 pop() 函数:

let ex = ExternalDisplayViewController()
ex.pop(str: "Hello!")

当我运行时,我在 External View 中的以下位置收到错误信息:“Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value”:

self.webView.evaluateJavaScript("go('\(str)')", completionHandler: nil)

谢谢!

编辑(2023年3月1日):

抱歉,我对 Swift 和使用多个视图还不太熟悉。如果我使用 viewDidLoad()loadView(),我得到相同的结果:

import UIKit
import WebKit

class ExternalDisplayViewController: UIViewController, WKUIDelegate {

    private var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let config = WKWebViewConfiguration()
        config.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
        config.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
        webView = WKWebView(frame: view.frame, configuration: config)
        webView.scrollView.contentInsetAdjustmentBehavior = .never
        webView.uiDelegate = self

        // load local html file
        let bundleURL = Bundle.main.resourceURL!.absoluteURL
        let html = bundleURL.appendingPathComponent("external.html")
        webView.loadFileURL(html, allowingReadAccessTo: bundleURL)
        view.addSubview(webView)
    }

    func pop(str: String) {
        if (self.isViewLoaded) {
            // 视图控制器可见
            print("视图控制器应该可见")
            self.webView.evaluateJavaScript("go('\(str)')", completionHandler: nil)
        } else {
            print("视图控制器未加载")
        }
    }
}

这会产生以下输出:

视图已加载?
视图控制器未加载

如何初始化这个视图,以便可以访问它?

英文:

I want to execute javascript in an external screen's webview. In my main view I'm trying to call the pop() function in the External View like this:

let ex = ExternalDisplayViewController()
ex.pop(str: "Hello!")

When I run it I get "Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value" at self.webView.evaluateJavaScript("go('\(str)')", completionHandler: nil) in my External View:

import UIKit
import WebKit

class ExternalDisplayViewController: UIViewController, WKUIDelegate {
    
    private var webView: WKWebView!

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        let config = WKWebViewConfiguration()
        config.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
        config.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
        webView = WKWebView(frame: view.frame, configuration: config)
        webView.scrollView.contentInsetAdjustmentBehavior = .never
        webView.uiDelegate = self
        
        // load local html file
        let bundleURL = Bundle.main.resourceURL!.absoluteURL
        let html = bundleURL.appendingPathComponent("external.html")
        webView.loadFileURL(html, allowingReadAccessTo:bundleURL)
        view.addSubview(webView)
    }
    
    func pop(str: String) {
        self.webView.evaluateJavaScript("go('\(str)')", completionHandler: nil)
    }
}

Thanks!

EDIT (2023-03-01):

Sorry, I'm new to Swift and working with multiple views. If I use viewDidLoad() or loadView() I'm getting the same result:

import UIKit
import WebKit

class ExternalDisplayViewController: UIViewController, WKUIDelegate {

    private var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let config = WKWebViewConfiguration()
        config.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
        config.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
        webView = WKWebView(frame: view.frame, configuration: config)
        webView.scrollView.contentInsetAdjustmentBehavior = .never
        webView.uiDelegate = self

        // load local html file
        let bundleURL = Bundle.main.resourceURL!.absoluteURL
        let html = bundleURL.appendingPathComponent("external.html")
        webView.loadFileURL(html, allowingReadAccessTo:bundleURL)
        view.addSubview(webView)
    }

    func pop(str: String) {
        if (self.isViewLoaded) {
            // viewController is visible
            print("view controller should be visible")
            self.webView.evaluateJavaScript("go('\(str)')", completionHandler: nil)
        } else {
            print("view controller is not loaded")
            /*_ = self.view
            self.webView.evaluateJavaScript("go('\(str)')", completionHandler: nil)*/
        }
    }
}

This produces the following output:

View Loaded?
view controller is not loaded

How can I initialize this view so it's accessible?

答案1

得分: 1

(1) 你应该保持对你的 ExternalDisplayViewController 的强引用,因为 Swift 的 ARC 可能会将其释放。你应该这样做:

class MainDisplayViewController: UIViewController {

    let ex = ExternalDisplayViewController() 

    func foo() {
        self.ex.pop(str: "Hello!") // 这可能不起作用,我将在 (3) 中解释更多。
    }
}

(2) UIViewController 的生命周期函数如 viewDidLoadviewWillLayoutSubviews 不会在其视图加载到视图层次结构中之前被调用。你可以从这里了解更多关于生命周期的信息。

你的代码中现在唯一会在 ExternalDisplayViewController 中被调用的函数是 init 函数,所以你需要将 WKWebView 的初始化代码移到其中。

class ExternalDisplayViewController: UIViewController, WKUIDelegate {
  
    init() {
        ... // 不要忘记在调用 self 之前先调用 super.init。
        self.setup()
    }

    func setup() {
        let config = WKWebViewConfiguration()
        ....
        self.view.addSubview(webView)
    }
}

(3) 根据你何时何地调用 pop 函数,你可能无法从 webview 中调用你所期望的 javascript 函数,因为在你的 html 加载和 javascript 函数调用之间可能会发生竞争条件。你可以使用 WKNavigationDelegatedidFinish 代理函数来防止这种情况,该代理函数将告诉你 webview 何时完成加载 html。

class ExternalDisplayViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {

    func setup() {
        ....
        self.webView.uiDelegate = self
        self.webView.navigationDelegate = self
        ....
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.webView.evaluateJavaScript("go('\(str)')", completionHandler: nil)
    }
}

(4) 你也可以考虑将你的 WKWebView 代码移到主视图控制器中,因为你不打算呈现你的 ExternalDisplayViewController(假设我的假设是正确的)。

更新 2023-03-07

之所以不起作用是因为你实际上初始化了两个不同的 ExternalDisplayViewController

SceneDelegate

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    ....
    window.rootViewController = ExternalDisplayViewController()
    ....
}

ViewController

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    ....
    let ex = ExternalDisplayViewController()
    ....
}

这将不起作用,因为在外部显示器中呈现的 ExternalDisplayViewController 实例是从 SceneDelegate 初始化的。在 ViewController 中初始化的实例不被呈现,基本上什么也不做,并且会被 ARC 回收。

有几种解决方法。其中一种方法是使用通知中心,正如其他答案中提到的,但要记得在解散 ExternalDisplayViewController 时移除观察者,否则它可能会变成僵尸对象。

另一种更简单的方法是直接从你的 ViewController 中引用在外部显示器中呈现的 ExternalDisplayViewController 实例。

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name == "toEx", let msg = message.body as? String {
        guard let sceneDelegate = UIApplication.shared.connectedScenes.first(where: { $0.session.configuration.name == "External Display Configuration" })?.delegate as? SceneDelegate, let ex = sceneDelegate.window?.rootViewController as? ExternalDisplayViewController else {
            return
        }
        ex.pop(str: msg)
    }
}

"External Display Configuration" 是你在 AppDelegate 中为你的场景配置提供的名称。

希望这有所帮助。

英文:

> I want to execute javascript in an external screen's webview.

I'm not completely sure of what you meant by external screen so I am assuming that you want to somehow init an UIViewController with a WKWebView, loads external.html from your bundle and executes a javascript function without presenting the view controller. Please correct me if I'm wrong.

(1) You should keep a strong reference to your ExternalDisplayViewController as Swift's ARC might deallocate it. You should do something like this:

class MainDisplayViewController: UIViewController {

    let ex = ExternalDisplayViewController() 

    func foo() {
        self.ex.pop(str: "Hello!") // This might not work, will explain more in (3).
    }
}

(2) UIViewController lifecycle functions like viewDidLoad and viewWillLayoutSubviews won't be called until the its view is loaded into the view hierarchy. You can understand more about the lifecycle from here.

The only function that will be called in ExternalDisplayViewController in your codes now is the init function, so you will have to move the WKWebView initialisation codes into it.

class ExternalDisplayViewController: UIViewController, WKUIDelegate {
  
    init() {
        ... // Remember to super.init first before calling self.
        self.setup()
    }

    func setup() {
        let config = WKWebViewConfiguration()
        ....
        self.view.addSubview(webView)
    }
}

(3) Depending on where and when you call your pop function, you might not be able to call the javascript function you desire from the webview because there will be a race condition between the webview loading of your html and calling of the javascript function. You can prevent this by using WKNavigationDelegate's didFinish delegate function which will let you know when your webview has finish loading the html.

class ExternalDisplayViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {

    func setup() {
        ....
        self.webView.uiDelegate = self
        self.webView.navigationDelegate = self
        ....
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.webView.evaluateJavaScript("go('\(str)')", completionHandler: nil)
    }
}

(4) You can also consider moving your WKWebView codes into your main view controller since you are not planning to present your ExternalDisplayViewController (assuming my assumption is correct).

UPDATE 2023-03-07

The reason why it wasn't working is because you are actually initialising two different ExternalDisplayViewController.

SceneDelegate

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    ....
    window.rootViewController = ExternalDisplayViewController()
    ....
}

ViewController

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    ....
    let ex = ExternalDisplayViewController()
    ....
}

This will not work because the ExternalDisplayViewController instance presented in your external display is initialised from SceneDelegate. The instance initialised in ViewController is not presented and basically does nothing and will be dealloc by ARC.

There are a few ways to resolve this. One of it is to use NotificationCenter as mentioned by the other answer, but do remember to remove the observer when you dismiss your ExternalDisplayViewController else it may become a zombie object.

Another way which is slightly more straightforward is to reference the ExternalDisplayViewController instance that has been presented in your external display directly from your ViewController.

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name == "toEx", let msg = message.body as? String {
            guard let sceneDelegate = UIApplication.shared.connectedScenes.first(where: { $0.session.configuration.name == "External Display Configuration" })?.delegate as? SceneDelegate, let ex = sceneDelegate.window?.rootViewController as? ExternalDisplayViewController else {
            return
        }
        ex.pop(str: msg)
    }
}

"External Display Configuration" is the name you provided for your scene configuration in AppDelegate.

Hope this helps.

答案2

得分: 0

当你写下这一行

let ex = ExternalDisplayViewController()

Web视图将不会加载,你需要将视图推到导航控制器中,就像这样

self.navigationController?.pushViewController(ex, animated: true)

之后,你可以调用 ex.pop(str: "str"),但在调用这个函数之前,你需要等一小段时间,直到视图加载完成。你可以在 ExternalDisplayViewControllerviewDidLoad 函数的末尾添加以下代码:

NotificationCenter.default.post(name: NSNotification.Name(rawValue: "webViewDidLoad"), object: nil)

而不是调用 ex.pop(str: "Hello!"),请改成以下代码:

NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "webViewDidLoad"), object: nil, queue: nil) { _ in
    ex.pop(str: "str")
}

但要确保在调用 self.navigationController 时,你的视图控制器具有导航控制器。

英文:

When you write this line

let ex = ExternalDisplayViewController()

the web view will not load you have to push the view to the navigationController like this

self.navigationController?.pushViewController(ex, animated: true)

after that you can call ex.pop(str: "str") but you have to gave it a little time until the view loads before calling this function so what you can do is at the end of the function viewDidLoad in the ExternalDisplayViewController add this

NotificationCenter.default.post(name: NSNotification.Name(rawValue: "webViewDidLoad"), object: nil)

and instead of calling ex.pop(str: "Hello!") write this code instead

NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "webViewDidLoad"), object: nil, queue: nil) { _ in
        ex.pop(str: "str")
    }

But make sure your viewController has a navigationController when you call self.navigationController

huangapple
  • 本文由 发表于 2023年3月1日 15:36:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/75600728.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定