92

When writing a iPhone / iPad app with a UIWebView, the console isn't visible. this excellent answer shows how to trap errors, but I would like to use the console.log() as well.

Community
  • 1
  • 1
TinkerTank
  • 5,685
  • 2
  • 32
  • 41
  • 1
    Write it in the browser first, turn on Developer Tools, then look at the console output. – beatgammit Jun 28 '11 at 15:00
  • 1
    There's a lot of options presented in this similar question: *[Debug iPad Safari with a PC](https://stackoverflow.com/q/20408110/195835)* - these don't require any native code to use. – Simon East Dec 23 '22 at 14:07

7 Answers7

200

After consulting with an esteemed colleague today he alerted me to the Safari Developer Toolkit, and how this can be connected to UIWebViews in the iOS Simulator for console output (and debugging!).

Steps:

  1. Open Safari Preferences -> "Advanced" tab -> enable checkbox "Show Develop menu in menu bar"
  2. Start app with UIWebView in iOS Simulator
  3. Safari -> Develop -> i(Pad/Pod) Simulator -> [the name of your UIWebView file]

You can now drop complex (in my case, flot) Javascript and other stuff into UIWebViews and debug at will.

EDIT: As pointed out by @Joshua J McKinnon this strategy also works when debugging UIWebViews on a device. Simply enable Web Inspector on your device settings: Settings->Safari->Advanced->Web Inspector (cheers @Jeremy Wiebe)

UPDATE: WKWebView is supported too

Alexander Vasenin
  • 11,437
  • 4
  • 42
  • 70
NSTJ
  • 3,858
  • 2
  • 27
  • 34
  • 13
    Note, this strategy also works when debugging on real iOS devices. – Joshua J. McKinnon Nov 27 '13 at 03:00
  • 3
    +100 if I could. This is wonderful, it works for phone gap apps too! – Andy Novocin Feb 08 '14 at 18:00
  • This should be made the answer. This is wonderful and I agree with the above, this should get all the up votes – Roderic Campbell Mar 13 '14 at 02:03
  • Can anyone tell me please how can it be done with iOS devices? – Floydian Mar 26 '14 at 14:22
  • @Floydian what seems to be the problem? – NSTJ Mar 27 '14 at 00:29
  • 2
    Trying with an iPad, when I go to the develop menu on Safari, there are no Devices to choose. When I deploy on the simulator, it works like a charm. – Floydian Mar 27 '14 at 20:51
  • 11
    @Floydian you have to enable Web Inspector on the device. Settings->Safari->Advanced->Web Inspector. – Jeremy Wiebe Jun 12 '14 at 22:34
  • 1
    Whenever I set a breakpoint in a JS file this way, the debugger breaks there, but then the step buttons don't work. They don't respond to clicks. has anyone else seen this? – juanpaco Feb 17 '15 at 21:50
  • 2
    my app is not showing up in my develop menu. I have enabled web inspector. Safari shows up, but my app (which is current;y displaying 2 UIWebviews) is not detected.. any ideas? – narco Dec 17 '15 at 23:01
  • 1
    Don't miss out on @Floydian comment: you need to enable Web Inspector on the device as well. – ar34z Feb 25 '16 at 10:27
  • Any way to get this to work in the simulator? I don't have i(Pad/Pod) Simulator in my Develop menu in Safari. – Curtis Feb 03 '19 at 21:36
  • @Curtis it should definitely work in the Simulator+Safari - are you still having the issue? – NSTJ Feb 04 '19 at 01:51
  • Yes I have Xcode 10.1, and when I run my app in the simulator and go to Safari and click Develop, there is no Simulator option. If I run on my iPhone, it does show that iPhone in the Develop menu. – Curtis Feb 04 '19 at 04:41
  • 1
    @Curtis I had that issue too. It's working for me now. It's strange but I have to open a new instance of safari and also close the simulator and run the app again – maxi-code Jun 25 '19 at 22:14
82

I have a solution to log, using javascript, to the apps debug console. It's a bit crude, but it works.

First, we define the console.log() function in javascript, which opens and immediately removes an iframe with a ios-log: url.

// Debug
console = new Object();
console.log = function(log) {
  var iframe = document.createElement("IFRAME");
  iframe.setAttribute("src", "ios-log:#iOS#" + log);
  document.documentElement.appendChild(iframe);
  iframe.parentNode.removeChild(iframe);
  iframe = null;    
};
console.debug = console.log;
console.info = console.log;
console.warn = console.log;
console.error = console.log;

Now we have to catch this URL in the UIWebViewDelegate in the iOS app using the shouldStartLoadWithRequest function.

