2

*** Terminating app due to uncaught exception 'NSObjectNotAvailableException', reason: 'UIAlertView is deprecated and unavailable for UIScene based applications, please use UIAlertController!'

On alert from WKWebView it throws above error.

UIAlertView is called from Javascript in Webpage. I'm not calling UIAlertView manually.

import UIKit
import WebKit

class ViewController: UIViewController{

    @IBOutlet weak var webView: WKWebView!
    override func viewDidLoad() {
        super.viewDidLoad()

        let url = URL(string: "https://www.google.com/maps")!
        webView.load(URLRequest(url: url))
    }
}

After maps are loaded when I click on "Your Location", app crashes.

Nitesh
  • 1,564
  • 2
  • 26
  • 53
  • If you don't need JavaScript, disable javascript in WKPreferences(). your app won't crash. – Manikandan Jan 20 '20 at 12:04
  • @Manikandan I need JS. – Nitesh Jan 20 '20 at 12:06
  • Did you add the following keys in your plist file? because your app might try to show an alert for location permission alert. NSLocationAlwaysAndWhenInUseUsageDescription,NSLocationWhenInUseUsageDescription Watch Out – Manikandan Jan 20 '20 at 13:08
  • @Manikandan Yes I've both permissions. – Nitesh Jan 20 '20 at 13:15
  • Try setting a UIDelegate on your WKWebView and then implement the 3 methods of WKUIDelegate protocol that are called when javascript calls alert(), confirm() and prompt(). In your implementation use a UIAlertController. I wonder if the default handling uses the old UIAlertView. – seeinvisible Jan 22 '20 at 21:05
  • @seeinvisible Tried that but that methods are not called. App crashes before that. – Nitesh Jan 23 '20 at 02:30
  • I was able to repeat the crash. When javascript requests location, webkit requests location permission from the user using UIAlertView. I'm not sure what you can do about that except stopping using UIScene. – seeinvisible Jan 23 '20 at 08:29
  • @seeinvisible Yes but there has to be any other way to handle the crash. If not then will end up deleting SceneDelegate – Nitesh Jan 23 '20 at 08:32
  • What environment do you use? What else did you change/do not listed in post? I've created iOS/SingleView/Swift project from scratch copied your code into `ViewController` added WKWebView in Storyboard and linked outlet to it. Build & Run ... Click in `Your Location` ... all works. Xcode 11.2 (and 11.3) + iOS 13.2(.2). – Asperi Jan 23 '20 at 11:42
  • @Asperi you have SceneDelegate in your project ? – Nitesh Jan 23 '20 at 11:43
  • Definitely yes.. all new projects are created with `SceneDelegate` – Asperi Jan 23 '20 at 11:44
  • @Asperi Strange it crashes on my device. iOS 13.3 but I don’t think it should be different in iOS 13.2. On iOS 12 device it works. – Nitesh Jan 23 '20 at 11:47
  • The only thing I see is a `broken NSLayoutConstraint` warning messages in Console for `UIInputSetContainerView`, but application works properly. – Asperi Jan 23 '20 at 11:47
  • @Asperi web pages loads successfully. Only after clicking on your location it crashes. So I don’t think layout issue. – Nitesh Jan 23 '20 at 11:48
  • Can you attach or show generated crashlog somehow? – Asperi Jan 23 '20 at 12:01
  • Btw, does it crash only on a device or in simulator as well? – Asperi Jan 23 '20 at 17:01
  • @Asperi on simulator as well. – Nitesh Jan 23 '20 at 17:02

4 Answers4

5

I was able to reproduce the Error on the Simulator(version 13.2.2). I did a quick search and found that we can provide an alternate source for handling location permission and provide location updates to WKWebView. Below is the code that can get the job done.

ViewController.swift

import UIKit
import WebKit
class ViewController: UIViewController {

    var navigatorGeolocation = NavigatorGeolocation();
    var web: WKWebView!
    override func viewDidLoad() {
        super.viewDidLoad()

        let webViewConfiguration = WKWebViewConfiguration();
        navigatorGeolocation.setUserContentController(webViewConfiguration: webViewConfiguration);
        web = WKWebView(frame:.zero , configuration: webViewConfiguration)
        web.navigationDelegate = self;
        navigatorGeolocation.setWebView(webView: web);
        view.addSubview(web);
        web.translatesAutoresizingMaskIntoConstraints = false
        let constraints = [
            web.topAnchor.constraint(equalTo: view.topAnchor),
            web.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            web.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            web.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ]
        NSLayoutConstraint.activate(constraints)
        let url = URL(string: "https://www.google.com/maps")!
        web.load(URLRequest(url: url))
    }
}
extension ViewController: WKNavigationDelegate{
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        webView.evaluateJavaScript(navigatorGeolocation.getJavaScripToEvaluate());
    }
}

