Create a notification every time a given location data source has entered and/or exited a set of features or graphics.
Use case
Geotriggers can be used to notify users when they have entered or exited a geofence by monitoring a given set of features or graphics. They could be used to display contextual information to museum visitors about nearby exhibits, notify hikers when they have wandered off their desired trail, notify dispatchers when service workers arrive at a scene, or more.
How to use the sample
Observe a virtual walking tour of the Santa Barbara Botanic Garden. Information about the user's current Garden Section, as well as information about nearby points of interest within 10 meters will display or be removed from the UI when the user enters or exits the buffer of each feature.
How it works
- Create a
GeotriggerFeed
with aLocationDataSource
object (in this case, aSimulatedLocationDataSource
). - Create a
FeatureFenceParameters
class from aServiceFeatureTable
, a buffer distance at which to monitor each feature, an Arcade Expression, and a name for the specific geotrigger. - Create a
FenceGeotrigger
with the geotrigger feed, aFenceGeotrigger.RuleType
, and the fence parameters. - Create a
GeotriggerMonitor
with the fence geotrigger and callGeotriggerMonitor.start()
to begin listening for events that meet the fence rule type. - When the
notifications
emit, capture theGeotriggerNotificationInfo
. - For more information about the feature that triggered the notification, cast the
GeotriggerNotificationInfo
to aFenceGeotriggerNotificationInfo
and callFenceGeotriggerNotificationInfo.fenceGeoElement
. - Depending on the
FenceGeotriggerNotificationInfo.fenceNotificationType
display or hide information on the UI from theGeoElement
's attributes.
Relevant API
- ArcadeExpression
- FeatureFenceParameters
- FenceGeotrigger
- FenceGeotrigger.RuleType
- FenceGeotriggerNotificationInfo
- GeoElement
- Geotrigger
- GeotriggerFeed
- GeotriggerMonitor
- GeotriggerNotificationInfo
- ServiceFeatureTable
- SimulatedLocationDataSource
About the data
This sample uses the Santa Barbara Botanic Garden Geotriggers Sample ArcGIS Online Web Map which includes a georeferenced map of the garden as well as select polygon and point features to denote garden sections and points of interest. Description text and attachment images in the feature layers were provided by the Santa Barbara Botanic Garden and more information can be found on the Garden Sections & Displays portion of their website. All assets are used with permission from the Santa Barbara Botanic Garden. For more information, visit the Santa Barbara Botanic Garden website.
Tags
alert, arcade, fence, geofence, geotrigger, location, navigation, notification, notify, routing, trigger
Sample Code
// Copyright 2023 Esri
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import ArcGIS
import ArcGISToolkit
import SwiftUI
struct SetUpLocationDrivenGeotriggersView: View {
/// The view model for the sample.
@StateObject private var model = Model()
/// The error shown in the error alert.
@State private var error: Error?
/// A Boolean value indicating whether to show the popup.
@State private var isShowingPopup = false
/// A string for the fence geotrigger notification status.
@State private var fenceGeotriggerText = ""
/// A string for the display name of the currently nearby feature.
@State private var nearbyFeaturesText = ""
/// Starts the geotrigger monitors and handles posted notifications.
/// - Parameter geotriggerMonitors: The geotrigger monitors to start.
private func startGeotriggerMonitors(_ geotriggerMonitors: [GeotriggerMonitor]) async throws {
await withThrowingTaskGroup(of: Void.self) { group in
for monitor in geotriggerMonitors {
group.addTask { @MainActor in
try await monitor.start()
for await newNotification in monitor.notifications where newNotification is FenceGeotriggerNotificationInfo {
model.handleGeotriggerNotification(newNotification as! FenceGeotriggerNotificationInfo)
}
}
}
}
}
var body: some View {
MapView(map: model.map)
.locationDisplay(model.locationDisplay)
.task {
do {
// Load the map and its operational layers.
try await model.map.load()
// Create the geotrigger monitors.
let monitors = model.makeGeotriggerMonitors()
// Start geotrigger monitoring.
if !monitors.isEmpty {
try await startGeotriggerMonitors(monitors)
}
} catch {
self.error = error
}
}
.task(id: model.fenceGeotriggerStatus) {
// Set fence geotrigger text.
fenceGeotriggerText = model.fenceGeotriggerStatus.label
// Set nearby features text.
let features = model.nearbyFeatures
if features.isEmpty {
nearbyFeaturesText = "No nearby features."
} else {
nearbyFeaturesText = String(format: "Nearby: %@", ListFormatter.localizedString(byJoining: features.keys.sorted()))
}
}
.overlay(alignment: .top) {
// Status text overlay.
VStack {
Text(fenceGeotriggerText)
.frame(maxWidth: .infinity, alignment: .leading)
Text(nearbyFeaturesText)
.foregroundStyle(.orange)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(8)
.background(.thinMaterial, ignoresSafeAreaEdges: .horizontal)
}
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button("Current Section") {
model.setSectionPopup()
isShowingPopup = true
}
.disabled(!model.hasCurrentSection)
.opacity(isShowingPopup ? 0 : 1)
Button("Point of Interest") {
model.setPOIPopup()
isShowingPopup = true
}
.disabled(!model.hasPointOfInterest)
.opacity(isShowingPopup ? 0 : 1)
}
}
.floatingPanel(
selectedDetent: .constant(.full),
horizontalAlignment: .leading,
isPresented: $isShowingPopup
) {
PopupView(popup: model.popup!, isPresented: $isShowingPopup)
.showCloseButton(true)
.padding()
}
.task(id: isShowingPopup) {
if isShowingPopup {
// Stop location updates when the popup is showing.
await model.locationDisplay.dataSource.stop()
} else {
// Start location updates when no popup is showing.
try? await model.locationDisplay.dataSource.start()
}
}
.errorAlert(presentingError: $error)
}
}
extension SetUpLocationDrivenGeotriggersView {
/// The status of a fence geotrigger monitor.
enum FenceGeotriggerStatus: Equatable {
case notSet
case entered(featureName: String)
case exited(featureName: String)
/// A human-readable label for the geotrigger status.
var label: String {
switch self {
case .notSet:
return "Fence geotrigger info will be shown here."
case .entered(featureName: let featureName):
return "Entered the geofence of \(featureName)"
case .exited(featureName: let featureName):
return "Exited the geofence of \(featureName)"
}
}
}
}
#Preview {
NavigationStack {
SetUpLocationDrivenGeotriggersView()
}
}