How to prevent WKWebView to repeatedly ask for permission to access location?

こ雲淡風輕ζ 提交于 2019-11-29 01:53:05
Alexander Vasenin

Turns out it's quite hard, but possible to do. You have to inject JavaScript code which intercepts requests to navigator.geolocation and transfer them to your app, then get the location with CLLocationManager, then inject location back to the JavaScript.

Here is the brief scheme:

  1. Add WKUserScript to your WKWebView configuration which overrides methods of navigator.geolocation. Injected JavaScript should look like this:

    navigator.geolocation.getCurrentPosition = function(success, error, options) { ... };
    navigator.geolocation.watchPosition = function(success, error, options) { ... };
    navigator.geolocation.clearWatch = function(id) { ... };
    
  2. With WKUserContentController.add(_:name:) add script message handler to your WKWebView. Injected JavaScript should call your handler, like this:

    window.webkit.messageHandlers.locationHandler.postMessage('getCurrentPosition');
    
  3. When a web page will request a location, this method will fire userContentController(_:didReceive:) so your app would know web page is requesting location. Find your location with the help of CLLocationManager as usual.

  4. Now it's time to inject the location back to the requesting JavaScript with webView.evaluateJavaScript("didUpdateLocation({coords: {latitude:55.0, longitude:0.0}, timestamp: 1494481126215.0})"). Of course your injected JavaScript should have didUpdateLocation function ready to launch saved success handler.

Quite a long algorithm, but it works!

So following the steps outlined by @AlexanderVasenin, I created a gist which works perfectly.

Code Sample Here

Assuming index.html is the page you're trying to load.

  1. Override the HTML method navigator.geolocation.getCurrentPosition which is used to request for location info with this 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)

so whenever the webpage tries to call navigator.geolocation.getCurrentPosition, we override it by calling func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)

  1. the userContentController method then gets the location data from CLLocationManager and calls a method in the webpage to handle that response. In my case, the method is getLocation(lat,lng).

This is the full code.

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

<!DOCTYPE html>
<html>
    <body>

        <h1>Click the button to get your coordinates.</h1>

        <button style="font-size: 60px;" onclick="getUserLocation()">Try It</button>

        <p id="demo"></p>

        <script>
            var x = document.getElementById("demo");

            function getUserLocation() {
                if (navigator.geolocation) {
                    navigator.geolocation.getCurrentPosition(showPosition);
                } else {
                    x.innerHTML = "Geolocation is not supported by this browser.";
                }
            }

        function showPosition(position) {
            getLocation(position.coords.latitude,position.coords.longitude);
        }

        function getLocation(lat,lng) {
            x.innerHTML = "Lat: " +  lat+
            "<br>Lng: " + lng;
        }
        </script>

    </body>
</html>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!