JavaScript to Swift and back: Bridging location services in WKWebView

14 minutes

I recently stumbled upon an interesting issue while developing an app that included a WKWebView attempting to use location services. Initially, when this web view was displayed, a normal iOS location permission pop-up appeared—nothing unusual there. However, I was surprised to see that after allowing the app to access location services, a second pop-up immediately appeared, requesting permission to access location services on a specific webpage. Even worse, this second alert would persist even after terminating and reopening the app, resulting in a poor user experience.

As I couldn’t find a way to handle this through any of the WKWebView delegates, it became clear that I needed to intercept the location service requests from the browser, handle them natively within iOS, and then pass control back to the browser once the location-related work was complete. Initially, this idea seemed daunting, but it turned out to be more straightforward than I anticipated.

Understanding the Challenge

Setup: macOS Sonoma 14.6.1 | Xcode 15.4 | Swift 5.10

Let’s recreate the issue by setting up a WKWebView that points to the Geolocation API from browserleaks.com—a set of tools that offers a range of tests to evaluate the security and privacy of a web browser:

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

struct WebView: UIViewRepresentable {

    func makeUIView(context: Context) -> some UIView {
        let webpage = URL(string: "https://browserleaks.com/geo")!
        let view = WKWebView(frame: .zero, configuration: .init())
        view.load(.init(url: webpage))
        return view
    }

    func updateUIView(_ uiView: UIViewType, context: Context) { }

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
}

extension WebView {

    class Coordinator {
        let parent: WebView

        init(_ parent: WebView) {
            self.parent = parent
        }
    }
}

We’ll also need to add NSLocationWhenInUseUsageDescription to our app’s Info.plist to allow user location access:

NSLocationWhenInUseUsageDescription = "This app requires access to your location to enhance your experience by providing location-based features while you are using the app.";

Now, if we use the app as is, we quickly see the issue. Two permission pop-ups are shown: first, the one for the app in general, and then the one for the webpage. Even after granting all permissions, restarting the app triggers the webpage location permission request again:

Double location permission prompts disrupt user flow

Crafting the Solution

Let’s begin by understanding how JavaScript typically handles user location access. JavaScript accesses the user’s location using the Geolocation API, with the most important methods being:

  • getCurrentPosition(success, error, options) for obtaining a single, last known location,
  • watchPosition(success, error, options) for continuously updating the location,
  • clearWatch(id) for stopping location updates

We’ll need to override these methods. Begin by creating a new empty file in your project named JavaScriptInjection.js that will contain our JavaScript code. For now, leave it blank. Next, in XCode, select your project from the left sidebar, then choose your app name under the project. Go to the “Build Phases” tab, and ensure that this file is added as a resource in the “Copy Bundle Resources” section. This is necessary because we’ll be reading its contents later on.

Now, let’s set up the models. We’ll start by creating a Message struct, which we’ll use to decode the raw JavaScript messages (in JSON format) that will be sent to the Swift side.

import Foundation

struct Message: Codable, Sendable {

    let id: Double
    let command: Command
    let options: Options?
}

extension Message {

    enum Command: String, Codable {
        case getCurrentPosition, watchPosition, clearWatch
    }
}

extension Message {

    struct Options: Codable {

        /// The maximum age in milliseconds of a cached position that is acceptable to return.
        /// If the cached position is older than the specified `maximumAge`, the location will be re-fetched.
        /// A value of 0 indicates that no cached position should be used and a new position should be obtained.
        /// If not provided, there is no limit on the maximum age of cached position.
        let maximumAge: Double?

        /// The maximum time in milliseconds allowed for a location fetch operation to complete.
        /// If the timeout is reached before a position is found, the operation fails.
        /// If not provided, the location fetch can take an indefinite amount of time.
        let timeout: Double?

        /// A Boolean value indicating whether the location service should use the most accurate method
        /// available to determine the position of the device.
        /// Enabling high accuracy may use more battery power and take longer, but provides more precise results.
        /// Defaults to `false` if not provided.
        let enableHighAccuracy: Bool?
    }
}
  • The id is an integer because the Geolocation API operates this way:
    • watchPosition is expected to return an integer, which is then used in
    • clearWatch to stop updating the location,
  • command represents the function being called, which in our case will be one of the three mentioned above
  • options is a direct mapping of the parameters used in the JS functions. These may or may not be present, depending on the situation

