Saltar al contenido

¿Cómo evitar que WKWebView solicite repetidamente permiso para acceder a la ubicación?

Este grupo especializado pasados varios días de investigación y recopilar de datos, obtuvimos la respuesta, queremos que resulte útil para ti en tu proyecto.

Solución:

Resulta que es bastante difícil, pero se puede hacer. Tienes que inyectar código JavaScript que intercepta solicitudes a navigator.geolocation y transferirlos a su aplicación, luego obtenga la ubicación con CLLocationManager, luego inyecte la ubicación de nuevo en JavaScript.

Aquí está el breve esquema:

  1. Agregar WKUserScript para usted WKWebView configuración que anula los métodos de navigator.geolocation. El JavaScript inyectado debería verse así:

    navigator.geolocation.getCurrentPosition = function(success, error, options)  ... ;
    navigator.geolocation.watchPosition = function(success, error, options)  ... ;
    navigator.geolocation.clearWatch = function(id)  ... ;
    
  2. Con WKUserContentController.add(_:name:) agregue el controlador de mensajes de secuencia de comandos a su WKWebView. El JavaScript inyectado debería llamar a su controlador, así:

    window.webkit.messageHandlers.locationHandler.postMessage('getCurrentPosition');
    
  3. Cuando una página web solicita una ubicación, este método se activará userContentController(_:didReceive:) para que su aplicación sepa que la página web solicita la ubicación. Encuentra tu ubicación con la ayuda de CLLocationManager como siempre.

  4. Ahora es el momento de inyectar la ubicación de nuevo al JavaScript solicitante con webView.evaluateJavaScript("didUpdateLocation(coords: latitude:55.0, longitude:0.0, timestamp: 1494481126215.0)"). Por supuesto, su JavaScript inyectado debería tener didUpdateLocation función lista para iniciar el controlador de éxito guardado.

Es un algoritmo bastante largo, ¡pero funciona!

Debido a que no encontré una solución sobre cómo evitar esta estúpida solicitud de permiso duplicada, creé la clase rápida NavigatorGeolocation. El objetivo de esta clase es anular los JavaScript nativos navigator.geolocation API con una personalizada con 3 beneficios:

  1. Uso de desarrolladores frontend / JavaScript navigator.geolocation API de forma estándar sin tener cuidado de que se anule y use la invocación de código JS -> Swift detrás
  2. Mantenga toda la lógica fuera de ViewController tanto como sea posible
  3. No más solicitudes de permiso duplicadas feas y estúpidas (la primera para la aplicación y la segunda para la vista web):
    ingrese la descripción de la imagen aquí
    ingrese la descripción de la imagen aquí

@AryeeteySolomonAryeetey respondió alguna solución pero falta mi primer y segundo beneficio. En su solución, el desarrollador de frontend tiene que agregar al código JavaScript código específico para iOS. No me gustan estas desagradables adiciones a la plataforma, me refiero a la función de JavaScript getLocation invocado desde swift que nunca es utilizado por la plataforma web o android. Tengo una aplicación híbrida (web / android / ios) que usa webview en ios / android y quiero tener solo un código HTML5 + JavaScript idéntico para todas las plataformas, pero no quiero usar soluciones enormes como Apache Cordova (anteriormente PhoneGap).

Puede integrar fácilmente la clase NavigatorGeolocation a su proyecto: simplemente cree un nuevo archivo rápido NavigatorGeolocation.swift, copie el contenido de mi respuesta y en ViewController.swift agregue 4 líneas relacionadas con var navigatorGeolocation.

Creo que el Android de Google es mucho más inteligente que el iOS de Apple porque la vista web en Android no se molesta con la solicitud de permiso duplicado porque el usuario ya otorga / deniega el permiso para la aplicación. No hay seguridad adicional para pedirlo dos veces, ya que algunas personas defienden a Apple.

ViewController.swift:

