53

I need to change a function to evaluate JavaScript from UIWebView to WKWebView. I need to return result of evaluating in this function.

Now, I am calling:

[wkWebView evaluateJavaScript:call completionHandler:^(NSString *result, NSError *error)
{
    NSLog(@"Error %@",error);
    NSLog(@"Result %@",result);
}];

But I need get result like return value, like in UIWebView. Can you suggest a solution?

3
  • NSString *returnVal = [self.webView stringByEvaluatingJavaScriptFromString:@"func(\"arg\")"]; doesn't this work? Commented Nov 6, 2014 at 12:20
  • No this function is in UIWebView and is working, I need to change it to WKWebView. I can solve it with some callback, but it is too complicated in my project. Commented Nov 6, 2014 at 12:25
  • hm... strange, what does the console output ? after NSLogs Commented Nov 6, 2014 at 12:32

9 Answers 9

51

Update: This is not working on iOS 12+ anymore.


I solved this problem by waiting for result until result value is returned.

I used NSRunLoop for waiting, but I'm not sure it's best way or not...

Here is the category extension source code that I'm using now:

@interface WKWebView(SynchronousEvaluateJavaScript)
- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
@end

@implementation WKWebView(SynchronousEvaluateJavaScript)

- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script
{
    __block NSString *resultString = nil;
    __block BOOL finished = NO;

    [self evaluateJavaScript:script completionHandler:^(id result, NSError *error) {
        if (error == nil) {
            if (result != nil) {
                resultString = [NSString stringWithFormat:@"%@", result];
            }
        } else {
            NSLog(@"evaluateJavaScript error : %@", error.localizedDescription);
        }
        finished = YES;
    }];

    while (!finished)
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }

    return resultString;
}
@end

Example code:

NSString *userAgent = [_webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];

NSLog(@"userAgent: %@", userAgent);
Sign up to request clarification or add additional context in comments.

14 Comments

The issue with this code is that if a JS error is raised, your native function will run infinitely ! The solution would be to check a block safe boolean instead of resultString. Look below
will cause crash while multiple wkwebview are used;
This solution IS NOT WORKING anymore. The callback now called from the main Thread and if you lock the main thread in the while loop (like in this solution), the callback handler will be never called. evaluateJavaScript waits for main thread released, but it never happens because it locked in the loop.
Any ideas for iOS 12 or iOS 13?
This solution doesn't work in iOS 13. Does anyone has the working solution for iOS 13 ?
|
29

This solution also works if the javascript's code raise NSError:

- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script {
    __block NSString *resultString = nil;
    __block BOOL finished = NO;

    [self evaluateJavaScript:script completionHandler:^(id result, NSError *error) {
        if (error == nil) {
            if (result != nil) {
                resultString = [NSString stringWithFormat:@"%@", result];
            }
        } else {
            NSLog(@"evaluateJavaScript error : %@", error.localizedDescription);
        }
        finished = YES;
    }];

    while (!finished)
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }

    return resultString;
}

Comments

14

I just stumbled about the same problem and wrote a little Swift (3.0) WKWebView extension for it, thought I might share it:

extension WKWebView {
    func evaluate(script: String, completion: (result: AnyObject?, error: NSError?) -> Void) {
        var finished = false

        evaluateJavaScript(script) { (result, error) in
            if error == nil {
                if result != nil {
                    completion(result: result, error: nil)
                }
            } else {
                completion(result: nil, error: error)
            }
            finished = true
        }

        while !finished {
            RunLoop.current().run(mode: .defaultRunLoopMode, before: Date.distantFuture)
        }
    }
}

3 Comments

I can't use your code because of the RunLoop. My compiler can't find the RunLoop variable. I did import WKWebKit and Foundation... When I use the NSRunLoop.currentRunLoop().run() it keeps running, and I can't say for how long to run. What am I doing wrong?
I used: NSRunLoop.currentRunLoop().runMode("NSDefaultRunLoopMode", beforeDate: NSDate.distantFuture()) this worked! Thanks for your answer!!!
Sorry but what's the point of this if you are using a callback anyway?
5

Base on @mort3m's answer, here is a WKWebView extension working with Swift 5.