NavigatorGeolocation.swift

import WebKit
import CoreLocation

class NavigatorGeolocation: NSObject, WKScriptMessageHandler, CLLocationManagerDelegate {

    var locationManager = CLLocationManager();
    var listenersCount = 0;
    var webView: WKWebView!;
    var controller: WKUserContentController?

    override init() {
        super.init();
        locationManager.delegate = self;
    }

    func setUserContentController(webViewConfiguration: WKWebViewConfiguration) {
        controller = WKUserContentController();
        controller?.add(self, name: "listenerAdded")
        controller?.add(self, name: "listenerRemoved")
        webViewConfiguration.userContentController = controller!
    }

    func setWebView(webView: WKWebView) {
        self.webView = webView;
    }

    func locationServicesIsEnabled() -> Bool {
        return (CLLocationManager.locationServicesEnabled()) ? true : false;
    }

    func authorizationStatusNeedRequest(status: CLAuthorizationStatus) -> Bool {
        return (status == .notDetermined) ? true : false;
    }

    func authorizationStatusIsGranted(status: CLAuthorizationStatus) -> Bool {
        return (status == .authorizedAlways || status == .authorizedWhenInUse) ? true : false;
    }

    func authorizationStatusIsDenied(status: CLAuthorizationStatus) -> Bool {
        return (status == .restricted || status == .denied) ? true : false;
    }

    func onLocationServicesIsDisabled() {
        webView.evaluateJavaScript("navigator.geolocation.helper.error(2, 'Location services disabled');");
    }

    func onAuthorizationStatusNeedRequest() {
        locationManager.requestWhenInUseAuthorization();
    }

    func onAuthorizationStatusIsGranted() {
        locationManager.startUpdatingLocation();
    }

    func onAuthorizationStatusIsDenied() {
        webView.evaluateJavaScript("navigator.geolocation.helper.error(1, 'App does not have location permission');");
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if (message.name == "listenerAdded") {
            listenersCount += 1;

            if (!locationServicesIsEnabled()) {
                onLocationServicesIsDisabled();
            }
            else if (authorizationStatusIsDenied(status: CLLocationManager.authorizationStatus())) {
                onAuthorizationStatusIsDenied();
            }
            else if (authorizationStatusNeedRequest(status: CLLocationManager.authorizationStatus())) {
                onAuthorizationStatusNeedRequest();
            }
            else if (authorizationStatusIsGranted(status: CLLocationManager.authorizationStatus())) {
                onAuthorizationStatusIsGranted();
            }
        }
        else if (message.name == "listenerRemoved") {
            listenersCount -= 1;
            // no listener left in web view to wait for position
            if (listenersCount == 0) {
                locationManager.stopUpdatingLocation();
            }
        }
    }

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        // didChangeAuthorization is also called at app startup, so this condition checks listeners
        // count before doing anything otherwise app will start location service without reason
        if (listenersCount > 0) {
            if (authorizationStatusIsDenied(status: status)) {
                onAuthorizationStatusIsDenied();
            }
            else if (authorizationStatusIsGranted(status: status)) {
                onAuthorizationStatusIsGranted();
            }
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.last {
            webView.evaluateJavaScript("navigator.geolocation.helper.success('\(location.timestamp)', \(location.coordinate.latitude), \(location.coordinate.longitude), \(location.altitude), \(location.horizontalAccuracy), \(location.verticalAccuracy), \(location.course), \(location.speed));");
        }
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        webView.evaluateJavaScript("navigator.geolocation.helper.error(2, 'Failed to get position (\(error.localizedDescription))');");
    }

