Angular.js: Infinite digest on location change on iOS 9 w/ UIWebView (not in Safari/ WKWebView)

Created on 30 Jun 2015  ·  154Comments  ·  Source: angular/angular.js

The following simple HTML demonstrates the issue:

<!DOCTYPE html>
<html>
    <head>
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.16/angular.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.16/angular-route.js"></script>
        <script>
            angular.module('fail', ['ngRoute'])
            .config(function($routeProvider) {
                $routeProvider
                .when('/a', {
                    template: '<a ng-href="#/b">a</a>'
                })
                .when('/b', {
                    template: '<a ng-href="#/a">b</a>'
                })
                .otherwise({
                    redirectTo: '/a'
                });
            });
        </script>
    </head>
    <body ng-app="fail">
        <div ng-view></div>
    </body>
</html>

This runs as expected on most devices, but it throws an infinite digest exception on iOS 9.
I'm able to reproduce on both iPad Air 2 and iPad 4th generation with iOS 9 beta 2.
I realize it's probably an issue in iOS, but it might still be worth investigating.

iOS $location bug

Most helpful comment

Got a little bit further on this one. I've made a change in Angular regarding the location.
In the "update browser" part, I've changed the $rootScope.$evalAsync to $rootScope.$applyAsync.

The two methods appear to do exactly the same thing. The difference doesn't become evident until you look at the actual $digest execution. When AngularJS executes a digest, it walks the Scope tree and executes $watch() bindings until no more dirty data is produced. During this lifecycle, both the $applyAsync() queue and the $evalAsync() queue get flushed; but, this happens in two very different places.

The $applyAsync() queue only gets flushed at the top of the $digest before AngularJS starts checking for dirty data. As such, the $applyAsync() queue will be flushed, at most, one time during a $digest and will only get flushed if the queue was already populated before the $digest started.

The $evalAsync() queue, on the other hand, is flushed at the top of the while-loop that implements the "dirty check" inside the $digest. This means that any expression added to the $evalAsync() queue during a digest will be executed at a later point within the same digest.

To make this difference more concrete, it means that asynchronous expressions added by $evalAsync() from within a $watch() binding will execute in the same digest. Asynchronous expressions added by $applyAsync() from within a $watch() binding will execute at a later point in time (~10ms).

Hope this already helps out some of you :-).


// update browser
    $rootScope.$watch(function $locationWatch() {
      var oldUrl = trimEmptyHash($browser.url());
      var newUrl = trimEmptyHash($location.absUrl());
      var oldState = $browser.state();
      var currentReplace = $location.$$replace;
      var urlOrStateChanged = oldUrl !== newUrl ||
        ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);

      if (initializing || urlOrStateChanged) {
        initializing = false;

        $rootScope.$applyAsync(function() {
          var newUrl = $location.absUrl();
          var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
              $location.$$state, oldState).defaultPrevented;

          // if the location was changed by a `$locationChangeStart` handler then stop
          // processing this location change
          if ($location.absUrl() !== newUrl) return;

          if (defaultPrevented) {
            $location.$$parse(oldUrl);
            $location.$$state = oldState;
          } else {
            if (urlOrStateChanged) {
              setBrowserUrlWithFallback(newUrl, currentReplace,
                                        oldState === $location.$$state ? null : $location.$$state);
            }
            afterLocationChange(oldUrl, oldState);
          }
        });
      }

      $location.$$replace = false;

      // we don't need to return anything because $evalAsync will make the digest loop dirty when
      // there is a change
    });

All 154 comments

I met similar issue, which happened on ios 9, but run ok on other devices.

I reproduced this issue with the same code provided by santaslow on 1.4.1 / ios 9:

<!DOCTYPE html>
<html>
<head>
    <script src="../static/js/angular/angular.1.4.1.js"></script>
    <script src="../static/js/angular-route/angular-route.1.4.1.js"></script>
    <script>
        angular.module('fail', ['ngRoute'])
                .config(function ($routeProvider) {
                    $routeProvider
                            .when('/a', {
                                template: '<a ng-href="#/b">a</a>'
                            })
                            .when('/b', {
                                template: '<a ng-href="#/a">b</a>'
                            })
                            .otherwise({
                                redirectTo: '/a'
                            });
                }).factory('$exceptionHandler', ['$log', function($log) {
                    return function(exception, cause) {
                        var message = 'angularjs exception: '+exception.message+': caused by "' + cause+ '\njs stack:\n'+exception.stack;
                        $log.error(message);
                    };
                }]);
    </script>
</head>
<body ng-app="fail">
<div ng-view></div>
</body>
</html>

The code above run normally on desktop browser, android and ios 8 webview, but on ios 9 it will throw exception when I click the link:

2015-07-02 11:00:09 ... angularjs exception: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: []
http://errors.angularjs.org/1.4.1/$rootScope/infdig?p0=10&p1=%5B%5D: caused by "undefined
js stack:
file:///.../static/js/angular/angular.js:68:32
$digest@file:///.../static/js/angular/angular.js:15705:35
$apply@file:///.../static/js/angular/angular.js:15935:31
file:///.../static/js/angular/angular.js:12070:30
eventHandler@file:///.../static/js/angular/angular.js:3264:25

I can no longer reproduce in iOS 9 Beta 3.

I receive the same error with ios9 public beta (13A4293g)

I verified the code above on ios 9 beta 3 (13A4293g), no exception anymore. But the app with using ng-view still throw infdig exceptions on ios 9 beta 3.

