Swift: using evaluateJavascript - ios

I'm fairly new to Swift development, and I have a hybrid app I am developing that I have wired up authentication to. When a user authenticates with the fingerprint sensor on a device, I want to trigger a JS or otherwise interact with the WKWebView... but for some reason, I can't seem to get it to work. I can do simple things like change the window HREF... but if I do something more complex, it either does nothing, or fails.
Here is the code of my viewController:
import UIKit
import WebKit
import LocalAuthentication
class ViewController: UIViewController, WKScriptMessageHandler, WKUIDelegate, WKNavigationDelegate {
#IBOutlet var containerView : UIView! = nil
// #IBOutlet weak var webView: UIWebView!
var webView: WKWebView?
var contentController = WKUserContentController();
#IBOutlet var activityIndicatorView: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
// append the userAgent and ensure it contains our browser detect regEx
let userAgent = UIWebView().stringByEvaluatingJavaScriptFromString("navigator.userAgent")! + " iPad"
NSUserDefaults.standardUserDefaults().registerDefaults(["UserAgent" : userAgent])
// add JS content controller
var config = WKWebViewConfiguration()
config.userContentController = contentController
// instantiate the web view
let webView = WKWebView(frame: CGRectZero, configuration: config)
webView.setTranslatesAutoresizingMaskIntoConstraints(false)
webView.navigationDelegate = self
view.addSubview(webView)
// customize sizing
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[webView]|", options: NSLayoutFormatOptions.allZeros, metrics: nil, views: ["webView": webView]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[webView]|", options: NSLayoutFormatOptions.allZeros, metrics: nil, views: ["webView": webView]))
// open the URL for the app
let urlPath = "http://im_a_url"
let url: NSURL = NSURL(string: urlPath)!
let request = NSURLRequest(URL: url)
webView.loadRequest(request)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func webView(webView: WKWebView!, didStartProvisionalNavigation navigation: WKNavigation!) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
}
func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
// trigger authentication
authenticateUser()
}
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
}
func logInWithStoredCredentials() {
println("successful auth - log in");
// TO DO - use core data to stor user credentials
webView!.evaluateJavaScript("document.getElementById('anonymousFormSubmit').click();", nil)
}
func authenticateUser() {
// Get the local authentication context.
let context = LAContext()
// Declare a NSError variable.
var error: NSError?
// Set the reason string that will appear on the authentication alert.
var reasonString = "Authentication is needed to access aware360Suite."
// Check if the device can evaluate the policy.
if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error) {
[context .evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: reasonString, reply: { (success: Bool, evalPolicyError: NSError?) -> Void in
if success {
self.logInWithStoredCredentials()
// self.webView?.evaluateJavaScript("document.getElementById('auiAuthSubmitBtn').click();", nil)
}
else{
// If authentication failed then show a message to the console with a short description.
// In case that the error is a user fallback, then show the password alert view.
println(evalPolicyError?.localizedDescription)
switch evalPolicyError!.code {
case LAError.SystemCancel.rawValue:
println("Authentication was cancelled by the system")
case LAError.UserCancel.rawValue:
println("Authentication was cancelled by the user")
case LAError.UserFallback.rawValue:
println("User selected to enter custom password")
// self.showPasswordAlert()
default:
println("Authentication failed")
// self.showPasswordAlert()
}
}
})]
}
else{
// If the security policy cannot be evaluated then show a short message depending on the error.
switch error!.code{
case LAError.TouchIDNotEnrolled.rawValue:
println("TouchID is not enrolled")
case LAError.PasscodeNotSet.rawValue:
println("A passcode has not been set")
default:
// The LAError.TouchIDNotAvailable case.
println("TouchID not available")
}
}
}
}
The issue is in the successful authentication method:
func logInWithStoredCredentials() {
println("successful auth - log in");
// TO DO - use core data to use stored user credentials
webView!.evaluateJavaScript("document.getElementById('anonymousFormSubmit').click();", nil)
}
I cannot seem to get a handle to the webView here. If I attempt to actually evaluate script here, it throws the following error:
2015-02-10 17:07:32.912 A360[2282:462860] -[UIWebView evaluateJavaScript:completionHandler:]: unrecognized selector sent to instance 0x1741897f0
2015-02-10 17:07:32.916 A360[2282:462860] <NSXPCConnection: 0x178103960> connection to service named com.apple.CoreAuthentication.daemon: Warning: Exception caught during decoding of received reply to message 'evaluatePolicy:options:reply:', dropping incoming message and calling failure block.
Exception: -[UIWebView evaluateJavaScript:completionHandler:]: unrecognized selector sent to instance 0x1741897f0
I'm quite at a loss. I know I don't have a proper handle to the WebView here, because I know that if I attempt an operation like this immediately after having had a successful navigation from the view, it will work fine, e.g.:
func webView(webView: WKWebView!, didFinishNavigation navigation: WKNavigation!) {
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
webView.evaluateJavaScript("document.getElementById('anonymousFormSubmit').click();", nil)
}
So clearly in my logInWithStoredCredentials function it has lost the context of the webView.
How can I get a proper handle to the webView in my logInWithStoredCredentials func?
Sorry all - I know this is a rather basic question, but I have been banging my head against it for hours now, and this is a very pressing issue that I must resolve quickly.

