英文:
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
的生命周期函数如 viewDidLoad
和 viewWillLayoutSubviews
不会在其视图加载到视图层次结构中之前被调用。你可以从这里了解更多关于生命周期的信息。
你的代码中现在唯一会在 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 函数调用之间可能会发生竞争条件。你可以使用 WKNavigationDelegate
的 didFinish
代理函数来防止这种情况,该代理函数将告诉你 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")
,但在调用这个函数之前,你需要等一小段时间,直到视图加载完成。你可以在 ExternalDisplayViewController
的 viewDidLoad
函数的末尾添加以下代码:
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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论