I receive the same error with ios9 public beta (13A4293g)

Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: []
http://errors.angularjs.org/1.3.13/$rootScope/infdig?p0=10&p1=%5B%5D
file:///Users/mac5/Library/Developer/CoreSimulator/Devices/749DE7E3-D93F-47F9-A1FC-E3D54A1CCEEE/data/Containers/Bundle/Application/9B5EE368-F2A0-4C99-807B-EA17B2479E58/BAShops.app/www/lib/ionic/js/ionic.bundle.js:8762:32
$digest@file:///Users/mac5/Library/Developer/CoreSimulator/Devices/749DE7E3-D93F-47F9-A1FC-E3D54A1CCEEE/data/Containers/Bundle/Application/9B5EE368-F2A0-4C99-807B-EA17B2479E58/BAShops.app/www/lib/ionic/js/ionic.bundle.js:22980:35
$apply@file:///Users/mac5/Library/Developer/CoreSimulator/Devices/749DE7E3-D93F-47F9-A1FC-E3D54A1CCEEE/data/Containers/Bundle/Application/9B5EE368-F2A0-4C99-807B-EA17B2479E58/BAShops.app/www/lib/ionic/js/ionic.bundle.js:23205:31
file:///Users/mac5/Library/Developer/CoreSimulator/Devices/749DE7E3-D93F-47F9-A1FC-E3D54A1CCEEE/data/Containers/Bundle/Application/9B5EE368-F2A0-4C99-807B-EA17B2479E58/BAShops.app/www/lib/ionic/js/ionic.bundle.js:54879:24
eventHandler@file:///Users/mac5/Library/Developer/CoreSimulator/Devices/749DE7E3-D93F-47F9-A1FC-E3D54A1CCEEE/data/Containers/Bundle/Application/9B5EE368-F2A0-4C99-807B-EA17B2479E58/BAShops.app/www/lib/ionic/js/ionic.bundle.js:11713:25
dispatchEvent@[native code]
triggerMouseEvent@file:///Users/mac5/Library/Developer/CoreSimulator/Devices/749DE7E3-D93F-47F9-A1FC-E3D54A1CCEEE/data/Containers/Bundle/Application/9B5EE368-F2A0-4C99-807B-EA17B2479E58/BAShops.app/www/lib/ionic/js/ionic.bundle.js:2863:20
tapClick@file:///Users/mac5/Library/Developer/CoreSimulator/Devices/749DE7E3-D93F-47F9-A1FC-E3D54A1CCEEE/data/Containers/Bundle/Application/9B5EE368-F2A0-4C99-807B-EA17B2479E58/BAShops.app/www/lib/ionic/js/ionic.bundle.js:2852:20
tapTouchEnd@file:///Users/mac5/Library/Developer/CoreSimulator/Devices/749DE7E3-D93F-47F9-A1FC-E3D54A1CCEEE/data/Containers/Bundle/Application/9B5EE368-F2A0-4C99-807B-EA17B2479E58/BAShops.app/www/lib/ionic/js/ionic.bundle.js:2975:13

We are also receiving this error on public Beta 3 of iOS 9 with our own Angular application. It does not occur in iOS 8.

As a workaround, I wrote a simple directive to replace ng-view and angular-route.js. The solution worked well in our own application, all infdig exceptions disappeared on ios 9 beta/beta 3. Below is the simplified code, which is just for our own application, general use cases are not considered. I DO NOT recommend other people to use this:

(function (window) {
    'use strict';

    var myApp = angular.module("myApp");
    var $route = {};

    // replace $routeProvider.when with the function below:
    window.routeWhen = function(path, route) {
        $route[path] = route;
    };

    myApp.directive("myView", ['$compile', '$controller', '$http', '$rootScope', function ($compile, $controller, $http, $rootScope) {

        return {
            priority: -400,
            link: function (scope, element) {
                var parentScope = scope;
                scope = null;

                window.updateView = function (path) {
                    location.hash = '#'+url;
                    if (scope) scope.$destroy();
                    scope = parentScope.$new();

                    var route = $route[path];

                    var linkView = function(html) {
                        element.html(html);
                        var link = $compile(element.contents());
                        var controller = $controller(route.controller, {$scope: scope});
                        element.data('$ngControllerController', controller);
                        element.children().data('$ngControllerController', controller);
                        link(scope);
                        scope.$emit('$viewContentLoaded');
                        if (!$rootScope.$$phase && !scope.$$phase) scope.$apply();
                    };
                    if (route.templateCache) linkView(route.templateCache)
                    else if (route.template) {
                        route.templateCache = document.getElementById(route.template).innerHTML;
                        linkView(route.templateCache)
                    }
                    else $http.get(route.templateUrl;).success(function(html) {
                        route.templateCache = html;
                        linkView(html);
                    });
                };
                //updateView(initialPath);
                // call updateView(path) to set location at other places of the app
            }
        };
    }]);

})(window)

Just installed iOS 9 Beta 4 and still have the same issue. Anyone else?

I saw it in iOS 9 Beta 3 and I'm still seeing it in iOS 9 Beta 4.

+1

Yeah we're seeing the same issue as well even with angular ui router. Does anybody have a valid work around for this issue in the meantime?

Seeing the same issue in uiWebView on latest iOS9.

Does anyone have any updates on this issue?

This is still an issue. Reopening

It looks like an ios issue. Is this tracked on webkit somewhere?

+1

+1

Same here. Our Cordova app runs fine if running as web on the iPad Safari, but the infinite digest happens if it runs as a Cordova app (UIWebView).

Exactly the same issues as @borrull ! Already experimented with WKWebView and then the issue is non-existing. But we can't use WKWebView as we need Local File Serving (and we don't want to run a local server in our application) and cookies. So it has to do something with UIWebView in combination with Cordova/Mobile Safari on iOS 9. I'm currently debugging the $locationWatch in Angular because I see that our application wants to transition to a different location multiple times and then (after 10 times) the digest error is thrown.

same issue here on iOS9 béta4
Angular infinite $digest loop ;(

+1, only in UIWebView, not in WKWebView but we can only use UIWebView in our cordova app.

+1

If this is an iOS specific issue, please open an issue on the webkit issue tracker and provide a demo! +1 here will not likely change anything, as it really really sounds like a browser bug.

Did someone reported this bug to Apple for investigation?

I reported this bug to apple. But maybe you all should do the same to get their attention on the bug.

Can you give us a link so we can +1?

It's in our personal apple account via the bug reporter .. so no public link ;(

Could you post it to openradar and share the rdar id so we can dupe it!

same on iOS9 béta 5 :
works on mobile safari
works on WKWebview which we can't use because it can't serve local files and does not support NSProtocol
works NOT on UIWebView

same here on IOS 9 Beta 5

I filed a bug with Apple as well. The Open Radar link is: https://openradar.appspot.com/22186109 (This should help the ones lazy to file a bug). Please leave comments if you can improve the bug wording/explanation ;-) You can download the Xcode project to attach with the bug filing on the Open Radar ticket (Thanks to @santaslow for the JS in the OP)

I've created a version of the same Xcode project (from @borrull) but with ui-router instead of ng-route. Exactly the same issue. For the people who are interested, you can find the project here: http://s000.tinyupload.com/index.php?file_id=87281871603760127355

We're also seeing thus issue. I was able to chase down the issue to being that the location.* properties aren't updating immediately when angular is in the mix. If you attempt to assign a value to location.hash (like what is being done behind the location service), then immediately read it back in, the value hasn't changed. There appear to be some side effects occurring as a result of the jqlite handlers attached to popstate and hashchanged events.

I'll try and upload a sample when I'm on a computer.

+1

@CleverCoder Any updates on the sample?

I'll have to finish up the repro case in the morning, as the code as I've been tied up all weekend. Thanks for the nudge! As iOS 9 counts down, we have a vested interest in seeing this resolved. I'll upload something as soon as I can.

+1

I've reproduced what I believe to be the root cause, where setting the location hash or href properties don't "apply" immediately.
Here's a link to the XCode project:
https://www.dropbox.com/s/2jkwv2thhm86nly/iOS%209%20Location%20Bug.zip?dl=0
Let me know if you can't access the file.

Observe the resulting value in location.hash, as well as attaching Safari to debug. There seems to be something deferring the change as a result of some event plumbing based on the 'popstate' and 'hashchange' events.

I hope this is helpful.

  • Sean

We are using window.location.href instead of using state.go and it seems to be working for now. Less buggy.

The value of location.hash will be correct after a spin of the runloop. Angular can easily work around this issue by delaying the location.hash get in a setTimeout(..., 0). I think this would be ~2 changes to angular.js/src/ng/location.js.

@hober Tried the timeout with angular like this:

// update $location when $browser url changes
    $browser.onUrlChange(function(newUrl, newState) {
      $rootScope.$evalAsync(function() {
        var oldUrl = $location.absUrl();
        var oldState = $location.$$state;
        var defaultPrevented;

        $location.$$parse(newUrl);
        $location.$$state = newState;

        defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
            newState, oldState).defaultPrevented;

        // if the location was changed by a `$locationChangeStart` handler then stop
        // processing this location change
        if ($location.absUrl() !== newUrl) return;

        if (defaultPrevented) {
          $location.$$parse(oldUrl);
          $location.$$state = oldState;
          setTimeout(function(){ setBrowserUrlWithFallback(oldUrl, false, oldState) }, 0);
        } else {
          initializing = false;
          afterLocationChange(oldUrl, oldState);
        }
      });
      if (!$rootScope.$$phase) $rootScope.$digest();
    });

and

// update browser
    $rootScope.$watch(function $locationWatch() {
      var oldUrl = trimEmptyHash($browser.url());
      var newUrl = trimEmptyHash($location.absUrl());
      var oldState = $browser.state();
      var currentReplace = $location.$$replace;
      var urlOrStateChanged = oldUrl !== newUrl ||
        ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);

      if (initializing || urlOrStateChanged) {
        initializing = false;

        $rootScope.$evalAsync(function() {
          var newUrl = $location.absUrl();
          var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
              $location.$$state, oldState).defaultPrevented;

          // if the location was changed by a `$locationChangeStart` handler then stop
          // processing this location change
          if ($location.absUrl() !== newUrl) return;

          if (defaultPrevented) {
            $location.$$parse(oldUrl);
            $location.$$state = oldState;
          } else {
            if (urlOrStateChanged) {
              setTimeout(function(){ setBrowserUrlWithFallback(newUrl, currentReplace,
                  oldState === $location.$$state ? null : $location.$$state) }, 0);
            }
            afterLocationChange(oldUrl, oldState);
          }
        });
      }

      $location.$$replace = false;

      // we don't need to return anything because $evalAsync will make the digest loop dirty when
      // there is a change
    });

So I added a setTimout around the setBrowserUrlWithFallback method but it doesn't resolve the issue.

Here's a reduced test case that doesn't rely on Angular, and that demonstrates the workaround. How to actually implement the workaround in Angular is unclear to me. https://gist.github.com/hober/a29b6c28ac1744c800dd

Got a little bit further on this one. I've made a change in Angular regarding the location.
In the "update browser" part, I've changed the $rootScope.$evalAsync to $rootScope.$applyAsync.

The two methods appear to do exactly the same thing. The difference doesn't become evident until you look at the actual $digest execution. When AngularJS executes a digest, it walks the Scope tree and executes $watch() bindings until no more dirty data is produced. During this lifecycle, both the $applyAsync() queue and the $evalAsync() queue get flushed; but, this happens in two very different places.

The $applyAsync() queue only gets flushed at the top of the $digest before AngularJS starts checking for dirty data. As such, the $applyAsync() queue will be flushed, at most, one time during a $digest and will only get flushed if the queue was already populated before the $digest started.

The $evalAsync() queue, on the other hand, is flushed at the top of the while-loop that implements the "dirty check" inside the $digest. This means that any expression added to the $evalAsync() queue during a digest will be executed at a later point within the same digest.

To make this difference more concrete, it means that asynchronous expressions added by $evalAsync() from within a $watch() binding will execute in the same digest. Asynchronous expressions added by $applyAsync() from within a $watch() binding will execute at a later point in time (~10ms).

Hope this already helps out some of you :-).


