title: iOS 8.3 UIWebView JavaScript to native communication broken
body: <p><strong>EDIT: The solution was to return NO from the delegate method <code>webView:shouldStartLoadWithRequest:navigationType:</code> when it's a JavaScript message being received, this solved everything in all versions.</strong></p>
  
  <p>The behaviour of JavaScript to native (Obj-C or Swift) communication inside the <code>UIWebView</code> has changed in iOS 8.3.</p>
  
  <p>Let's say I want to send a couple of messages from JS to native code<sup>1</sup>:</p>
  
  <pre><code>window.open('myurlscheme://webview?message=1');
  window.open('myurlscheme://webview?message=2');
  window.open('myurlscheme://webview?message=3');
  window.open('myurlscheme://webview?message=4');
  </code></pre>
  
  <p>I use this delegate method to catch the messages in the native code:</p>
  
  <pre><code>- (BOOL)webView:(UIWebView *)inWeb shouldStartLoadWithRequest:(NSURLRequest *)inRequest navigationType:(UIWebViewNavigationType)inType {
      if ([inRequest.URL.scheme isEqualToString:@"myurlscheme"]) {
          NSLog(@"%@", inRequest.URL.absoluteString);
      }
  
      return YES;
  }
  </code></pre>
  
  <p>And I get the following in my Xcode console:</p>
  
  <pre><code>15:19:56.387 TestJSObjCComms[55731:766108] myurlscheme://webview?meaning=0
  15:19:56.404 TestJSObjCComms[55731:766108] myurlscheme://webview?meaning=1
  16:20:06.300 TestJSObjCComms[57967:819448] void SendDelegateMessage(NSInvocation *): delegate (webView:decidePolicyForNavigationAction:request:frame:decisionListener:) failed to return after waiting 10 seconds. main run loop mode: kCFRunLoopDefaultMode
  15:20:06.407 TestJSObjCComms[55731:766108] myurlscheme://webview?meaning=2
  15:20:16.409 TestJSObjCComms[55731:766108] myurlscheme://webview?meaning=3
  15:20:26.411 TestJSObjCComms[55731:766108] myurlscheme://webview?meaning=4
  </code></pre>
  
  <p>Notice the timestamps of each message. The first two come in straightaway, but consecutive messages come in at <em>exactly 10 second intervals</em>. During the 30 seconds in which the last three messages arrive, <strong>the UI freezes</strong>! The error in the console corresponds to the main thread (UI thread) being blocked for 10 seconds by some code, which was killed by the system after exceeding this time limit.</p>
  
  <p>I'm guessing that the UIWebView executes in its own thread on iOS 8.3, but I can't make much sense of this yet. According to <a href="https://stackoverflow.com/a/6490430/1305067">this answer</a>, the <code>UIWebView</code> allows JavaScript to execute for a maximum of 10 seconds. So I guess the JavaScript is somehow freezing, and only when it is killed does the message get through to the native code.</p>
  
  <p>Also, I think that the reason that <code>window.open(myURL)</code> works better than <code>window.location = myURL</code> is because it must be creating a new thread or execution environment, which allows it to send its message to the native code without squashing previous messages before they arrive.</p>
  
  <p><sup>1</sup>I use <code>window.open(myUrl)</code> instead of <code>window.location = myUrl</code>, which would only have allowed the last message to arrive at the native code (see Appendix 1 for details on the different methods available).</p>
  
  <p><strong>EDIT 1</strong>
  Oh no. <code>setTimeout</code> comes to the "rescue", again. It seems that Test Case 3 works on iOS 8.3, 8.1 and 7.0 if you wrap the subsequent messages in a <code>setTimeout</code> with <em>about</em> a 50 millisecond delay.</p>
  
  <pre><code>var buffer = [];
  function doRequest(url) {
      buffer.push(url);
      if (buffer.length === 1) {
          window.location = url;
      }
  }
  
  function resolveRequest(request) {
      var index = buffer.indexOf(request);
      if (index != -1) {
          buffer.splice(index, 1);
      }
      if (buffer.length) {
          setTimeout(function() {
              window.location = buffer[0];
          }, 50);
      }
  }
  </code></pre>
  
  <p>But I'm looking for a permanent solution which does not introduce hacks and delays. Going down this path is like building your app on pillars of sand.</p>
  
  <p><strong>APPENDIX 1: A Guide to UIWebView's JavaScript &lt;-> Native Communication.</strong></p>
  
  <p><strong>Test Cases</strong></p>
  
  <p>1) Send two or more consecutive messages via <code>window.location = myURL</code> to native code (using <code>window.location.href = myURL</code>, <code>window.location.replace(myURL)</code>, etc. is equivalent). </p>
  
  <p>2) Send two or more consecutive messages via <code>window.open(myURL)</code> or <code>window.open(myURL, ‘_blank’)</code> or by creating one of the following tags and setting its <code>src</code> attribute: <code>&lt;iframe&gt;</code>, <code>&lt;embed&gt;</code>. Note that the following tags don't work: <code>&lt;audio&gt;</code>, <code>&lt;video&gt;</code>, <code>&lt;script&gt;</code>, <code>&lt;style&gt;</code>.</p>
  
  <p>3) Send two or more consecutive messages using a buffer of requests. This makes sure to only call the next request when the current request has already invoked the <code>UIWebView</code> delegate method <code>webView:shouldStartLoadWithRequest:navigationType:</code>.</p>
  
  <p>Example JavaScript:</p>
  
  <pre><code>var buffer = [];
  function doRequest(url) {
      buffer.push(url);
      if (buffer.length === 1) {
          window.location = url;
      }
  }
  
  function resolveRequest(request) {
      var index = buffer.indexOf(request);
      if (index != -1) {
          buffer.splice(index, 1);
      }
      if (buffer.length) {
          window.location = buffer[0];
      }
  }
  </code></pre>
  
  <p>Native Obj-C:</p>
  
  <pre><code>- (BOOL)webView:(UIWebView *)inWeb shouldStartLoadWithRequest:(NSURLRequest *)inRequest navigationType:(UIWebViewNavigationType)inType {
      if ([inRequest.URL.scheme isEqualToString:@"myurlscheme"]) {
          NSString *javaScript = [NSString stringWithFormat:@"resolveRequest('%@')", inRequest.URL.absoluteString];
          [self.webView stringByEvaluatingJavaScriptFromString:javaScript];
          NSLog(@"%@", inRequest.URL.absoluteString);
      }
  
      return YES;
  }
  </code></pre>
  
  <p><strong><em>TEST RESULTS (using iPhone 5 Simulator)</em></strong></p>
  
  <p><strong><em>iOS 7.1</em></strong></p>
  
  <p>Case 1: Only the last message arrives. </p>
  
  <p>Case 2: The messages arrive sequentially with a delay of about a quarter of a second between each (i.e. not too bad).</p>
  
  <p>Case 3: The messages arrive sequentially with a delay of about a quarter of a second between each (i.e. not too bad).</p>
  
  <p><strong><em>iOS 8.1</em></strong></p>
  
  <p>Case 1: Same as iOS 7.1</p>
  
  <p>Case 2: The first two usually arrive straightaway, but the next ones come in at 10 seconds intervals (meanwhile, <strong>the UI freezes</strong>).</p>
  
  <p>Case 3: Same as iOS 7.1</p>
  
  <p><strong><em>iOS 8.3</em></strong></p>
  
  <p>Case 1: Same as iOS 7.1</p>
  
  <p>Case 2: Same as iOS 8.1</p>
  
  <p>Case 3: The first two usually arrive straightaway, but the next ones come in at 10 seconds intervals (meanwhile, <strong>the UI freezes</strong>).</p>