Geofencing
Getting Started
Section titled “Getting Started”| GitHub | |
| Downloads |
Geofencing lets your app react when a user enters, exits, or stays in (dwells in) a geographic area — even when the app is not running. Unlike GPS tracking, geofencing is managed by the operating system and is highly battery efficient, making it ideal for location-triggered background tasks.
Platform Notes
Section titled “Platform Notes”| Feature | Android | iOS | Windows |
|---|---|---|---|
| Max Regions | 60 | 20 | Unlimited |
| Background Support | Yes | Yes | Yes |
| Google Play Fallback | GPS-direct when unavailable | N/A | N/A |
| Dwell | Native (loitering delay) | Calculated by Shiny - usually reported at exit | Timer in Shiny |
- MyApp/
1using Shiny.Locations;23namespace MyApp.Delegates;45public partial class MyGeofenceDelegate : IGeofenceDelegate6{7 public MyGeofenceDelegate()8 {9 }1011 public Task OnStatusChanged(GeofenceState newStatus, GeofenceRegion region)12 {13 throw new NotImplementedException();14 }15}1617#if ANDROID18public partial class MyGeofenceDelegate : IAndroidForegroundServiceDelegate19{20 public void Configure(AndroidX.Core.App.NotificationCompat.Builder builder)21 {22 23 }24}25#endifCreating Your Delegate
Section titled “Creating Your Delegate”Implement IGeofenceDelegate to handle geofence enter/exit events. This delegate fires in the background when a boundary is crossed.
public class MyGeofenceDelegate : IGeofenceDelegate{ readonly ILogger<MyGeofenceDelegate> logger;
// Full dependency injection support - treat as a singleton public MyGeofenceDelegate(ILogger<MyGeofenceDelegate> logger) { this.logger = logger; }
public async Task OnStatusChanged(GeofenceState newStatus, GeofenceRegion region) { if (newStatus == GeofenceState.Entered) { this.logger.LogInformation("Entered region: {Region}", region.Identifier); // send a notification, sync data, trigger an action, etc. } else if (newStatus == GeofenceState.Exited) { this.logger.LogInformation("Exited region: {Region}", region.Identifier); } else if (newStatus == GeofenceState.Dwelling) { // only for regions with a DwellTime - see "Dwell" below this.logger.LogInformation("Dwelling in region: {Region}", region.Identifier); } }}Using the Geofence Manager
Section titled “Using the Geofence Manager”Inject IGeofenceManager to manage geofence regions at runtime.
Requesting Permission
Section titled “Requesting Permission”Always request access before starting monitoring:
IGeofenceManager geofenceManager; // injected
var access = await geofenceManager.RequestAccess();if (access != AccessState.Available){ // Handle denied/restricted - show user messaging return;}Starting & Stopping Monitoring
Section titled “Starting & Stopping Monitoring”// Start monitoring a regionawait geofenceManager.StartMonitoring(new GeofenceRegion( "work", new Position(43.6532, -79.3832), // latitude, longitude Distance.FromMeters(200) // radius));
// Stop monitoring a specific regionawait geofenceManager.StopMonitoring("work");
// Stop monitoring all regionsawait geofenceManager.StopAllMonitoring();If you re-register the same regions on every launch, use TryStartMonitoring to avoid duplicates. It only starts monitoring when a region with that identifier isn’t already monitored, and — when replaceIfExists is true (the default) — stops and restarts an existing region so changed position or notification settings take effect. It returns true if the region already existed, false if it was newly added.
var region = new GeofenceRegion( "work", new Position(43.6532, -79.3832), Distance.FromMeters(200));
// Idempotent: safe to call repeatedly without duplicating or throwingbool alreadyExisted = await geofenceManager.TryStartMonitoring(region);
// Add without replacing an existing region of the same identifierawait geofenceManager.TryStartMonitoring(region, replaceIfExists: false);GeofenceRegion Options
Section titled “GeofenceRegion Options”var region = new GeofenceRegion( Identifier: "coffee-shop", Center: new Position(43.6532, -79.3832), Radius: Distance.FromMeters(100), SingleUse: false, // true = auto-remove after first trigger NotifyOnEntry: true, // fire delegate on enter NotifyOnExit: true // fire delegate on exit){ DwellTime = TimeSpan.FromMinutes(5) // optional - fire delegate after staying inside this long};| Parameter | Type | Default | Description |
|---|---|---|---|
Identifier |
string |
required | Unique ID for the region |
Center |
Position |
required | Latitude/longitude of the circle center |
Radius |
Distance |
required | Radius of the circular geofence |
SingleUse |
bool |
false |
Automatically stop monitoring after first trigger (after the dwell, when DwellTime is set) |
NotifyOnEntry |
bool |
true |
Fire delegate when entering the region |
NotifyOnExit |
bool |
true |
Fire delegate when exiting the region |
DwellTime |
TimeSpan? |
null |
Fire delegate with GeofenceState.Dwelling once the device has stayed inside this long. Set with an object initializer |
A dwell reports that the user has stayed in a region, not just passed through it. Set DwellTime and handle
GeofenceState.Dwelling in your delegate. It is independent of the entry/exit flags, so a region can report only the dwell:
await geofenceManager.StartMonitoring(new GeofenceRegion( "coffee-shop", new Position(43.6532, -79.3832), Distance.FromMeters(100), NotifyOnEntry: false, NotifyOnExit: false){ DwellTime = TimeSpan.FromMinutes(5)});Entry and exit are always tracked, whatever the flags say. The flags only decide what reaches your delegate. Leaving before the
dwell time is up means no Dwelling. On Android, iOS and GPS-direct, if the device is already inside when monitoring starts,
no Entered is reported, but that time counts toward the dwell.
No GPS or extra permission is involved. When Dwelling arrives differs by platform:
| Android | Windows | iOS | |
|---|---|---|---|
| Dwell source | Native loitering delay | Shiny timer | Shiny: calculated at exit, or a timer while the app runs |
Dwelling arrives |
While still inside | While still inside | Usually at exit, just before Exited |
For someone at the gym for 45 minutes with a 20-minute DwellTime:
| Time | Android | Windows | iOS |
|---|---|---|---|
| 0:00 walk in | Entered |
Entered |
Entered |
| 0:20 | Dwelling |
Dwelling |
nothing, unless the app is running |
| 0:45 walk out | Exited |
Exited |
Dwelling, then Exited |
Querying State
Section titled “Querying State”// Get all currently monitored regionsvar regions = geofenceManager.GetMonitorRegions();
// Request the current state of a region (inside or outside)var state = await geofenceManager.RequestState(region);// GeofenceState.Entered, GeofenceState.Exited, or GeofenceState.Unknown
// Check current permission status without promptingvar status = geofenceManager.CurrentStatus;Android without Google Play Services
Section titled “Android without Google Play Services”Geofencing on Android runs on Google Play Services’ GeofencingClient. The GPS-direct fallback (AddGpsDirectGeofencing) was
removed when geofencing moved into its own package, so devices without Google Play Services get no geofence events. If you need
region checks on those devices, run Shiny.Gps and test your regions against each reading with region.IsPositionInside(reading.Position).
How It Works
Section titled “How It Works”Shiny handles all the platform complexity behind a single API:
| Platform | Details |
|---|---|
| Android | Uses Google Play Services geofencing when available, falls back to GPS-direct otherwise |
| iOS 18+ | Uses the modern CLMonitor API |
| iOS < 18 | Uses the legacy CLLocationManager region monitoring API |
| Windows | Uses Windows.Devices.Geolocation.Geofencing |
Monitored regions are automatically persisted and restored on app restart — you don’t need to re-register them.
AI Coding Assistant
Section titled “AI Coding Assistant”Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skillsStep 2 — Install the plugin:
claude plugin install shiny@shinyOne plugin installs all 48 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.
Step 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skillsStep 2 — Install the plugin:
copilot plugin install shiny@shinyOne plugin installs all 48 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.


