I want to have a WKWebView with JavaScript handling in SwiftUI. From Swift Variables Initialization, I am doing the following: (I am using https://github.com/kylehickinson/SwiftUI-WebView to provide a wrapper for WKWebView in SwiftUI that also add valuable layout constraints.)
struct ContentView: View {
var body: some View {
WebView(webView: myWebView)
.onAppear {
let url = Bundle.main.url(forResource: "index", withExtension: "html")!
myWebView.loadFileURL(url, allowingReadAccessTo: url)
let request = URLRequest(url: url)
myWebView.load(request)
}
}
class JSHandler : NSObject, WKScriptMessageHandler {
var contentView: ContentView?
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print("documentReady")
contentView!.myWebView.evaluateJavaScript("<Long Script to Transfer Data>") { (result, error) in
}
}
}
let myJSHandler = JSHandler()
var myWebView: WKWebView = {
let config = WKWebViewConfiguration()
let controller = WKUserContentController()
controller.add(myJSHandler , name: "documentReady")
config.userContentController = controller
return WKWebView(frame: .zero, configuration: config)
}()
}
But then I learned from Instance member cannot be used on type that this doesn't work because the closure doesn't have reference to self. I need the WKWebView to have dedicated config object so I can't just use the other constructor. I need a reference to it to do evaluateJavaScript.
How to make this work?
EDIT 1: Add the body and mention the framework used to wrap WKWebView.
EDIT 2: Added code to clarify that I need two way communications from WKWebView to native app via WKScriptMessageHandler (to get notified when the HTML document is ready) and from native app to WKWebView via evaluateJavaScript (to transfer data upon the HTML document is ready).
One possible solution is to configure WKWebView in init
struct ContentView: View {
private var myWebView: WKWebView
init() {
let config = WKWebViewConfiguration()
let controller = WKUserContentController()
controller.add(myJSHandler , name: "documentReady")
config.userContentController = controller
myWebView = WKWebView(frame: .zero, configuration: config)
}
var body: some View {
WebView(webView: myWebView)
.onAppear {
let url = Bundle.main.url(forResource: "index", withExtension: "html")!
myWebView.loadFileURL(url, allowingReadAccessTo: url)
let request = URLRequest(url: url)
myWebView.load(request)
}
}
class JSHandler : NSObject, WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print("documentReady")
message.webView?.evaluateJavaScript("<Long Script to Transfer Data>") { (result, error) in
}
}
}
let myJSHandler = JSHandler()
}
Just make myWebView a lazy variable, This makes sure that the jsHandler is initialised before WebView.
lazy var myWebView: WKWebView = {
let config = WKWebViewConfiguration()
let controller = WKUserContentController()
controller.add(myJSHandler , name: "documentReady")
config.userContentController = controller
return WKWebView(frame: .zero, configuration: config)
}()
Related
Im trying to open external links from a IOS App with local html files. Im using Xcode 12.5 and swift 5+.
The code in my ViewController is simple, just opening the index.html in a folder (www).
The problem now is that i need to open external links http or https in Safari of the iPhone or the iPad and not in the local app. How can i filter that? Thanks for ideas.
Here is my code so far - updated:
import UIKit
import WebKit
import PDFKit
class ViewController: UIViewController, WKUIDelegate {
var webView: WKWebView!
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.uiDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
// let myURL = URL(string:"www/index.html")
// let myRequest = URLRequest(url: myURL!)
// webView.load(myRequest)
let url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "www")!
webView.loadFileURL(url, allowingReadAccessTo: url)
let request = URLRequest(url: url)
webView.load(request)
}
// WKWebViewNavigationDelegate
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// Check for links.
if navigationAction.navigationType == .linkActivated {
// Make sure the URL is set.
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
// Check for the scheme component.
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if components?.scheme == "http" || components?.scheme == "https" {
if navigationAction.targetFrame == nil {
UIApplication.shared.open(url)
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
// Open the link in the external browser.
UIApplication.shared.open(url)
// Cancel the decisionHandler because we managed the navigationAction.
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
}
Apparently your webView(_:decidePolicyFor:decisionHandler:) delegate function never gets called. Check it by adding a breakpoint or a print.
To fix this, just set the navigationDelegate of your WKWebView and conform to WKNavigationDelegate:
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate { // <- conform to WKNavigationDelegate
var webView: WKWebView!
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.navigationDelegate = self // <- set the navigationDelegate property
webView.uiDelegate = self
view = webView
}
}
I have a button on my website which has href https://live.example.net/?dynamic_encypted_query-string, So I wanted to open this url in my webview of ios app. Currently I'm building a web app through capacitor framework where I wrote a code to my WebViewController.swift file
class WebViewController: UIViewController, WKUIDelegate {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://live.example.net/?*")
let myRequest = URLRequest(url: url!)
webView.load(myRequest)
// Do any additional setup after loading the view.
}
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.uiDelegate = self
view = webView
}
}
This opens the domain(https://live.example.net) in the web view but i wanted to open it with the query string data.
let url = URL(string: "https://live.example.net/?*")
I think this doesn't work to fetch the query string data as well.
Here are the functions and props inside my ViewController Class,
webViewContainer is a just a View. The page does render because it prints WEBVIEW MSG WORKING to console, but it does not appear where the View is, or it does but is just white. Removing the native-webview communication and making a direct reference from a WebView to the variable works as expected, but I need to retain the messaging functionality.
#IBOutlet weak var webViewContainer: UIView!
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let contentController = WKUserContentController()
contentController.add(self, name: "callback")
let config = WKWebViewConfiguration()
config.userContentController = contentController
print(webViewContainer.frame);
webView = WKWebView(frame: webViewContainer.frame, configuration: config);
webView.loadHTMLString("<html><body><script>try {webkit.messageHandlers.callback.postMessage('WEBVIEW MSG WORKING');} catch(err) {console.log('Can not reach native code');}</script><h1>WEBVIEW SHOWS</h1></body></html>", baseURL: URL(string: "http://localhost"));
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let response = message.body as? String else { return }
print(response)
}
You missed the line
self.webViewContainer.addSubview(webView)
Adding it to the end of viewDidLoad method should fix your issue.
I am totally new to Xcode and the syntax is totally escaping me.
I have one thing left to do and then I've achieved what I need to within Xcode.
I need to insert a function that allows the WebView to process popups, but I have no idea where to begin inserting the code.
This is the current code:
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let webView = WKWebView()
let htmlPath = Bundle.main.path(forResource: "index", ofType: "html")
let folderPath = Bundle.main.bundlePath
let baseUrl = URL(fileURLWithPath: folderPath, isDirectory: true)
do {
let htmlString = try NSString(contentsOfFile: htmlPath!, encoding: String.Encoding.utf8.rawValue)
webView.loadHTMLString(htmlString as String, baseURL: baseUrl)
} catch {
// catch error
}
webView.navigationDelegate = self
view = webView
}
}
How do I edit this in order to allow popups?
Please remember that I don't know how to add the solutions that I'm finding, so please show me where to insert the snippet as well.
Try this code, hope it helps!
class ViewController: UIViewController {
var webView: WKWebView!
var popupWebView: WKWebView?
var urlPath: String = "WEBSITE_URL"
override func viewDidLoad() {
super.viewDidLoad()
setupWebView()
loadWebView()
}
//MARK: Setting up webView
func setupWebView() {
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
preferences.javaScriptCanOpenWindowsAutomatically = true
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
webView.uiDelegate = self
webView.navigationDelegate = self
view.addSubview(webView)
}
func loadWebView() {
if let url = URL(string: urlPath) {
let urlRequest = URLRequest(url: url)
webView.load(urlRequest)
}
}
}
extension ViewController: WKUIDelegate {
//MARK: Creating new webView for popup
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
popupWebView = WKWebView(frame: view.bounds, configuration: configuration)
popupWebView!.autoresizingMask = [.flexibleWidth, .flexibleHeight]
popupWebView!.navigationDelegate = self
popupWebView!.uiDelegate = self
view.addSubview(popupWebView!)
return popupWebView!
}
//MARK: To close popup
func webViewDidClose(_ webView: WKWebView) {
if webView == popupWebView {
popupWebView?.removeFromSuperview()
popupWebView = nil
}
}
}
}
I use WKWebView and I want to be notified when website is fully loaded. The webView:didFinishNavigation method of WKNavigationDelegate is fired when document.readyState is either interactive or complete and I want to be sure that site was completely loaded. I came up with the solution which uses JavaScript injection. Here is my MWE:
import UIKit
import WebKit
class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDelegate {
var webView: WKWebView!
#IBOutlet weak var loadLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let contentController = WKUserContentController()
let scriptPath = NSBundle.mainBundle().pathForResource("script", ofType: "js")!
let scriptString = try! String(contentsOfFile: scriptPath)
let script = WKUserScript(source: scriptString, injectionTime: .AtDocumentStart, forMainFrameOnly: true)
contentController.addUserScript(script)
contentController.addScriptMessageHandler(self, name: "readyHandler")
let configuration = WKWebViewConfiguration()
configuration.userContentController = contentController
webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.navigationDelegate = self
loadLabel.text = nil
}
#IBAction func loadWebsite() {
webView.loadRequest(NSURLRequest(URL: NSURL(string: "http://stackoverflow.com")!))
loadLabel.text = "Loading..."
}
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
print("message received")
loadLabel.text = "Complete"
}
}
And this is the content of script.js file:
document.onreadystatechange = function () {
if(document.readyState === "complete"){
webkit.messageHandlers.readyHandler.postMessage("");
}
}
userContentController:didReceiveScriptMessage method is always called on iOS Simulator, but on the actual device (iPhone 6 in my case) it isn't called most of the times. Any idea what can be wrong about it or what's the other way of checking if website is completely loaded?
For some reason you need to add the webView to a visible view for this to work on the device. If you don't want the webView to be visible, add it and then set the hidden property to true.
For the code example above:
func viewDidLoad(){
...
webView.hidden = true
view.addSubview(webView)
}