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:
-
Agregar
WKUserScript
para ustedWKWebView
configuración que anula los métodos denavigator.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) ... ;
-
Con
WKUserContentController.add(_:name:)
agregue el controlador de mensajes de secuencia de comandos a suWKWebView
. El JavaScript inyectado debería llamar a su controlador, así:window.webkit.messageHandlers.locationHandler.postMessage('getCurrentPosition');
-
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 deCLLocationManager
como siempre. -
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 tenerdidUpdateLocation
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:
- 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 - Mantenga toda la lógica fuera de ViewController tanto como sea posible
- No más solicitudes de permiso duplicadas feas y estúpidas (la primera para la aplicación y la segunda para la vista web):
@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.
- 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)
- los
userContentController
luego obtiene los datos de ubicación deCLLocationManager
y llama a un método en la página web para manejar esa respuesta. En mi caso, el método esgetLocation(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.