    func getJavaScripToEvaluate() -> String {
        let javaScripToEvaluate = """
            // management for success and error listeners and its calling
            navigator.geolocation.helper = {
                listeners: {},
                noop: function() {},
                id: function() {
                    var min = 1, max = 1000;
                    return Math.floor(Math.random() * (max - min + 1)) + min;
                },
                clear: function(isError) {
                    for (var id in this.listeners) {
                        if (isError || this.listeners[id].onetime) {
                            navigator.geolocation.clearWatch(id);
                        }
                    }
                },
                success: function(timestamp, latitude, longitude, altitude, accuracy, altitudeAccuracy, heading, speed) {
                    var position = {
                        timestamp: new Date(timestamp).getTime() || new Date().getTime(), // safari can not parse date format returned by swift e.g. 2019-12-27 15:46:59 +0000 (fallback used because we trust that safari will learn it in future because chrome knows that format)
                        coords: {
                            latitude: latitude,
                            longitude: longitude,
                            altitude: altitude,
                            accuracy: accuracy,
                            altitudeAccuracy: altitudeAccuracy,
                            heading: (heading > 0) ? heading : null,
                            speed: (speed > 0) ? speed : null
                        }
                    };
                    for (var id in this.listeners) {
                        this.listeners[id].success(position);
                    }
                    this.clear(false);
                },
                error: function(code, message) {
                    var error = {
                        PERMISSION_DENIED: 1,
                        POSITION_UNAVAILABLE: 2,
                        TIMEOUT: 3,
                        code: code,
                        message: message
                    };
                    for (var id in this.listeners) {
                        this.listeners[id].error(error);
                    }
                    this.clear(true);
                }
            };

            // @override getCurrentPosition()
            navigator.geolocation.getCurrentPosition = function(success, error, options) {
                var id = this.helper.id();
                this.helper.listeners[id] = { onetime: true, success: success || this.noop, error: error || this.noop };
                window.webkit.messageHandlers.listenerAdded.postMessage("");
            };

            // @override watchPosition()
            navigator.geolocation.watchPosition = function(success, error, options) {
                var id = this.helper.id();
                this.helper.listeners[id] = { onetime: false, success: success || this.noop, error: error || this.noop };
                window.webkit.messageHandlers.listenerAdded.postMessage("");
                return id;
            };

            // @override clearWatch()
            navigator.geolocation.clearWatch = function(id) {
                var idExists = (this.helper.listeners[id]) ? true : false;
                if (idExists) {
                    this.helper.listeners[id] = null;
                    delete this.helper.listeners[id];
                    window.webkit.messageHandlers.listenerRemoved.postMessage("");
                }
            };
        """;

        return javaScripToEvaluate;
    }
}

The NavigatorGeolocation code is taken from this Answer posted by mikep

(Tip: Read the comments also)

Output(Simulator->Debug->Location->Apple): enter image description here

Sahil Manchanda
  • 9,812
  • 4
  • 39
  • 89
1

I've tried this (after running into this bug on my own) on the iOS 13.4 beta, and it seems to have been solved.

weakdan
  • 2,616
  • 3
  • 12
  • 13
-1

It gives you the answer:

Terminating app due to uncaught exception 'NSObjectNotAvailableException', reason: 'UIAlertView is deprecated and unavailable for UIScene based applications, please use UIAlertController!'

You need to use UIAlertController

let alert = UIAlertController(title: "My Alert", 
                              message: "This is an alert.", 
                              preferredStyle: .alert) 
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Default action"), style: .default, handler: { _ in 
    print("The \"OK\" alert occured.")
}))
self.present(alert, animated: true, completion: nil)
Scriptable
  • 19,402
  • 5
  • 56
  • 72
  • this alert is popped by Javascript. I haven't code anything for it. By default alert it crashes. – Nitesh Jan 20 '20 at 11:57
  • Websites or javascript in general don't use UIAlertView, that is an old iOS thing. Are you using a JS framework to create native apps? – Scriptable Jan 20 '20 at 11:59
  • I'm just loading a website and in that there is some alert and during that app crashes. I haven't added any JS code. – Nitesh Jan 20 '20 at 12:01
  • can you show your code for the WKWebView? a website cannot just load a UIAlertView. Its not a web thing, its specifically a iOS thing. what would the website show on desktop or android?? it can't present native alerts. There must be some code creating it somewhere – Scriptable Jan 20 '20 at 12:04
  • https://stackoverflow.com/questions/58188069/uialertview-is-deprecated-and-unavailable-for-uiscene-based-applications-please Check this. When Maps are loaded click on Your Location. – Nitesh Jan 20 '20 at 12:17
-2

App is crashing because scene base application is not support UIAlert View. To solve this issue you need to remove the scene base application and you can use window base app

you need to Remove following

1 - Remove the SceneDelegate file

2 - Remove UISceneSession Lifecycle two methods (configurationForConnecting connectingSceneSession and didDiscardSceneSessions sceneSessions )

3 - Remove Application Scene Manifest Entry info plist

Now Run the project

make sure you have window property in AppDelegate

Ram Mani
  • 1,009
  • 1
  • 10
  • 15