// update browser
    $rootScope.$watch(function $locationWatch() {
      var oldUrl = trimEmptyHash($browser.url());
      var newUrl = trimEmptyHash($location.absUrl());
      var oldState = $browser.state();
      var currentReplace = $location.$$replace;
      var urlOrStateChanged = oldUrl !== newUrl ||
        ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);

      if (initializing || urlOrStateChanged) {
        initializing = false;

        $rootScope.$applyAsync(function() {
          var newUrl = $location.absUrl();
          var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
              $location.$$state, oldState).defaultPrevented;

          // if the location was changed by a `$locationChangeStart` handler then stop
          // processing this location change
          if ($location.absUrl() !== newUrl) return;

          if (defaultPrevented) {
            $location.$$parse(oldUrl);
            $location.$$state = oldState;
          } else {
            if (urlOrStateChanged) {
              setBrowserUrlWithFallback(newUrl, currentReplace,
                                        oldState === $location.$$state ? null : $location.$$state);
            }
            afterLocationChange(oldUrl, oldState);
          }
        });
      }

      $location.$$replace = false;

      // we don't need to return anything because $evalAsync will make the digest loop dirty when
      // there is a change
    });

Here's another approach. I'm not that familiar with the Angular codebase, but the logic seems rational. The browser url(...) function currently depends on the location.href immediately returning the correct URL. Because this method is called in the same run loop, within the $digest stabilization cycle, it continues to get the old URL. This patch leverages a 'pendingHref' to track the assignment, returning that value instead, if it is set. Once the value is aligned with location.href, the pending value is cleared. During a set of the url, a timer is set with 0ms to catch the case where a url() get isn't called. It's not perfect, but the logic seems to work. This is mainly to consider an alternative approach that doesn't create delays in performance. This is based on Angular tag v1.4.3.

