【问题标题】:How to prevent WKWebView to repeatedly ask for permission to access location?如何防止 WKWebView 重复请求访问位置的权限?
【发布时间】:2017-02-01 14:03:00
【问题描述】:

我的应用中有一个WKWebView,当我开始浏览 www.google.com 或任何其他需要定位服务的网站时,会出现一个弹出窗口,询问是否允许访问设备的位置,即使我已经同意分享我的位置。

我为管理此位置信息所做的唯一一件事就是在我的info.plist 中添加了NSLocationWhenInUseUsageDescription 属性。

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

【问题讨论】:

    标签: ios geolocation webkit wkwebview


    【解决方案1】:

    为简单起见,这里有一个 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

    【讨论】:

      【解决方案2】:

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

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

      @AryeeteySolomonAryeetey 回答了一些解决方案,但它缺少我的第一个和第二个好处。在他的解决方案中,前端开发人员必须为 iOS 添加特定的 JavaScript 代码。我不喜欢这个丑陋的平台添加 - 我的意思是 JavaScript 函数 getLocation 从 swift 调用,它从未被 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 不会为重复的权限请求而烦恼,因为用户已经为应用授予/拒绝了权限。由于有些人为 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 {
              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;
          }
      
      }
      

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

      【讨论】:

      • 这是一个很好的解决这个尴尬问题的方法!干得好:拍手:
      • btw.. 这些是实施地理定位功能的推荐规则:w3.org/TR/geolocation-API
      • 完全复制了这个,我仍然得到提示。
      • @MdImranChoudhury 所以你不需要这个解决方案?也许苹果设备型号/年份有所不同,或者同时 iOS 改变了这种愚蠢的行为?我不再测试它,因为我总是应用我的解决方案。你能发布设备型号和iOS版本吗?
      • @mikep 是的,我正在使用视图委托...如果它有效,那就没有错 :) 这是使它在我的实现中起作用的唯一方法。感谢您的解决方案。
      【解决方案3】:

      基于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】:

        因此,按照@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>
        

        【讨论】:

        • 太棒了!你让我今天一整天都感觉很好!多年来我一直在寻找解决方案!请让我知道我是否可以向您捐款或以任何其他方式感谢您!
        • saaryeetey.cos20@gmail.com :)
        • 我好像有点兴奋太早了,因为我仍然遇到问题(在实现你的代码之后,它不再给我双重权限请求,但我没有注意到现在我的位置脚本不再起作用 :(...) 我可以通过您的邮件地址与您联系以寻求帮助吗?
        【解决方案5】:

        事实证明这很困难,但可以做到。您必须注入 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 函数准备好启动保存的成功处理程序。

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

        【讨论】:

        • 这拯救了我的一天:)
        • 嗨...你能提供GitHub的例子吗?
        • @PanMluvčí 抱歉,现在我没有时间发布完整的解决方案。不过我可以回答具体问题。
        • 嗨@AlexanderVasenin,你能提供一个详细的答案或GitHub的例子表示赞赏
        • @SamCrawford 不知道。我为几年前结束的项目做了这个解决方案。
        猜你喜欢
        • 1970-01-01
        • 2021-08-18
        • 2013-03-28
        • 2014-10-04
        • 1970-01-01
        • 2016-06-02
        • 2022-08-09
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多