Show your device’s real-time location while inside a building by using signals from indoor positioning beacons.

Use case
An indoor positioning system (IPS) allows you to locate yourself and others inside a building in real time. Similar to GPS, it puts a blue dot on indoor maps and can be used with other location services to help navigate to any point of interest or destination, as well as provide an easy way to identify and collect geospatial information at their location.
How to use the sample
When the device is within range of an IPS beacon, toggle “Show Location” to change the visibility of the location indicator in the map view. The system will ask for permission to use the device’s location if the user has not yet used location services in this app. It will then start the location display with auto-pan mode set to navigation.
When there are no IPS beacons nearby or other errors occur while initializing the indoors location data source, it will seamlessly fall back to the current device location as determined by GPS.
How it works
- Load an IPS-aware map. This can be a web map hosted as a portal item in ArcGIS Online, an Enterprise Portal, or a mobile map package (.mmpk) created with ArcGIS Pro.
- Create and load an
IndoorPositioningDefinition(stored with IPS-aware maps), then create anIndoorsLocationDataSourcefrom it. - Handle location change events to respond to floor changes or read other metadata for locations.
- Assign the
IndoorsLocationDataSourceto the map view’s location display. - Enable and disable the map view’s location display using
start()andstop(). Device location will appear on the display as a blue dot and update as the user moves throughout the space. - Use the
autoPanModeproperty to change how the map behaves when location updates are received.
Relevant API
- IndoorPositioningDefinition
- IndoorsLocationDataSource
- LocationDisplay
- LocationDisplay.AutoPanMode
- Map
- MapView
About the data
This sample uses an IPS-aware web map that displays Building L on the Esri Redlands campus. Please note: you would only be able to use the indoor positioning functionalities when you are inside this building. Swap the web map to test with your own IPS setup.
Additional information
- Location and Bluetooth permissions are required for this sample.
- To learn more about IPS, read the Indoor positioning article on ArcGIS Developer website.
- To learn more about how to deploy the indoor positioning system, read the Deploy ArcGIS IPS article.
Tags
beacon, BLE, blue dot, Bluetooth, building, facility, GPS, indoor, IPS, location, map, mobile, navigation, site, transmitter
Sample code
// Copyright 2024 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 ArcGISimport CoreLocationimport SwiftUI
struct ShowDeviceLocationUsingIndoorPositioningView: View { /// The data model for the sample. @StateObject private var model = Model() /// The error shown in the error alert. @State private var error: (any Error)?
var body: some View { MapView(map: model.map) .locationDisplay(model.locationDisplay) .overlay(alignment: .top) { HStack(alignment: .center, spacing: 10) { VStack(alignment: .leading) { // The floor number of a location when in a building // where the ground floor is 0. Negative numbers // indicate floors below ground level. if let floor = model.currentFloor { let humanReadableFloor = if floor >= .zero { // Add 1 to above the ground floor levels. floor + 1 } else { // Don't change the underground floor levels. floor } Text("Current floor: \(humanReadableFloor)") } if let accuracy = model.horizontalAccuracy { let formatStyle = Measurement<UnitLength>.FormatStyle.measurement( width: .abbreviated, usage: .asProvided, numberFormatStyle: .number.precision(.fractionLength(2)) ) Text("Accuracy: \(Measurement<UnitLength>(value: accuracy, unit: .meters), format: formatStyle)") } } .opacity(model.currentFloor != nil ? 1 : 0) VStack(alignment: .leading) { let sourceType = model.positionSource == "GNSS" ? "Satellites" : "Transmitters" Text("Data source: \(model.positionSource ?? "None")") Text("Number of \(sourceType): \(model.signalSourceCount ?? 0)") } } .frame(maxWidth: .infinity) .padding(8) .background(.thinMaterial, ignoresSafeAreaEdges: .horizontal) } .overlay(alignment: .center) { if model.isLoading { ProgressView( """ Loading indoor data """ ) .padding() .background(.ultraThinMaterial) .clipShape(.rect(cornerRadius: 10)) .shadow(radius: 50) .multilineTextAlignment(.center) } } .task { // Request location permission if it has not yet been determined. let locationManager = CLLocationManager() if locationManager.authorizationStatus == .notDetermined { locationManager.requestWhenInUseAuthorization() } do { try await model.loadIndoorsData() } catch { self.error = error } // Start to receive updates from the location data source. await model.updateDisplayOnLocationChange() } .onDisappear { Task { await model.locationDisplay.dataSource.stop() } } .errorAlert(presentingError: $error) }}
#Preview { ShowDeviceLocationUsingIndoorPositioningView()}// Copyright 2024 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 ArcGISimport Combineimport Foundation
extension ShowDeviceLocationUsingIndoorPositioningView { @MainActor class Model: ObservableObject { /// An IPS-aware and floor-aware web map for all three floors of /// Esri Building L in Redlands. let map = Map(item: PortalItem(portal: .arcGISOnline(connection: .anonymous), id: .indoorsMap))
/// A Boolean value indicating whether the indoors data is loaded. @Published private(set) var isLoading = false
/// The current floor level. @Published private(set) var currentFloor: Int?
/// The horizontal accuracy of the location (in meters). @Published private(set) var horizontalAccuracy: Double?
/// The source of the location data. @Published private(set) var positionSource: String?
/// The number of BLE sensors or GNSS satellites used for providing location. @Published private(set) var signalSourceCount: Int?
/// The map's location display. let locationDisplay: LocationDisplay = { // By default, uses the device's system location. let locationDisplay = LocationDisplay(dataSource: SystemLocationDataSource()) locationDisplay.autoPanMode = .compassNavigation return locationDisplay }()
/// Loads the indoors data. func loadIndoorsData() async throws { isLoading = true defer { isLoading = false } try await map.load() // For manually creating the indoors location data source only. await map.tables.load() if let floorManager = map.floorManager { // Load the floor manager if the map is floor-aware. // Most IPS-aware maps are also floor-aware. try await floorManager.load() // Only displays the ground floor when initialized. for level in floorManager.levels { level.isVisible = level.verticalOrder == .zero } } try await setUpIndoorsLocationDataSource() }
/// Sets the indoors location data source on the location display /// using the map's indoor positioning definition. private func setUpIndoorsLocationDataSource() async throws { if let indoorPositioningDefinition = map.indoorPositioningDefinition { // Gets indoor positioning definition from the IPS-aware map // and uses it to set the IndoorsLocationDataSource. try await indoorPositioningDefinition.load() let dataSource = IndoorsLocationDataSource(definition: indoorPositioningDefinition) locationDisplay.dataSource = dataSource } else { throw SetupError() } // Starts the location display. try await locationDisplay.dataSource.start() }
/// Updates the location when the location data source is triggered. func updateDisplayOnLocationChange() async { for await location in locationDisplay.dataSource.locations { // The floor level from the location. if let floor = location.additionalSourceProperties[.floor] as? Int, let levelID = location.additionalSourceProperties[.floorLevelID] as? String { currentFloor = floor // Only displays the current floor. if let floorManager = map.floorManager { for level in floorManager.levels { level.isVisible = FloorLevel.ID(levelID) == level.id } } } // The position source where the location data was sourced from: // GNSS (Satellites), BLE (Bluetooth Low Energy), // or AppleIPS (Apple's proprietary location system), etc. if let source = location.additionalSourceProperties[.positionSource] as? String { positionSource = source switch source { case "GNSS": signalSourceCount = location.additionalSourceProperties[.satelliteCount] as? Int default: // Bluetooth, Cellular, WiFi, etc. signalSourceCount = location.additionalSourceProperties[.transmitterCount] as? Int } } // The horizontal accuracy of the location in meters. horizontalAccuracy = location.horizontalAccuracy } } }}
private extension PortalItem.ID { /// Esri campus Building L IPS data. static var indoorsMap: Self { Self("8fa941613b4b4b2b8a34ad4cdc3e4bba")! }}
private extension ShowDeviceLocationUsingIndoorPositioningView.Model { /// An error returned when the indoors data required to setup the sample is malformatted. struct SetupError: LocalizedError { var errorDescription: String? { .init( localized: "Cannot initialize indoors location data source.", comment: "No indoor positioning definition is found." ) } }}