44

I want to switch to WKWebView from UIWebView. In my code, the web view instance is created offscreen (without attaching to the view hierarchy) and URL is loaded. When loading is done, the web view is presented to the user in a separate UIWindow (like an interstitial ad). Everything works fine with UIWebView; but after switching to WKWebView, the angular javascript code behaves in an unexpected way.

After doing some research, I've figured out that javascript timers (and HTTP loaders) are suspended when the WKWebView instance is not attached to the view hierarchy: if you start a timer in javascript which runs in detached WKWebView - it won't be fired until the web view is attached.

Can anyone suggest a possible workaround (besides attaching a hidden web view to the key window and then moving it to another window)?

I've created a sample project to demonstrate the issue (or, more likely, a WebKit feature)

https://dl.dropboxusercontent.com/u/148568148/WebKitViewTest.zip

The test application loads a web page with javascript which schedules a timer and loads HTTP request. You can use switches to show/hide web view (sets 'hidden' property) and attach/detach it from the view hierarchy.

Sample app running on iOS 8.0

If the web view is attached (hidden or visible) and you hit Load, the javascript works fine: you can see success or failure alert dialog. If it's detached - the timer will only fire when it's attached to the view hierarchy (switch "Detached" to "on", hit "Load", wait a couple of seconds and switch back to "off"). You can also check console.log from Safari Web Inspector.

Here's the test web page source:

<!doctype html>
<html ng-app="project">
  <head>
    <script
      src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.16/angular.js"></script>
    <script src="project.js"></script>
  </head>
  <body>
    <div id="simple" ng-controller="MyController" data-ng-init="callNotify(message);">
    </div>
  </body>
</html>

And the javascript:

angular.
  module('project', []).
  controller('MyController', ['$scope','notify', function ($scope, notify) {
  $scope.callNotify = function(msg) {
    notify(msg);
  };
}]).
  factory('notify', ['$window', '$http', '$q', '$timeout', function(win, $http, $q, $timeout) {
  return function(msg) {
    deferred = $q.defer();

    $timeout(function() {
      $http.get('http://jsonplaceholder.typicode.com/posts').
      success(function(data, status, headers, config) {
        alert("success");
        console.log("success")
      }).
        error(function(data, status, headers, config) {
        alert("error");
        console.log("error")
      });
    });
    return deferred.promise;
  };
}]);
Alok C
  • 2,787
  • 3
  • 25
  • 44
Alex Lementuev
  • 598
  • 4
  • 6
  • 1
    Did you ever find any solution for this? We've been seeing the same problem recently. – Accatyyc Aug 25 '16 at 15:00
  • 5
    I ended up adding this view to the hierarchy and making it invisible – Alex Lementuev Aug 27 '16 at 19:19
  • I see. I did the same thing, seems like it's the "best" solution for now – Accatyyc Aug 27 '16 at 19:39
  • Am I imagining things or did the iOS documentation used to be better? – WiseOldDuck Dec 08 '16 at 20:02
  • 1
    I ended up going the same route. WKWebview resource loading and javascript execution in general do not work when it is not attached to the view heierarchy, and especially in iOS 9.0, but you'll only find this out from stack exchange forums and banging your head against the wall. Although it's always a pretty easy fix to stick it into the view heierachy, I find it really upsetting that such fundamental behavior is totally undocumented by Apple, AND inconsistent over version releases (8.0 and 10.0 very different from 9.0). – Badam Baplan Jul 10 '17 at 17:47
  • Did you try calling webView.didMoveToSuperview() without actually adding it to a superView? – Santhosh R Nov 15 '17 at 23:04
  • 1
    @AlexLementuev care to post that as an answer? – Benjamin Gruenbaum Mar 07 '18 at 13:49

2 Answers2

5

Having run into the same problem, I never found any solution except to add the WKWebView to the view hierarchy and move it offscreen with AutoLayout. Fortunately, this workaround does work reliably.

Steve Landey
  • 2,609
  • 25
  • 25
0

I had exactly the same issue and have solved it with UIStackView. I created StackNavigationController, minimalistic drop-in replacement for UINavigationController, which basically adds new views to stack, while hiding all of them but the last one. Therefore, all views are still in hierarchy, executing and loading, but only the last one is visible. No animations, lots of missing API, feel free to build on that.

public class StackNavigationController: UIViewController {

    private(set) var viewControllers: [UIViewController] = []
    private var stackView: UIStackView { return view as! UIStackView }

    override public func loadView() {
        let stackView = UIStackView()
        stackView.axis = .vertical
        stackView.distribution = .fillEqually
        view = stackView
    }

    func pushViewController(_ viewController: UIViewController, animated: Bool) {
        let previousView = stackView.arrangedSubviews.last
        viewControllers.append(viewController)
        stackView.addArrangedSubview(viewController.view)
        previousView?.isHidden = true
    }

    func popToRootViewController(animated: Bool) {
        while stackView.arrangedSubviews.count > 1 {
            stackView.arrangedSubviews.last?.removeFromSuperview()
            viewControllers.removeLast()
        }
        stackView.arrangedSubviews.first?.isHidden = false
    }

}

My story:

  1. I have a UINavigationController with WKWebView(1) as root view
  2. WKWebView(1) has a hyperlink with target: _blank parameter set
  3. After tapping the link, I catch WKUIDelegate's webView(_:didRequestNewWebView:) and push another WKWebView(2) onto UINavigationController
  4. WKWebView(2) performs a request to server
  5. Server responds with JavaScript message to WKWebView(1)
  6. I never get the message, because WKWebView(1) is halted
Mateusz
  • 454
  • 4
  • 13