如何防止WKWebView反复询问访问位置的权限?

Ran*_*ndy 12 webkit geolocation ios wkwebview

我有一个WKWebView在我的应用程序中,当我开始浏览www.google.com或任何其他需要位置服务的网站时,会出现一个弹出窗口,即使我已经接受分享,也要求获得访问设备位置的权限我的位置.

我唯一能做的就是管理这个位置的东西是我添加了NSLocationWhenInUseUsageDescription属性info.plist.

我在网上找不到任何答案,所以任何想法都会非常感激.

Ale*_*nin 11

事实证明这很难,但有可能做到.您必须注入JavaScript代码,拦截请求navigator.geolocation并将其传输到您的应用程序,然后获取位置CLLocationManager,然后将位置注入JavaScript.

这是简短的方案:

  1. 添加WKUserScriptWKWebView覆盖方法的配置中navigator.geolocation.注入的JavaScript应如下所示:

    navigator.geolocation.getCurrentPosition = function(success, error, options) { ... };
    navigator.geolocation.watchPosition = function(success, error, options) { ... };
    navigator.geolocation.clearWatch = function(id) { ... };
    
    Run Code Online (Sandbox Code Playgroud)
  2. WKUserContentController.add(_:name:)脚本消息处理程序添加到您的WKWebView.注入的JavaScript应该调用你的处理程序,如下所示:

    window.webkit.messageHandlers.locationHandler.postMessage('getCurrentPosition');
    
    Run Code Online (Sandbox Code Playgroud)
  3. 当网页请求位置时,此方法将触发,userContentController(_:didReceive:)以便您的应用知道网页正在请求位置.CLLocationManager在平常的帮助下找到您的位置.

  4. 现在是时候将位置注入请求的JavaScript了webView.evaluateJavaScript("didUpdateLocation({coords: {latitude:55.0, longitude:0.0}, timestamp: 1494481126215.0})").当然,你注入的JavaScript应该有didUpdateLocation功能,可以启动已保存的成功hanlder.

相当长的算法,但它的工作原理!

  • 嗨...您能提供GitHub示例吗? (3认同)
  • @AlexanderVasenin大家好,您能提供详细的答案还是可以欣赏GitHub示例? (2认同)

mik*_*kep 10

因为我没有找到如何避免这种愚蠢的重复权限请求的解决方案,所以我创建了 swift 类 NavigatorGeolocation。此类的目的是navigator.geolocation使用自定义 API覆盖原生 JavaScript 的API,具有 3 个好处:

  1. 前端/JavaScript 开发人员navigator.geolocation通过标准方式使用API 而不注意它被覆盖并使用代码调用 JS --> Swift 在后面
  2. 尽可能将所有逻辑保留在 ViewController 之外
  3. 不再有丑陋和愚蠢的重复权限请求(应用程序第一个,网页视图第二个): 在此处输入图片说明 在此处输入图片说明

@AryeeteySolomonAryeetey 回答了一些解决方案,但它缺少我的第一个和第二个好处。在他的解决方案中,前端开发人员必须将 iOS 的特定代码添加到 JavaScript 代码中。我不喜欢这个丑陋的平台添加 - 我的意思是getLocation从 swift 调用的JavaScript 函数,它从未被 web 或 android 平台使用过。我有一个混合应用程序 (web/android/ios),它在 ios/android 上使用 webview,我希望所有平台只有一个相同的 HTML5 + JavaScript 代码,但我不想使用像 Apache Cordova(以前称为 PhoneGap)这样的大型解决方案。

您可以轻松地将 NavigatorGeolocation 类集成到您的项目中 - 只需创建新的 swift 文件 NavigatorGeolocation.swift,从我的答案中复制内容,然后在 ViewController.swift 中添加 4 行与 var 相关的行navigatorGeolocation

我认为 Google 的 Android 比 Apple 的 iOS 聪明得多,因为 Android 中的 webview 不会为重复的权限请求而烦恼,因为用户已经授予/拒绝了应用程序的权限。没有额外的安全性要求它两次,因为有些人为 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());
    }

}
Run Code Online (Sandbox Code Playgroud)

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 {
        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;
    }

}
Run Code Online (Sandbox Code Playgroud)

2021/02 更新:我删除了无用的方法 NavigatorGeolocation.setUserContentController() 因为 WKWebViewConfiguration.userContentController 可以通过 webView.configuration.userContentController.add() 添加到 NavigatorGeolocation.setWebView() 所以在 ViewController 中实现 NavigatorGeolocation 更简单(减去一行)

  • 就我而言,我必须在 ```func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!)``` 中调用 ```webView.evaluateJavaScript(navigatorGeolocation.getJavaScripToEvaluate());``` 而不是 ``` func webView(_ webView: WKWebView, didFinish 导航: WKNavigation!)``` (2认同)

Ary*_*tey 5

因此,按照@AlexanderVasenin 概述的步骤,我创建了一个完美运行的要点。

代码示例在这里

假设 index.html 是您要加载的页面。

  1. navigator.geolocation.getCurrentPosition使用此脚本覆盖用于请求位置信息的HTML 方法
 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)
Run Code Online (Sandbox Code Playgroud)

因此,每当网页尝试调用时navigator.geolocation.getCurrentPosition,我们都会通过调用来覆盖它func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)

  1. 然后,该userContentController方法从中获取位置数据CLLocationManager并调用网页中的方法来处理该响应。就我而言,方法是getLocation(lat,lng)

这是完整的代码。

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)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

索引.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>
Run Code Online (Sandbox Code Playgroud)