WKWebView evaluateJavaScript not returning html - ios

I am trying to parse the html returned from a WKWebView load() with evaluateJavaScript but it never prints anything. Am I doing this right? Any other ways? didFinish does print.
import UIKit
import WebKit
class MyWebViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: self.view.frame)
webView.navigationDelegate = self
let url = NSURL (string: "https://google.com");
let request = NSURLRequest(url: url! as URL)
webView.load(request as URLRequest)
self.view.addSubview(webView)
self.view.sendSubview(toBack: webView)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.outerHTML.toString()", completionHandler: { (html: AnyObject?, error: NSError?) in
print(html!)
} as? (Any?, Error?) -> Void)
print("didFinish")
}
}

Using evaluateJavaScript with a WKWebView is a bit tricky.
Since I think this answer would be useful to many people, rather than address your specific question with a short code snippet and a comment that you need to implement WKScriptMessageHandler, I'm going to post a full, complete example that you can use to see how everything works together.
To use this, create a "Single View Application" iOS project in Xcode and paste this over the default ViewController.swift file.
//
// WebViewController.swift
// WKWebViewExample
//
// Created by par on 4/2/17.
// Copyright © 2017 par. All rights reserved. MIT License.
//
import UIKit
import WebKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let webViewController = WebViewController()
// install the WebViewController as a child view controller
addChildViewController(webViewController)
let webViewControllerView = webViewController.view!
view.addSubview(webViewControllerView)
webViewControllerView.translatesAutoresizingMaskIntoConstraints = false
webViewControllerView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
webViewControllerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
webViewControllerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
webViewControllerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
webViewController.didMove(toParentViewController: self)
}
}
class WebViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
private var webView: WKWebView!
private var webViewContentIsLoaded = false
init() {
super.init(nibName: nil, bundle: nil)
self.webView = {
let contentController = WKUserContentController()
contentController.add(self, name: "WebViewControllerMessageHandler")
let configuration = WKWebViewConfiguration()
configuration.userContentController = contentController
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.scrollView.bounces = false
webView.navigationDelegate = self
return webView
}()
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(webView)
webView.translatesAutoresizingMaskIntoConstraints = false
webView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
webView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
webView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if !webViewContentIsLoaded {
let url = URL(string: "https://stackoverflow.com")!
let request = URLRequest(url: url)
webView.load(request)
webViewContentIsLoaded = true
}
}
private func evaluateJavascript(_ javascript: String, sourceURL: String? = nil, completion: ((_ error: String?) -> Void)? = nil) {
var javascript = javascript
// Adding a sourceURL comment makes the javascript source visible when debugging the simulator via Safari in Mac OS
if let sourceURL = sourceURL {
javascript = "//# sourceURL=\(sourceURL).js\n" + javascript
}
webView.evaluateJavaScript(javascript) { _, error in
completion?(error?.localizedDescription)
}
}
// MARK: - WKNavigationDelegate
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// This must be valid javascript! Critically don't forget to terminate statements with either a newline or semicolon!
let javascript =
"var outerHTML = document.documentElement.outerHTML.toString()\n" +
"var message = {\"type\": \"outerHTML\", \"outerHTML\": outerHTML }\n" +
"window.webkit.messageHandlers.WebViewControllerMessageHandler.postMessage(message)\n"
evaluateJavascript(javascript, sourceURL: "getOuterHMTL")
}
// MARK: - WKScriptMessageHandler
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let body = message.body as? [String: Any] else {
print("could not convert message body to dictionary: \(message.body)")
return
}
guard let type = body["type"] as? String else {
print("could not convert body[\"type\"] to string: \(body)")
return
}
switch type {
case "outerHTML":
guard let outerHTML = body["outerHTML"] as? String else {
print("could not convert body[\"outerHTML\"] to string: \(body)")
return
}
print("outerHTML is \(outerHTML)")
default:
print("unknown message type \(type)")
return
}
}
}

I know it has been a while since this was answered and I found that this didn't really work for me. I used the below instead.
I could not get the evaluateJavaScript to work until i added the setTimeout() function. For what it is worth here is my code below.
webView.evaluateJavaScript("setTimeout(function(){
// Do something here
},10);") { (result1, error) in
if error == nil {
print(result1 ?? "")
}
}

Related

Error Domain=WebKitErrorDomain Code=102 "Frame load interrupted" iOS WKwebview WKnavigationdelegate

I´m working on an app that will show a website. But I'm running into errors when loading the website in app. I´ts also working perfectly fine on android.
The error:
WebPageProxy::didFailProvisionalLoadForFrame: frameID = 3, domain =
WebKitErrorDomain, code = 102 Error Domain=WebKitErrorDomain Code=102
"Frame load interrupted"
UserInfo={_WKRecoveryAttempterErrorKey=<WKReloadFrameErrorRecoveryAttempter:
0x600001922120>, NSErrorFailingURLStringKey=https://site.site/login,
NSErrorFailingURLKey=https://site.site/login,
NSLocalizedDescription=Frame load interrupted} Failed Provisinal
Navigation
My code:
// ViewController.swift
import UIKit
import WebKit
import SafariServices
import PushNotifications
class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
let pushNotifications = PushNotifications.shared
var webView: WKWebView!
var manifest = WebAppManifest.shared
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage){
if let objStr = message.body as? String {
var arr = [" "," "]
if objStr.contains(";") {
arr = objStr.components(separatedBy: ";")
}
let tokenProvider = BeamsTokenProvider(authURL: "https://site.site/beams/token") { () -> (AuthData) in
let headers : [String: String] = [:]
var queryParams : [String: String] = [:]
if objStr.contains(";") {
queryParams = ["api_token":arr[1]]
}
return AuthData(headers: headers, queryParams: queryParams)
}
if objStr.contains(";") {
self.pushNotifications.setUserId(arr[0], tokenProvider: tokenProvider, completion: { error in
guard error == nil else {
print(error.debugDescription)
return
}
print("Succesfully authenticated with Pusher Beams")
})
} else {
self.pushNotifications.setUserId(objStr, tokenProvider: tokenProvider, completion: { error in
guard error == nil else {
print(error.debugDescription)
return
}
print("Succesfully authenticated with Pusher Beams")
})
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
let contentcontroller = WKUserContentController()
contentcontroller.add(self, name: "callbackHandler")
let config = WKWebViewConfiguration()
config.userContentController = contentcontroller
// Set-up the UI
self.view.backgroundColor = UIColor(fromHex: manifest.theme_color)
// Creates the web view engine
webView = WKWebView()
view.addSubview(webView)
webView.navigationDelegate = self
// Display attribute
var guide: AnyObject = self.view.safeAreaLayoutGuide
if manifest.display == "fullscreen" {
guide = self.view
webView.scrollView.contentInsetAdjustmentBehavior = .always
}
// Make the Web View take the whole screen
webView.translatesAutoresizingMaskIntoConstraints = false
webView.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true
webView.rightAnchor.constraint(equalTo: guide.rightAnchor).isActive = true
webView.leftAnchor.constraint(equalTo: guide.leftAnchor).isActive = true
webView.bottomAnchor.constraint(equalTo: guide.bottomAnchor).isActive = true
// It will enable navigation gestures on the web view
webView.allowsBackForwardNavigationGestures = true
let url = URL(string: "https://" + manifest.origin + manifest.start_url)!
print(pushNotifications.getDeviceInterests())
webView.load(URLRequest(url: url))
}
override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
get {
switch manifest.orientation {
case "landscape":
return UIInterfaceOrientationMask.landscape
case "portrait":
return UIInterfaceOrientationMask.portrait
default:
return UIInterfaceOrientationMask.all
}
}
set { self.supportedInterfaceOrientations = newValue }
}
override var prefersStatusBarHidden: Bool {
return manifest.display == "fullscreen"
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return UIColor(fromHex: manifest.theme_color).isDark() ? .lightContent : .darkContent
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
view.setNeedsLayout()
}
// MARK: loadWebPage function
func loadWebPage(url: URL) {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.query = nil
// If already has a query then append a param
// otherwise create a new one
if let query = url.query {
// If isFromMobile param already exists jsut reassign the existing query
if query.contains("source=TWA&platform=iOS") {
components?.query = query
} else {
components?.query = query + "&source=TWA&platform=iOS"
}
} else {
components?.query = "source=TWA&platform=iOS"
}
print(components!.url!)
let customRequest = URLRequest(url: components!.url!)
webView!.load(customRequest)
}
// MARK: NavigationAction handler
// Defines if it should navigate to a new URL
// Logic to check if the destination URL is in the scope
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
print(navigationAction.request.url?.absoluteURL)
if let host = navigationAction.request.url?.host,
let path = navigationAction.request.url?.path{
if host.contains(manifest.origin) &&
path.starts(with: manifest.scope){
let query = navigationAction.request.url?.query
if query != nil{
print("two")
// MARK: Pusher clear state
if path.contains("logout") || path.contains("login") {
pushNotifications.clearAllState {
print("Cleared all state!")
}
}
decisionHandler(.allow)
} else {
print("one")
loadWebPage(url: navigationAction.request.url!)
decisionHandler(.cancel)
}
return
} else {
print("wrong")
// Destination URL is out of the scope
if navigationAction.request.url?.scheme == "http" ||
navigationAction.request.url?.scheme == "https" {
// Opens an In-App browser
decisionHandler(.cancel)
let safariVC = SFSafariViewController(url: navigationAction.request.url!)
safariVC.preferredBarTintColor = UIColor(fromHex: manifest.theme_color)
safariVC.preferredControlTintColor =
UIColor(fromHex: manifest.theme_color).isDark() ? UIColor.white : UIColor.black
present(safariVC, animated: true)
} else {
// It looks like a different protocol
// We ask the OS to open it
UIApplication.shared.open(navigationAction.request.url!)
}
}
} else {
decisionHandler(.cancel)
}
}
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse,
decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
print("error")
if let response = navigationResponse.response as? HTTPURLResponse {
if response.statusCode >= 400 {
print("400 error")
handleError()
}
}
decisionHandler(.allow)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
print(error, "Failed Navigation")
handleError()
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
print(error, "Failed Provisinal Navigation")
handleError()
}
// Error handling in case the content of the PWA can't be loaded
func handleError() {
webView.loadHTMLString("<h1>:(</h1>", baseURL: nil)
let alert = UIAlertController(title: "Error", message: "There was a problem loading the App from the Internet. Please check your connection and try again", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: .cancel, handler: { (action) in
exit(100)
}))
present(alert, animated: true)
}
}
I have already made multiple apps of this type of but never had this error pop up. Any suggestion on how to fix this?