Let’s also add an error enum here, to handle any potential issues. This enum will be a direct mapping from the GeolocationPositionError:

import Foundation

enum LocationServicesError: Int, Error {
    /// The acquisition of the geolocation information failed because the page didn't have the necessary permissions, for example because it is blocked by a Permissions Policy
    case denied = 1

    /// The acquisition of the geolocation failed because at least one internal source of position returned an internal error.
    case unavailable

    /// The time allowed to acquire the geolocation was reached before the information was obtained.
    case timeout
}

Based on our review of the Geolocation API documentation and our understanding of how location services are managed in iOS, let’s create an interface for dependency injection into the WebView. This interface will enable us to invoke functions from the WKWebView after receiving and decoding the messages we’ve defined. Here’s what we need:

import CoreLocation

protocol LocationServicesBridge: Actor {

    /// Before calling `getCurrentPosition` or `watchPosition`,
    /// we must first check if the necessary permissions are granted.
    /// If permissions are not granted, we'll need to notify the JavaScript side.
    /// If permissions are in place, we can proceed by either retrieving a single
    /// location or starting continuous location updates.
    func checkPermission() async throws

    /// Get a single, last known location
    /// - Parameters:
    ///   - options: An instance of `Message.Options` that specifies options such as maximum age, timeout, and accuracy for the location updates.
    /// - Returns: The most recent location available, or `nil` if no location data is available.
    func location(
        options: Message.Options?
    ) -> CLLocation?

    /// Begin continuous location updates using the specified ID
    /// - Parameters:
    ///   - id: The unique identifier for the location update session.
    ///   - options: An instance of `Message.Options` that specifies options such as maximum age, timeout, and accuracy for the location updates.
    /// - Returns: An asynchronous stream of `CLLocation` objects representing the device’s location over time.
    func startUpdatingLocation(
        id: Double,
        options: Message.Options?
    ) -> AsyncStream<CLLocation>

    /// Stop continuous location updates associated with the specified ID
    /// - Parameter id: The unique identifier for the location update session to be stopped.
    func stopUpdatingLocation(id: Double)
}

In your app, this is an opportunity to implement an interface and extend your own location service provider by adding the necessary methods. However, for demo purposes, I’ve provided a default implementation below. It’s a straightforward example that uses CLLocationManager under the hood to manage location services, handling permissions, starting and stopping location updates, and responding to location-related events as required.

You can ignore the following code; it’s just here so you can quickly copy and paste it to try it out. There’s nothing special going on, even though it seems like a lot! 😅

import CoreLocation
import Foundation

// Define an actor class that handles location services and acts as a bridge to your application's logic.
final actor LocationServicesManager: NSObject,
        LocationServicesBridge,
        Observable,
        CLLocationManagerDelegate {

    // MARK: - Properties

    // The CLLocationManager instance responsible for managing location services.
    private let locationManager: CLLocationManager

    // Continuation for asynchronously streaming location updates.
    private var continuations: [Double: AsyncStream<CLLocation>.Continuation?] = [:]

    // Continuation for handling permission requests asynchronously.
    private var authorizationContinuation: CheckedContinuation<Void, Error>?

    // Keep track of requests
    private var requests = Set<Double>()

    // MARK: - Initializer

    /// Initializes a new instance of DefaultLocationServicesBridge.
    /// - Parameter locationManager: A custom CLLocationManager instance, defaulting to a new instance if not provided.
    public init(
        locationManager: CLLocationManager = .init()
    ) {
        self.locationManager = locationManager
        super.init()

        // Set the location manager's delegate to self.
        self.locationManager.delegate = self
    }

    // MARK: - LocationServicesBridge Protocol Conformance

    /// Checks and requests location permissions as needed.
    /// Throws an error if the user denies or restricts access to location services.
    public func checkPermission() async throws {
        switch locationManager.authorizationStatus {
        case .notDetermined:
            // If permission status is not determined, request permission asynchronously.
            try await withCheckedThrowingContinuation { continuation in
                Task {
                    askForPermission(authorizationContinuation: continuation)
                }
            }

        case .restricted, .denied:
            // If permission is restricted or denied, throw an error.
            throw LocationServicesError.denied

        case .authorizedAlways, .authorizedWhenInUse:
            // If permission is granted, do nothing and proceed.
            break

        @unknown default:
            // Handle any future cases that might be added to the authorization status enum.
            break
        }
    }

    /// Retrieves the most recent known location.
    /// - Parameters:
    ///   - options: An instance of `Message.Options` that specifies options such as maximum age, timeout, and accuracy for the location updates.
    /// - Returns: The last known location, or `nil` if no location is available.
    func location(options _: Message.Options?) -> CLLocation? {
        // TODO: Start the service and return the most recent location. For now, this should suffice:
        return locationManager.location
    }

    /// Begins streaming continuous location updates.
    /// - Parameters:
    ///   - id: A unique identifier for the location update session
    ///   - options: An instance of `Message.Options` that specifies options such as maximum age, timeout, and accuracy for the location updates.
    /// - Returns: An `AsyncStream` that yields `CLLocation` objects as the device's location updates.
    func startUpdatingLocation(id: Double, options _: Message.Options?) -> AsyncStream<CLLocation> {
        let stream = AsyncStream<CLLocation>.makeStream()
        continuations[id] = stream.continuation
        locationManager.startUpdatingLocation()
        requests.insert(id)
        return stream.stream
    }

    /// Stops the continuous location updates associated with the specified session ID.
    /// - Parameter id: The unique identifier for the location update session to stop.
    func stopUpdatingLocation(id: Double) {
        continuations[id]??.finish()
        continuations[id] = nil
        requests.remove(id)
        if requests.isEmpty {
            locationManager.stopUpdatingLocation()
        }
    }

    // MARK: - Permission Request Helper

    /// Requests location permission from the user.
    /// - Parameter authorizationContinuation: A continuation that will be resumed once the user responds to the permission request.
    func askForPermission(authorizationContinuation: CheckedContinuation<Void, Error>) {
        self.authorizationContinuation = authorizationContinuation
        Task { @MainActor in
            locationManager.requestWhenInUseAuthorization()
        }
    }

    // MARK: - Location Handling

    /// Sends the most recent location update to the continuation.
    /// - Parameter location: The new location to send.
    func send(location: CLLocation) {
        print("Location did update", location)
        continuations.forEach { $0.value?.yield(location) }
    }

    /// Handles the result of a location permission request.
    /// - Parameter status: The authorization status returned by the system.
    func proceed(withAuthorization status: CLAuthorizationStatus) {
        defer { authorizationContinuation = nil }

        switch status {
        case .authorizedWhenInUse, .authorizedAlways:
            // If permission is granted, resume the continuation without error.
            authorizationContinuation?.resume()
            return

        case .notDetermined, .denied, .restricted:
            // If permission is denied or restricted, throw an error.
            authorizationContinuation?.resume(throwing: LocationServicesError.denied)
            return

        @unknown default:
            // Handle any future cases that might be added to the authorization status enum.
            return
        }
    }

    // MARK: - CLLocationManagerDelegate Conformance

    /// Delegate method that is called when the location manager receives new location data.
    /// - Parameter manager: The CLLocationManager object that generated the event.
    /// - Parameter locations: An array of CLLocation objects containing the location data.
    nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        Task { await send(location: location) }
    }

    /// Delegate method that is called when the authorization status for the app changes.
    /// - Parameter manager: The CLLocationManager object that generated the event.
    nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        let status = manager.authorizationStatus
        guard status != .notDetermined else { return }
        Task { await proceed(withAuthorization: status) }
    }
}

Next, we’ll need to create an object that conforms to WKScriptMessageHandler. Its primary responsibility is to implement the func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) method. This method is called by the browser whenever a message is sent from the JavaScript side to Swift. We’ll explore the triggering mechanism shortly, but the main focus here is to decode the incoming JSON message into our model and then respond accordingly:

import Foundation
import CoreLocation
import WebKit

actor JavaScriptLocationAdapter: NSObject, WKScriptMessageHandler {

    private weak var webView: WKWebView?
    private weak var bridge: LocationServicesBridge?

    func bind(webView: WKWebView, to bridge: LocationServicesBridge?) {
        self.webView = webView
        self.bridge = bridge
    }

    // MARK: - Confirming to `WKScriptMessageHandler`

    nonisolated func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        print(message.body)
        
        guard
            let payload = message.body as? String,
            let data = payload.data(using: .utf8),
            let message = try? JSONDecoder().decode(Message.self, from: data)
        else {
            assertionFailure("Failed to decode a message")
            return
        }

        Task { await _handle(message: message) }
    }

    // MARK: - Helpers

    private func _handle(message: Message) async {
        // More to come...
    }
}

Note that we use a weak reference to the WKWebView to enable calling back to the JavaScript side when needed. Similarly, we maintain a weak reference to the LocationServicesBridge to handle requests from JavaScript for location data.

First, let’s add some private helper methods to send data back to the JavaScript side. These methods use the evaluateJavaScript(_) function on the WKWebView instance to execute JavaScript code (as you’ll notice, we’re calling some JavaScript functions that haven’t been defined yet. We’ll define these functions in the final step when we implement the JavaScriptInjection.js file):

// ...

// MARK: - Making calls back to JS

@MainActor
private func _update(location: CLLocation, forId id: Double) async {
    let coordinate = location.coordinate
    let lat = coordinate.latitude
    let lon = coordinate.longitude
    let alt = location.altitude
    let ha = location.horizontalAccuracy
    let va = location.verticalAccuracy
    let hd = location.course
    let spd = location.speed

    _ = try? await webView?.evaluateJavaScript(
        "navigator.geolocation.iosWatchLocationDidUpdate(\(id),\(lat),\(lon),\(alt),\(ha),\(va),\(hd),\(spd))"
    )
}

@MainActor
private func _updateFailed(id: Double, error: LocationServicesError) async {
    _ = try? await webView?.evaluateJavaScript(
        "navigator.geolocation.iosWatchLocationDidFailed(\(id),\(error.rawValue))"
    )
}

@MainActor
private func _send(id: Double, location: CLLocation) async {
    let coordinate = location.coordinate
    let lat = coordinate.latitude
    let lon = coordinate.longitude
    let alt = location.altitude
    let ha = location.horizontalAccuracy
    let va = location.verticalAccuracy
    let hd = location.course
    let spd = location.speed

    _ = try? await webView?.evaluateJavaScript(
        "navigator.geolocation.iosLastLocation(\(id),\(lat),\(lon),\(alt),\(ha),\(va),\(hd),\(spd))"
    )
}

@MainActor
private func _sendFailed(id: Double, error: LocationServicesError) async {
    _ = try? await webView?.evaluateJavaScript(
        "navigator.geolocation.iosLastLocationFailed(\(id),\(error.rawValue))"
    )
}

// ...

Now let’s extend the function where we handle a received message:

// ...

// MARK: - Helpers

private func _handle(message: Message) async {
    switch message.command {
    case .getCurrentPosition:
        do {
            try await bridge?.checkPermission()
        } catch let error as LocationServicesError {
            await _sendFailed(id: message.id, error: error)
            return
        } catch {
            await _sendFailed(id: message.id, error: .denied)
            return
        }

        guard let location = await bridge?.location(options: message.options) else {
            await _sendFailed(id: message.id, error: .unavailable)
            return
        }

        await _send(id: message.id, location: location)

    case .watchPosition:
        do {
            try await bridge?.checkPermission()
        } catch let error as LocationServicesError {
            await _updateFailed(id: message.id, error: error)
            return
        } catch {
            await _updateFailed(id: message.id, error: .denied)
            return
        }

        guard let stream = await bridge?.startUpdatingLocation(
            id: message.id,
            options: message.options
        ) else {
            await _updateFailed(id: message.id, error: .unavailable);
            return
        }

        for await location in stream {
            await _update(location: location, forId: message.id)
        }

    case .clearWatch:
        await bridge?.stopUpdatingLocation(id: message.id)
    }
}

// ...

This method processes incoming commands from JavaScript related to geolocation services. It checks for the necessary location permissions and, based on the command received, either retrieves the current location, starts continuous location updates, or stops ongoing updates. The method ensures proper error handling and sends the appropriate responses or updates back to the JavaScript side (by using evaluateJavaScript(_)).

Let’s bring everything together in our WKWebView. Our goal is to enable sending and receiving messages between the web view and the app. This is accomplished using the WKUserContentController, an object that manages interactions between JavaScript and the web view. It is used both to inject JavaScript code into webpages and to install custom functions that can be called from within the app. Let’s inject our JavaScript code to override the necessary functions, and also set up listeners for when specific functions are called. The key point here is to remember the name you assign when adding a WKScriptMessageHandler. We’ll need to reference that later. Here’s how the updated WebView looks now:

import WebKit

struct WebView: UIViewRepresentable {

    // 1
    let locationServicesBridge: LocationServicesBridge

    func makeUIView(context: Context) -> some UIView {
        let webpage = URL(string: "https://browserleaks.com/geo")!
        let userController = WKUserContentController()

        // 2
        let jsURL = Bundle.main.url(forResource: "JavaScriptInjection", withExtension: "js")!
        let jsContent = try! String(contentsOf: jsURL, encoding: .utf8)
        userController.addUserScript(.init(source: jsContent, injectionTime: .atDocumentStart, forMainFrameOnly: true))

        // 3
        let jsLocationServices = JavaScriptLocationAdapter()
        userController.add(
            jsLocationServices,
            name: "locationHandler"
        )

        // 4
        let config = WKWebViewConfiguration()
        config.userContentController = userController

        let view = WKWebView(frame: .zero, configuration: config)
        view.load(.init(url: webpage))

        Task {
            // 5
            await jsLocationServices.bind(
                webView: view,
                to: locationServicesBridge
            )
        }

        return view
    }

    func updateUIView(_ uiView: UIViewType, context: Context) { }

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
}

extension WebView {
    ...
}
  1. The locationServicesBridge is injected into the WebView struct to provide access to location services,
  2. The JavaScript file JavaScriptInjection.js is loaded and injected into the webpage at the start of document loading. In the final step, we’ll override the Geolocation methods and add our custom implementations here. For now, the file remains empty,
  3. The JavaScriptLocationAdapter is set up as a message handler to allow communication between JavaScript in the WebView and Swift. This allows the JavaScript running in the WebView to communicate with Swift by sending messages to locationHandler (the name we chose), which will be handled by the JavaScriptLocationAdapter,
  4. A WKWebViewConfiguration is created and customized with the user content controller, then used to initialize the WebView,
  5. The WebView is bound to the location services bridge, enabling interaction between the WebView and native location services

Update the main app entry point, where a default implementation of LocationServicesBridge is injected.

struct ContentView: View {

    @State private var locationServicesBridge = LocationServicesManager()

    var body: some View {
        WebView(
            locationServicesBridge: locationServicesBridge
        )
    }
}

The final step is implementing the JavaScript. To effectively communicate between the web view and the iOS environment, follow these steps:

  • from WebView to iOS: use window.webkit.messageHandlers.<messageHandlerName>.postMessage(...) in your JavaScript. Ensure <messageHandlerName> matches the one specified in the WebView (e.g., “locationHandler”). This sends a message from the WebView to the native iOS code,
  • from iOS to WebView: use the evaluateJavaScript method on the WKWebView instance in your iOS code to execute JavaScript within the WebView
//
// Section 1.: Helper variables
//

let idCounter = 0; // ID counter for unique identification ~ JavaScript is single-threaded
const locationWatchCallbacks = new Map();
const locationCallbacks = new Map();

//
// Section 2.: Helpers
//

function createPayload(name, id, options) {
    return JSON.stringify({
        id: id,
        command: name,
        options: options
    });
}

//
// Section 3.: Overrides
//

// https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition
navigator.geolocation.getCurrentPosition = function(success, error, options) {
    const id = idCounter++;  // Generate a unique ID using the counter
    locationCallbacks.set(id, { success, error }); // Store the callbacks using the id
    const payload = createPayload("getCurrentPosition", id, options); // Payload dispatched to iOS
    window.webkit.messageHandlers.locationHandler.postMessage(payload); // Send command
};

// https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition
navigator.geolocation.watchPosition = function(success, error, options) {
    const id = idCounter++;  // Generate a unique ID using the counter
    locationWatchCallbacks.set(id, { success, error }); // Store the callbacks using the id
    const payload = createPayload("watchPosition", id, options); // Payload dispatched to iOS
    window.webkit.messageHandlers.locationHandler.postMessage(payload); // Send command
    return id;
};

// https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/clearWatch
navigator.geolocation.clearWatch = function(id) {
    locationWatchCallbacks.delete(id); // Remove the callbacks using the id
    const payload = createPayload("clearWatch", id, null); // Payload dispatched to iOS
    window.webkit.messageHandlers.locationHandler.postMessage(payload); // Send command
};

This code modifies the default behavior of the navigator.geolocation methods in JavaScript to integrate with a native iOS app via WKWebView. It starts by defining helper variables:

  • idCounter is a number we’ll use to generate unique IDs. Using an ID counter for callbacks is safe in our case because JavaScript is single-threaded, ensuring that operations like incrementing the counter and handling IDs happen sequentially without the risk of concurrent modifications. However, you could also use timestamps in milliseconds or other more advanced generators if needed,
  • locationWatchCallbacks is a map that stores the success and error callbacks for each watchPosition call,
  • locationCallbacks is a map that holds the callbacks for getCurrentPosition calls, and
  • A helper function, createPayload, which formats the command, ID, and options into a JSON string, which is then sent as the message body to the iOS app.

The code then overrides the standard getCurrentPosition, watchPosition, and clearWatch methods from the Geolocation API. In the getCurrentPosition override, the success and error callbacks are stored, and a message is then sent to the iOS app using the locationHandler in window.webkit.messageHandlers to request the location. The watchPosition override works similarly. The generated ID is used to store the callbacks in locationWatchCallbacks, allowing the app to manage continuous location tracking.

Finally, the clearWatch method stops a specific watch session by deleting the corresponding callbacks and notifying the iOS app to stop location updates.

As the final step, add the JavaScript functions that will be invoked from the JavaScriptLocationAdapter.swift file. These functions will be called using the evaluateJavaScript(_) method to pass control back to the JavaScript side and trigger the completion blocks. They will handle the results of location requests, updating the corresponding success or error callbacks stored earlier:

...

//
// Section 4.: Call from Swift
//

// Section 4.1: Callbacks used when `watchPosition` request is processed in iOS world

navigator.geolocation.iosWatchLocationDidUpdate = function(id, lat, lon, alt, ha, va, hd, spd) {
    const callbacks = locationWatchCallbacks.get(id);
    if (callbacks) {
        const { success } = callbacks;
        success({
            coords: {
                latitude: lat,
                longitude: lon,
                altitude: alt,
                accuracy: ha,
                altitudeAccuracy: va,
                heading: hd,
                speed: spd
            }
        });
    }

    return null;
}

navigator.geolocation.iosWatchLocationDidFailed = function(id, code) {
    const callbacks = locationWatchCallbacks.get(id);
    if (callbacks) {
        const { error } = callbacks;
        error({
            code: code,
            message: null
        });
    }

    locationWatchCallbacks.delete(id); // Remove the callback after it's used
    return null;
}

// Section 4.2: Callbacks used when `getCurrentPosition` request is processed in iOS world

navigator.geolocation.iosLastLocation = function(id, lat, lon, alt, ha, va, hd, spd) {
    const callbacks = locationCallbacks.get(id);
    if (callbacks) {
        const { success } = callbacks;
        success({
            coords: {
                latitude: lat,
                longitude: lon,
                altitude: alt,
                accuracy: ha,
                altitudeAccuracy: va,
                heading: hd,
                speed: spd
            }
        });

        locationCallbacks.delete(id); // Remove the callback after it's used
    }

    return null;
}

navigator.geolocation.iosLastLocationFailed = function(id, code) {
    const callbacks = locationCallbacks.get(id);
    if (callbacks) {
        const { error } = callbacks;
        error({
            code: code,
            message: null
        });
    }

    locationCallbacks.delete(id); // Remove the callback after it's used

    return null;
}
  • iosWatchLocationDidUpdate: This function is called whenever the location updates during a watch session. It triggers the success callback with the new location data, identified by the session ID,
  • iosWatchLocationDidFailed: This function is called when there is an error during a watch session. It triggers the error callback with an error message, using the session ID to identify the correct callback,
  • iosLastLocation: This function is called when a single location request is successfully completed. It triggers the success callback with the location data and then clears the stored callbacks,
  • iosLastLocationFailed: This function is called when a single location request fails. It triggers the error callback with an error message and clears the stored callbacks

Wrapping Up

That’s it! Uninstall the app to reset the permissions, and then reinstall it. When you try again, you’ll notice the result—no more double pop-ups!”

Streamlined Location Access for Better Usability

By intercepting and handling the location requests within the native iOS environment, we’ve successfully streamlined the user experience. This approach ensures that the app only requests location permission once, providing a smoother and more professional experience for the end-user.

This method not only resolves the issue but also opens up possibilities for handling other JavaScript-native interactions within your iOS apps. Experiment with this setup and see how it can be extended to other use cases.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *