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

时间:2016-09-23 16:19:30

标签: ios geolocation webkit wkwebview

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

我管理此位置的唯一方法就是在NSLocationWhenInUseUsageDescription添加了info.plist属性。

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

5 个答案:

答案 0 :(得分:10)

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

以下是简要方案:

  1. WKUserScript添加到WKWebView配置中,该配置会覆盖navigator.geolocation的方法。注入的JavaScript应如下所示:

    navigator.geolocation.getCurrentPosition = function(success, error, options) { ... };
    navigator.geolocation.watchPosition = function(success, error, options) { ... };
    navigator.geolocation.clearWatch = function(id) { ... };
    
  2. 使用WKUserContentController.add(_:name:)向您的WKWebView添加脚本邮件处理程序。注入的JavaScript应该调用你的处理程序,如下所示:

    window.webkit.messageHandlers.locationHandler.postMessage('getCurrentPosition');
    
  3. 当网页请求某个位置时,此方法会触发userContentController(_:didReceive:),以便您的应用知道网页正在请求位置。在CLLocationManager的帮助下照常查找您的位置。

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

  5. 相当长的算法,但它有效!

答案 1 :(得分:0)

因此,按照@AlexanderVasenin概述的步骤,我创建了一个工作正常的要点。

Code Sample Here

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

  1. 使用此脚本重写用于请求位置信息的HTML方法navigator.geolocation.getCurrentPosition
 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)

因此,只要网页尝试调用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)
            }
        }
    }
}

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>

答案 2 :(得分:0)

因为我没有找到解决方案来避免这个愚蠢的重复权限请求,所以我创建了快捷类NavigatorGeolocation。此类的目的是使用具有以下3种优点的自定义变量覆盖本机JavaScript的navigator.geolocation API:

  1. 前端/ JavaScript开发人员使用navigator.geolocation API 标准方法,无需注意它会被覆盖并使用代码 调用JS->后面的Swift
  2. 将所有逻辑尽可能保留在ViewController之外
  3. 不再有丑陋且愚蠢的重复权限请求(一个用于应用程序,另一个用于webview): enter image description here enter image description here

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

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

我认为Google的Android比Apple的iOS聪明得多,因为Android的Webview不会因重复的权限请求而烦恼,因为用户已经为应用授予/拒绝了权限。有人会捍卫苹果,所以再也没有安全问题要问两次。

ViewController.swift

import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

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

    override func loadView() {
        super.loadView();
        let webViewConfiguration = WKWebViewConfiguration();
        navigatorGeolocation.setUserContentController(webViewConfiguration: webViewConfiguration);
        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 setUserContentController(webViewConfiguration: WKWebViewConfiguration) {
        let 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;
    }

}

答案 3 :(得分:0)

基于 accepted answer,我能够让 WKWebView 访问用户位置(如果您有权限)在网站上使用,例如在 macOS(以前是 OSX)上使用地图的网站;尽管这在 macOS 上的 iOS 上开箱即用是一种完全不同的舞蹈。

使用 Swift 5

创建一个实现 WKScriptMessageHandler 协议的类。最好这需要是一个单独的对象,因为它将被 WKUserContentController 保留。

JavaScript 发送消息时会调用该方法

final class Handler: NSObject, WKScriptMessageHandler {
    weak var web: Web?
        
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        switch message.body as? String {
            case "getCurrentPosition":

                let location = /* get user location using CoreLocation, as a CLLocation object */
                web?.evaluateJavaScript(
                            "locationReceived(\(location.coordinate.latitude), \(location.coordinate.longitude), \(location.horizontalAccuracy));")

            default: break
            }
    }
}

需要添加到用户控制器的 JavaScript

let script = """
var locationSuccess = null;

function locationReceived(latitude, longitude, accuracy) {
    var position = {
        coords: {
            latitude: latitude,
            longitude: longitude,
            accuracy: accuracy
        }
    };

    if (locationSuccess != null) {
        locationSuccess(position);
    }

    locationSuccess = null;
}

navigator.geolocation.getCurrentPosition = function(success, error, options) {
    locationSuccess = success;
    window.webkit.messageHandlers.handler.postMessage('getCurrentPosition');
};

"""

使用 WKWebViewConfiguration 上的处理程序实例化您的 WKWebView,并将处理程序的弱引用分配给 webview

还将 JavaScript 作为用户脚本添加到 WKUserContentController


let handler = Handler()
let configuration = WKWebViewConfiguration()
configuration.userContentController.add(handler, name: "handler")
configuration.userContentController.addUserScript(.init(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true))

let webView = WKWebView(frame: .zero, configuration: configuration)
handler.web = webView

答案 4 :(得分:0)

为了简单起见,这里有一个 SwiftUI 版本

import SwiftUI
import WebKit

struct ContentView: View {
    var body: some View {
        WebView()
    }
}

struct WebView: UIViewRepresentable {
    func makeUIView(context: Context) -> WKWebView {
        let webView = WKWebView()
        WebScriptManager.shared.config(webView)
        return webView
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
        webView.load(URLRequest(url: URL(string: "https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API/Using_the_Geolocation_API#result")!))
    }
}
struct ScriptMessageCall {
    let name: String
    let body: String
    let callback: String
}

let GEOGetCurrentPosition = ScriptMessageCall(name: "geolocation", body: "getCurrentPosition", callback: "getCurrentPositionCallback")

class WebScriptManager: NSObject, WKScriptMessageHandler {
    static let shared = WebScriptManager()
    
    private override init() {}
    
    let injectScript = """
        navigator.geolocation.getCurrentPosition = function(success, error, options) {
          webkit.messageHandlers.\(GEOGetCurrentPosition.name).postMessage("\(GEOGetCurrentPosition.body)");
        };

        function \(GEOGetCurrentPosition.callback)(latitude, longitude) {
          console.log(`position: ${latitude}, ${longitude}`);
        };
    """

    var webView: WKWebView!
    
    func config(_ webView: WKWebView) {
        self.webView = webView
        let controller = self.webView.configuration.userContentController
        controller.addUserScript(WKUserScript(source: injectScript, injectionTime: .atDocumentEnd, forMainFrameOnly: false))
        controller.add(self, name: GEOGetCurrentPosition.name)
    }

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == GEOGetCurrentPosition.name, (message.body as? String) == GEOGetCurrentPosition.body {
            webView.evaluateJavaScript("\(GEOGetCurrentPosition.callback)(0, 0)", completionHandler: nil)
        }
    }
}

您可以通过Enabling Web Inspector

查看console.log