- (BOOL)webView:(UIWebView *)webView2 
shouldStartLoadWithRequest:(NSURLRequest *)request 
 navigationType:(UIWebViewNavigationType)navigationType {

    NSString *requestString = [[[request URL] absoluteString] stringByReplacingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
    //NSLog(requestString);

    if ([requestString hasPrefix:@"ios-log:"]) {
        NSString* logString = [[requestString componentsSeparatedByString:@":#iOS#"] objectAtIndex:1];
                               NSLog(@"UIWebView console: %@", logString);
        return NO;
    }

    return YES;
}
ThomasW
  • 16,981
  • 4
  • 79
  • 106
TinkerTank
  • 5,685
  • 2
  • 32
  • 41
37

Here's the Swift solution: (It's a bit of a hack to get the context)

  1. You create the UIWebView.

  2. Get the internal context and override the console.log() javascript function.

    self.webView = UIWebView()
    self.webView.delegate = self
    
    let context = self.webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as! JSContext
    
    let logFunction : @convention(block) (String) -> Void =
    {
        (msg: String) in
    
        NSLog("Console: %@", msg)
    }
    context.objectForKeyedSubscript("console").setObject(unsafeBitCast(logFunction, AnyObject.self), 
                                                         forKeyedSubscript: "log")
    
Leslie Godwin
  • 2,601
  • 26
  • 18
  • 3
    +100! saved me TONS of time, great hack, requires 0 changes in JS code. Thanks!! Just my 2 cents for future readers: don't forget to link `JavaScriptCore` framework to your project and `import` it in your webview swift file. – mindbomb Dec 02 '15 at 02:39
  • Works for me with Swift 4... you haveto cast "log" to NSString..context.objectForKeyedSubscript("console").setObject(unsafeBitCast(logFunction, to: AnyObject.self), forKeyedSubscript: "log" as NSString) – Serge Jan 16 '19 at 03:47
28

Starting from iOS7, you can use native Javascript bridge. Something as simple as following

 #import <JavaScriptCore/JavaScriptCore.h>

JSContext *ctx = [webview valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
ctx[@"console"][@"log"] = ^(JSValue * msg) {
NSLog(@"JavaScript %@ log message: %@", [JSContext currentContext], msg);
    };
webo80
  • 3,365
  • 5
  • 35
  • 52
Ji Fang
  • 3,288
  • 1
  • 21
  • 18
  • Out of interest, where is the ideal place to put this code? – Leslie Godwin Nov 17 '15 at 06:27
  • OK, figured it out. Just after you created the `UIWebview` you can setup any `JSContext` stuff. – Leslie Godwin Nov 17 '15 at 08:02
  • 4
    Does `JSContext` still work in iOS 8+ with `WKWebView`? – Nikolai Samteladze Jan 12 '16 at 18:06
  • I put it into `- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType` and it works perfectly! – Artur Bartczak May 25 '16 at 10:36
  • @NikolaiSamteladze: I tried with `WKWebView` and iOS 11.4.1 and he can't find `documentView` and crashes. I saw this [answer](https://stackoverflow.com/questions/25792131/how-to-get-jscontext-from-wkwebview) and it seems that, it isn't possible this way. – testing Mar 01 '19 at 11:04
9

NativeBridge is very helpful for communicating from a UIWebView to Objective-C. You can use it to pass console logs and call Objective-C functions.

https://github.com/ochameau/NativeBridge

console = new Object();
console.log = function(log) {
    NativeBridge.call("logToConsole", [log]);
};
console.debug = console.log;
console.info = console.log;
console.warn = console.log;
console.error = console.log;

window.onerror = function(error, url, line) {
    console.log('ERROR: '+error+' URL:'+url+' L:'+line);
};

The advantage of this technique is that things like newlines in log messages are preserved.

ThomasW
  • 16,981
  • 4
  • 79
  • 106
tangent405
  • 91
  • 1
  • 2
  • +1. Note to [Apache Cordova](http://cordova.apache.org/) users - Cordova already handles `console.log`, but the `window.onerror` function in this answer is very useful! – mpontillo Jul 02 '13 at 17:39
  • For Appcelerator/Titanium developers: this too works to debug your Ti.UI.WebView – Byters Mar 22 '18 at 20:27
1

Swift 5

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
      webView.evaluateJavaScript("your javascript string") { (value, error) in
          if let errorMessage = (error! as NSError).userInfo["WKJavaScriptExceptionMessage"] as? String {
                print(errorMessage)
          }
      }
 }
Barath
  • 1,656
  • 19
  • 19
0

Tried Leslie Godwin's fix but was getting this error:

'objectForKeyedSubscript' is unavailable: use subscripting

For Swift 2.2, here's what worked for me:

You will need to import JavaScriptCore for this code to compile:

import JavaScriptCore

if let context = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") {
    context.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }")
    let consoleLog: @convention(block) String -> Void = { message in
        print("javascript_log: " + message)
    }
    context.setObject(unsafeBitCast(consoleLog, AnyObject.self), forKeyedSubscript: "_consoleLog")
}

Then in your javascript code, calling console.log("_your_log_") will print in Xcode console.

Better yet, add this code as an extension to UIWebView:

import JavaScriptCore

extension UIWebView {
    public func hijackConsoleLog() {
        if let context = valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") {
            context.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }")
            let consoleLog: @convention(block) String -> Void = { message in
                print("javascript_log: " + message)
            }
            context.setObject(unsafeBitCast(consoleLog, AnyObject.self), forKeyedSubscript: "_consoleLog")
        }
    }
}

And then call this method during your UIWebView initialization step:

let webView = UIWebView(frame: CGRectZero)
webView.hijackConsoleLog()
parag
  • 937
  • 13
  • 11