I'm trying to evaluate some JavaScript in my iOS app using JSContext. It's working well however I am not able to catch console.log() statements. How can I get the results of these function calls so I can print them in Swift as well?
Example Code
let jsSource = "var testFunct = function(message) { console.log(\"kud\"); return \"Test Message: \" + message;}"
let context = JSContext()
context?.evaluateScript(jsSource)
let testFunction = context?.objectForKeyedSubscript("testFunct")
let result = testFunction?.call(withArguments: ["the message"])
print(result!)
Example Logs
Test Message: the message
In case anyone is struggling with the same thing, here's the answer in Swift 4.
let javascriptContext = JSContext()
javascriptContext?.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }")
let consoleLog: #convention(block) (String) -> Void = { message in
print("console.log: " + message)
}
javascriptContext?.setObject(unsafeBitCast(consoleLog, to: AnyObject.self), forKeyedSubscript: "_consoleLog" as (NSCopying & NSObjectProtocol)!)
Now you can call console.log() in any subsequent evaluateScript calls javascriptContext.
Related
I have a function: I need to know from Wiktionary API if a user´s input german noun / adjective exists and its grammatical gender.
But the function always returns undefined although:
I asked for a return
the function is async
I used .onload on the function call waiting for the response
Could you please explain me what I am doing wrong? It´s the first time I work with APIs and I am pretty confused.
Thanks
check(userInput).onload;
async function check(word) {
$.getJSON('http://de.wiktionary.org/w/api.php?action=parse&format=json&prop=text|revid|displaytitle&callback=?&page=' + word,
function (json) {
try {
let variable = json.parse.text['*'];
let nounM = variable.search('id=\"Substantiv,_m\">');
let nounF = variable.search('id=\"Substantiv,_f\">');
let nounN = variable.search('id=\"Substantiv,_n\">');
let adjective = variable.search("href=\"#Adjektiv");
let gender = Math.max(nounM,nounF,nounN,adjective);
console.log(gender);
return gender;
} catch (e) {
console.log(e);
return undefined;
}
});
};
// ContentView.swift
// Shared
import Foundation
import SwiftUI
import JavaScriptCore
class cube {
var result: String
func do_js(text: String) -> String {
let jsSource = "var testFunct = function(message) { return \"Test Message: \" + message;}"
var context = JSContext()
context?.evaluateScript(jsSource)
let testFunction = context?.objectForKeyedSubscript("testFunct")
var result = testFunction?.call(withArguments: [text]).toString()
return result!
}
}
struct ContentView: View {
cube().do_js(text: "Hello world") // Starts forom here
var show_text = lol().result
var body: some View {
Text(show_text)
.font(.body)
.fontWeight(.black)
.foregroundColor(Color.red)
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
(Sorry, I'm beginner and come to swift even not from js, but from python! So it's incredibly new for me. But js more understandable for me from python.)
enter image description here
Here's the simplest version. In this version, it basically uses your original pattern where doJS returns a value. The disadvantage to this version is that doJS will get called every time the view renders.
class Cube {
func doJS(text: String) -> String? {
let jsSource = "var testFunct = function(message) { return \"Test Message: \" + message;}"
let context = JSContext()
context?.evaluateScript(jsSource)
let testFunction = context?.objectForKeyedSubscript("testFunct")
return testFunction?.call(withArguments: [text]).toString()
}
}
struct ContentView: View {
var body: some View {
Text(Cube().doJS(text: "Hello, world!") ?? "No result")
.font(.body)
.fontWeight(.black)
.foregroundColor(Color.red)
.padding()
}
}
And here's a slightly different version. In this version, Cube is an ObservableObject with a #Published value that stores the result. It will only get called once in onAppear.
class Cube : ObservableObject {
#Published var result : String?
func doJS(text: String) {
let jsSource = "var testFunct = function(message) { return \"Test Message: \" + message;}"
let context = JSContext()
context?.evaluateScript(jsSource)
let testFunction = context?.objectForKeyedSubscript("testFunct")
result = testFunction?.call(withArguments: [text]).toString()
}
}
struct ContentView: View {
#StateObject var cube = Cube()
var body: some View {
Text(cube.result ?? "No result")
.font(.body)
.fontWeight(.black)
.foregroundColor(Color.red)
.padding()
.onAppear {
cube.doJS(text: "Hello, world!")
}
}
}
I have this code:
const func = () => {
throw new Error('hey') + 'boo'
return 'OK'
}
try {
const val = func()
console.log(val)
} catch (error) {
console.log(error)
}
When launched the result is a console line:
"Error: heyboo"
But this is not clear reading the code.
What's the reason for this?
It's because you're doing new Error('hey') + 'boo' and throwing that (which may be surprising). Here's what the code is doing:
Creates the error object
Does + on the error object and 'boo', which converts the error object to string ("Error: hey") and appends "boo" to it
Throws the resulting "Error: heyboo" string
...which you then catch and display.
JavaScript is slightly unusual compared to other languages in that it lets you throw any value, including a string. You aren't restricted to throwing Error objects.
This code does the same thing, hopefully making it a bit clearer by breaking things into steps:
const func = () => {
// Create error
const error = new Error("hey");
// Append `"boo"` to it to get a string
const str = error + "boo";
// Throw the string
throw str;
// This is never reached
return "OK";
};
try {
const val = func();
console.log(val);
} catch (error) {
console.log(error);
}
I implemented the WKScriptMessageHandler protocol and I have defined the userContentController(:didReceiveScriptMessage:) method.
When there is a error in the Javascript, I get (in the WKScriptMessage object) something not really useful like:
{
col = 0;
file = "";
line = 0;
message = "Script error.";
type = error;
}
On the other hand, if I open the Safari Web Inspector, I can see the real error which is (for instance):
TypeError: FW.Ui.Modal.sho is not a function. (In 'FW.Ui.Modal.sho', 'FW.Ui.Modal.sho' is undefined)
Is there a way to get that error back in my native code?
EDIT:
Just to clarify, the javascript code is written by a javascript developer (who doesn't have access to the native source code, so he can't debug the app via Xcode). The code he writes it's then pushed to the iOS app (downloaded from an enterprise app store).
You can wrap the expression in a try catch block before evaluating it. Then have the JavaScript return the error message if it fails. Here is an example taken from the Turbolinks-iOS adapter, available on GitHub.
extension WKWebView {
func callJavaScriptFunction(functionExpression: String, withArguments arguments: [AnyObject?] = [], completionHandler: ((AnyObject?) -> ())? = nil) {
guard let script = scriptForCallingJavaScriptFunction(functionExpression, withArguments: arguments) else {
NSLog("Error encoding arguments for JavaScript function `%#'", functionExpression)
return
}
evaluateJavaScript(script) { (result, error) in
if let result = result as? [String: AnyObject] {
if let error = result["error"] as? String, stack = result["stack"] as? String {
NSLog("Error evaluating JavaScript function `%#': %#\n%#", functionExpression, error, stack)
} else {
completionHandler?(result["value"])
}
} else if let error = error {
self.delegate?.webView(self, didFailJavaScriptEvaluationWithError: error)
}
}
}
private func scriptForCallingJavaScriptFunction(functionExpression: String, withArguments arguments: [AnyObject?]) -> String? {
guard let encodedArguments = encodeJavaScriptArguments(arguments) else { return nil }
return
"(function(result) {\n" +
" try {\n" +
" result.value = " + functionExpression + "(" + encodedArguments + ")\n" +
" } catch (error) {\n" +
" result.error = error.toString()\n" +
" result.stack = error.stack\n" +
" }\n" +
" return result\n" +
"})({})"
}
private func encodeJavaScriptArguments(arguments: [AnyObject?]) -> String? {
let arguments = arguments.map { $0 == nil ? NSNull() : $0! }
if let data = try? NSJSONSerialization.dataWithJSONObject(arguments, options: []),
string = NSString(data: data, encoding: NSUTF8StringEncoding) as? String {
return string[string.startIndex.successor() ..< string.endIndex.predecessor()]
}
return nil
}
}
I am trying to access a JSON object in a URL inside the UIWebView. Here is my code for that -
func webView(myWebView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let requestUrl:NSURL = request.URL!
let url:String = requestUrl.absoluteString
if url.rangeOfString("token") != nil {
print("exists")
let index = url.rangeOfString("token=", options: .BackwardsSearch)?.endIndex
let tokenValue = url.substringFromIndex(index!)
if let data = tokenValue.dataUsingEncoding(NSUTF8StringEncoding) {
do {
let json: AnyObject? = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments) as! NSDictionary
print(json)
} catch {
print("Something went wrong")
}
}
self.tokenField.text = "\(tokenValue)"
}
The 'request URL' is coming from the following JS -
var tokenObj = {"accessToken":"abc123"};
window.location.href = "didtap://LoginButton?token=" + tokenObj;
The problem here is when I am trying to access the JSON Object using Swift, I see the URL coming in as didtap://LoginButton?token=[object%20Object]
Here is also a screenshot of my debugger.
I am looking for the JSON object as is, so I can use the token back in my app where needed.
You can use substring.stringByRemovingPercentEncoding will solve your problem