52

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?

pkamb
  • 33,281
  • 23
  • 160
  • 191
redak105
  • 559
  • 1
  • 5
  • 6
  • `NSString *returnVal = [self.webView stringByEvaluatingJavaScriptFromString:@"func(\"arg\")"];` doesn't this work? – l0gg3r Nov 06 '14 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. – redak105 Nov 06 '14 at 12:25
  • hm... strange, what does the console output ? after NSLogs – l0gg3r Nov 06 '14 at 12:32

9 Answers9

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);
Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
UnknownStack
  • 1,350
  • 16
  • 18
  • Use stringByEvaluatingJavaScriptFromString: name instead to leave your existing UIWebView code untouched. – Steve Moser May 31 '15 at 01:48
  • 3
    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 – Brams Jun 17 '15 at 14:36
  • Marked this one up for being first (and ending a full morning of WTF). Please update this answer to use the BOOL isFinished check noted below for 5 stars. – eGanges Jul 01 '16 at 18:39
  • 1
    will cause crash while multiple wkwebview are used; – Nikita Dec 15 '16 at 12:33
  • This causes crash in High Sierra but was working fine under previous versions – AP. Feb 03 '18 at 15:59
  • 3
    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. – Mike Keskinov Sep 04 '18 at 00:21
  • @MikeKeskinov is correct, this is not working on iOS 12+ anymore. People should migrate to an async solution by now. – Hans Knöchel Sep 20 '18 at 10:10
  • This works fine. The issue is that you're using `.distantFuture` - if the runloop doesn't exit, then your loop will never end. A better alternative is `.distantPast` or `Date(timeIntervalSinceNow: 0.1)` – mattsven Feb 01 '19 at 04:02
  • The above solution is not working in iOS12+ but working in 11.x, It's crashing in RunLoop.current.run(mode: RunLoopMode.defaultRunLoopMode, before: Date.distantFuture). Anyother solution for same implementation in iOS12.0 – Arvind Kumar Sep 12 '19 at 13:07
  • 2
    Any ideas for iOS 12 or iOS 13? – Dmitry Oct 28 '19 at 01:21
  • For iOS 12,13 its possible to use dispatch_semaphore: dispatch_semaphore create before the evaluate call, dispatch_semaphore_signal inside the block and dispatch_semafore_wait after the block. – void Nov 25 '19 at 19:45
  • I just added a snappet as the answer above, https://stackoverflow.com/a/59039131/985280 – void Nov 25 '19 at 19:50
  • 1
    Can anyone describe what is wrong with this solution? I've checked it on iOS 12.4.1 and 13.3 and it worked good (called from the main thread). – Mateusz Dec 17 '19 at 14:16
  • 2
    This solution doesn't work in iOS 13. Does anyone has the working solution for iOS 13 ? – Keerthiraj Jul 07 '20 at 13:51
30

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;
}
Slavcho
  • 2,792
  • 3
  • 30
  • 48
Brams
  • 584
  • 4
  • 9
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)
        }
    }
}
mort3m
  • 481
  • 1
  • 7
  • 17
  • 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? – Wesley Lalieu Aug 12 '16 at 16:11
  • I used: NSRunLoop.currentRunLoop().runMode("NSDefaultRunLoopMode", beforeDate: NSDate.distantFuture()) this worked! Thanks for your answer!!! – Wesley Lalieu Aug 12 '16 at 16:20
  • 7
    Sorry but what's the point of this if you are using a callback anyway? – Alexandre G May 10 '18 at 04:14
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)
        }
    }
}
Nicolas Mandica
  • 803
  • 1
  • 10
  • 16
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);
    }
}];
Steve Yost
  • 725
  • 6
  • 5
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
Tri Do
  • 11
  • 1
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)
                }
            }
        })
    }
}
boraseoksoon
  • 2,164
  • 1
  • 20
  • 25
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;
}
ma sa
  • 11
  • 1
-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. 
void
  • 1,343
  • 11
  • 26
  • @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. – void Dec 04 '19 at 03:45
  • 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. – Ivan Dec 13 '19 at 13:38