WKWebView: Completion handler passed to -[WebWidgetVC webView:decidePolicyForNavigationAction:decisionHandler:] was not called

I'm using swift in my project. i have WebWidgetVC (which is UIViewController) and inside of that UIViewController i have a WKWebView. i want for example if the url contain "https" denied the WKWebView and if the url contain "http" the WKWebView allowed. this is my codes:
import UIKit
import WebKit
class WebWidgetVC: BaseViewController {
var webView: WKWebView!
var widgetUrl: String?
var widget: Widget?
private var moreInfoView: WidgetInfoView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
if let wrappedUrl = widgetUrl, let url = URL(string: wrappedUrl) {
let preference = WKPreferences()
preference.javaScriptEnabled = true
preference.javaScriptCanOpenWindowsAutomatically = false
preference.setValue(false, forKey: "allowFileAccessFromFileURLs")
let configuration = WKWebViewConfiguration()
configuration.preferences = preference
configuration.setValue(false, forKey: "allowUniversalAccessFromFileURLs")
configuration.websiteDataStore = WKWebsiteDataStore.default()
webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.uiDelegate = self
webView.navigationDelegate = self
webView.load(URLRequest(url: url))
webView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(webView)
NSLayoutConstraint.activate([
webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
webView.topAnchor.constraint(equalTo: view.topAnchor),
webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
view.sendSubviewToBack(webView)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setNeedsStatusBarAppearanceUpdate()
}
override var preferredStatusBarStyle: UIStatusBarStyle {
if #available(iOS 13.0, *) {
return .darkContent
} else {
return .default
}
}
#IBAction func showMoreInfo(_ sender: UIButton) {
guard let widget = widget else {
return
}
moreInfoView = WidgetInfoView(frame: view.frame)
view.addSubview(moreInfoView)
moreInfoView.activeReceiptView(widget: widget)
}
#IBAction func dismissVC(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
}
// MARK: - Safari webkit delegate
extension WebWidgetVC: WKNavigationDelegate, WKUIDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
let request = navigationAction.request
let urlScheme = request.url?.scheme
if urlScheme == "http" {
decisionHandler(.allow)
} else if urlScheme == "https" {
decisionHandler(.cancel)
}
}
}
when program calling decisionHandler(.allow), getting this error:
Completion handler passed to -[WebWidgetVC webView:decidePolicyForNavigationAction:decisionHandler:] was not called
how can i fix this problem?
Updated
i put some breakpoints inside decidePolicyFor function and find out that after executing decisionHandler(.allow) code, this function called again.

