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!
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?
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.