It looks like you have UIWebView instead of WKWebView in your UI:
Exception: -[UIWebView evaluateJavaScript:completionHandler:]: unrecognized selector sent to instance 0x1741897f0
You should instantiate a WKWebView instance instead. Also note that WKWebView cannot be created in Interface Builder, you will have to add it to the view hierarchy at runtime.

You have var webView: WKWebView? in your class but then redefine it in viewDidLoad as let webView = WKWebView(frame: CGRectZero, configuration: config)
Remove the let in front of the webView and it should work.

Related

why I can’t open a App Store link inside a web view?

I'm getting always this error :
WebPageProxy::didFailProvisionalLoadForFrame: frameID=3, domain=WebKitErrorDomain, code=102
Normal links are working but the AppStore one is not working
what I want is the Link to open the AppStore I can't do it locally because the web is loaded from a Qualtrics web.
I try it adding the navigationAction function but that doesn't work, what I'm guessing is that maybe the request is taking some time and i need a way of load that data on an async way but to be honest i really dont know
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let html = """
Appstore link dont open</span></span><br />
Normal link </span></span><br />
"""
var loadStatusChanged: ((Bool, Error?) -> Void)? = nil
func makeCoordinator() -> WebView.Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let view = WKWebView()
view.navigationDelegate = context.coordinator
view.loadHTMLString(html, baseURL: nil)
return view
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
class Coordinator: NSObject, WKNavigationDelegate {
let parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
}
}
struct ContentView: View {
var body: some View {
WebView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Some links on tapping them might activate actions / redirect with url schemes that are non HTTPs like
_blank to open a new tab
mailto to launch the mail application
some other deep link techniques familiar to device OSs
I believe the app store link uses a combination of the above and WKWebView cannot handle non HTTPs schemes.
What you can do is to listen to URLs that fail using WKNavigationDelegate and handle them accordingly
I am not using SwiftUI but I think you can get the picture.
Set up using the same HTML as you with both the links
class ViewController: UIViewController, WKNavigationDelegate
{
override func viewDidAppear(_ animated: Bool)
{
super.viewDidAppear(animated)
let html = """
Appstore link dont open</span></span><br />
Normal link </span></span><br />
"""
let webview = WKWebView()
webview.frame = view.bounds
webview.navigationDelegate = self
view.addSubview(webview)
webview.loadHTMLString(html, baseURL: nil)
}
}
Then I implement these WKNavigationDelegate functions
decidePolicyFor navigationAction (documentation link) to allow even urls that do not follow the HTTPs scheme to be allowed to be processed
this navigation fail delegate function webView didFailProvisionalNavigation and check if iOS can handle the open in a new tab, mail, deep link etc so in your case it would open the app store
You could also implement the same logic as point 2 in this
WKNavigationDelegate function just in case
// MARK: WKNavigationDelegates
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void)
{
decisionHandler(.allow)
}
func webView(_ webView: WKWebView,
didFailProvisionalNavigation navigation: WKNavigation!,
withError error: Error)
{
manageFailedNavigation(webView,
didFail: navigation,
withError: error)
}
func webView(_ webView: WKWebView,
didFail navigation: WKNavigation!,
withError error: Error)
{
manageFailedNavigation(webView,
didFail: navigation,
withError: error)
}
private func manageFailedNavigation(_ webView: WKWebView,
didFail navigation: WKNavigation!,
withError error: Error)
{
// Check if this failed because of mailto, _blank, deep links etc
// I have commented out how to check for a specific case like open in a new tab,
// you can try to handle each case as you wish
if error.localizedDescription
== "Redirection to URL with a scheme that is not HTTP(S)"
//let url = webView.url, url.description.lowercased().range(of: "blank") != nil
{
// Convert error to NSError so we can access the url
let nsError = error as NSError
// Get the url from the error
// This key could change in future iOS releases
if let failedURL = nsError.userInfo["NSErrorFailingURLKey"] as? URL
{
// Check if the action can be handled by iOS
if UIApplication.shared.canOpenURL(failedURL)
{
// Request iOS to open handle the link
UIApplication.shared.open(failedURL, options: [:],
completionHandler: nil)
}
}
}
}
Give this a go and check if this fixes your issue. On my side, both links seem to work fine:

Sending message to WKWebView throws error

I am using WKWebView to load HTML in the popup of my Safari App Extension. I am trying to send a message to this page using webView.evaluateJavaScript("myFunction()") but it fails with error message EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0).
I thought the page isn't loaded in the webView at first and hence throws this error but that is not the case here. The page loads completely but I get this error for some reason. Here is my code.
#IBOutlet var webView: WKWebView!
func webView(_ webView: WKWebView,didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript("myFunction()", completionHandler: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
webView.configuration.userContentController.add(self, name: "popup")
webView.configuration.userContentController.add(self, name: "print")
webView.navigationDelegate = self
view.addSubview(webView!)
self.view = webView
if let url = Bundle.main.url(forResource: "MyPopup", withExtension: "html") {
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
}
}
I tried checking error inside the completionHandler but it doesn't go there. Any Ideas?
I believe you need to use main thread to execute JS in the webView. Try wrapping the call to webView.evaluateJavaScript() in DispatchQueue.main.async { }.

Issue when setting cookies of WKWebView by copying cookies from HTTPCookieStorage to WKWebsiteDataStore

I want to show a website in a WKWebView in my iOS app for which I need to set specific http cookies for the user's session. The cookies are already stored in the shared HttpCookieStorage of the app.
At the moment, I try to copy them from there to the WKWebsiteDataStore of the web view before loading the website with the following code:
class ViewerViewController: UIViewController {
// MARK: - Properties
var webView: WKWebView!
// MARK: - Life Cycle
override func loadView() {
webView = WKWebView()
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
addCookiesToWebView {
print("Load website")
if let url = Bundle.main.url(forResource: "MyWebsite", withExtension: "html"), let html = try? String(contentsOf: url) {
self.webView.loadHTMLString(html, baseURL: url)
}
}
}
// MARK: - Functions
func addCookiesToWebView(completionHandler: #escaping (() -> Void)) {
let sharedCookies = HTTPCookieStorage.shared.cookies!
let webViewCookieStore = webView.configuration.websiteDataStore.httpCookieStore
var count = 0
sharedCookies.forEach { cookie in
DispatchQueue.main.async {
webViewCookieStore.setCookie(cookie) {
count += 1
print("Added cookie \(cookie.name)")
if count == sharedCookies.count {
completionHandler()
}
}
}
}
}
}
Now, what is incomprehensible for me, is that this sometimes works and the user is authenticated in his user session, but sometimes it doesn't.
It seems like there might be a race condition that leads to the web view loading the website before all the cookies have been copied? When I researched this problem I noticed that other people have problems with cookies in WKWebView as well, but I didn't find any solution so far that actually solves my issue.
Is there a bug in my code or is there a better approach for copying multiple cookies from HttpCookieStorage to a WKWebView?

iOS - web view cookies not set

I am trying to set cookies in my iOS like this:
let url = URL(string: "url")!
let jar = HTTPCookieStorage.shared
let cookieHeaderField = ["Set-Cookie": "key1=value1, key2=value2"]
let cookies = HTTPCookie.cookies(withResponseHeaderFields: cookieHeaderField, for: url)
jar.setCookies(cookies, for: url, mainDocumentURL: url)
let request = URLRequest(url: url)
viewerWebKit.load(request)
Then I am printing them like this:
viewerWebKit.configuration.websiteDataStore.httpCookieStore.getAllCookies( { (cookies) in
cookies.forEach({ (cookie) in
print(cookie.name)
})
})
All cookies are printed and they seem to be set normally. But when I use the Web inspector of my safari to see, if they are really set then nothing is there. No cookies are set. What is the problem? Do I need to accept them? Is safari blocking them? How can I set them normally, to be visible in web inspector?
I also tried this approach:
import UIKit
import WebKit
class ViewWrapper: UIViewController, WKNavigationDelegate {
var loginToken: String?
#IBOutlet weak var viewerWebKit: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
var urlRequest = URLRequest(url: URL(string: "url")!)
urlRequest.httpShouldHandleCookies = true
let newcookie = HTTPCookie(properties: [
.domain: "domain",
.path: "",
.name: "key",
.value: "value",
.secure: "FALSE",
.expires: NSDate(timeIntervalSinceNow: 31556926)
])
viewerWebKit.configuration.websiteDataStore.httpCookieStore.setCookie(newcookie!, completionHandler: {
self.viewerWebKit.load(urlRequest)
})
viewerWebKit.configuration.websiteDataStore.httpCookieStore.getAllCookies( { (cookies) in
cookies.forEach({ (cookie) in
print(cookie.name)
})
})
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func loadView() {
viewerWebKit = WKWebView()
viewerWebKit.navigationDelegate = self
view = viewerWebKit
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
title = webView.title
}
func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
cookieStore.getAllCookies({ (cookies) in
cookies.forEach({ (cookie) in
print(cookie.name)
})
})
}
}
but it didn't work too.
This is what I see in safari debug console:
Cookies are not set.
This is what I see in Xcode's console.
So here it seems to be set. But it is not in reality. Printing code prints cookies. But they are not all visible in safari console. How is that possible? Cookies csrftoken and sessionid are set by website, not by my app. And they are visible in both printing and debug console.
Set cookie through this
urlRequest.httpShouldHandleCookies = true
also after request create set
self.configuration.websiteDataStore.httpCookieStore.setCookie("your_http_cookie", completionHandler: {
// Do whatever you want. I suggest loading your webview after cookie is set.
})
Implement this observers WKHTTPCookieStoreObserver in your class.
WKWebsiteDataStore.default().httpCookieStore.add(self)
func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
// Must implement otherwise wkwebview cookie not sync properly
self.httpCookieStore.getAllCookies { (cookies) in
cookies.forEach({ (cookie) in
// print your cookie here
})
}
}

