12

I am working on passing data from swift to javascript inside a WKWebView

I have a custom class:

class AllInfo: AnyObject {
    var title = "Special Title"
    var description = "Special Description"
}

and initialize it with

var info = AllInfo()

I then have a WKWebView with which i pass a WKUserScript with which I have a source property of:

source: "changeDisplay('\(info)')"

My problem is how do I access this object in the javascript. I have attempted to access it like a javascript object as well as associative array with no luck. Here's the js funciton:

function changeDisplay(passedInfo) {
    document.querySelector('h1').innerHTML = passedInfo.title
    document.querySelector('h2').innerHTML = passedInfo.description
}
setTimeout(function () {changeDisplay();}, 5000);

EDIT: When I do attempt to access the object like this, I get undefined.

So my questions are:

Can I pass an AnyObject to JavaScript and access it? If not, what type should I make the swift class so that I can easily pass it.

I am tempted to just create a javascript object in swift as a string and pass that, but I feel there's a better way.

Thanks

EDIT: I answered how I was able to pass data as JSON below.

Deciple
  • 1,864
  • 3
  • 16
  • 19

5 Answers5

23

In .swift, define and call this method

func evaluateJavaScriptForData(dictionaryData: [String: AnyObject]) {
    // Convert swift dictionary into encoded json
    let serializedData = try! NSJSONSerialization.dataWithJSONObject(dictionaryData, options: .PrettyPrinted)
    let encodedData = serializedData.base64EncodedStringWithOptions(.EncodingEndLineWithLineFeed)
    // This WKWebView API to calls 'reloadData' function defined in js
    webView.evaluateJavaScript("reloadData('\(encodedData)')") { (object: AnyObject?, error: NSError?) -> Void in
        print("completed with \(object)")
    }
}

In .js/.html file

function reloadData(sBinaryParam) {
    // This 'atob' decodes the Base64 encoded data sent by swift
    var sDecodedParam = window.atob(sBinaryParam);
    var oData = JSON.parse(sDecodedParam);
    // This 'oData' is your js object (dictionary)
    return true;
}
Ashok
  • 6,224
  • 2
  • 37
  • 55
  • Perfect. This worked beautifully. However I think this is for any JSON response. And not custom objects. – esh Sep 30 '16 at 05:37
  • @BlackFlam3 could you not turn custom objects into JSON object first? – Crashalot Oct 24 '16 at 21:10
  • @Crashalot yes I am missing something indeed. So I found it easy to turn a JSON to custom object, and well, possibly I need to write a reverse function to turn a custom object into JSON. – esh Oct 25 '16 at 05:21
  • 1
    This did not work if the data contains Chinese characters. `atob` isn't able to decode UTF-8, unfortunately: http://stackoverflow.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings – Crashalot Oct 28 '16 at 04:33
5

Answering my own question:

JSON is (probably) the best type to pass to javascript.

I removed the class and instead created an NSDictionary like (or type your class as NSDictionary):

var allInfoRawSwift = ["title": "This is the title", "description": "this is the description"]

Then convert it to JSON (inside do/catch if swift 2):

let allInfoJSON = try NSJSONSerialization.dataWithJSONObject(allInfoRawSwift, options: NSJSONWritingOptions(rawValue: 0))

The convert JSON to string:

let allInfoJSONString = NSString(data: allInfoJSON, encoding: NSUTF8StringEncoding)!

Then add the souce propery like:

source: "changeDisplay(\(allInfoJSONSting))"

To debug, it's best to have your function just set a global variable to whatever your passing, then use safari's developer mode to access a web console and inspect the global variable.

Let me know if there's a better way or if you have any tips.

Thanks

Deciple
  • 1,864
  • 3
  • 16
  • 19
5

Passing a native object to javascript is complex, especially for WKWebView which runs in multi-process mode. Any operation regarding the native object needs to cross process boundary. WKWebView has no language binding support between native and javascript. However, WKWebView supports message passing API. You have to wrap it for complex interactions between JS and native.

I created a project named XWebView which provides language binding styled API based on the raw message passing of WKWebView. It's written in Swift.

Regarding your example, the object has to be injected in javascript namespace firstly:

let webView = WKWebView(frame: frame, configuration: WKWebViewConfiguration())
webView.loadPlugin(AllInfo(), namespace: "someInfo")

You can access the object in javascript:

console.log(window.someInfo.title);
window.someInfo.title = "Some title";

To expose an Swift object to javascript, properties and methods must be dynamic dispatching. This means, properties must be dynamic, methods must has @objc attribute. (See https://developer.apple.com/swift/blog/?id=27 for dynamic dispatching). For simple, inherit from NSObject.

epologee
  • 11,229
  • 11
  • 68
  • 104
soflare
  • 751
  • 5
  • 16
0

Here's an update of the Swift code from Ashok's accepted answer, compatible with Swift 3.0.2:

func evaluateJavaScriptForData(dictionaryData: [String: AnyObject]) {
    // Convert swift dictionary into encoded json
    let serializedData = try! JSONSerialization.data(withJSONObject: swiftObject, options: .prettyPrinted)
    let encodedData = serializedData.base64EncodedString(options: .endLineWithLineFeed)
    // This WKWebView API to calls 'reloadData' function defined in js
    webView.evaluateJavaScript("reloadData('\(encodedData)')", completionHandler: { result, error in
        print("Completed Javascript evaluation.")
        print("Result: \(result)")
        print("Error: \(error)")
    })
}
Jamie Birch
  • 5,839
  • 1
  • 46
  • 60
  • the quotes are not proper above here is how you can pass the data let script = "getSessionIdUserId('\(sessionId)', '\(userId)')" self.webView?.evaluateJavaScript(script, completionHandler: { (obj:Any?, error:Error?) in }) – ashish May 01 '17 at 12:43
0

Here is my suggestion (using json as data example):

  1. Create json:

var json = ["value1": "One", "value2": "two"]

  1. Encode to a string (I'm not using base64):

guard let json = try? JSONEncoder().encode(permissionsJSON), let jsonString = String(data: json, encoding: .utf8) else { return }

  1. Send:

webView.evaluateJavaScript("window.evite.webview.yourName('(jsonString)')")