Geofencing
GPS-driven geofence monitoring for iOS and Android. Built on Shiny.Locations for background GPS and Shiny.Spatial for spatial queries.
Why Spatial Geofencing?
Section titled “Why Spatial Geofencing?”Traditional platform geofencing is limited to 20 regions on iOS and 60 on Android. Spatial geofencing removes that limit entirely — point the monitor at one or more spatial database tables containing city, state, or province polygons and it detects region enter/exit automatically using the R*Tree index.
| Platform Geofencing | Spatial Geofencing | |
|---|---|---|
| Max regions | 20 (iOS) / 60 (Android) | Unlimited |
| Region shapes | Circles only | Any polygon (with holes) |
| Data source | Register individually | Spatial database tables |
| Detection | OS-level callbacks | GPS + R*Tree spatial query |
| Battery | Very efficient (OS-managed) | Configurable GPS intervals |
-
Install the NuGet package
Terminal window dotnet add package Shiny.Spatial.Geofencing -
Implement the delegate
public class MyGeofenceDelegate : ISpatialGeofenceDelegate{public Task OnRegionChanged(SpatialRegionChange change){var name = change.Region.Properties.GetValueOrDefault("name") ?? "Unknown";var action = change.Entered ? "Entered" : "Exited";Console.WriteLine($"{action}: {name}");return Task.CompletedTask;}} -
Register in
MauiProgram.csbuilder.Services.AddSpatialGps<MyGeofenceDelegate>(config =>{config.MinimumDistance = Distance.FromMeters(300);config.MinimumTime = TimeSpan.FromMinutes(1);config.Add(CopyAssetToAppData("us-states.db"), "states").Add(CopyAssetToAppData("us-cities.db"), "cities");}); -
Start monitoring
// Inject ISpatialGeofenceManagerawait geofences.RequestAccess();await geofences.Start();
Supported Platforms
Section titled “Supported Platforms”| Framework | Notes |
|---|---|
net10.0-ios | iOS 15+ with background GPS |
net10.0-android | Android 5+ with background GPS |
Bundling Databases
Section titled “Bundling Databases”Add() requires a file path on disk. For databases bundled as MAUI raw assets (Resources/Raw), copy the file to AppDataDirectory first — SQLite cannot open files directly from the app package.
builder.Services.AddSpatialGps<MyGeofenceDelegate>(config => config .Add(CopyAssetToAppData("ca-cities.db"), "cities") .Add(CopyAssetToAppData("us-states.db"), "states"));
static string CopyAssetToAppData(string assetFileName){ var destPath = Path.Combine(FileSystem.AppDataDirectory, assetFileName); if (!File.Exists(destPath)) { using var source = FileSystem.OpenAppPackageFileAsync(assetFileName) .GetAwaiter().GetResult(); using var dest = File.Create(destPath); source.CopyTo(dest); } return destPath;}API Reference
Section titled “API Reference”ISpatialGeofenceManager
Section titled “ISpatialGeofenceManager”The main interface for controlling geofence monitoring. Inject it into your pages or view models.
public interface ISpatialGeofenceManager{ bool IsStarted { get; } Task<AccessState> RequestAccess(); Task Start(); Task Stop(); Task<IReadOnlyList<SpatialCurrentRegion>> GetCurrent(CancellationToken cancelToken = default);}| Method | Description |
|---|---|
IsStarted | Whether geofence monitoring is active |
RequestAccess() | Requests GPS permissions from the user |
Start() | Begins background GPS monitoring and region detection |
Stop() | Stops monitoring |
GetCurrent() | Gets the current GPS position and queries all monitored tables to determine which region(s) the device is in |
ISpatialGeofenceDelegate
Section titled “ISpatialGeofenceDelegate”Implement this interface to receive geofence enter/exit events.
public interface ISpatialGeofenceDelegate{ Task OnRegionChanged(SpatialRegionChange change);}SpatialRegionChange
Section titled “SpatialRegionChange”Event data for geofence transitions. Each event represents entering or exiting a single region.
public record SpatialRegionChange( string TableName, SpatialFeature Region, bool Entered);| Property | Description |
|---|---|
TableName | The spatial table that was matched |
Region | The SpatialFeature being entered or exited |
Entered | true for entry, false for exit |
SpatialCurrentRegion
Section titled “SpatialCurrentRegion”Returned by ISpatialGeofenceManager.GetCurrent(). One entry per monitored table.
public record SpatialCurrentRegion(string TableName, SpatialFeature? Region);Region is null when the device is not inside any feature in that table.
SpatialMonitorConfig
Section titled “SpatialMonitorConfig”Configuration for which databases and tables to monitor.
public class SpatialMonitorConfig{ public List<SpatialMonitorEntry> Entries { get; } public Distance? MinimumDistance { get; set; } // default: 300m public TimeSpan? MinimumTime { get; set; } // default: 1 minute public SpatialMonitorConfig Add(string databasePath, string tableName);}| Property | Default | Description |
|---|---|---|
MinimumDistance | 300 meters | Minimum distance the device must move before a new GPS reading is processed |
MinimumTime | 1 minute | Minimum time between GPS readings |
Entries | Empty | List of database/table pairs to monitor |
Usage Examples
Section titled “Usage Examples”Delegate with notifications
Section titled “Delegate with notifications”public class MyGeofenceDelegate( ILogger<MyGeofenceDelegate> logger, INotificationManager notifications) : ISpatialGeofenceDelegate{ public async Task OnRegionChanged(SpatialRegionChange change) { var regionName = change.Region.Properties.GetValueOrDefault("name") ?? "Unknown"; var action = change.Entered ? "Entered" : "Exited";
logger.LogInformation("{Action} {Region} in {Table}", action, regionName, change.TableName); await notifications.Send("Geofence", $"{action}: {regionName}"); }}ViewModel with start/stop
Section titled “ViewModel with start/stop”public class GeofenceViewModel(ISpatialGeofenceManager geofences){ public bool IsMonitoring => geofences.IsStarted;
public async Task ToggleMonitoring() { if (geofences.IsStarted) { await geofences.Stop(); } else { await geofences.RequestAccess(); await geofences.Start(); } }
public async Task CheckCurrentRegions() { var regions = await geofences.GetCurrent(); foreach (var r in regions) { var name = r.Region?.Properties.GetValueOrDefault("name") ?? "None"; Console.WriteLine($"{r.TableName}: {name}"); } }}Multi-table monitoring
Section titled “Multi-table monitoring”Monitor both state and city boundaries simultaneously:
builder.Services.AddSpatialGps<MyGeofenceDelegate>(config => config .Add(CopyAssetToAppData("us-states.db"), "states") .Add(CopyAssetToAppData("us-cities.db"), "cities"));Your delegate receives separate events for each table. For example, driving into Denver produces:
TableName = "states",Region = Colorado,Entered = trueTableName = "cities",Region = Denver,Entered = true
How It Works
Section titled “How It Works”Under the hood, SpatialGpsDelegate listens to GPS readings via Shiny.Locations and runs a spatial intersection query against each monitored table on every reading:
- A GPS reading arrives with the device’s current latitude/longitude
- For each monitored table, the delegate queries
table.Query().Intersecting(point).FirstOrDefault() - If the matched region differs from the previous reading, enter/exit events fire
- The delegate compares by feature ID, so moving within the same region produces no events
The spatial query uses the two-pass pipeline — the R*Tree index eliminates 99%+ of candidates before any geometry computation, keeping each GPS reading efficient even against large databases.