extension WKWebView {
    func evaluate(script: String, completion: @escaping (Any?, Error?) -> Void) {
        var finished = false

        evaluateJavaScript(script, completionHandler: { (result, error) in
            if error == nil {
                if result != nil {
                    completion(result, nil)
                }
            } else {
                completion(nil, error)
            }
            finished = true
        })

        while !finished {
            RunLoop.current.run(mode: RunLoop.Mode(rawValue: "NSDefaultRunLoopMode"), before: NSDate.distantFuture)
        }
    }
}

5 Comments

How does one use this code within a typical webView.evaluateJavaScript(jsCode) { (value, error) in statement?
You have to use this new function evaluate instead of evaluateJavaScript.
I totally missed that it was just evaluate. Late night I suppose. Code is working great, btw!
Hey @Nicolas, I'm getting new crashing errors with this code (introduced with Swift 5 I believe).
@JustinBush It's working well with Swift 5 for me. What is your error ?
4

I've found that the value of final statement in your injected javascript is the return value passed as the id argument to the completion function, if there are no exceptions. So, for example:

[self.webview evaluateJavaScript:@"var foo = 1; foo + 1;" completionHandler:^(id result, NSError *error) {
    if (error == nil)
    {
        if (result != nil)
        {
            NSInteger integerResult = [result integerValue]; // 2
            NSLog(@"result: %d", integerResult);
        }
    }
    else
    {
        NSLog(@"evaluateJavaScript error : %@", error.localizedDescription);
    }
}];

Comments

1

Base on @mort3m's comment. Here is working Objective-C version.

@implementation WKWebView(SynchronousEvaluateJavaScript)

- (void)stringByEvaluatingJavaScriptFromString:(NSString *)script completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler
{
    __block BOOL finished = FALSE;
    [self evaluateJavaScript:script completionHandler:^(id result, NSError *error) {
        if (error == nil) {
            if (result != nil) {
                completionHandler(result, error);
            }
        } else {
            completionHandler(NULL, error);
        }
        finished = TRUE;
    }];

    while(!finished) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
}

@end

Comments

1

Swift 5.7

All you need should be

webView.evaluateJavaScript("your js code") { res, err in 
    // TODO
}

a full code snippet example is

let js = """
function add(x, y) {
    return x + y
}

add(1, 2)
"""

webView.evaluateJavaScript(js) { res, err in
    print("res : \(res)") 
}

if you prefer async/await:

let js = """
function add(x, y) {
    return x + y
}

add(1, 2)
"""

Task {
    let res = try? await webView.evaluate(javascript: js)
    print("res : \(res)")
}

extension WKWebView {
    
    @discardableResult
    func evaluate(javascript: String) async throws -> Any {
        return try await withCheckedThrowingContinuation({ continuation in
            evaluateJavaScript(javascript, in: nil, in: .page) { result in
                switch result {
                case .success(let output):
                    continuation.resume(returning: output)
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        })
    }
}

Comments

0

Only this works,the answers above not work for me.

- (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script
{
    __block NSString *resultString = nil;
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    [self evaluateJavaScript:script completionHandler:^(id result, NSError *error) {
        if (error == nil) {
            if (result != nil) {
                resultString = [NSString stringWithFormat:@"%@", result];
            }
        } else {
            NSLog(@"evaluateJavaScript error : %@", error.localizedDescription);
        }
        dispatch_semaphore_signal(sem);
    }];
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    
    return resultString;
}

Comments

-2

It's possible to use dispatch semaphore. It works on iOS12+

Example:

    __block NSString *resultString = nil;
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    [self evaluateJavaScript:script completionHandler:^(id result, NSError *error) {
        if (error == nil) {
            if (result != nil) {
                resultString = [NSString stringWithFormat:@"%@", result];
                dispatch_semaphore_signal(sem);
            }
        } else {
            NSLog(@"evaluateJavaScript error : %@", error.localizedDescription);
        }
        finished = YES;
    }];

    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

    //process resultString here. 

2 Comments

@Shizam you maybe willing to ensure that your WKWebview is loaded first. Such approach doesnt work indeed when you are trying to check if document is ready, meanwhile it works fine for me just for extracting some innerHTML.
The documentation says: "The completion handler always runs on the main thread." So if this code is called on main thread it will block forever.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.