diff --git a/src/ng/browser.js b/src/ng/browser.js
index 928de95..3b9957e 100644
--- a/src/ng/browser.js
+++ b/src/ng/browser.js
@@ -87,7 +87,9 @@ function Browser(window, document, $log, $sniffer) {
   var cachedState, lastHistoryState,
       lastBrowserUrl = location.href,
       baseElement = document.find('base'),
-      reloadLocation = null;
+      reloadLocation = null,
+      pendingHref = null,
+      pendingHrefTimer = null;

   cacheState();
   lastHistoryState = cachedState;
@@ -124,6 +126,18 @@ function Browser(window, document, $log, $sniffer) {
     if (location !== window.location) location = window.location;
     if (history !== window.history) history = window.history;

+    // Schedule cleaning up pendingHref on the next run loop for setting URL. This is to handle
+    // the case where the browser doesn't update the location.* properties immediately
+    if (!pendingHrefTimer && pendingHref && url) {
+      pendingHrefTimer = setTimeout(function () {
+        if (location.href == pendingHref) {
+          console.log('Actual href updated... setting pendingHref to null from setTimeout');
+          pendingHref = null;
+        }
+        pendingHrefTimer = null;
+      }, 0);
+    }
+
     // setter
     if (url) {
       var sameState = lastHistoryState === state;
@@ -147,6 +161,7 @@ function Browser(window, document, $log, $sniffer) {
         // Do the assignment again so that those two variables are referentially identical.
         lastHistoryState = cachedState;
       } else {
+        pendingHref = url;
         if (!sameBase || reloadLocation) {
           reloadLocation = url;
         }
@@ -161,10 +176,22 @@ function Browser(window, document, $log, $sniffer) {
       return self;
     // getter
     } else {
+      var href = location.href.replace(/%27/g, "'");
+      if (pendingHref) {
+        //console.log('.. using pendingHref for url() return value');
+        href = pendingHref;
+      }
+
+      if (location.href == pendingHref) {
+        console.log('Actual href updated... setting pendingHref to null in getter');
+        pendingHref = null;
+      }
+
+      //var href = location.href.replace(/%27/g,"'");
       // - reloadLocation is needed as browsers don't allow to read out
       //   the new location.href if a reload happened.
       // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
-      return reloadLocation || location.href.replace(/%27/g,"'");
+      return reloadLocation || href;
     }
   };

Thanks @CleverCoder for the solution! Seems to work like a charm! :+1:

@CleverCoder
would be great if you do a pull request with this to angular team.

@viattik I would kind of surprise me if the angular team would adopt a workaround for UIWebView on iOS9 since the bug is in UIWebView (Apple) itself. But you can always try...

@raftheunis87
There are lots of bugs in different browsers and lots of workarounds for those bugs in angular code.
Although they're not officially support UIWebView, lots of hybrid apps will be broken and using angular will be impossible in hybrid apps in latest iOS until Apple fix that bug. And that's pretty big issue I would say.
So I would give it a try.

@viattik I totally agree. And btw: Apple communicated to us that it's unlikely that they will fix the UIWebView bug. So indeed: give it a try ;-)

Folks, if you could post this as a webkit bug, ideally with a repro case, I'll follow up with some of our contacts on the WebKit side. https://bugs.webkit.org/

@naomiblack
Not sure if it's a webkit bug. Cause it only happens in UIWebView on iOS9. Safari on iOS9 works fine.

@raftheunis87 thanks for your code suggestions, worked perfectly

@raftheunis87 @CleverCoder is thr a way to work with ionic-angular ? can you be more specific?

@abrahamrkj I don't have any experience with ionic. But are their customizations in angular when using ionic? Otherwise I would say that the same fix would work with ionic-angular...

@raftheunis87 https://github.com/driftyco/ionic/tree/master/js this is the angular they are using.

@CleverCoder +1 for the pull request. I agree with @viattik that this is an important issue as it will break a lot of hybrid apps.

A pull request may be in the near future, although I hesitate since I'm not as close to the Angular codebase as others. I will revisit the solution soon and try to make it bullet-proof. It does seem strange that a property on the window.location object doesn't change 'immediately'. In the tests I've done, I did notice that the change did stick as long as the 'popstate' and 'hashchange' event hooks weren't in place, leading me to think the cause of the deferred change may actually be do something else.. Perhaps the timing of those events have changed (I think that's what I was observing).
I'm going to watch this over the next few days, and if nothing better surfaces, I'll dig a little deeper to confirm what I know so far, and that there's not a better place to address the change in behavior. Sorry if this is confusing. There's a lot going on under the hood that I still don't fully understand with regard to those events.
Cheers!

... and yes, @borrull, i agree. If Apple doesn't make any more changes, then this is a serious ticking time bomb that will really lead to bad press and finger pointing. I'm not a fan of the timers as a workaround (I'd rather have improved logic flow around these built-in properties), but if we can't depend on the value changing once it's set, then where do we draw the line? What other properties can't we trust? It's an odd one.

@CleverCoder Just wanted to say thanks, your patch really saved the day!

@CleverCoder Thanks for the given workaround.

I derived a solution that uses the decorator feature from angular and comes without patching the angular source.

With cssua this setup can be configured to be used only in specific environments.

    app.config(['$provide', ($provide) => {
        $provide.decorator('$browser', ['$delegate', ($delegate) => {
            var origUrl = $delegate.url;

            var pendingHref = null;
            var pendingHrefTimer = null;

            var newUrl = function (url, replace, state) {

                if (url) {
                    // setter
                    var result = origUrl(url, replace, state);

                    if (window.location.href != url) {

                        if (pendingHref != url) {
                            pendingHref = url;

                            if (pendingHrefTimer) clearTimeout(pendingHrefTimer);

                            pendingHrefTimer = setTimeout(function () {
                                if (window.location.href == pendingHref) {
                                    pendingHref = null;
                                }
                                pendingHrefTimer = null;
                            }, 0);
                        }
                    }

                    return result;
                } else {
                    // getter
                    if (pendingHref == window.location.href) {
                        pendingHref = null;
                    }

                    return pendingHref || origUrl(url, replace, state);
                }
            };

            $delegate.url = newUrl;

            return $delegate;
        }]);
    }]);

@CleverCoder See #12635

@jd-carroll: That's really interesting. I might revisit this a little later today when I have time. Pretty swamped with stuff. That just creates more mystery, since that seems like a separate issue that shouldn't introduce a delay in location.* values updating.

@realityfilter: Funny you mention the decorator... I just finished implementing something using the Angular decorator functionality. Nice!

Hey all,
I just wanted to add that this fix introduced a bug in our code that was easy to fix.

Our templates had anchor tags that used href="#" and ng-click="someCall()". The href was causing the site to go to index.html using this fix. Removing the href fixed the issue.

Our app is breaking during back button navigation in Ionic it first goes to new view then comes back to old view partially and then moves back again to new view on IOS9 beta any resolutions for Ionic

Same issue with iOS 9 beta 5 13A4325c, Angular 1.4.0 (with cordova-ios 3.9.1). Hopefully the underlying UIWebView bug will be fixed!

Same issue occurring on Angular v1.2.27

I have traced it down to the 1.2.27 release, the bug is not in the prior 1.2.26 release.

Specifically, this commit is the culprit.

@damrbaby Looks like I could have something to do with it.

But the fact that it is working with the latest version of angular in Mobile Safari clearifies that is has to do something with uiWebView on iOS 9. So the change they made in the Angular source code isn't necessarily a bad thing.

@CleverCoder @realityfilter @jyc66 I just wanted to say thank you, you just saved my day.

Issue is still present on iOS9 GM Seed so get your apps updated people!

I can confirm, that the issue is still present in iOS9 GM (13A340)

So this means that Apple broke something and we have to update our apps again (some of which haven't changed for months or even over a year) to keep them from crashing. Makes sense :( . I'd rather have Apple fix it for iOS9 launches. Going to the latest Angular version in an old app is bound to break some other stuff too.

I highly doubt Angular is the only framework that is having issues with iOS9?

So, @adamdbradley, @perrygovier, and @mhartington from the Ionic Team have been working all day on a fix that will work for Ionic and for plain Angular apps as well. The goal is to be a drop-in fix that does not require modifying Angular, and will (hopefully) work across most 1.2+ Angular versions.

Here is our current bundled solution that decorates and fixes $browser by applying this patch. Note: this is based on Angular 1.4.3 and is sort of a "clone with fixes" of browser.js from Angular proper: https://github.com/driftyco/ionic/blob/ios9-patch/js/angular/service/decorators/ios9-browser-fix.js

We also placed the patch on our CDN. I don't recommend using the CDN file for production, it's only there so it's easier to test with right now.

To test it out, place this script tag below your angular or ionic.bundle.js file:

<script src="https://code.ionicframework.com/patch/ios9-$browser-patch.js"></script>

Also, right now it applies the patch whether you're running on iOS9 or not. That will soon be fixed such that it runs only on the iOS 9 UIWebView.

Follow the corresponding Ionic issue here: https://github.com/driftyco/ionic/issues/4082#issuecomment-139079725

HI All,

UI-sref works like a charm but $state.go breaks the back button animation
and the page flickers a lot even after applying this fix.

Regards,
Ajay Singh

On Thu, Sep 10, 2015 at 6:23 AM, Max Lynch [email protected] wrote:

So, @adamdbradley https://github.com/adamdbradley, @perrygovier
https://github.com/perrygovier, and @mhartington
https://github.com/mhartington from the Ionic Team have been working
all day on a fix that will work for Ionic and for plain Angular apps as
well. The goal is to be a drop-in fix that does not require modifying
Angular, and will (hopefully) work across most 1.2+ Angular versions.

Here is our current bundled solution that decorates and fixes $browser by
applying this patch
https://github.com/angular/angular.js/issues/12241#issuecomment-130744518.
Note: this is based on Angular 1.4.3 and is sort of a "clone with fixes" of
browser.js from Angular proper:
https://github.com/driftyco/ionic/blob/ios9-patch/js/angular/service/decorators/ios9-browser-fix.js

We also placed the patch on our CDN. I don't recommend using the CDN file
for production, it's only there so it's easier to test with right now.

Please test it out and let us know how it goes, thanks.

To test it out, place this script tag below your angular or
ionic.bundle.js file:

Also, right now it applies the patch whether you're running on iOS9 or
not. That will soon be fixed such that it runs only on the iOS 9 UIWebView.


Reply to this email directly or view it on GitHub
https://github.com/angular/angular.js/issues/12241#issuecomment-139082474
.

I can confirm this bug is still present in iOS 9.1 Beta 1

We use $state.go a lot and it's still flickering between states / not transitioning correctly.

Sorry the fix works for our app now only thing is we need to include https
CND link instead of http CDN looks like ios9 is only allow https inside the
app so after using https our app worked.

https://code.ionicframework.com/patch/ios9-$browser-patch.js

On Thu, Sep 10, 2015 at 3:22 PM, Tyler Crammond [email protected]
wrote:

We use $state.go a lot and it's still flickering between states / not
transitioning correctly.


Reply to this email directly or view it on GitHub
https://github.com/angular/angular.js/issues/12241#issuecomment-139189216
.

ios9-$browser-patch.js works for me, too. But it generates some jshint errors. Is there a way to contribute to the patch to help remove the errors?

I don't think this patch is entirely sound as it doesn't allow you to update the location from outside Angular. This is probably not a problem for most applications but we are working to see if there is a better solution...

ios9-$browser-patch.js didn't work for me. When i'm moving from my list view to a detail view i use $state.go() to move between the pages. The first time I do this it slides across and then straight back, I then click again and it works. UI-sref works as desired, but it won't allow me to do the conditional logic I need to do.

Unfortunately downloading the patch, and storing it and loading it from ./lib/ (I checked that it is loaded), didn't change anything for me:

Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: []
http://errors.angularjs.org/1.3.13/$rootScope/infdig?p0=10&p1=%5B%5D

iOS 9 GM, ionic 1.0.0.

Jorisw, try disabling the ios9 detection code and see if it works:
var isIOS9 = (navigator.userAgent.indexOf('Version/9.') != -1) && (navigator.appVersion.indexOf('9_0') != -1);
if (!isIOS9) {
// do not patch if not iOS9 UIWebView
return $browser;
}

@jyc66 That works, thanks. How do I make sure it doesn't break iOS < 9 ?

The isIOS9 method is faulty. It should check against:

> navigator.userAgent
< "Mozilla/5.0 (iPhone; CPU iPhone OS 9_0 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13A340 (2065230368)" = $1
> navigator.appVersion
< "5.0 (iPhone; CPU iPhone OS 9_0 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13A340 (2065230368)" = $2

Yeah, alternatively, you can test for OS's less than 9 and disable the fix in those cases. The current check will break on iOS 9.1 which is coming out soon as well.

I'm doing this now, until I or someone comes up with a better one:

  // only provide the patch for iOS9 on UIWebView
  var isIOS9 = (navigator.userAgent.indexOf(' OS 9') != -1) && (navigator.appVersion.indexOf(' OS 9') != -1);
  if (!isIOS9) {
    // do not patch if not iOS9 UIWebView
    return $browser;
  }

Yah, that's similar to what I'm doing.

Does anyone know if applying the patch to iOS versions >= 9.0 will cause issues if/when Apple fixes the issue in a later release of iOS?

@JeremyPlease I'm loading 9.1 beta right now and will check it out

Worked for me when I disabled the user agent checking, serving the patch as part of the minified bundle. It's a non-ionic, angular app. Will investigate further later tonight and make sure everything works as expected.

Thanks to the ionic team for putting in the effort!

So 9.1beta one doesn't have the fix yet. Not surprised.

This may seem odd - but anyone seeing the String.split() function not working correctly when the patch is applied on iOS 9? (tested so far only on simulator). Here's a screen shot from Safari's debugger: https://www.dropbox.com/s/hxgct9y0f9z6yci/Screenshot%202015-09-11%2000.40.26.png?dl=0

I'm perplexed! Works fine on iOS8.4, and Chrome of course.

Update: String.split doesn't seem to work in iOS 9 iPhone 6 and 6s simulator for me, nothing to do with the patch.


By the way - here's what I used for the user agent checking:

    var isIOS9WebView = (navigator.userAgent.indexOf('Safari') === -1) && (navigator.appVersion.indexOf('OS 9') !== -1);

    if (!isIOS9WebView) {
      // do not patch if not iOS9 UIWebView
      return $browser;
    }

I believe the best solution maybe in Igor's pull request, but this works for me while not applying the patch on iOS 9 Safari, but to the UIWebView only.

@dac09 I want to test your commet, but I don't know which file has to modified...
can U tell me which file has to modified?

I am using @CleverCoder's patch, which ionic's patch is based on, without a restriction that it should run in iOS9 UIWebView only, and it has no consequences on UIWebView iOS8, safari, or Android for that matter.

Hi folks, can you please test this patch: https://gist.github.com/IgorMinar/863acd413e3925bf282c

It should work with Angular 1.2 – 1.4.5 and doesn't require any Angular updates. Instructions on how to apply this to your app are in the gist.

We are going to have a proper fix in 1.4.6, but in the meantime this standalone patch is meant to ease the implementation and deployment of a fix so that you can roll it out quickly.

Hi @IgorMinar - I'm getting the following error in my Angular 1.3 (Ionic beta-14) deployment. I don't think the .decorator method is available on module in 1.3?

Uncaught TypeError: angular.module(...).decorator is not a function(anonymous function) @ angular-ios9-uiwebview.patch.js:33

@rajatrocks uhh.. good point. module.decorator is 1.4 feature only. let me change the patch.

@rajatrocks I updated the gist

Thanks @IgorMinar, "installs" fine now. Will leave my comments on the Ionic thread since they're more related to that.

@IgorMinar I have include your js file and add following in my app.js. It doesn't fix my issue.

@IgorMinar Your patch didn't fix my issue, but the one from @mlynch did.

To ensure it's only applied to the iOS platform, I've created a simple Cordova hook:
https://gist.github.com/DURK/f2acd6bca4759e719801

Update: Ah, I see the patch now contains a check so it's only applied on iOS9.

@IgorMinar Following up on what @DURK and @jprangenberg reported, there appears to be an issue with the "isIOS9UIWebView" function when testing on an iPad Air 2. If I force the shim to always be applied, the patch works.

From my comment at: https://github.com/driftyco/ionic/issues/4082#issuecomment-139567128

@IgorMinar, your patch works great on iOS 9 iPhone UIWebView, but there are some issues with the user agent detection for iPad.

iOS 9 iPhone user agent in UIWebView:
Mozilla/5.0 (iPhone; CPU iPhone OS 9_0 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13A340

iOS 9 iPad user agent in UIWebView:
Mozilla/5.0 (iPad; CPU OS 9_0 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13A340

The regex that will reliably match all of these is:

function isIOS9UIWebView(userAgent) {
    return /(iPhone|iPad|iPod);.*OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent);
}

I've forked @IgorMinar's gist and made the changes here: https://gist.github.com/JeremyPlease/72cb3bf98279fefed4e3

@DURK cordova hook is not necessary. the patch doesn't do anything on platforms that don't need it.

@JeremyPlease Safari on iOS9 is not affected. we really want to apply the patch only when in UIWebView.

@JeremyPlease Safari iOS 9 isn't the problem, so @IgorMinar's check is correct - to limit it specifically to UIWebViews, I believe.

Works great for me.

I realized this is just an issue in UIWebView, so I've updated my gist to not apply when in Safari:

function isIOS9UIWebView(userAgent) {
    return /(iPhone|iPad|iPod);.*OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent);
}

I published v1.0.3 of the patch

@IgorMinar Does this fix just works, if you use ngRoute? The fix doesn't work for me… :-(

@jprangenberg yes. my app with ngroute works fine on iphone/ipad. can you provide more details? also make sure you are using v1.0.3+ of the patch

@IgorMinar Version 1.0.3 of the patch is working correctly in my environment.

index.html:

<script src="lib/ionic/js/ionic.bundle.js"></script>
<script src="lib/ngCordova/dist/ng-cordova.min.js"></script>
<script src="js/libs/angular-ios9-uiwebview.patch.js"></script>

app.js:

angular.module('starter', [
    'ionic',
    'ngCordova',
    'ngIOS9UIWebViewPatch',
    'ionic.service.core',
    'ionic.service.push',
    'angular-loading-bar',
    'starter.services',
    'starter.controllers'
])

app.js (Route):

$stateProvider.state('app.regions', {
    cache: false,
    url: "/states/:state_id/regions",
    views: {
        'menuContent': {
            templateUrl: "templates/regions.html",
            controller: 'RegionsCtrl'
        }
    }
})

It's not possible to navigate through the app. If you click on a list-item you see the current view. The history button changes. If you click on the history button, you can see the view of the click on the list-item.

<ion-list>
    <ion-item ng-repeat="state in states" href="#/app/states/{{state.id}}/regions">
        {{state.name}}
    </ion-item>
</ion-list>

@IgorMinar It doesn't work for me. Does someone have any ideas?

@jprangenberg are you using the patched version of the Ionic bundle or a release?

@perrygovier I'm using the version 1.0.3 of @IgorMinar

@jprangenberg right, but the ionic.bundle.js. What version is that?

Version of my ionic.bundle.js:

window.ionic.version = '1.0.0';

@perrygovier Thanks for your help!

Out of curiosity, does using the 1.1.0 version of the bundle help?
http://code.ionicframework.com/1.1.0/js/ionic.bundle.min.js

You'll probably need the updated CSS too
http://code.ionicframework.com/1.1.0/css/ionic.min.css

Hey @jprangenberg, while we're all working on this together, lets continue this ionic related discussion on the ionic issue so we don't spam regular angular users driftyco/ionic#4082

So this bug doesn't affect WKWEBVIEW when using https://github.com/Telerik-Verified-Plugins/WKWebView ?

The bug is only in iOS 9's UIWebView. It is not present when using WKWebView.

Thanks for your work on this guys, but it's not working for me unfortunately. I'm getting broken animations, pages not loading at all, pages not loading correctly, basically the router is completely wonky.

I'm using Ionic bundle 1.1.0 and iOS 9.1 on an iPhone 6, along with the patch file at https://code.ionicframework.com/patch/ios9-$browser-patch.js. I'm seeing errors like this:

Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: []

Any suggestions?

Thanks!

@scottopolis did you try Igor's patch: https://gist.github.com/IgorMinar/863acd413e3925bf282c

@petebacondarwin ya thanks, Igor's patch works for me. My page transition animations are choppy, but the router seems to be fixed. I'll keep an eye on this issue.

Some testing here and the patch from @IgorMinar is causing issues with the back button animations and controllers. I am seeing the controller for the screen I am backing away from getting created again when I click back. This is causing animations to run in that view as it is navigated off the screen, all kinds of weird.

So far I am seeing no issues using the nightly from ionic but updating the iOS9 detection.

var userAgent = navigator.userAgent, 
      isIOS9 = /(iPhone|iPad|iPod).* OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent);

I've just pushed v1.1.0 of the patch: https://gist.github.com/IgorMinar/863acd413e3925bf282c

Together with the ionic team, we are no longer able to repro any flicker with this version of the patch.

Please upgrade and let us know if you see any more issues. (also make sure you are not using any other patches for this issue in addition to the v1.1.0 of the patch).

We'll land a fix in the master next week, so that Angular v1.4.6 doesn't need to be patched any more.

This new version of the patch is working perfectly for me. Thanks @IgorMinar!

Does released 1.4.6 fix this issue for good?

@alexislg2 - yes this release should fix the issue so you do not need to apply the patch.

Confirmed, works for me on the simulator and device, just need my users to wait 2 weeks for Apple approval ;-(

@dbroadhurst I heard you might be able to quote the https://openradar.appspot.com/22186109 bug report to fast track your update to your app.

You can follow this WebKit bug for updates.

Hi, you seem to have solved the issue of iOS9 async call on hash change on Angular.js.

I've made researches, also posted on stackoverflow but the only solution I found until now is in your angular browser.js patch.

I'm not really familiar with angular and I would like to understand what you did make you fix available in
every web application based on Hash Routing

Would you explain how you identified and fixed the problem?

http://stackoverflow.com/questions/32719631/how-to-fix-window-location-issue-in-ios9-uiwebview?noredirect=1#comment53359668_32719631

@lchenneberg - the commit that fixes the problem in AngularJS is here https://github.com/angular/angular.js/commit/8d39bd8abf423517b5bff70137c2a29e32bff76d

The problem is that this particular browser does not update the value for window.location.href until the next run of the JavaScript event loop. This means that if you write to that value then immediately read it back you get a different value:

console.log(window.location.href)   // -> http://my.domain.com/path/to/page
window.location.href = 'http://my.domain.com/path/to/other/page';
console.log(window.location.href)   // -> http://my.domain.com/path/to/page

// next tick of the event loop

console.log(window.location.href)   // -> http://my.domain.com/path/to/other/page

Notice that the second console.log returns the old value, not the new value. After the current event loop completes, the value is updated, as can be seen in the third console.log

The fix that we have come up with, is to cache the value that we wrote, if the browser is not updating synchronously, and then to use that value from then on, instead of the value returned from window.location.href, until there is a hashchange event, which tells us that the browser has finally sorted itself out.

Hope that helps.

@IgorMinar 's patch worked well for me. I'm using the following versions of Angular & Ionic:

window.ionic.version "1.0.1"
angular.version
Object {full: "1.3.13", major: 1, minor: 3, dot: 13, codeName: "meticulous-riffleshuffle"}

@IgorMinar's patch worked for us as well. Thanks!!

But I have another question one of you may be able to answer:

does launching a web page added to the iPad home screen open the page using UIWebView or WKWebView?

Or, more importantly, does accessing a web application in this way take advantage of the Nitro rendering engine?

@tpeiffer they would use wkwebview, since it's just safari without the address bar.

Hi, I have this exact same issue with an old version of angular: 1.0.6
I see that all the fixes are for newer versions, do you know where should I be looking to see if I can fix this problem? Thanks.

@tzamora Have you tried applying @IgorMinar's patch? Perhaps 1.0.6 is too early for it to work.

@petebacondarwin the @IgorMinar's patch refers to a file called browser.js. I dont have any file called browser.js

FYI, a fix for the underlying issue landed in WebKit the other day:
http://trac.webkit.org/changeset/190092
http://trac.webkit.org/changeset/190100

I got this working with the js patch, but not straight away. New to this (angularjs and Ionic) so if someone could check this and let me know if I've missed anything major I'd really appreciate it! (e.g. is the platform object very inefficient etc)

Note:

  1. I downloaded the ios9-$browser-patch.js local
  2. I found that it didn't solve my problem, but forcing isIOS9UIWebView to return true, worked great
  3. I rewrote the patch check using Ionic platform methods, which works fine now. (I've left the old code commented here to show the evolution)
function isIOS9UIWebView(userAgent) {
      return (/9\.[0-9]\.[0-9]/.test(ionic.Platform.version()) && /iOS/.test(ionic.Platform.device() ));

      //return true;
      //return (navigator.userAgent.indexOf(' OS 9') != -1) && (navigator.appVersion.indexOf(' OS 9') != -1);
      //return /(iPhone|iPad|iPod).* OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent);
      // only provide the patch for iOS9 on UIWebView
    }

I ran into this problem on 8.4 on the simulator; so I don't think it's limited to just iOS 9. I just updated the agent string check to include versions iOS 8 & 9. Here's the agent string.

"Mozilla/5.0 (iPhone; CPU iPhone OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12H141 (140307121489296)"

+1

Hi

Can someone confirm if this was solved on ios 9.2? (beta)

Thanks :)

I cannot replicate the issue as of this mornings update to iOS 9.2 (13C75) on iPhone 6. Looking good so far. This bug is still open though - https://openradar.appspot.com/22186109

If this patch is applied, could it have any negative effect on iOS 9.2 and up? I will test this with our own app, but I want to make sure I'm not introducing issues that don't show when testing our particular situation.

As far as I can see, all the patches use a regex that filters iOS 9 and up, but not specifically 9.x up to and including 9.1.

Hey guys, this issue is currently happening in iPhone 6 with iOS 9, can anyone let me know why?
https://forum.ionicframework.com/t/ios-9-beta-slide-menu-app-transition-issue/30768
is another patch that I need to apply? Please let me know, I really need to fix that screen overlap issue.

Updating to iOS 9.2 seems to solve it for me too.

@bruno-serfe this is an issue on how iOS handles window.location that is present only in iOS 9.0.x. If you can update to Angular 1.4.6+, then there is nothing else you have to do as this version contains the fix. If you cannot upgrade, then the patch at https://github.com/angular/angular.js/issues/12241#issuecomment-139446288 has the same fix.

As stated before, this issue is present only in iOS 9.0.x as this was fixed in iOS 9.1.0.

The issue posted on ionic looks like the same issue, so the same fix should work both ways.

@lgalfaso thanks for the reply, I'll try to fix a digest error I've found, if that doesn't solve the issue I'll try upgrading angular, thanks for the reply!.

Thanks @lgalfaso :+1:

I had this problem when I added the web app to home screen and used it as standalone, however upgrading from AngularJS 1.4.5 to 1.5 completely solved the problem and speeded up the navigation as hell!

@volgwfang The issue that you're having is not related to this topic?

Was this page helpful?
0 / 5 - 0 ratings