UIWebView.loading returns false

With swift using the following code:
super.viewDidLoad()
let webView = UIWebView(frame: self.view.bounds)
view.addSubview(webView)
let URL = NSURL(string: "http://www.google.com")
webView.loadRequest(NSURLRequest(URL: URL!))
println(webView.loading)
It prints false and the screen is blank. How is this resolved?
This is perfectly normal behaviour. The UIWebView does not actually start loading content until the UI event, in this case viewDidLoad, has finished executing. So checking it immediately returns false because it has not started just yet.
If you want want to track the success or failure of the UIWebView you should implement the UIWebViewDelegate in your view controller. That way you get callbacks when the request has finished loading or if it has failed.
I'm uncertain exactly on the internals of how UIWebView loads its data or what exactly happens when you call loadRequest, but contrary to my expectations, nothing seems to actually happen until the method which called loadRequest has returned.
Consider the following code:
#IBAction func buttonPress(sender: UIButton) {
let webview = UIWebView(frame: self.view.bounds)
view.addSubview(webview)
let url = NSURL(string: "http://www.google.com")
webview.delegate = self
webview.loadRequest(NSURLRequest(URL: url!))
println("buttonPress: webview.loading? \(webview.loading)")
}
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
println("webview asking for permission to start loading")
return true
}
func webViewDidStartLoad(webView: UIWebView) {
println("webview did start loading")
}
func webView(webView: UIWebView, didFailLoadWithError error: NSError) {
println("webview did fail load with error: \(error)")
}
func webViewDidFinishLoad(webView: UIWebView) {
println("webview did finish load!")
}
Here, the println in the buttonPress method always executes before any of the other methods. In fact, seemingly no matter what sort of code we put after the loadRequest call, it all executes before the web view even asks shouldStartLoadingWithRequest:.
webview.loadRequest(NSURLRequest(URL: url!))
for _ in 1...10000 {
println("buttonPress: webview.loading? \(webview.loading)")
}
Ten thousand iterations, and yet nothing starts for the webview until after buttonPress method returns.
Meanwhile, webview.loading will only return true between webViewDidStartLoad and one of the two ways the load can stop (failure/success) (webView(webView:didFailLoadWithError:) or webViewDidFinishLoad()).
IF you implement the UIWebViewDelegate protocol and set the web view's delegate, you can implement these methods to keep track of the loading process. If, for whatever reason, your web view isn't loading the URL, implementing webView(webView:didFailLoadWithError:) is the only way to get any sort of diagnostic informatin to determine what failed with the load.

Resources