How to get JS event handler with URL in swift

class FeedBackFormViewController: UIViewController, WKUIDelegate, WKScriptMessageHandler {
#IBOutlet weak var webViewShowing: UIView!
var formWebView: WKWebView!
private let fileString = UserSingleton.shared.feedbackFormLink
override func viewDidLoad() {
super.viewDidLoad()
setupJSFile()
formWebView.navigationDelegate = self
previewFiles()
}
// show files in web view
private func previewFiles() {
if let fileString = fileString, fileString != "" {
let url = URL(string: fileString)
let myRequest = URLRequest(url: url!)
UserSingleton.shared.showHUD()
formWebView.load(myRequest)
}else {
Alerts.shared.show(alert: .error, message: "no file found", type: .error)
}
}
private func setupJSFile() {
let config = WKWebViewConfiguration()
let js = "document.addEventListener('submit', function(){ window.webkit.messageHandlers.clickListener.postMessage('My hovercraft is full of eels!'); })"
let script = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
config.userContentController.addUserScript(script)
config.userContentController.add(self, name: "submit")
formWebView = WKWebView(frame: UIScreen.main.bounds, configuration: config)
webViewShowing.addSubview(formWebView)
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.name)
}
#IBAction func dismissFeedbackFormVC(_ sender: UIButton) {
self.dismiss(animated: true, completion: nil)
}
}
extension FeedBackFormViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!){
webView.evaluateJavaScript("document.readyState") { (result, error) in
if let result = result {
print(result)
}
}
}
}
only call wk navigation when the web view is loaded and I click anywhere no trigger call in userContentController any event
You'd need to add this script for execution, just append it at the end of the func setupJSFile:
formWebView.configuration.userContentController.addUserScript(script)
also, you'd need to change your JS script to
document.addEventListener('click', function(){ window.webkit.messageHandlers['iosListener'].postMessage('click clack!'); })

IOS WKWebview Cache Clearing

I have made a very simple WKWebview IOS app that embeds my Wordpress website ...
import UIKit
import WebKit
class ViewController: UIViewController {
private lazy var url = URL(string: "http://www.myblog.com")!
private weak var webView: WKWebView!
init (url: URL, configuration: WKWebViewConfiguration) {
super.init(nibName: nil, bundle: nil)
self.url = url
navigationItem.title = ""
}
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
override func viewDidLoad() {
super.viewDidLoad()
initWebView()
webView.loadPage(address: url)
}
private func initWebView() {
let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
view.addSubview(webView)
self.webView = webView
webView.navigationDelegate = self
webView.uiDelegate = self
webView.translatesAutoresizingMaskIntoConstraints = false
webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
webView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
webView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
webView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
}
}
extension ViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard let host = webView.url?.host else { return }
navigationItem.title = host
}
}
extension ViewController: WKUIDelegate {
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
guard navigationAction.targetFrame == nil,
let url = navigationAction.request.url else { return nil }
let vc = ViewController(url: url, configuration: configuration)
if let navigationController = navigationController {
navigationController.pushViewController(vc, animated: false)
return vc.webView
}
present(vc, animated: true, completion: nil)
return nil
}
}
extension WKWebView {
func loadPage(address url: URL) { load(URLRequest(url: url)) }
func loadPage(address urlString: String) {
guard let url = URL(string: urlString) else { return }
loadPage(address: url)
}
}
There is a caching plugin on the actual website and I would like for whenever the app loads on a users device - for the app to pull in a fresh copy of the url. Is this possible?
Can anyone advise where to start or how i can adjust my current code to get started?
Thank you!
Push notifications has nothing to do with the WKWebview.
First you need to handle events in the backend/wordpress. From the mobile side, you need the following:
1- Generating a P12 or P8 certificate and put it on the server to trust APN
2- Register for push notifications in AppDelegate class.
3- Handle receiving a push notification in AppDelegate class too.

Xcode WKWebView code to allow WebView to process popups

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
}
}
}
}

Resources