import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate 

    var webView: WKWebView!;
    var navigatorGeolocation = NavigatorGeolocation();

    override func loadView() 
        super.loadView();
        let webViewConfiguration = WKWebViewConfiguration();
        webView = WKWebView(frame:.zero , configuration: webViewConfiguration);
        webView.navigationDelegate = self;
        navigatorGeolocation.setWebView(webView: webView);
        view.addSubview(webView);
    

    override func viewDidLoad() 
        super.viewDidLoad();
        let url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "webapp");
        let request = URLRequest(url: url!);
        webView.load(request);
    

    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!;

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

    func setWebView(webView: WKWebView) 
        webView.configuration.userContentController.add(self, name: "listenerAdded");
        webView.configuration.userContentController.add(self, name: "listenerRemoved");
        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 

    func authorizationStatusIsDenied(status: CLAuthorizationStatus) -> Bool 

    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)  this.listeners[id].onetime) 
                            navigator.geolocation.clearWatch(id);
                        
                    
                ,
                success: function(timestamp, latitude, longitude, altitude, accuracy, altitudeAccuracy, heading, speed) 
                    var position =  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 ;
                window.webkit.messageHandlers.listenerAdded.postMessage("");
            ;

            // @override watchPosition()
            navigator.geolocation.watchPosition = function(success, error, options) 
                var id = this.helper.id();
                this.helper.listeners[id] = ;
                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;
    


ACTUALIZACIÓN 2021/02: He eliminado el método inútil NavigatorGeolocation.setUserContentController () porque WKWebViewConfiguration.userContentController se puede agregar en NavigatorGeolocation.setWebView () a través de webView.configuration.userContentController.add () Por lo tanto, la implementación de NavigatorGeolocation en ViewController una línea es más simple (

Entonces, siguiendo los pasos descritos por @AlexanderVasenin, creé una esencia que funciona perfectamente.

Ejemplo de código aquí

Suponiendo que index.html es la página que está intentando cargar.

  1. Anular el método HTML navigator.geolocation.getCurrentPosition que se utiliza para solicitar información de ubicación con este script
 let scriptSource = "navigator.geolocation.getCurrentPosition = function(success, error, options) window.webkit.messageHandlers.locationHandler.postMessage('getCurrentPosition');;"
 let script = WKUserScript(source: scriptSource, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
 contentController.addUserScript(script)

así que cada vez que la página web intente llamar navigator.geolocation.getCurrentPosition, lo anulamos llamando func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)

  1. los userContentController luego obtiene los datos de ubicación de CLLocationManager y llama a un método en la página web para manejar esa respuesta. En mi caso, el método es getLocation(lat,lng).

Este es el código completo.

ViewController.swift

import UIKit
import WebKit
import CoreLocation

class ViewController: UIViewController , CLLocationManagerDelegate, WKScriptMessageHandler
    var webView: WKWebView?
    var manager: CLLocationManager!

    override func viewDidLoad() 
        super.viewDidLoad()

        manager = CLLocationManager()
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyBest
        manager.requestAlwaysAuthorization()
        manager.startUpdatingLocation()

        let contentController = WKUserContentController()
        contentController.add(self, name: "locationHandler")

        let config = WKWebViewConfiguration()
        config.userContentController = contentController

        let scriptSource = "navigator.geolocation.getCurrentPosition = function(success, error, options) window.webkit.messageHandlers.locationHandler.postMessage('getCurrentPosition');;"
        let script = WKUserScript(source: scriptSource, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
        contentController.addUserScript(script)

        self.webView = WKWebView(frame: self.view.bounds, configuration: config)
        view.addSubview(webView!)

        webView?.uiDelegate = self
        webView?.navigationDelegate = self
        webView?.scrollView.delegate = self
        webView?.scrollView.bounces = false
        webView?.scrollView.bouncesZoom = false

        let url = Bundle.main.url(forResource: "index", withExtension:"html")
        let request = URLRequest(url: url!)

        webView?.load(request)
    

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) 
        if message.name == "locationHandler",let  messageBody = message.body as? String 
            if messageBody == "getCurrentPosition"
                let script =
                    "getLocation((manager.location?.coordinate.latitude ?? 0) ,(manager.location?.coordinate.longitude ?? 0))"
                webView?.evaluateJavaScript(script)
            
        
    

index.html



    

        

Click the button to get your coordinates.

Valoraciones y comentarios

¡Haz clic para puntuar esta entrada!
(Votos: 2 Promedio: 5)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *