I still remember standing on the sidewalk outside a client’s office, watching the beta testers drive around the block for the twentieth time.
We had built a sleek Turbo Native app for a property management company. The web views were fast, the native navigation was smooth, and everyone was happy—until the product manager asked the inevitable question: “Can we automatically check in a technician when they arrive at a job site?”
My stomach dropped. I knew what this meant. We were about to step out of the cozy world of web views and into the wild, unpredictable wilderness of Core Location, geofencing, and background execution. And we had to make it work inside a Turbo Native wrapper—a hybrid app that was, at its heart, a web app pretending to be native.
What followed was a journey of frustration, late‑night debugging sessions, and eventually, a breakthrough that felt less like engineering and more like alchemy. This is the story of how we brought geofencing into Turbo Native—and how I learned that working with location is less about writing code and more about respecting the invisible boundaries of the physical world.
The Hybrid Trap
Turbo Native (formerly Turbo Native for iOS) is a gift to full‑stack developers. It lets you wrap your Rails web app in a native shell, giving you native navigation, push notifications, and a few other perks, while keeping the bulk of your UI in the familiar territory of HTML, CSS, and JavaScript.
But geofencing? That’s a different beast. The web has the Geolocation API, which works well enough for a one‑time “where am I” query. But for monitoring regions in the background—detecting when a user enters or leaves a predefined area—you need the full power of Core Location on iOS. And that lives in the native layer, not in the web view.
We had to figure out how to let the native side do the heavy lifting of monitoring, and then communicate those events to the JavaScript side so our Rails‑powered views could react. It was like teaching two musicians to play the same piece without a conductor.
The Art of Bridging
If you’ve worked with Turbo Native, you know that the bridge between Swift and JavaScript is usually the TurboSession and message handlers. You can inject a JavaScript interface into the web view, or use WKWebView’s postMessage mechanism. We chose the latter because it felt cleaner: the native side sends events to the web view, and the web view listens with window.addEventListener.
Here’s a stripped‑down version of what our Swift side looked like:
import CoreLocation
import UIKit
import WebKit
class GeofencingManager: NSObject, CLLocationManagerDelegate {
private let locationManager = CLLocationManager()
private weak var webView: WKWebView?
func startMonitoring(webView: WKWebView) {
self.webView = webView
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
// Create a region (e.g., a job site)
let center = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
let region = CLCircularRegion(center: center,
radius: 100,
identifier: "JobSite_123")
region.notifyOnEntry = true
region.notifyOnExit = true
locationManager.startMonitoring(for: region)
}
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
sendEventToWebView(name: "didEnterRegion", payload: ["identifier": region.identifier])
}
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
sendEventToWebView(name: "didExitRegion", payload: ["identifier": region.identifier])
}
private func sendEventToWebView(name: String, payload: [String: Any]) {
guard let webView = webView else { return }
let script = """
window.dispatchEvent(new CustomEvent('\(name)', { detail: \(jsonString(payload)) }));
"""
webView.evaluateJavaScript(script, completionHandler: nil)
}
}
On the JavaScript side, we could listen like this:
window.addEventListener('didEnterRegion', (event) => {
const identifier = event.detail.identifier;
// Call Rails‑backed API or update the UI
Turbo.visit(`/job_sites/${identifier}/arrive`);
});
Simple, right? It was, until we realized that background execution, battery life, and user permissions would turn this elegant bridge into a minefield.
The Invisible Trade‑offs
Geofencing is not a “set it and forget it” feature. It’s a negotiation between your app’s needs and the operating system’s constraints.
Permission Dialogues – Asking for “Always” location permission is a delicate moment. If you get it wrong, users will tap “Allow While Using” and your geofencing will stop working as soon as the app goes to the background. We learned to present a clear, empathetic explanation before the system dialog appeared—using a native screen that explained why we needed to track them even when the app was closed. This single change increased “Always” acceptance from 30% to 85%.
Battery Life – Every geofence you monitor consumes power. The system batches region updates to save battery, but you still need to be smart. We limited the number of active regions to 20 (Apple’s recommended maximum) and aggressively removed regions for completed jobs. We also used the accuracy parameter to balance precision with power: a radius of 100 meters was enough for our use case, and it let iOS use cell tower triangulation instead of constant GPS.
Testing in the Real World – You can simulate geofencing in the simulator, but it’s a lie. The real world has trees, buildings, and spotty GPS. We had to physically drive to locations to test. I spent an entire afternoon walking around a construction site with a debug build, watching logs, and adjusting radius values. It felt absurd, but it was the only way to understand how the system behaved.
The Web View’s Blind Spots
One of the hardest lessons came when we realized that the web view—our precious Turbo Native shell—has no knowledge of the native app’s lifecycle. If the user killed the app, our CLLocationManager would stop monitoring. When the app restarted, we had to re‑register all the regions. That meant persisting the list of active regions (we stored them in the app’s UserDefaults) and re‑starting monitoring on every launch.
We also had to handle the case where the app was launched in the background due to a geofence event. In that scenario, there’s no visible web view. We needed to perform a silent sync with the server and optionally show a local notification to alert the user. That meant adding a push notification layer (or using UNUserNotificationCenter) to communicate with the user when the app was in the background.
The Artistic Mindset
After weeks of wrestling with Core Location, I realized that geofencing is less like programming and more like painting with invisible ink. You define boundaries that no one sees, and you trust that the system will whisper to your app when a user crosses them. But the medium is messy: GPS drift, battery‑saving throttling, and user permissions can all blur the lines.
The art lies in setting expectations. We built a simple UI in the web view that showed the status of geofencing—whether it was enabled, how many active regions there were, and a history of recent events. This transparency helped users understand why the app was behaving the way it was. When a technician arrived at a site but didn’t get an immediate check‑in, they knew it was because the system was waiting for a stable GPS fix, not because the app was broken.
The Moment It Clicked
The breakthrough came during a user acceptance test. We sat in a van with a technician who was skeptical of the whole idea. He drove toward a job site, and as he pulled into the driveway, the app chimed and automatically opened the work order. His eyes widened. “It just knows,” he said.
That moment made all the complexity worthwhile. Geofencing, when done right, creates magic—a sense that the app is anticipating the user’s needs. And in a Turbo Native world, where most of the app is just a web view, that sprinkle of native magic can be the difference between a forgettable hybrid app and a beloved tool.
Lessons for Senior Full‑Stack Developers
If you’re embarking on this journey, here’s what I wish someone had told me before I started:
- Respect the user’s privacy. Ask for “Always” permission only after explaining why. Give them a way to turn it off in settings. Build trust.
- Test on real devices. The simulator is useful for logic, but the real world is where geofencing lives or dies. Walk, drive, and use Xcode’s debug location simulation to approximate real conditions.
- Embrace async. Geofencing events are asynchronous and can happen when your web view isn’t even loaded. Design your JavaScript to be resilient: use an event queue if the page isn’t ready, and replay events when the view appears.
- Monitor your own app. Add logging (with user consent) to see how often regions trigger. You’ll discover that users don’t always drive exactly through the center of your circles—adjust your radii based on real data.
- Know your limits. iOS limits the number of monitored regions per app (currently 20). Design your system to activate and deactivate regions dynamically based on the user’s current location or time of day.
The Art of the Invisible
Geofencing in a Turbo Native app is ultimately about bridging two worlds: the web’s flexibility and the platform’s intimate awareness of the physical world. It’s a reminder that the best hybrid apps aren’t just web apps wrapped in native shells—they’re conversations between the two layers, each contributing what it does best.
Our technicians now start their day with a list of jobs, and the app quietly monitors their location. When they arrive, they don’t have to tap anything. The app knows. It feels like a sixth sense, and it’s become the feature that users rave about.
As senior developers, we often obsess over architecture patterns and performance metrics. But sometimes, the most rewarding work is the kind that disappears into the background—making the app feel less like software and more like an extension of the real world.
That’s the art. That’s the journey.
Top comments (0)