## version-6.0 ### 3D Models # 3D Models > !3D Model Mapper :::tip Note that the MapView class instantiates the Models class and exposes it as MapView.models. Use MapView.models to utilize Models's methods. ::: ## Adding 3D Models to a Map Adding 3D models to a map can be a great way to represent landmarks to help users find key locations. They could also be used to show the location of assets or represent furniture to provide a rich indoor layout. Mappedin SDKs support models in Graphics Library Transmission Format (GLTF) and GL Transmission Format Binary (GLB) format. Models with nested meshes are not supported and should not be used. 3D Models can be added to the map using the MapView.models.add()/>) method. The `add` method requires a Coordinate to place the model and a URL of the model file. Optionally, the model's interactivity, rotation, scale and more can also be set using the `options` parameter, which accepts a AddModelOptions. Models can be updated by calling the MapView.updateState()/>) method. :::tip A complete example demonstrating 3D models can be found in the Mappedin iOS GitHub repo: ModelsDemoViewController.swift ::: The following code samples demonstrate adding a 3D model to the map. ```swift // Create coordinate let coordinate = Coordinate(latitude: 45.0, longitude: -75.0) // Get model URL from bundle guard let modelUrl = Bundle.main.mappedinAssetURL(forResource: "model", withExtension: "glb", subdirectory: "3d_assets") else { print("Model not found") return } // Create scale let scale: AddModelOptions.Scale = .perAxis(x: 1.0, y: 1.0, z: 1.0) // Create options let opts = AddModelOptions( interactive: true, visible: true, verticalOffset: 0.0, visibleThroughGeometry: false, rotation: [0.0, 0.0, 197.0], scale: scale ) // Add the model mapView.models.add(coordinate: coordinate, url: modelUrl, options: opts) { _ in } ``` ## Mappedin 3D Model Library The Mappedin 3D Assets Library is a collection of 3D models that can be used to represent landmarks, assets, and furniture on a map. It is optimized for use with Mappedin SDKs. These models are used in the 3D Model Mapper tool, which allows you to place models on a map and customize their appearance. The ModelsDemoViewController.swift example demonstrates how to use the JSON file exported from the 3D Model Mapper to add 3D models to a map. These models are available in the Mappedin iOS Demo Project in GitHub within the 3d_assets directory. ### Model List The Mappedin 3D Assets Library contains the following models. Each model's default blue color and white Mappedin logo color can be customized when adding it to the map. Airplane Bathtub Bed Bookshelf Box Cardboard Can Garbage Can Recycling Car Chair Computer Couch Couch Curved Couch Outward Curve Desk Desk Chair Dryer EV Charger Floor Lamp Fountain High Bench Hot Tub Kitchen Sink Kiosk Plant 1 Plant 2 Privacy Booth Refrigerator Round Table Self Checkout Shipping Container Shopping Shelves Stove Toilet Tree Pine Tree Pine Short Truck TV Vending Machine Washer Whiteboard Wood Stove ### Annotations # Annotations > Map annotations add visual or textual elements to maps, providing extra information about specific locations or features. Map makers can choose from many annotations included in Mappedin Maker to add to a map and access them using Mappedin SDKs. :::tip A complete example demonstrating Annotations with icons can be found in the Mappedin iOS GitHub repo: MarkersDemoViewController.swift ::: Annotations are organized into the following groups. These are just a few examples of annotations, each group contains many more. | Annotation Group | Examples | | ----------------- | -------------------------------------------- | | Access Features | Keybox, roof access | | Building Sides | Alpha, bravo, charlie, delta | | Equipment Rooms | Boiler Room, Sprinkler control room | | Fire Ratings | Fire and smoke wall 1 hour, firewall 3 hours | | Fire Safety | Fire Blanket, Fire Hose Reel | | General Systems | Emergency Generator, Smoke Control Panel | | Hazards | Biohazard, Explosive | | Safety | Accessible Elevator, Eyewash Station | | Utility Shutoff | Gas Valve, Main Water Valve | | Ventilation | Chimney, Exhaust Fan | | Water Connections | Sprinkler FDC, Public Hydrant | | Water Systems | Fire Pump, Pressurized Water Tank | Incorporating annotations help provide a safer space for all. It allows users to easily locate key elements in the map. !Mappedin v6 Annotations ## Using Annotations An app may choose to display all annotations, annotations of a specific group or no annotations at all. The following code sample logs the name of each annotation. ```swift mapView.mapData.getByType(.annotation) { [weak self] (result: Result<[Annotation], Error>) in guard let self = self else { return } if case .success(let annotations) = result { annotations.forEach { annotation in print("Annotation: \(annotation.name)") } } } ``` ## Annotation Icons Mappedin provides a set of icons to represent annotations. The example below demonstrates using these icons and placing them as Markers on the map. `Annotation.icon.url` contains the URL of the icon to use. :::tip A complete example demonstrating Annotations with icons can be found in the Mappedin iOS GitHub repo: MarkersDemoViewController.swift ::: ```swift {12} mapView.mapData.getByType(.annotation) { [weak self] (result: Result) in guard let self = self else { return } if case .success(let annotations) = result { let opts = AddMarkerOptions( interactive: .True, placement: .single(.center), rank: .tier(.high) ) // Add markers for all annotations that have icons annotations.forEach { annotation in let iconUrl = annotation.icon?.url ?? "" let markerHtml = """ """ self.mapView.markers.add(target: annotation, html: markerHtml, options: opts) { _ in } } // Remove markers that are clicked on self.mapView.on(Events.click) { [weak self] clickPayload in guard let self = self, let clickedMarker = clickPayload?.markers?.first else { return } self.mapView.markers.remove(marker: clickedMarker) { _ in } } } } ``` ### API Reference # API Reference ## Latest Version Mappedin SDK for iOS v6.4.0 ## Previous Versions v6.3.0v6.2.0v6.2.0-beta.1v6.2.0-beta.0v6.2.0-alpha.3v6.2.0-alpha.2v6.2.0-alpha.1v6.1.0-alpha.1v6.0.0-alpha.0 ### Areas & Shapes # Areas & Shapes > ## Areas An Area represents a logical region on the map. They do not represent a physical attribute like a wall or desk, but rather a region that can be used to trigger events, display information, or affect wayfinding. An area is made up of a polygon stored as a GeoJSON Feature, which can be accessed using Area.geoJSON. The Feature can be passed to other methods in Mappedin SDKs. :::tip A complete example demonstrating Areas and Shapes can be found in the Mappedin iOS GitHub repo: AreaShapesDemoViewController.swift ::: Shapes.add() accepts an FeatureCollection and can be used to display one or more areas on the map. The code snippet below demonstrates how to get an `Area` and create a `Shape` and add it to the map with a `Label`. ```swift private func loadAreasAndShapes() { // Get current floor for zone avoidance mapView.currentFloor { [weak self] result in guard let self = self else { return } if case .success(let floor) = result { self.currentFloor = floor // Get all areas self.mapView.mapData.getByType(.area) { [weak self] (result: Result<[Area], Error>) in guard let self = self else { return } if case .success(let areas) = result { // Find the Forklift Area self.forkLiftArea = areas.first { $0.name == "Forklift Area" } if let area = self.forkLiftArea { self.createShapeFromArea( area: area, labelText: "Maintenance Area", color: "red", opacity: 0.7, altitude: 0.2, height: 0.1 ) } // Find the Maintenance Area self.maintenanceArea = areas.first { $0.name == "Maintenance Area" } if let area = self.maintenanceArea { self.createShapeFromArea( area: area, labelText: "Forklift Area", color: "orange", opacity: 0.7, altitude: 0.2, height: 1.0 ) } // Get origin and destination for paths self.setupPathEndpoints() } } } } } private func createShapeFromArea(area: Area, labelText: String, color: String, opacity: Double, altitude: Double, height: Double) { // Get the GeoJSON Feature from the area guard let feature = area.geoJSON else { return } // Create a FeatureCollection containing the single Feature let featureCollection = FeatureCollection(features: [feature]) // Draw the shape using the typed API mapView.shapes.add( geometry: featureCollection, style: PaintStyle(color: color, altitude: altitude, height: height, opacity: opacity) ) { _ in } // Label the area mapView.labels.add(target: area, text: labelText) { _ in } } ``` Within Mappedin Maker, it is possible to create an area and set it to be off limits for wayfinding. This means that the area will be excluded from the route calculation and directions will be rerouted around the area. This is useful for creating areas that are permanently off limits. At runtime, it is also possible to use an area as an exclusion zone for wayfinding. This is useful for creating areas that are temporarily off limits. Below is a code snippet that demonstrates how to use an `Area` to define a region that a wayfinding route should avoid at runtime. Refer to the Dynamic Routing section of the Wayfinding Guide for an interactive example that demonstrates clicking to set an exclusion zone. DirectionZone is the type of the GetDirectionsOptions.zones property that is passed to MapData.getDirections, MapData.getDirectionsMultiDestination()/>) and MapData.getDistance()/>). These zones can be used to affect the route calculation by excluding a polygon from the route. :::tip A complete example demonstrating Areas and Shapes can be found in the Mappedin iOS GitHub repo: AreaShapesDemoViewController.swift ::: The following code snippet demonstrates how to use an `Area` to define a region that a wayfinding route should avoid. ```swift private func drawPath(avoidZone: Bool) { // Remove existing paths mapView.paths.removeAll() guard let origin = origin, let destination = destination, let maintenanceArea = maintenanceArea else { return } // Create NavigationTargets let from = NavigationTarget.mapObject(origin) let to = NavigationTarget.door(destination) // Create zone for avoidance if needed var options: GetDirectionsOptions? if avoidZone, let feature = maintenanceArea.geoJSON, let currentFloor = currentFloor { let zone = DirectionZone( cost: Double.greatestFiniteMagnitude, floor: currentFloor, geometry: feature ) options = GetDirectionsOptions(zones: [zone]) } // Get directions mapView.mapData.getDirections(from: from, to: to, options: options) { [weak self] result in guard let self = self else { return } if case .success(let directions) = result, let directions = directions { // Draw the path let pathColor = avoidZone ? "cornflowerblue" : "green" self.mapView.paths.add( coordinates: directions.coordinates, options: AddPathOptions(color: pathColor) ) { _ in } } } } ``` ## Shapes The Shapes class draws 3 dimensional shapes on top of a map. The shapes are created using GeoJSON geometry, which could be a Polygon/>), MultiPolygon/>) (array of polygons) or a LineString/>). :::tip Access the Shapes class through the MapView class using MapView.shapes. ::: Shapes are added by calling Shapes.add() and removed individually by calling Shapes.remove()/>) and passing in the Shape object to be removed. All shapes can be removed at once by calling Shapes.removeAll()/>). The following code example adds a shape to the map. ```swift mapView.shapes.add( geometry: shapeGeometry, style: PaintStyle( color: "red", altitude: 0.2, height: 2.0, opacity: 0.7 ) ) { [weak self] result in if case .success(let createdShape) = result { shape = createdShape } } ``` ### Blue Dot # Blue Dot > The Blue Dot is a visual marker in mapping apps that shows a user's real-time location. It serves as a reference point, helping users identify their position and navigate efficiently. GPS, Wi-Fi, or other tracking technologies typically power the Blue Dot to ensure location accuracy. Mappedin SDKs provide a simple way to add a Blue Dot to a map. :::tip A complete example demonstrating Blue Dot can be found in the Mappedin iOS GitHub repo: BlueDotDemoViewController.swift ::: !Blue Dot :::warning The Blue Dot will be hidden when the accuracy is greater than 50 meters. ::: ## Enable & Disable Blue Dot With the BlueDot class, an app can display a user's location by creating a new BlueDot instance and calling its enable()/>) method. This will display a prompt for the user to allow or deny sharing their location with the web page. If permission is given, a device's geolocation is displayed on the map as a Blue Dot. The `enable` method accepts a BlueDotOptions object as a parameter, which can be used to change the color of the Blue Dot, accuracy shading and heading indicator. It also allows a developer to enable Blue Dot debug logging, set the size of the Blue Dot, indicate whether it should watch the browser's geolocation for updates and to set the timeout value used to set the Blue Dot to inactive after a period of no location updates. When no longer required, the Blue Dot can be disabled using the BlueDot.disable()/>) method. :::tip Note that the MapView class instantiates the BlueDot class and exposes it as MapView.blueDot. Use MapView.blueDot to utilize BlueDot' methods. ::: The following example demonstrates how to enable the Blue Dot, setting `BlueDotOptions` parameters to customize the colors and heading indicator. It also sets the initial state of the Blue Dot to inactive and disables the watch device position. ```swift let options = BlueDotOptions( accuracyRing: BlueDotOptions.AccuracyRing(color: "#2266ff", opacity: 0.25), color: "#2266ff", heading: BlueDotOptions.Heading(color: "#2266ff", opacity: 0.6), initialState: .inactive, radius: 12, watchDevicePosition: false ) mapView.blueDot.enable(options: options) { [weak self] result in guard let self = self else { return } if case .success = result { DispatchQueue.main.async { self.statusLabel.text = "BlueDot enabled - tap map to place" } } self.refreshStateDisplay() } ``` ## Status The Blue Dot has a number of visual statuses that are used to convey information to the user. - When the Blue Dot position given to Mappedin SDKs has a high accuracy (accuracy value of 0), it is displayed as bright blue. !Blue Dot - A semi-transparent blue shadow is displayed underneath the Blue Dot to indicate accuracy range in meters. The Blue Dot will be hidden when the accuracy is greater than 50 meters. !Blue Dot Accuracy Range - A heading can be shown to indicate the direction the user is facing. !Blue Dot Heading - After the BlueDotOptions.timeout has passed (default of 30 seconds), the Blue Dot is displayed as greyed. This indicates the user may have moved while no updates were received. !Blue Dot Timeout ## Follow Mode A user may pan the camera away and lose track of their Blue Dot position. An app may want to snap the camera to the user's location to reorient themselves. While this could be done using camera focus on, an app can also leverage Blue Dot's follow mode. Follow mode has multiple modes that are defined within FollowMode, which are: - `positionOnly`: Camera position follows the Blue Dot's position. - `positionAndHeading`: Camera position follows the Blue Dot's position. Camera bearing matches the Blue Dot's heading. - `positionAndPathDirection`: Camera position follows the Blue Dot's position. Camera bearing is calculated based on the Navigation path. - `nil`: Disables follow mode. Example: ```kotlin mapView.blueDot.follow(FollowMode.positionOnly) ``` ## Indoor Positioning Mappedin SDKs can use the device's location APIs for position updates and display a Blue Dot on the map based on the given location. For indoor environments, an indoor positioning system may be required to provide an accurate location because satellite based positioning is not available indoors and cellular based positioning may not be accurate enough for indoor navigation. Mappedin SDKs can use the location provided from an indoor positioning system to display the Blue Dot. An example of an indoor positioning system is the Apple Maps Program, which provides a location service for indoor use. A Mappedin Map can be exported to IMDF format and imported into the Apple Maps Program. For more information refer to the Mappedin IMDF Export Page. For development purposes, indoor positions can be simulated using emulators or by using pre-generated location data. The Mappedin SDK for iOS provides two methods to set the Blue Dot's position from an external positioning source: BlueDot.forcePosition()/>) and BlueDot.reportPosition()/>). ### forcePosition The BlueDot.forcePosition()/>) method overrides all positioning sensors with a fixed position for a specified duration. While active, GPS, compass, and other sensors are ignored. After `durationMs` elapses, normal sensor fusion resumes. It accepts a BlueDot.ForcePositionTarget with `latitude`, `longitude`, an optional `heading`, and an optional `floorLevel` (the numeric elevation of the floor). Use `forcePosition()` when the app has an authoritative position from an external system that should be treated as ground truth. Examples include a Visual Positioning System (VPS) scan, a QR code scan, a Near Field Communication (NFC) tag read or a manual user correction. ```swift let target = BlueDot.ForcePositionTarget( latitude: 43.5186, longitude: -80.5394, heading: 90.0, floorLevel: 1 ) mapView.blueDot.forcePosition(position: target, durationMs: 30000) { result in // Position forced for 30 seconds } ``` ### reportPosition The BlueDot.reportPosition()/>) method feeds a position into the positioning fusion engine where it is blended with other active sensors using confidence weighting. The ManualPositionOptions parameter accepts `latitude`, `longitude`, optional `accuracy`, optional `heading`, optional `floorLevel`, optional `confidence` (between 0 and 1, default 0.5) and optional `timestamp`. A higher `confidence` value gives the reported position more influence over the fused result. Use `reportPosition()` when integrating an Indoor Positioning System (IPS), Bluetooth Low Energy (BLE) beacons, Wi-Fi Round Trip Time (RTT) or any external positioning source that should contribute to the Blue Dot's position alongside device sensors rather than replace them entirely. ```swift let options = ManualPositionOptions( latitude: 43.5186, longitude: -80.5394, accuracy: 5.0, heading: 90.0, floorLevel: 1, confidence: 0.8 ) mapView.blueDot.reportPosition(options: options) { result in // Position blended with other sensors } ``` ## Events Blue Dot fires events that can be used to respond to changes to its position, status and errors. The following events are available: - BlueDotEvents.click: Fired when the Blue Dot is clicked. - BlueDotEvents.deviceOrientationUpdate: Fired when the device orientation is updated. - BlueDotEvents.error: Fired when an error occurs. - BlueDotEvents.followChange: Fired when the Blue Dot's follow mode changes. - BlueDotEvents.positionUpdate: Fired when Blue Dot's position is updated. - BlueDotEvents.statusChange: Fired when Blue Dot's status changes. ### Blue Dot Click The BlueDotEvents.click event is fired when the Blue Dot is clicked. ```swift // Listen for BlueDot clicks mapView.blueDot.on(BlueDotEvents.click) { [weak self] payload in guard let self = self, let payload = payload else { return } self.logEvent("click: (\(String(format: "%.4f", payload.coordinate.latitude)), \(String(format: "%.4f", payload.coordinate.longitude)))") } ``` ### Blue Dot Device Orientation Update The BlueDotEvents.deviceOrientationUpdate event is fired when the device orientation is updated. ```swift // Listen for device orientation updates mapView.blueDot.on(BlueDotEvents.deviceOrientationUpdate) { [weak self] payload in guard let self = self, let payload = payload else { return } self.logEvent("device-orientation-update: \(payload.orientation.value)") } ``` ### Blue Dot Position Update The BlueDotEvents.positionUpdate event is fired when the Blue Dot's position is updated. It provides the coordinate, floor, accuracy and heading from the last position update. ```swift // Listen for position updates mapView.blueDot.on(BlueDotEvents.positionUpdate) { [weak self] payload in guard let self = self, let payload = payload else { return } let floorName = payload.floor?.name ?? "nil" let heading = payload.heading.map { String(format: "%.0f°", $0) } ?? "nil" self.logEvent("position-update: (\(String(format: "%.4f", payload.coordinate.latitude)), \(String(format: "%.4f", payload.coordinate.longitude))) floor=\(floorName) heading=\(heading)") } ``` ### Blue Dot Status Change The BlueDotEvents.statusChange event is fired when Blue Dot status changes. It provides the new status of the Blue Dot and the action that caused the status change. The status pertains to how the Blue Dot is displayed on the map. Refer to the Blue Dot Status section above for images of each status. ```swift // Listen for status changes mapView.blueDot.on(BlueDotEvents.statusChange) { [weak self] payload in guard let self = self, let payload = payload else { return } self.logEvent("status-change: \(payload.status.rawValue) (action: \(payload.action.rawValue))") } ``` ### Blue Dot Error The BlueDotEvents.error event is fired when an error occurs. ```swift // Listen for errors mapView.blueDot.on(BlueDotEvents.error) { [weak self] payload in guard let self = self, let payload = payload else { return } self.logEvent("error: [\(payload.code)] \(payload.message)") } ``` ## Generating Test Location Data The Mappedin Blue Dot Location Generator can be used to generate location data for testing. To create test data, load a map in the generator app and choose a Start and End space. Click Generate to generate the data and observe the Blue Dot on the map using the generated data for its position. Press the Download button to download the location data in JSON format. The data can be customized by specifying the parameters below: - **Venue Settings**: Add in a key, secret and map ID to load a different map. - **Environment**: Whether to load the map from Mappedin's North American or European environment. By default, maps exist in the North American environment. - **Jitter**: The amount of random variation in the position data from the navigation path. - **Accuracy**: The accuracy used in the data. In a real world scenario this value represents how accurate the indoor positioning system believes the result to be and is used to draw the light blue shading around the Blue Dot. - **Distance Between Updates**: The physical distance between each position update. Lower values will result in smoother movement of the Blue Dot. - **Start Space**: The space to start the Blue Dot's movement. - **End Space**: The space to end the Blue Dot's movement. - **Time between updates**: The time between each position update can be used to control the speed of the Blue Dot's movements. - **Path Width**: Controls the width of the path shown on the map. ### Blue Dot Location Generator :::tip A complete example demonstrating Blue Dot can be found in the Mappedin iOS GitHub repo: BlueDotDemoViewController.swift ::: ### Building & Floor Selection # Building & Floor Selection > ## Floor Selection When mapping a building with multiple floors, each floor has its own unique map. These are represented by the Floor class, which are accessed using MapData.getByType()/>). The currently displayed Floor can be accessed using MapView.currentFloor/>). :::tip A complete example demonstrating Building and Floor Selection can be found in the Mappedin iOS GitHub repo: BuildingFloorSelectionDemoViewController.kt ::: ### Changing Floors When initialized, MapView displays the Floor with the elevation that's closest to 0. This can be overridden by setting Show3DMapOptions.initialFloor to a Floor.id and passing it to MapView.show3dMap()/>). ```swift // Setting the initial floor to Floor.id 'm_123456789'. self.mapView.show3dMap(options: Show3DMapOptions(initialFloor: "m_123456789")) { r2 in if case .success = r2 { // Successfully initialized the map view. } else if case .failure(let error) = r2 { // Failed to initialize the map view. } } ``` The Floor displayed in a MapView can also be changed during runtime by calling MapView.setFloor()/>) and passing in a Floor.id. ```swift // Set the floor to Floor.id 'm_987654321'. mapView.setFloor(floorId: "m_987654321") { result in switch result { case .success: print("Floor changed successfully") case .failure(let error): print("Error: \(error)") } } ``` ### Listening for Floor Changes Mappedin SDK for iOS provides the ability to listen for floor changes on a MapView. The code below listens for the `floor-change` event and logs the new floor and building name to the console. ```swift mapView.on(Events.floorChange) { [weak self] payload in guard let self = self, let payload = payload else { return } print("Floor changed to: \(payload.floor.name) in building: \(payload.floor.floorStack?.name ?? "unknown")") } ``` ## Building Selection Mappedin Maps can contain one to many buildings with each building having one to many floors. Each building has its floors organized into a FloorStack object. When multiple buildings are present, there are multiple FloorStack objects, one for each building. A FloorStack contains one to many Floor objects, each representing the map of each level of the building. The FloorStack object is accessed by using MapData.getByType()/>). ```swift // Get all floor stacks mapView.mapData.getByType(.floorStack) { [weak self] (result: Result<[FloorStack], Error>) in guard let self = self else { return } if case .success(let stacks) = result { self.floorStacks = stacks.sorted { $0.name.localizedCompare($1.name) == .orderedAscending } // Get all floors in the floor stack self.mapView.mapData.getByType(.floor) { [weak self] (floorsResult: Result<[Floor], Error>) in guard let self = self else { return } if case .success(let floors) = floorsResult { print("Floors: \(floors)") } } } } ``` ## Determine If a Coordinate Is Inside a Building The FloorStack object has a geoJSON property that can be used to determine if a coordinate is inside a building. The code below listens for a click event on the map and determines if the clicked coordinate is inside a building. If it is, the building name is logged to the console. If it is not, a message is logged to the console. ```swift // Filter to get only buildings (type == .building) self.buildings = self.floorStacks.filter { $0.type == .building } // Act on the click event to check if the coordinate is within any building. mapView.on(Events.click) { [weak self] payload in guard let self = self, let payload = payload else { return } let coordinate = payload.coordinate var matchingBuildings: [String] = [] // This demonstrates how to detect if a coordinate is within a building. // For click events, this can be detected by checking the ClickEvent.floors. // Checking the coordinate is for demonstration purposes and useful for non click events. for building in self.buildings { if self.isCoordinateWithinFeature(coordinate: coordinate, feature: building.geoJSON) { matchingBuildings.append(building.name) } } DispatchQueue.main.async { let message: String if !matchingBuildings.isEmpty { message = "Coordinate is within building: \(matchingBuildings.joined(separator: ", "))" } else { message = "Coordinate is not within any building" } self.showToast(message: message) } } /// Check if a coordinate is within a GeoJSON Feature. /// This implements point-in-polygon checking similar to @turf/boolean-contains. private func isCoordinateWithinFeature(coordinate: Coordinate, feature: Feature?) -> Bool { guard let geometry = feature?.geometry else { return false } let point = [coordinate.longitude, coordinate.latitude] switch geometry { case .polygon(let coordinates): return isPointInPolygon(point: point, polygon: coordinates) case .multiPolygon(let coordinates): return coordinates.contains { polygon in isPointInPolygon(point: point, polygon: polygon) } default: return false } } /// Check if a point is inside a polygon using the ray-casting algorithm. /// The polygon is represented as a list of linear rings (first is outer, rest are holes). private func isPointInPolygon(point: [Double], polygon: [[[Double]]]) -> Bool { guard !polygon.isEmpty else { return false } // Check if point is inside the outer ring let outerRing = polygon[0] guard isPointInRing(point: point, ring: outerRing) else { return false } // Check if point is inside any hole (if so, it's not in the polygon) for i in 1.. Bool { guard ring.count >= 4 else { return false } // A valid ring needs at least 4 points (3 + closing point) let x = point[0] let y = point[1] var inside = false var j = ring.count - 1 for i in 0.. y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi) if intersect { inside = !inside } j = i } return inside } /// Shows a toast-like message at the bottom of the screen. private func showToast(message: String) { let toastLabel = UILabel() toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.7) toastLabel.textColor = .white toastLabel.textAlignment = .center toastLabel.font = UIFont.systemFont(ofSize: 14) toastLabel.text = message toastLabel.alpha = 1.0 toastLabel.layer.cornerRadius = 10 toastLabel.clipsToBounds = true toastLabel.numberOfLines = 0 let maxWidth = view.frame.size.width - 40 let expectedSize = toastLabel.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude)) let labelWidth = min(expectedSize.width + 20, maxWidth) let labelHeight = expectedSize.height + 16 toastLabel.frame = CGRect( x: (view.frame.size.width - labelWidth) / 2, y: view.frame.size.height - 150, width: labelWidth, height: labelHeight ) view.addSubview(toastLabel) UIView.animate(withDuration: 2.0, delay: 1.0, options: .curveEaseOut, animations: { toastLabel.alpha = 0.0 }, completion: { _ in toastLabel.removeFromSuperview() }) } ``` :::tip A complete example demonstrating Building and Floor Selection can be found in the Mappedin iOS GitHub repo: BuildingFloorSelectionDemoViewController.kt ::: ### Camera # Camera > Controlling the view of the map allows apps to create visually rich experiences using Mappedin SDKs. This guide shows how to focus the map view on targets, move the camera and listen to camera events. :::tip A complete example demonstrating Camera can be found in the Mappedin iOS GitHub repo: CameraDemoViewController.swift ::: !Mappedin v6 Camera :::tip Note that the MapView class instantiates the Camera class and exposes it as MapView.Camera. Use MapView.Camera to utilize Camera's methods. ::: ## Focus the Camera on Targets Mappedin SDK for iOS built in functionality to focus the camera on one ore more targets using a single or array of FocusTarget objects. When multiple targets are used, the SDK will ensure that all targets are visible. The following sample code acts on the click event to focus on the Space the user clicked on. Note that the Space must be set to interactive to allow it to be clickable. Refer to the Interactivity guide for more information on how to set a space to interactive. ```swift // Focus the camera on the click location. mapView.on(Events.click) { [weak self] clickPayload in guard let self = self, let click = clickPayload else { return } self.mapView.camera.focusOn(coordinate: click.coordinate) } ``` ## Controlling the Camera To control the camera, set it with new pitch, bearing or zoom using Camera.set()/>). It accepts a CameraTarget object that contains bearing, center coordinate, pitch and zoom level. These parameters are all optional, allowing the camera to by moved by adjusting one or more of these values. ### CameraTarget - **bearing:** Bearing for the camera target in degrees. - **center:** Center `Coordinate` for the camera target. - **pitch:** Pitch for the camera target in degrees. - **zoomLevel:** Zoom level for the camera target in mercator zoom levels. ```swift let transform = CameraTarget(bearing: 30, pitch: 80, zoomLevel: 19.5) self.mapView.camera.set(target: transform) { result in switch result { case .success: print("Camera set successfully") case .failure(let error): print("Camera set failed: \(error)") } } ``` ## Animation The camera can also be animated into position using the same transforms described in the Controlling the Camera section above. This is done using the Camera.animateTo()/>) method. Similar to Camera.set()/>), the `animateTo` method accepts a CameraTarget object, but it is optional. This allows the camera to changed by adjusting its `bearing`, `pitch` or `zoomLevel` without specifying a new target to move to. In addition, `animateTo` also accepts CameraAnimationOptions that allow setting of the duration of the animation in milliseconds and setting the easing curve. The next code snippet animates to the center of the camera's current position and zooms in to a zoom level of 21.0, pitches the camera to 60.0 degrees and rotates the camera to 180.0 degrees over 3000 milliseconds using the easing function `ease-in-out`. ```swift mapView.camera.center { [weak self] result in guard let self = self else { return } if case .success(let center) = result, let center = center { let transform = CameraTarget( bearing: 180.0, center: center, pitch: 60.0, zoomLevel: 21.0 ) let options = CameraAnimationOptions(duration: 3000, easing: EasingFunction.EASE_IN_OUT, interruptible: nil) self.mapView.camera.animateTo(target: transform, options: options) { _ in } } } ``` The types of easing animations are defined in EasingFunction. ### EasingFunction - **Linear:** This function implies a constant rate of change. It means the animation proceeds at the same speed from start to end. There's no acceleration or deceleration, giving a very mechanical feel. - **Ease-in:** This function causes the animation to start slowly and then speed up as it progresses. Initially, there's gradual acceleration, and as the function moves forward, the rate of change increases. - **Ease-out:** Contrary to ease-in, ease-out makes the animation start quickly and then slow down towards the end. It begins with a faster rate of change and gradually decelerates. - **Ease-in-out:** This function combines both ease-in and ease-out. The animation starts slowly, speeds up in the middle, and then slows down again towards the end. It offers a balance of acceleration and deceleration. ## Resetting The Camera While there is no method in Mappedin SDKs to reset the camera to its default position, this can be easily built into an app. To do so, store the camera position after the map is shown and then use those values to reposition the camera at a later time. The code sample below stores the initial camera position. When a user clicks on a location it first focuses on that space. The next click will return the camera to its original position. ```swift private var defaultPitch: Double? private var defaultZoomLevel: Double? private var defaultBearing: Double? private var defaultCenter: Coordinate? ... // Store default camera values mapView.camera.pitch { [weak self] result in if case .success(let pitch) = result { self?.defaultPitch = pitch } } mapView.camera.zoomLevel { [weak self] result in if case .success(let zoomLevel) = result { self?.defaultZoomLevel = zoomLevel } } mapView.camera.bearing { [weak self] result in if case .success(let bearing) = result { self?.defaultBearing = bearing } } mapView.camera.center { [weak self] result in if case .success(let center) = result { self?.defaultCenter = center } } ... // Set the camera to the default position let transform = CameraTarget( bearing: defaultBearing, center: defaultCenter, pitch: defaultPitch, zoomLevel: defaultZoomLevel ) mapView.camera.set(target: transform) { _ in } ``` ## Listening to Camera Events There are several camera events that can be acted on by setting a listener with mapView.on(Events.cameraChange)/>). The camera-change event contains a CameraTransform object. CameraTransform provides the bearing, center coordinate, pitch and zoom level. ```swift // Log camera change events to the console. mapView.on(Events.cameraChange) { [weak self] cameraTransform in guard self != nil, let transform = cameraTransform else { return } print("Camera changed to bearing: \(transform.bearing), pitch: \(transform.pitch), zoomLevel: \(transform.zoomLevel), center: Lat: \(transform.center.latitude), Lon: \(transform.center.longitude)") } ``` :::tip A complete example demonstrating Camera can be found in the Mappedin iOS GitHub repo: CameraDemoViewController.swift ::: ### Connections # Connections > Map connections are pathways in multi-floor and multi-building maps that enable vertical or horizontal movement between levels, spaces or buildings. They include stairs, elevators, and escalators, providing essential links for seamless navigation and spatial continuity across the map. The Connection includes details such as name, description, floors, coordinates and more. Connection contains an array of `coordinates`, with each `coordinate` representing the location of the connection on a specific floor. In the case of an elevator, each `coordinate` would have `latitude` and `longitude` values that are the same with different `floorIds`. Stairs may have `coordinates` with the same or different `latitude` and `longitude` values on each floor, depending on whether a building's stairs are vertically aligned. The following sample code retrieves all connections from the map and logs the name of each connection. ```swift mapView.mapData.getByType(.connection) { [weak self] (result: Result<[Connection], Error>) in guard self != nil else { return } if case .success(let connections) = result { connections.forEach { connection in print("Connection: ", connection.name) } } } ``` ### Dynamic Focus # Dynamic Focus > A single venue may include multiple nearby buildings, each contained within a FloorStack. Dynamic Focus enables seamless exploration across these buildings by revealing indoor content as the camera pans over each structure. There are two ways to implement Dynamic Focus. **1. Dynamic Focus Class** The first approach is to use the DynamicFocus API. It provides a simple interface for implementing Dynamic Focus, allowing automatic switching between building facades and building maps as they move into and out of focus when the map is panned. **2. Dynamic Focus using MapView.updateState** The second approach is to use the MapView.updateState() method to update the map view when the camera pans over a building. This approach is more flexible and allows for more control over Dynamic Focus behavior, but it requires more code to implement. This can allow for custom behaviour, such as showing the building map of different elevations. This can be useful when floors of nearby buildings don't share the same elevation, such as when level 2 of one building is aligned with level 3 of another building. It could also allow for showing and hiding markers or labels when focus is transitioned to a new building. !Mappedin Dynamic Focus _Mappedin Dynamic Focus with Auto Focus Enabled_ ## Implementing the Mappedin Dynamic Focus API The Mappedin Dynamic Focus API offers a simple API for implementing Dynamic Focus. If custom behaviour is required refer to the Implementing Dynamic Focus using MapView section. :::tip A complete example demonstrating the Dynamic Focus API can be found in the Mappedin iOS GitHub repo: DynamicFocusDemoViewController.swift ::: ### Usage :::tip Note that the MapView class instantiates the DynamicFocus class and exposes it as MapView.dynamicFocus. Use MapView.dynamicFocus to utilize DynamicFocus' methods. ::: Dynamic Focus can be enabled by calling DynamicFocus.enable()/>). When enabled, the Dynamic Focus will automatically transition the currently visible map from one building to the next as the camera pans over it. The following example demonstrates how to enable Dynamic Focus with auto focus enabled, setting the floor on focus and specifying the indoor and outdoor zoom thresholds. ```swift let options = DynamicFocusOptions( autoFocus: true, indoorZoomThreshold: 17.0, outdoorZoomThreshold: 17.0, setFloorOnFocus: true, ) mapView.dynamicFocus.enable(options: options) { [weak self] result in guard let self = self else { return } if case .success = result { DispatchQueue.main.async { self.statusLabel.text = "Dynamic Focus enabled - zoom and pan to see auto focus" } } } ``` To disable Dynamic Focus, call DynamicFocus.disable()/>). ```swift mapView.dynamicFocus.disable { [weak self] _ in DispatchQueue.main.async { self?.statusLabel.text = "Dynamic Focus disabled" } self?.refreshStateDisplay() } ``` Dynamic Focus has a number of options that can be enabled either at instantiation or at any runtime by calling DynamicFocus.updateState/>). ### Auto Focus Dynamic Focus' auto focus provides default behavior to transition the currently visible map from one building to the next as the camera pans over it. The default behavior can be overridden by setting the DynamicFocusOptions.autoFocus option to `false`, allowing the developer to control the focus manually. ### Setting FloorStack Default Floor The default floor is the Floor that is visible when focus is transitioned to a new FloorStack (building). The default Floor for a FloorStack can be set by calling DynamicFocus.setDefaultFloorForStack()/>) with the floor stack and floor ID. The default floor can be reset by calling DynamicFocus.resetDefaultFloorForStack()/>) with the floor stack. ## Implementing Dynamic Focus using MapView Dynamic Focus can be implemented using the MapView.updateState method. This approach is more flexible and allows for full customization of Dynamic Focus behavior, but it requires more code to implement. For a simple implementation of Dynamic Focus, refer to the Mappedin Dynamic Focus API section. :::tip A complete example demonstrating a Dynamic Focus implementation using MapView.updateState can be found in the Mappedin iOS GitHub repo: DynamicFocusManualDemoViewController.swift ::: Implementing Dynamic Focus using MapView.updateState requires the app to switch the visibility of a building Facade and Floor when the camera pans over a building. The Facade represents the look of the building from the outside, with its roof on. The Floor represents the look of the building from the inside, showing the indoor map. ### Listen for Changes to Facades In view The FacadesInViewChange event is emitted when the facades in view change. This event is emitted when the camera pans over a building. The event provides the list of facades in view. The app can use this event to update the visibility of the facades and floors. The first Facade in the array contains the one that is most centered in view. ```swift // When facades come into view, switch to show that building's interior mapView.on(Events.facadesInViewChange) { [weak self] payload in guard let self = self, let payload = payload else { return } self.logEvent("facades-in-view-change: \(payload.facades.count) facade(s)") // If a facade is in view, switch to that building if !payload.facades.isEmpty { let facade = payload.facades.first! let floorStackId = facade.floorStack // Only switch if it's a different building if floorStackId != self.currentSelectedFloorStackId { if let floorStack = self.allFloorStacks.first(where: { $0.id == floorStackId }) { print("[DynamicFocusManual] Facade in view - switching to building: \(floorStack.name)") // Don't focus camera - user is already panning to this location self.switchToBuilding(floorStack, focusCamera: false) } } } else { // No facades in view - switch to outdoor // Don't focus camera - user panned away, keep camera where it is if let outdoorFloorStack = self.allFloorStacks.first(where: { $0.type == .outdoor }), outdoorFloorStack.id != self.currentSelectedFloorStackId { print("[DynamicFocusManual] No facades in view - switching to outdoor") self.switchToBuilding(outdoorFloorStack, focusCamera: false) } } } ``` The following code demonstrates how to switch to a building by manually managing facades and floors. This does not call setFloorStack or setFloor for buildings - instead it manages visibility of floors and facades directly while staying on the outdoor floor. This allows facades to remain rendered while showing building interiors. If the user has previously selected a floor, it will be restored when the building is switched to. If the user hasn't previously selected a floor a floor matching the current elevation will be shown if available, if not the default floor will be shown. ```swift /// Switch to viewing a building by manually managing facades and floors. /// This does NOT call setFloorStack or setFloor for buildings - instead it manages /// visibility of floors and facades directly while staying on the outdoor floor. /// This allows facades to remain rendered while showing building interiors. /// - Parameter focusCamera: Whether to focus the camera on the target. Set to false when /// switching due to panning (facades-in-view-change with 0 facades) to avoid snapping back. private func switchToBuilding(_ floorStack: FloorStack, focusCamera: Bool = true) { currentSelectedFloorStackId = floorStack.id print("[DynamicFocusManual] switchToBuilding: \(floorStack.name) (\(floorStack.id)) focusCamera=\(focusCamera)") // Check if this is an Outdoor-type floor stack let isOutdoor = floorStack.type == .outdoor if isOutdoor { // Switching to Outdoor - close the previously open facade (if any) print("[DynamicFocusManual] Switching to Outdoor view") // Only close the facade that was previously opened (not all facades) if let openFacadeId = currentlyOpenFacadeId, let facadeToClose = allFacades.first(where: { $0.id == openFacadeId }) { closeFacade(facadeToClose) } currentlyOpenFacadeId = nil // Set floor to the outdoor floor let defaultFloorId = floorStack.defaultFloor if let floor = getFloorById(defaultFloorId) { mapView.setFloor(floorId: defaultFloorId) // Only focus if explicitly requested (e.g., from picker selection) if focusCamera { mapView.camera.focusOn(floor: floor) } } } else { // Switching to a Building - open its facade (hide it) and show its floors print("[DynamicFocusManual] Switching to Building: \(floorStack.name)") // Find the facade for the new building let newFacade = allFacades.first { $0.floorStack == floorStack.id } // Close the previously open facade (if different from the new one) if let openFacadeId = currentlyOpenFacadeId, openFacadeId != newFacade?.id, let facadeToClose = allFacades.first(where: { $0.id == openFacadeId }) { closeFacade(facadeToClose) } // Open the new building's facade (if it has one) if let facade = newFacade { openFacade(facade) currentlyOpenFacadeId = facade.id } else { // Building has no facade - clear the tracking variable to avoid // attempting to close an already-closed facade on subsequent transitions currentlyOpenFacadeId = nil // Still need to show floors even without a facade, otherwise // the building interior would be invisible showFloors(building: floorStack) } // Determine which floor to show: // 1. Use stored preference for this building (highest priority) // 2. Try to find a floor matching current elevation // 3. Fall back to default floor let floorsInStack = getFloorsForFloorStack(floorStack) let storedPref = floorToShowByBuilding[floorStack.id] let elevationMatch = floorsInStack.first { $0.elevation == currentElevation } let defaultFloor = getFloorById(floorStack.defaultFloor) let floorToShow = storedPref ?? elevationMatch ?? defaultFloor if let floor = floorToShow { // Store this as the preference for this building floorToShowByBuilding[floorStack.id] = floor // Update the global elevation tracker currentElevation = floor.elevation // NOTE: We do NOT call mapView.setFloor() here! // Calling setFloor causes the SDK to internally manage facades, hiding all // facades except the active building's. Instead, we manually manage visibility // using updateState/animateState while staying on the outdoor floor conceptually. // Floor visibility is handled via showFloors() called above. // Only focus camera if explicitly requested (e.g., from picker selection) if focusCamera { mapView.camera.focusOn(floor: floor) } } } } private func openFacade(_ facade: Facade) { guard let floorStack = allFloorStacks.first(where: { $0.id == facade.floorStack }) else { return } print("[DynamicFocusManual] openFacade: \(floorStack.name) (setting opacity to 0)") // First, show the floor we want to see showFloors(building: floorStack) // Animate the facade out (hide it to reveal interior) mapView.animateState(facade: facade, state: FacadeUpdateState(opacity: 0.0)) } private func closeFacade(_ facade: Facade) { let floorStack = allFloorStacks.first { $0.id == facade.floorStack } print("[DynamicFocusManual] closeFacade: \(floorStack?.name ?? "unknown") (setting opacity to 1)") // Animate the facade in (show it to hide interior) mapView.animateState(facade: facade, state: FacadeUpdateState(opacity: 1.0)) { [weak self] _ in guard let self = self, let floorStack = floorStack else { return } // Hide all floors for this building after animation completes let floorsInBuilding = self.getFloorsForFloorStack(floorStack) floorsInBuilding.forEach { floor in self.mapView.updateState( floor: floor, state: FloorUpdateState(visible: false) ) } } } ``` ### Listen for Changes to the Floor The floorChange event is emitted when the floor changes, for example when the user selects a new floor from a floor selector or when they click on a navigation marker to go up or down stairs or an elevator. The event provides the new floor. The app can use this event to update the visibility of the facades and floors. ```swift // Act on the floor-change event to update the level selector mapView.on(Events.floorChange) { [weak self] payload in guard let self = self, let payload = payload else { return } let newFloor = payload.floor guard let newFloorStack = newFloor.floorStack else { return } let newFloorStackId = newFloorStack.id print("[DynamicFocusManual] floor-change: \(newFloor.name) (floorStackId=\(newFloorStackId))") // Store this floor as the preference for its floor stack // This preserves user selections across building switches self.floorToShowByBuilding[newFloorStackId] = newFloor // Only update elevation for non-outdoor floors // Switching to outdoor shouldn't reset the current elevation let isOutdoorFloor = newFloorStack.type == .outdoor if !isOutdoorFloor { self.currentElevation = newFloor.elevation } self.logEvent("floor-change: \(newFloor.name) (\(newFloorStack.name))") } ``` ### Enterprise Data # Enterprise Data > :::note > The Enterprise Data classes described in this guide are populated in Mappedin CMS, which requires a Solutions Tier subscription. > ::: ## Enterprise Locations An EnterpriseLocation contains metadata about a location, such as its name, description, logo, phone number, social medial links, hours of operation and more. They can be accessed using the MapData.getByType()/>) method as shown below. ```swift mapView.mapData.getByType(MapDataType.enterpriseLocation) { [weak self] (result: Result<[EnterpriseLocation], Error>) in switch result { case .success(let enterpriseLocations): print("getByType success: \(enterpriseLocations)") case .failure(let e): print("getByType error: \(e)") } } ``` ## Enterprise Categories An EnterpriseCategory groups one or more EnterpriseLocation. These allow similar locations to be sorted in a logical fashion. For example a mall may group locations into Food Court, Footwear and Women's Fashion. They can be accessed using the MapData.getByType()/>) method as shown below. ```swift mapView.mapData.getByType(MapDataType.enterpriseCategory) { [weak self] (result: Result<[EnterpriseCategory], Error>) in switch result { case .success(let enterpriseCategories): print("getByType success: \(enterpriseCategories)") case .failure(let e): print("getByType error: \(e)") } } ``` EnterpriseCategory can contain one or more sub EnterpriseCategory that are accessed from its `children` member. ## Enterprise Venue The EnterpriseVenue class holds metadata about the map, which includes the map name, supported languages, default language, top locations and more. It can be accessed using the MapData.getByType()/>) method as shown below. ```swift mapView.mapData.getByType(MapDataType.enterpriseVenue) { [weak self] (result: Result<[EnterpriseVenue], Error>) in switch result { case .success(let enterpriseVenue): print("getByType success: \(enterpriseVenue)") case .failure(let e): print("getByType error: \(e)") } } ``` ## Search :::tip Note that the MapData class instantiates the Search class and exposes it as MapView.mapData.search. Use MapView.mapData.search to utilize Search' methods. ::: The Search functionality allows users to search for locations, categories, and other points of interest within the venue. :::tip A complete example demonstrating Search can be found in the Mappedin iOS GitHub repo: SearchDemoViewController.swift ::: Here are two ways to enable search: 1. Enable Search on map initialization: ```swift // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps let options = GetMapDataWithCredentialsOptions( key: "5eab30aa91b055001a68e996", secret: "RJyRXKcryCMy4erZqqCbuB1NbR66QTGNXVE0x3Pg6oCIlUR1", mapId: "mappedin-demo-mall", search: SearchOptions(enabled: true) ) mapView.getMapData(options: options) { [weak self] r in ``` 2. Enable Search via method: ```swift mapView.mapData.search.enable { [weak self] result in guard let self = self else { return } if case .success = result { print("Search enabled") self.loadAllLocations() } } ``` ### Search Query Use Search.query/>) to search for locations based on a string input: - EnterpriseLocationOptions: Specific places such as stores, restaurants, or washrooms. - EnterpriseCategoryOptions: Groups of locations, such as "Food Court" or "Electronics." - PlaceOptions: Any main objects that can be searched for such as Space, Door, PointOfInterest Search query returns a list of matching SearchResult based on the input string. SearchResult include information about the type of match, the score (relevance), and detailed metadata about the matching items. #### Example Search Query ```swift mapView.mapData.search.query(term: suggestion.suggestion) { [weak self] result in guard let self = self else { return } if case .success(let searchResult) = result { print("Search result: \(searchResult)") } } ``` ### Search Suggestions Use Search.suggest/>) to fetch real-time suggestions based on partial input. This is useful for creating an autocomplete feature for a search bar. #### Example Code ```swift mapView.mapData.search.suggest(term: searchText) { [weak self] result in guard let self = self else { return } if case .success(let suggestions) = result { print("Suggestions: \(suggestions)") } } ``` :::tip A complete example demonstrating Search can be found in the Mappedin iOS GitHub repo: SearchDemoViewController.swift ::: ### Getting Started # Getting Started > Mappedin SDK for iOS helps to deliver the rich indoor mapping experience of a venue, inside iOS apps. The Mappedin SDK for iOS is a native interface to Mappedin JS. The SDK is a dependency built using Swift, and it automatically handles any authentication, network communication, fetching of map data, its display, and basic user interaction, such as panning, tapping, and zooming. The SDK allows a developer to build their own interactions. Additional layers can be rendered on top of the map. :::info Mappedin SDK for iOS is supported on iOS versions 13.0 and above. ::: ## Xcode Project Setup ### Add Mappedin SDK Using Swift Package Manager Add the Mappedin SDK to your Xcode project using Swift Package Manager: 1. In Xcode, go to **File > Add Package Dependencies** 2. Enter the repository URL: `https://github.com/MappedIn/ios.git` 3. Select version `6.4.0` 4. Click **Add Package** Alternatively, you can add the package dependency to your `Package.swift` file: ```swift dependencies: [ .package(url: "https://github.com/MappedIn/ios.git", from: "6.4.0") ] ``` The latest version can be found in the iOS GitHub repository releases. ### Add Permissions If you plan to display user location, add the following permissions to your `Info.plist` file: ```xml NSLocationWhenInUseUsageDescription This app needs access to your location to show you on the map NSLocationAlwaysAndWhenInUseUsageDescription This app needs access to your location to show you on the map NSBluetoothAlwaysUsageDescription This app uses Bluetooth to determine your location inside buildings ``` Update the description strings to match your app's specific use case. :::tip The Mappedin iOS GitHub repository contains a reference project that demonstrates how to use the Mappedin SDK for iOS. ::: ### Display a Map The core class of the Mappedin SDK for iOS is MapView, which is responsible for instantiating an WKWebView that loads Mappedin JS. MapView is the core class of which all other views and data models can be accessed. #### Load Map Data Call MapView.getMapData/>) to load map data from Mappedin servers. This function must be called first and map data must be loaded before any other Mappedin functions can be called. ```swift // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps let options = GetMapDataWithCredentialsOptions( key: "mik_yeBk0Vf0nNJtpesfu560e07e5", secret: "mis_2g9ST8ZcSFb5R9fPnsvYhrX3RyRwPtDGbMGweCYKEq385431022", mapId: "660c0c6e7c0c4fe5b4cc484c" ) // Load the map data. mapView.getMapData(options: options) { [weak self] r in guard let self = self else { return } if case .success = r { print("getMapData success") } else if case .failure(let error) = r { print("getMapData error: \(error)") } } ``` #### Show a Map Call MapView.show3dMap/>) to display the map. ```swift self.mapView.show3dMap(options: Show3DMapOptions()) { r2 in if case .success = r2 { print("show3dMap success") } } ``` The following sample code combines the loading of map data and the display of the map and includes a function to be called when the map is ready. ```swift import UIKit import Mappedin final class DisplayMapDemoViewController: UIViewController { private let mapView = MapView() private let loadingIndicator = UIActivityIndicatorView(style: .large) override func viewDidLoad() { super.viewDidLoad() title = "Display a Map" view.backgroundColor = .systemBackground let container = mapView.view container.translatesAutoresizingMaskIntoConstraints = false view.addSubview(container) // Add loading indicator loadingIndicator.translatesAutoresizingMaskIntoConstraints = false loadingIndicator.startAnimating() view.addSubview(loadingIndicator) NSLayoutConstraint.activate([ container.leadingAnchor.constraint(equalTo: view.leadingAnchor), container.trailingAnchor.constraint(equalTo: view.trailingAnchor), container.topAnchor.constraint(equalTo: view.topAnchor), container.bottomAnchor.constraint(equalTo: view.bottomAnchor), loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps let options = GetMapDataWithCredentialsOptions( key: "mik_yeBk0Vf0nNJtpesfu560e07e5", secret: "mis_2g9ST8ZcSFb5R9fPnsvYhrX3RyRwPtDGbMGweCYKEq385431022", mapId: "64ef49e662fd90fe020bee61" ) // Load the map data. mapView.getMapData(options: options) { [weak self] r in guard let self = self else { return } if case .success = r { print("getMapData success") // Display the map. self.mapView.show3dMap(options: Show3DMapOptions()) { r2 in if case .success = r2 { DispatchQueue.main.async { self.loadingIndicator.stopAnimating() } self.onMapReady() } else if case .failure(let error) = r2 { DispatchQueue.main.async { self.loadingIndicator.stopAnimating() } print("show3dMap error: \(error)") } } } else if case .failure(let error) = r { print("getMapData error: \(error)") } } } // Place your code to be called when the map is ready here. private func onMapReady() { print("show3dMap success - Map displayed") } } ``` #### Result The app should display something that looks like this in the iPhone Emulator: And zooming in to have a closer look: ## Create a Key & Secret ## Caching and Loading Map Data There are two methods available to cache and reload map data: - Caching and reloading map data as a Mappedin Venue Format (MVF) file - Caching and reloading map data as JSON Whenever possible, it is recommended to cache and load map data as a Mappedin Venue Format (MVF) file because it is more efficient and faster to load than JSON. However, if you need to manipulate the map data before displaying it, you can cache and load it as JSON instead. ### Caching and Loading Map Data as a MVF File A Mappedin Venue Format (MVF) file can be downloaded using Mappedin REST API endpoints. Instructions for downloading an MVF file can be found in the Getting Started with MVF v3 guide. This file can be cached and loaded using the MapView.hydrateMapDataFromURL/>) function. :::tip A complete example demonstrating downloading, caching and loading an MVF file can be found in the Mappedin iOS GitHub repo: CacheMVFDemoViewController.swift ::: ```swift // Save MVF data to cache let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] let cacheFileURL = cachesDirectory.appendingPathComponent("cached-mvf-12345.zip") try mvfData.write(to: cacheFileURL) // Get the mappedin-cache:// URL guard let mvfUrl = FileManager.default.mappedinCacheURL(forFileName: "cached-mvf-12345.zip") else { return } // Hydrate map data from URL mapView.hydrateMapDataFromURL(url: mvfUrl, options: nil) { result in switch result { case .success: // Map data hydrated, now call show3dMap case .failure(let error): print("Error: \(error)") } } ``` ### Caching and Loading Map Data as JSON :::warning Loading map data using `MapView.hydrateMapData` should not be used for large maps. Use `MapView.hydrateMapDataFromURL` instead. ::: JSON representing the map data retrieved from MapView.getMapData/>) can be cached and loaded using the MapView.hydrateMapData/>) function. This method accepts a JSON object that can be created using the MapData.toBinaryBundle/>) function. :::tip A complete example demonstrating caching and loading map data as JSON can be found in the Mappedin iOS GitHub repo: CacheMapDataDemoViewController.swift ::: The code snippet below demonstrates how to cache map data JSON using the MapData.toBinaryBundle/>) function. ```swift private func saveToCache(mapId: String) { mapView.mapData.toBinaryBundle(downloadLanguagePacks: true) { [weak self] result in guard let self = self else { return } switch result { case .success(let bundle): if let bundle = bundle { do { let cacheFileURL = self.getCacheFileURL(mapId: mapId) try bundle.main.write(to: cacheFileURL) print("CacheMapDataDemo: Map data cached successfully to \(cacheFileURL.path)") print("CacheMapDataDemo: Cache size: \(bundle.main.count) bytes") self.updateStatus("Cached for offline use!") } catch { print("CacheMapDataDemo: Failed to save cache: \(error)") } } else { print("CacheMapDataDemo: toBinaryBundle returned nil") } case .failure(let error): print("CacheMapDataDemo: toBinaryBundle error: \(error)") } } } ``` The code snippet below demonstrates how to load map data JSON from the app's assets using the MapView.hydrateMapData/>) function. ```swift // Read MVF zip file as bytes guard let url = Bundle.main.url(forResource: "map", withExtension: "zip"), let zipData = try? Data(contentsOf: url) else { return } // Create backup object with binary data let mainArray = zipData.map { Int($0) } let backup: [String: Any] = [ "type": "binary", "main": mainArray ] // Hydrate map data with credentials let options = GetMapDataWithCredentialsOptions( key: "your-key", secret: "your-secret", mapId: "your-map-id" ) mapView.hydrateMapData(backup: backup, options: options) { result in switch result { case .success: // Map data hydrated, now call show3dMap case .failure(let error): print("Error: \(error)") } } ``` ## Offline Loading Mode Offline loading of Mappedin Venue Format (MVF) files is appropriate for scenarios where the map data is not fetched using Mappedin SDKs. This approach is particularly useful in offline or low-connectivity environments—such as air gapped networks, cruise ships, or remote locations. It's also beneficial when you need full control over content distribution, for example, to meet internal security requirements or to ensure that a specific version of the map is always available, regardless of network conditions. An MVF file can be downloaded from Mappedin Maker or fetched using Mappedin REST API endpoints. Instructions for downloading an MVF file can be found in the Getting Started with MVF v3 guide. :::tip A complete example demonstrating loading an MVF file from the app's assets can be found in the Mappedin iOS GitHub repo: OfflineModeDemoViewController.swift ::: The code snippet below demonstrates how to load an MVF file included in the app. ```swift // Use Bundle.mappedinAssetURL to generate a URL the WebView can fetch // This is more efficient than reading bytes and passing them through hydrateMapData guard let mvfUrl = Bundle.main.mappedinAssetURL(forResource: "school-demo-multifloor-mvfv3", withExtension: "zip") else { print("MVF file not found in bundle") loadingIndicator.stopAnimating() return } // Hydrate the map data from the local MVF file URL mapView.hydrateMapDataFromURL(url: mvfUrl) { [weak self] result in guard let self = self else { return } switch result { case .success: print("hydrateMapDataFromURL success") // Display the map self.mapView.show3dMap(options: Show3DMapOptions()) { r2 in switch r2 { case .success: DispatchQueue.main.async { self.loadingIndicator.stopAnimating() } self.onMapReady() case .failure(let error): DispatchQueue.main.async { self.loadingIndicator.stopAnimating() } print("show3dMap error: \(error)") } } case .failure(let error): DispatchQueue.main.async { self.loadingIndicator.stopAnimating() } print("hydrateMapDataFromURL error: \(error)") } } ``` ## Reusing a MapView Across Screens MapView renders inside a WKWebView. Each time a new MapView is created, that WebView must boot, download the Mappedin Venue Format (MVF) data, and render the map again. When several screens, such as a directory, a map tab, or a location detail screen, each create their own MapView, this initialization cost is paid on every navigation, resulting in a slow experience with repeated loading delays. The solution is to create a single MapView for the lifetime of a host view controller and physically reparent that one WebView into whichever screen needs it. The WebView boots once and stays warm, so navigating between screens moves the existing map instead of recreating it. The following sections describe the key parts of this pattern. :::tip A complete example demonstrating how to reuse a single MapView across screens can be found in the Mappedin iOS GitHub repo: ReusableMapViewDemoViewController.swift and ReusableMapScreenViewController.swift. ::: ### Creating and Loading the Map Once A host view controller owns the single MapView and loads it exactly once. Each screen is a child view controller that displays the shared map; the map is never created or destroyed inside a screen. ```swift final class ReusableMapViewDemoViewController: UITabBarController { private let mapView = MapView() // True once the map has loaded and rendered for the first time. private(set) var isMapReady = false override func viewDidLoad() { super.viewDidLoad() // Load the single shared map exactly once. loadMap() } private func loadMap() { // See Demo API Key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps let options = GetMapDataWithCredentialsOptions( key: "mik_yeBk0Vf0nNJtpesfu560e07e5", secret: "mis_2g9ST8ZcSFb5R9fPnsvYhrX3RyRwPtDGbMGweCYKEq385431022", mapId: "660c0c6e7c0c4fe5b4cc484c" ) mapView.getMapData(options: options) { [weak self] dataResult in guard let self, case .success = dataResult else { return } self.mapView.show3dMap(options: Show3DMapOptions()) { [weak self] showResult in guard let self, case .success = showResult else { return } DispatchQueue.main.async { self.isMapReady = true } } } } } ``` ### Reparenting the Map into Each Screen The shared map's view is exposed through `MapView.view`. Moving it between screens is a matter of removing it from its current superview and adding it to the new screen's container. The host view controller exposes a helper that reparents and constrains the shared view. ```swift // In the host view controller. Reparents the shared map's WebView // into the given container. func attachMap(to container: UIView) { let mapWebView = mapView.view mapWebView.removeFromSuperview() mapWebView.translatesAutoresizingMaskIntoConstraints = false // Insert at the bottom of the z-order so overlays, such as a loading // indicator already in the container, stay visible on top. container.insertSubview(mapWebView, at: 0) NSLayoutConstraint.activate([ mapWebView.leadingAnchor.constraint(equalTo: container.leadingAnchor), mapWebView.trailingAnchor.constraint(equalTo: container.trailingAnchor), mapWebView.topAnchor.constraint(equalTo: container.topAnchor), mapWebView.bottomAnchor.constraint(equalTo: container.bottomAnchor), ]) } ``` Each screen view controller claims the shared map as it becomes visible in `viewWillAppear`. The map is never destroyed here, leaving it ready for the next screen. ```swift final class ReusableMapScreenViewController: UIViewController { private weak var host: ReusableMapViewDemoViewController? private let mapContainer = UIView() override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // Move the single shared map into this screen. host?.attachMap(to: mapContainer) } } ``` ### Focusing the Map Per Screen Because every screen shares one map, each screen can drive it independently. When a screen takes ownership of the map, it can focus the Camera on a relevant Space, so the shared map presents differently depending on the active screen. ```swift // In the host view controller. Focuses the shared map on the space // chosen for the given screen index, once the map is ready. func focus(forScreen index: Int) { guard isMapReady, let target = targetSpace(for: index) else { return } mapView.camera.focusOn(space: target) } ``` ### Cleaning Up Because the map is shared, it must outlive the individual screens. Destroy it only when the host view controller itself is finished by calling `MapView.destroy` in `deinit`. ```swift deinit { // Tear down the shared map only when the whole demo is finished. mapView.destroy() } ``` ## Debug Mode Use Debug Mode to get a closer look at how various map components behave and interact. Here's how to enable it: ### 1. Enable Debug Mode To activate the debug interface, call the following function in your code: ```swift mapView.enableDebug() ``` MapView.enableDebug/>) displays a panel on the right side of the map, revealing several tools and controls for direct interaction with the map's elements. ### 2. Navigating the Debug Panel The debug panel provides access to a variety of controls: **Geometry Controls** - Adjust individual geometry settings, such as color, opacity, visibility. These controls make it easy to see how different elements respond to changes. **Interactivity** - Use toggles to turn interactivity on/off - Change colors and hover effects to highlight specific areas **Scene Controls** - Manage the overall scene settings, such as scaling, positioning, and group management. Toggle the visibility of groups or containers within the scene - Adjust padding, scale, and watermark positioning - Markers & Labels — Add, remove, or edit markers and labels directly through the panel **Camera Controls** - Fine-tune camera settings, including zoom levels, pitch, and center position - Set minimum and maximum zoom levels - Adjust the focus to a specific area or level !enable debug ### Icons # Icons > Mappedin SDK for iOS provides access to Mappedin's icon library through the Icons class. Icons are Content Delivery Network (CDN) backed Scalable Vector Graphics (SVG) assets served from a global CDN with North America and Europe regions, and can be looked up by name, type, subtype, category, or tags. :::tip A complete example demonstrating Icons can be found in the Mappedin iOS GitHub repo: IconsDemoViewController.swift ::: !Mappedin Icon Package :::tip Note that the MapView class instantiates the Icons class and exposes it as MapView.icons. The Icons API is map-independent: it registers automatically as soon as the bridge is ready and does not require a map to be loaded. ::: ## Initialization The Icons extension is initialized automatically with the North America CDN. To serve icons from a different region, call Icons.initialize()/>) with an IconCdnRegion. The default region is IconCdnRegion.na and the European CDN is IconCdnRegion.eu. ```swift // Re-initialize with the European CDN mapView.icons.initialize(region: .eu) { _ in // Ready to look up and fetch icons from the EU region. } ``` ## Icon Types Icons are organized into a hierarchy of types, subtypes, and categories. Each icon has an IconType that classifies it by its primary usage context. | Type | Description | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `IconType.categories` | Icons for map locations and amenities. Further classified by IconSubtype. | | `IconType.small` | Compact icons designed for Labels on maps. | | `IconType.input` | Icons for UI input elements. | | `IconType.metadata` | Fallback and miscellaneous icons. | ### Subtypes Icons with `IconType.categories` are further classified by IconSubtype: | Subtype | Description | | ----------------------- | ---------------------------------------------------------------------------------------- | | `IconSubtype.locations` | Location category icons organized by business type (e.g., Food, Fashion, Entertainment). | | `IconSubtype.amenities` | Amenity icons (e.g., Washrooms, Accessibility). | | `IconSubtype.general` | General purpose category icons. | | `IconSubtype.system` | System icons (e.g., Elevator, Escalator, Stairs). | ### Categories Location icons within `IconSubtype.locations` can be further filtered by IconCategory, which represents business domains such as `foodAndDrink`, `fashion`, `entertainment`, `hospital`, `transportation`, and many more. ### Icon Gallery The Icons API can be used to build a gallery of all available icons, organized by type. The following code sample fetches all icons of a given type and renders each one's SVG. A complete gallery example is available in IconGalleryViewController.swift. ```swift mapView.icons.getByType(type: .categories) { result in guard case .success(let icons) = result else { return } icons.forEach { icon in mapView.icons.fetchSvg(name: icon.name) { svgResult in guard case .success(let svg) = svgResult else { return } // Render the SVG, for example in a WKWebView cell. } } } ``` ## Looking Up Icons The Icons class provides several methods for finding icons. Each method delivers one or more MappedinIcon values through a `Result` callback. A MappedinIcon contains the icon's name, url, `type`, tags, and other metadata. ```swift // Look up a single icon by its unique name mapView.icons.getByName(name: "information") { result in if case .success(let icon) = result { print(icon.url) // Full CDN URL to the SVG file print(icon.tags) } } // Filter by type mapView.icons.getByType(type: .categories) { result in /* ... */ } // Filter by subtype mapView.icons.getBySubtype(subtype: .amenities) { result in /* ... */ } // Filter by business category mapView.icons.getByCategory(category: .foodAndDrink) { result in /* ... */ } // Search by tags (case-insensitive) mapView.icons.getByTags(tags: ["Washroom", "Restroom"]) { result in /* ... */ } // Get all icons mapView.icons.getAll { result in /* ... */ } ``` | Method | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | getByName()/>) | Returns a single MappedinIcon by its unique name. | | getByType()/>) | Returns all icons matching an IconType. | | getBySubtype()/>) | Returns all icons matching an IconSubtype. | | getByCategory()/>) | Returns all icons matching an IconCategory. | | getByTags()/>) | Returns all icons matching any of the provided tags. Case-insensitive. | | getAll()/>) | Returns every icon in the library. | | getSmallIcon()/>) | Returns the compact variant of an icon, if one exists. | ## Fetching and Displaying Icons The fetchSvg()/>) method retrieves the raw SVG markup for an icon from the CDN (or from the in-memory cache if it has been pre-fetched). The returned string can be rendered in any view that displays SVG content, such as a `WKWebView`. ```swift mapView.icons.fetchSvg(name: "information") { result in guard case .success(let svg) = result else { return } // svg is the raw SVG markup. Render it, for example inside a WKWebView. } ``` ## Styling Icons with currentColor Mappedin icon SVGs use `fill="currentColor"`. When rendering an icon in a `WKWebView`, the icon inherits the CSS `color` of its container, so the fill color can be set without modifying the SVG. Alternatively, the `currentColor` token can be replaced directly in the SVG string before rendering. ```swift mapView.icons.fetchSvg(name: "information") { result in guard case .success(let svg) = result else { return } let coloredSvg = svg.replacingOccurrences(of: "currentColor", with: "#ff5733") // Render coloredSvg. } ``` :::tip A complete example demonstrating recoloring icons in real time with a color picker can be found in the Mappedin iOS GitHub repo: ColorPickerViewController.swift ::: ## Pre-fetching Icons For applications that need icons available instantly without waiting for network requests, the Icons class provides several pre-fetching methods. Pre-fetched SVGs are stored in an in-memory cache and returned immediately by subsequent fetchSvg()/>) calls. ```swift // Pre-fetch specific icons by name mapView.icons.prefetch(names: ["information", "elevator-up", "book"]) { _ in // Check whether an icon is already cached mapView.icons.isCached(name: "information") { result in let cached = (try? result.get()) ?? false print("isCached = \(cached)") } } // Pre-fetch all icons of a given type mapView.icons.prefetchByType(type: .small) { _ in /* ... */ } // Pre-fetch all icons of a given subtype mapView.icons.prefetchBySubtype(subtype: .amenities) { _ in /* ... */ } // Pre-fetch all icons in a business category mapView.icons.prefetchByCategory(category: .foodAndDrink) { _ in /* ... */ } ``` | Method | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | prefetch()/>) | Pre-fetches a list of icons by name. | | prefetchByType()/>) | Pre-fetches all icons of a given IconType. | | prefetchBySubtype()/>) | Pre-fetches all icons of a given IconSubtype. | | prefetchByCategory()/>) | Pre-fetches all icons in a given IconCategory. | | isCached()/>) | Returns `true` if the icon's SVG is already in the cache. | | clearCache()/>) | Clears all cached SVG content. | :::tip A complete example demonstrating pre-fetching by name, type, subtype, and category, along with a comparison of cached versus uncached fetch performance, can be found in the Mappedin iOS GitHub repo: PrefetchDemoViewController.swift ::: ## Using Icons of Mappedin CMS Categories The category icons configured in the Mappedin Content Management System (CMS) are included in the icon library. The EnterpriseCategory.iconFromDefaultList property provides the icon name that maps directly to an icon in the library. When displaying icons on Labels, the compact Small icon variant works best. The getSmallIcon()/>) method or the MappedinIcon.smallIcon property returns the name of the small variant for a given icon. Because the SVGs use `fill="currentColor"`, the fill color can be replaced in the SVG string before passing it to the Label's icon appearance. The following code sample looks up the small icon for an enterprise location's first category, recolors it, and adds a label for each space of that location. ```swift let category = location.categories.first.flatMap { categoriesById[$0] } let iconName = category?.iconFromDefaultList ?? "information" let color = category?.color ?? "black" mapView.icons.getByName(name: iconName) { iconResult in let smallIconName: String switch iconResult { case .success(let icon): smallIconName = icon.smallIcon ?? "small-information-desk" case .failure: smallIconName = "small-information-desk" } mapView.icons.fetchSvg(name: smallIconName) { svgResult in guard case .success(let svg) = svgResult else { return } let coloredSvg = svg.replacingOccurrences(of: "currentColor", with: "#fafafa") let appearance = LabelAppearance(color: color, icon: coloredSvg, iconPadding: 10) let labelOptions = AddLabelOptions(labelAppearance: appearance) for spaceId in location.spaces { guard let space = spacesById[spaceId] else { continue } mapView.labels.add(target: space, text: location.name, options: labelOptions) } } } ``` :::tip A complete example that loads the Mappedin Demo Enterprise map and labels each enterprise location with its category icon can be found in the Mappedin iOS GitHub repo: EnterpriseCategoryIconsViewController.swift ::: ### Images, Textures & Colors # Images, Textures & Colors > !Images and Textures Images, textures and colors can enhance the fidelity of an indoor map. They can be used to add custom branding, highlight important features, or provide additional information to users. Images can be placed on any Anchorable target on the map and given a `verticalOffset` to control the height at which the image is displayed. Textures can be applied to the top or sides of a MapObject, Space, Door or Wall. ## Requirements & Considerations JPEG and PNG images are supported for both images and textures. It's important to consider the size of all unique image files displayed on a map at one time. Using many unique images may cause instability on mobile devices with limited GPU memory. Mappedin SDKs will cache and reuse images that have the same URL, resulting in reduced memory usage. The following calculations illustrates how much memory is used for a given image: **Formula:** `width * height * 4 bytes/pixel = memory used` **512 x 512 Pixel Image:** `512px * 512px * 4 bytes/pixel = 1MB` **4096 x 4096 Pixel Image:** `4096px * 4096px * 4 bytes/pixel = 64MB` ## Creating Images for Spaces When creating images to be used as the floor of a space, it's important that the image dimensions match the space to avoid it leaking into adjacent spaces. An SVG export of the map can be useful to determine the correct dimensions. The following steps demonstrate how to create an image that matches a space's dimensions using Figma. Similar steps could be used in other image editing tools that support the SVG format. 1. Log into Mappedin Maker and select the map you want to create an image for. 2. Click on the **Download** button and choose `Download as SVG`. 3. Import the SVG into Figma (**File** > **Place Image**). 4. Select the geometry to create the image for by clicking near the center of it (not the edge). 5. Choose **File** > **Place** Image to place the image you want to use as the floor. 6. Click the geometry to fill. 7. Click **Export** (bottom right). 8. Choose format (PNG or JPEG), size and export. The exported image should match the dimensions of the space. ## Images The Image3D class is accessed through MapView.image3D and used to add and remove images to and from the map. Images can be placed on any Door, Space, or Coordinate. AddImageOptions is used to configure the image and specifies its width, height, rotation, vertical offset and whether the image is rotated to face the camera. The following example places an image on a Space named "Arena". :::tip A complete example demonstrating Images can be found in the Mappedin iOS GitHub repo: Image3DDemoViewController.swift ::: ```swift mapView.mapData.getByType(.space) { [weak self] (result: Result<[Space], Error>) in guard let self = self else { return } if case .success(let spaces) = result { // Find the Arena Floor space self.arenaFloor = spaces.first { $0.name == "Arena Floor" } // Add the default hockey image to the arena floor if let floor = self.arenaFloor { let imageName = self.getImageName(for: 0) let opts = AddImageOptions( height: 448 * self.pixelsToMeters, width: 1014 * self.pixelsToMeters, flipImageToFaceCamera: false, rotation: 239.0, verticalOffset: 1.0 ) self.mapView.image3D.add(target: floor, url: imageName, options: opts) { _ in } } } } // Helper function to get the image resource. private func getImageName(for index: Int) -> String { let imageName: String switch index { case 0: imageName = "arena_hockey" case 1: imageName = "arena_basketball" case 2: imageName = "arena_concert" default: imageName = "arena_hockey" } // Use mappedin-asset:// URL scheme. if let assetUrl = Bundle.main.mappedinAssetURL(forResource: imageName, withExtension: "png") { return assetUrl } // Fallback to just the name if image not found. return imageName } ``` ## Textures & Colors Walls, doors, floors and objects can be given textures or colors, which can be applied individually to the top or sides. The GeometryState is used to configure the texture and color, which is applied by calling MapView.updateState(). When applying textures to walls it is important to set Show3DMapOptions.shadingAndOutlines to false when calling MapView.show3dMap()/>). This will prevent lines from being drawn between the top and side textures. Be sure to review the Requirements & Considerations section before applying textures. :::tip A complete example demonstrating Colors & Textures can be found in the Mappedin iOS GitHub repo: ColorsAndTexturesDemoViewController.swift ::: ### Doors The top and sides of a door can be given textures or colors. The example below demonstrates how to set the color of all interior and exterior doors. Doors can also be made visible on an individual basis by passing in a Door object to MapView.updateState()/>). ```swift // Make interior doors visible, sides brown and top yellow mapView.updateState( doors: .interior, state: DoorsUpdateState( color: "brown", opacity: 0.6, visible: true, topColor: "yellow" ) ) // Make exterior doors visible, sides black and top blue mapView.updateState( doors: .exterior, state: DoorsUpdateState( color: "black", opacity: 0.6, visible: true, topColor: "blue" ) ) ``` ### Objects Objects can be given textures or colors, which can be applied individually to the top and sides. The following example demonstrates how to set the texture of the side and color of the top of all objects. ```swift // Update all objects with side texture and top color mapView.mapData.getByType(.mapObject) { [weak self] (result: Result<[MapObject], Error>) in guard let self = self else { return } switch result { case .success(let objects): for object in objects { self.mapView.updateState( mapObject: object, state: GeometryUpdateState( texture: GeometryUpdateState.Texture(url: objectSideURL), topColor: "#9DB2BF" ) ) } case .failure(let error): print("Error getting objects: \(error)") } } ``` ### Spaces The floor of a space can be given a texture or color. When creating an image for the floor of a space, it's important that the image dimensions match the space to avoid it leaking into adjacent spaces. Refer to the Creating Images for Spaces section for more information. The following example demonstrates how to set the texture of the floor for all spaces. ```swift // Update all spaces with floor texture mapView.mapData.getByType(.space) { [weak self] (result: Result<[Space], Error>) in guard let self = self else { return } switch result { case .success(let spaces): for space in spaces { self.mapView.updateState( space: space, state: GeometryUpdateState( topTexture: GeometryUpdateState.Texture(url: floorURL) ) ) } case .failure(let error): print("Error getting spaces: \(error)") } } ``` ### Walls The exterior and interior walls of a building can be targeted with different textures and colors. The following example demonstrates how to set the texture of an exterior wall and the colors of interior walls. Note that both types of walls support textures and colors. ```swift // Update interior walls with colors mapView.updateState( walls: .interior, state: WallsUpdateState( color: "#526D82", topColor: "#27374D" ) ) // Update exterior walls with textures mapView.updateState( walls: .exterior, state: WallsUpdateState( texture: WallsTexture(url: exteriorWallURL), topTexture: WallsTexture(url: exteriorWallURL) ) ) ``` ### Interactivity # Interactivity > This guide explains how to enhance a map's interactivity by making components clickable. Interactivity can greatly improve the user experience and user engagement with a map. :::tip A complete example demonstrating interactivity can be found in the Mappedin iOS GitHub repo: InteractivityDemoViewController.swift ::: !Mappedin v6 Interactivity ## Interactive Spaces A Space can be set to interactive to allow a user to click on it. When interactivity is enabled for a space, it enables a hover effect that highlights the space when the cursor is moved over the space. The following code enables interactivity for all spaces: ```swift // Set all spaces to be interactive so they can be clicked mapView.mapData.getByType(.space) { [weak self] (result: Result<[Space], Error>) in guard let self = self else { return } if case .success(let spaces) = result { spaces.forEach { space in self.mapView.updateState(target: space, state: ["interactive": true]) { _ in } } } } ``` ## Interactive Labels A Label added to the map can be set to interactive to allow users to click on it and have the click event captured by an app. The code sample below adds a label to each space with a name and sets the label's interactivity to true. ```swift // Add interactive labels to all spaces with names. self.mapView.mapData.getByType(.space) { (spacesResult: Result<[Space], Error>) in if case .success(let spaces) = spacesResult { spaces.forEach { space in guard !space.name.isEmpty else { return } self.mapView.labels.add( target: space, text: space.name, options: AddLabelOptions(interactive: true) ) } } } ``` After enabling interactivity, click events on the label can be captured using Mapview.on(Events.click)/>). The Events object passed into the `on` method contains an array of labels that were clicked. The following code sample captures the click event and checks if the user clicked on a label. If they did, it logs the id of the label that was clicked and removes it from the map. ```swift self?.mapView.on(Events.click) { clickPayload in if let click = clickPayload, let label = click.labels?.first { print("removing label: \(label.text)") self?.mapView.labels.remove(label: label) } } ``` ## Interactive Markers A Marker can be set to interactive, allowing it to be clickable and have an app act on the click event. The `interactive` property of a Marker can be set to `true`, `false` or `pointer-events-auto`. - `false` - The Marker is not interactive. - `true` - The Marker is interactive and the click event is captured by the `mapView.on(Events.click)` event handler. - `pointer-events-auto` - The Marker is interactive and mouse events are passed to the Marker's HTML content. The code sample below adds a marker for each annotation and sets the marker's interactivity to true. ```swift mapView.mapData.getByType(.annotation) { [weak self] (result: Result<[Annotation], Error>) in guard let self = self else { return } if case .success(let annotations) = result { let opts = AddMarkerOptions( interactive: .True, placement: .single(.center), rank: .tier(.high) ) // Add markers for all annotations that have icons annotations.forEach { annotation in let iconUrl = annotation.icon?.url ?? "" let markerHtml = """
\(annotation.name)
""" self.mapView.markers.add(target: annotation, html: markerHtml, options: opts) { _ in } } } } ``` After enabling interactivity, click events on the label can be captured using Mapview.on(Events.click)/>). The Events object passed into the `on` method contains an array of markers that were clicked. The following code sample captures the click event and checks if the user clicked on a marker. If they did, it logs the id of the marker that was clicked and removes it from the map. ```swift // Remove markers that are clicked on self.mapView.on(Events.click) { [weak self] clickPayload in guard let self = self, let clickedMarker = clickPayload?.markers?.first else { return } self.mapView.markers.remove(marker: clickedMarker) { _ in } } ``` ## Handling Click Events Mappedin SDK for iOS enables capturing and responding to click events through its event handling system. After enabling interactivity, click events can be captured using Mapview.on(Events.click)/>). The Events `click` event passes ClickPayload that contains the objects the user clicked on. It allows developers to obtain information about user interactions with various elements on the map, such as: 1. **coordinate** - A Coordinate object that contains the latitude and longitude of the point where the user clicked. 2. **facades** - An array of Facade objects that were clicked. 3. **floors** - An array of Floor objects. If the map contains multiple floors, floors under the click point are included. 4. **labels** - An array of Label objects that were clicked. 5. **markers** - An array of Marker objects that were clicked. 6. **models** - An array of Model objects that were clicked. 7. **objects** - An array of MapObject objects that were clicked. 8. **paths** - An array of Path objects that were clicked. 9. **pointerEvent** - A PointerEvent object that contains the pointer event data. 10. **shapes** - An array of Shape objects that were clicked. 11. **spaces** - An array of Space objects that were clicked. Use `Mapview.on(Events.Click)` to capture click as shown below. ```swift // Set up click listener mapView.on(Events.Click) { clickPayload -> clickPayload ?: return@on handleClick(clickPayload) } private func handleClick(_ clickPayload: ClickPayload) { var message = "" // Use the map name as the title (from floors) let title = clickPayload.floors?.first?.name ?? "Map Click" // If a label was clicked, add its text to the message if let labels = clickPayload.labels, !labels.isEmpty { message.append("Label Clicked: ") message.append(labels.first?.text ?? "") message.append("\n") } // If a space was clicked, add its location name to the message if let spaces = clickPayload.spaces, !spaces.isEmpty { message.append("Space clicked: ") message.append(spaces.first?.name ?? "") message.append("\n") } // If a path was clicked, add it to the message if let paths = clickPayload.paths, !paths.isEmpty { message.append("You clicked a path.\n") } // Add the coordinates clicked to the message message.append("Coordinate Clicked: \nLatitude: ") message.append(clickPayload.coordinate.latitude.description) message.append("\nLongitude: ") message.append(clickPayload.coordinate.longitude.description) print("title: \(title), message: \(message)") } ``` :::tip A complete example demonstrating interactivity can be found in the Mappedin iOS GitHub repo: InteractivityDemoViewController.swift ::: ### Labels # Labels > Labels display text and images anchored to specific points on a map. They rotate, show, or hide based on priority and zoom level, providing information about location names, points of interest, and more. Effective labels help apps convey additional information, such as room names, points of interest, main entrances, or other useful contextual details. :::tip A complete example demonstrating Labels can be found in the Mappedin iOS GitHub repo: LabelsDemoViewController.swift ::: !Mappedin v6 Labels :::tip Note that the MapView class instantiates the Labels class and exposes it as MapView.labels. Use MapView.labels to utilize Labels' methods. ::: ## Adding & Removing Individual Labels Labels can be added individually to a map by calling Labels.add()/>). A label can be added to any Anchorable target. Refer to the Labels.add()/>) documentation for the complete list of targets. The following code sample adds an interactive (clickable) label to each space that has a name: ```swift mapView.mapData.getByType(MapDataType.space) { [weak self] (result: Result<[Space], Error>) in switch result { case .success(let spaces): spaces.forEach { space in guard !space.name.isEmpty else { return } let color = self?.colors.randomElement() let appearance = LabelAppearance(color: color, icon: space.images.first?.url ?? self?.svgIcon) self?.mapView.labels.add(target: space, text: space.name, options: AddLabelOptions(labelAppearance: appearance, interactive: true)) } case .failure(let e): print("getByType error: \(e)") } } ``` Labels can be removed by using the Labels.remove(label)/>) method, passing in the label to be removed as shown below: ```swift mapView.labels.remove(label: label) ``` ## Interactive Labels Labels added to the map can be set to interactive to allow users to click on them. For more information, refer to the Interactive Labels section of the Interactivity Guide. ## Label Icons Icons can be added to labels in `SVG`, `PNG`, `JPEG` and `WebP` formats. Icons are clipped in a circle to prevent overflow. Three clipping methods of `contain`, `fill` and `cover` can be set in the LabelAppearance.iconFit parameter with `contain` being the default. | Fill | Contain | Cover (default) | | ----------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------- | | !Floating Label fill | !Floating Label contain | !Floating Label cover | The LabelAppearance.iconPadding property sets the amount of space between the icon and the border. The icon may shrink based on this value. | `padding: 0` | `padding: 10` | | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | | !Floating Label fill with 0 padding | !Floating Label fill with 10 padding | Label icons can be configured to be shown or hidden based on the current zoom level using LabelAppearance.iconVisibleAtZoomLevel. A value below 0 will result in the icon always being hidden. Setting this value to 0 ensures icons show up at maxZoom (fully zoomed in) and 1 configures them to always be displayed. ## Label Appearance Labels can have their appearance styled to match the visual theme of an app or to make groups of labels easily distinguishable from one another. The following declaration of LabelAppearance describes these customisable attributes. | Option | Type | Description | Default | | ------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | `margin` | `number` | Margin around the label text and pin in pixels. This will affect label density. Minimum is 6px. | 6 | | `maxLines` | `number` | Number of lines to display when text spans multiple lines. | 2 | | `textSize` | `number` | Text size in pixels | 11.5 | | `maxWidth` | `number` | Maximum width of text in pixels. | 150 | | `lineHeight` | `number` | Line height sets the height of a line box. It's commonly used to set the distance between lines of text. | 1.2 | | `color` | ColorString | A ColorString for the label text and pin. | `#333` | | `outlineColor` | ColorString | A ColorString for the outline around the label text and pin. | `white` | | `textColor` | ColorString | A ColorString representing just the text color. Defaults to the same as `color`. | - | | `textOutlineColor` | ColorString | A ColorString representing just the text outline. Defaults to the same as `outlineColor`. | - | | `pinColor` | ColorString | A ColorString representing just the pin color. Defaults to the same as `color`. | - | | `pinOutlineColor` | ColorString | A ColorString representing just the pin outline. Defaults to the same as `outlineColor`. | - | | `pinColorInactive` | ColorString | A ColorString representing just the pin color when the label is inactive. Defaults to the same as `pinColor`. | - | | `pinOutlineColorInactive` | ColorString | A | | `icon` | `string` | An icon to be placed inside the label pin. Can be an SVG string or a path to a PNG or JPEG. | - | | `iconSize` | `number` | Size of the icon in pixels. Requires `icon` to be set. | 20 | | `iconScale` | `number` \| [Interpolation | Scale the icon uniformly. Specify a number or an Interpolation object. | 1 | | `iconPadding` | `number` | Padding between the icon and the marker's border in pixels. | 2 | | `iconFit` | `'fill'` \| `'contain'` \| `'cover'` | How the icon should fit inside the marker. Options: `fill` (stretch to fill), `cover` (maintain aspect ratio and fill), `contain` (maintain aspect ratio and fit inside). | `cover` | | `iconOverflow` | `'visible'` \| `'hidden'` | Whether the icon should overflow the circle of the marker. Options: `visible`, `hidden`. | `hidden` | | `iconVisibleAtZoomLevel` | `number` | Defines the zoom level at which the icon becomes visible. Infinity ensures never visible, -Infinity ensures always visible. | -Infinity | The code sample below demonstrates how to use some label styling options. Labels are added to each space with a name using the space's image if one is set, or a default SVG icon if one is not. It also sets the label text color to a random color from a list of colors. ```swift let svgIcon = """ """ let colors = ["#FF610A", "#4248ff", "#891244", "#219ED4"] mapView.mapData.getByType(MapDataType.space) { [weak self] (result: Result<[Space], Error>) in switch result { case .success(let spaces): spaces.forEach { space in guard !space.name.isEmpty else { return } let color = self?.colors.randomElement() let appearance = LabelAppearance(color: color, icon: space.images.first?.url ?? self?.svgIcon) self?.mapView.labels.add(target: space, text: space.name, options: AddLabelOptions(labelAppearance: appearance, interactive: true)) } case .failure(let e): print("getByType error: \(e)") } } ``` ## Label Ranking Ranking can be added to labels to control which label will be shown when more than one label occupies the same space. The label with the highest rank will be shown. If labels do not overlap, ranking will have no effect. Rank values are low, medium, high, and always-visible and are defined in CollisionRankingTier. The code below add a label with a high ranking where a user clicks on the map. ```swift mapView.mapView.on(Events.click) { [weak self] clickPayload in guard let self = self, let clickPayload = clickPayload else { return } mapView.labels.add(target: clickPayload.coordinate, text: "I'm a high ranking label!", options: AddLabelOptions(rank: CollisionRankingTier.high)) } ``` ## Enabling and Disabling Labels Labels can be dynamically enabled or disabled using `mapView.updateState()`/>). When a label is disabled, it will be hidden from view but remain in the map's memory. This is useful for managing label visibility based on conditions like zoom level or user interaction, without the overhead of repeatedly adding and removing labels. Use `mapView.getState()`>) to check a label's current state, which returns the label's current properties including its enabled status. Here's an example on how to enable/disable labels on click: ```swift mapView.on(Events.click) { [weak self] clickPayload in guard let self = self, let clickPayload = clickPayload else { return } if self.label == nil { // Add the label if it doesn't exist. mapView.labels.add(target: clickPayload.coordinate, text: "Click to Toggle") { result in if case .success(let createdLabel) = result { self.label = createdLabel } } } else if let currentLabel = self.label { // Toggle the label. mapView.getState(label: currentLabel) { result in if case .success(let state) = result, let labelState = state { print(labelState) self.mapView.updateState( label: currentLabel, state: LabelUpdateState(enabled: !labelState.enabled) ) } } } } ``` :::tip A complete example demonstrating Labels can be found in the Mappedin iOS GitHub repo: LabelsDemoViewController.swift ::: ## Flat Labels with Text3D Text3D creates three-dimensional text labels that are rendered directly on the map. These labels are perfect for displaying space names, room numbers, or any text that needs to be visible from any angle in the 3D environment. Text3D labels are added to the map using a Coordinate using the Text3D.add()/>) method. The look of these labels can be customized using AddText3DPointOptions, which supports custom font size, color, outline width and color as well as stroke width and color. :::tip A complete example demonstrating Labels can be found in the Mappedin iOS GitHub repo: Text3DDemoViewController.swift ::: The code below demonstrates how to add a Text3D label to the map. ```swift let appearance = InitializeText3DState( color: "#ffa500", fontSize: 2.0, outlineColor: "#000000", outlineWidth: 0.7, strokeColor: "#00ffff", strokeWidth: 0.05 ) let options = AddText3DPointOptions( appearance: appearance, rotation: 180.0 ) self.mapView.text3D.add( target: coordinate, content: "Hello, world!", options: options ) { result in switch result { case .success(let text3DView): if let view = text3DView { print("Text3D added with id: \(view.id)") } case .failure(let error): print("Failed to add Text3D: \(error)") } } ``` ### Location Profiles & Categories # Location Profiles & Categories > A Location refers to something that represents a physical position on the map, such as an annotation, area, connection (e.g. stairs or elevator), door, point of interest or space. Locations may have a LocationProfile attached to them. The profiles contain information about the location such as its name, description, social media links, opening hours, logo and more. A LocationProfile can have zero to many LocationCategory attached to it. A LocationCategory may have a child or parent category. For example, the parent `Food & Drink` LocationCategory has children such as `Bar`, `Cafe`, `Food Court` and `Mediterranean Cuisine`. :::tip A complete example demonstrating location profiles and categories can be found in the Mappedin iOS GitHub repo: LocationsDemoViewController.swift ::: ## LocationProfile A LocationProfile can be accessed by using the `locationProfiles` property of a location. For example, use Space.locationProfiles to access every LocationProfile of a space. Every LocationProfile can also be access using the MapData.getByType()/>) method as shown below. ```swift mapView.mapData.getByType(MapDataType.locationProfile) { [weak self] (result: Result<[LocationProfile], Error>) in switch result { case .success(let locationProfiles): locationProfiles.forEach { locationProfile in print(locationProfile.name ?? "") } case .failure(let e): print("getByType error: \(e)") } } ``` ## LocationCategory To access the LocationCategory of a LocationProfile, use the LocationProfile.categories property, which contains a list of IDs of its LocationCategory. Then use the MapData.getById()/>) method to get the LocationCategory by its ID. Note that a LocationCategory can have either parent or child categories. These are accessible using the LocationCategory.parent and LocationCategory.children properties respectively. Every LocationCategory can also be access using the MapData.getByType() method as shown below. ```swift mapView.mapData.getByType(MapDataType.locationCategory) { [weak self] (result: Result<[LocationCategory], Error>) in switch result { case .success(let locationCategories): locationCategories.forEach { locationCategory in print(locationCategory.name) } case .failure(let e): print("getByType error: \(e)") } } ``` :::tip A complete example demonstrating location profiles and categories can be found in the Mappedin iOS GitHub repo: LocationsDemoViewController.swift ::: ### Markers # Markers > Mappedin SDK for iOS allows adding and removing Marker on a map. Markers are elements containing HTML that the Mappedin SDK anchors to any Anchorable target. They are automatically rotated and repositioned when the camera moves. :::tip A complete example demonstrating Markers can be found in the Mappedin iOS GitHub repo: MarkersDemoViewController.swift ::: !Mappedin JS v6 Markers :::tip Note that the MapView class instantiates the Markers class and exposes it as MapView.markers. Use MapView.markers to utilize Markers' methods. ::: ## Creating Markers Markers are added to the map by referencing a target that can be a Door, Space, Coordinate or any Anchorable target. The following code sample adds a marker to a coordinate. ```swift let opts = AddMarkerOptions( interactive: .True, placement: .single(.center), rank: .tier(.high) ) let markerHtml = """
This is a Marker!
""" self.mapView.markers.add(target: coordinate, html: markerHtml, options: opts) { _ in } ``` ## Placement Marker placement is set using the `options` argument of Markers.add()/>). Options accepts a AddMarkerOptions value, which has a member named `placement`. When provided, the point of the Marker described by the Placement is placed next to the target. For example, using `center` will place the Marker's center at the target. Both a single placement and an array of placements can be provided. When an array is provided, the Marker is placed at the first target that has space available to display the Marker. Placement positions are defined in MarkerPlacement which contains 9 values. The placement points are as follows, with the default being center. ### MarkerPlacement - `center` - `top` - `left` - `bottom` - `right` - `topLeft` - `topRight` - `bottomLeft` - `bottomRight` Marker content can be styled using CSS that references the placement of the marker. This can allow the creation of a tooltip or bubble-style marker that points to its target. This is done by using the CSS Selector: ```css .mappedin-marker[data-placement='']; ``` Where `` is the placement position. Here is an example of adding a triangle pointer to the Marker's target of `left`: ```CSS .marker:before { content: ''; width: 0; height: 0; top: calc(50% - 10px); left: -10px; z-index: 1; position: absolute; border-bottom: 10px solid transparent; border-top: 10px solid transparent; z-index: -1; } .mappedin-marker[data-placement="left"] .marker:before { left: auto; right: -5px; border-left: 10px solid #333333; } ``` ## Removing Markers Markers can be removed individually by using the Markers.remove(marker)/>) method, passing in the marker to be removed as shown below. ```swift mapView.markers.remove(marker: marker) { _ in } ``` To remove all markers from a map, call Markers.removeAll()/>). ```swift mapView.markers.removeAll() { _ in } ``` ## Marker Rank Ranking can be added to markers to control which marker will be shown when more than one marker occupies the same space. The marker with the highest rank will be shown. If markers do not overlap, ranking will have no effect. Rank values `low`, `medium`, `high` and `always-visible` as defined in CollisionRankingTier. The code below adds markers where a user clicks on the map. The marker rank is cycled with each click. If the user clicks and adds multiple markers in the same location, the marker with the highest rank is shown and lower ranking markers are hidden. ```swift // Ranks for marker collision priority - cycles through medium, high, always-visible private let ranks: [CollisionRankingTier] = [.medium, .high, .alwaysVisible] private var rankIndex = 0 ... // Act on the click event and add a marker with a cycling rank. // Observe how higher ranking markers are shown when they collide with lower ranking markers. mapView.on(Events.click) { [weak self] clickPayload in guard let self = self, let clickPayload = clickPayload else { return } let currentRank = self.ranks[self.rankIndex] let rankName = currentRank.rawValue let markerTemplate = """

Marker Rank: \(rankName)

""" // Add a marker with the current rank at the clicked coordinate. self.mapView.markers.add( target: clickPayload.coordinate, html: markerTemplate, options: AddMarkerOptions(rank: .tier(currentRank)) ) // Cycle to the next rank self.rankIndex += 1 if self.rankIndex == self.ranks.count { self.rankIndex = 0 } } ``` ## Moving Markers Markers can be repositioned after they have been added to a map. There are two ways of doing this: - Markers.setPosition()/>) instantly repositions a Marker - Markers.animateTo()/>) animates a Marker to a new position The Marker's position can be set to a Coordinate, Space or Door. The `animateTo` method takes an `options` parameter of AnimationOptions that defines the animation duration and EasingFunction, which can be EasingFunction.linear, EasingFunction.easeIn, EasingFunction.easeOut or EasingFunction.easeInOut. - **Linear**: This function implies a constant rate of change. It means the animation proceeds at the same speed from start to end. There's no acceleration or deceleration, giving a very mechanical feel. - **Ease In**: This function causes the animation to start slowly and then speed up as it progresses. Initially, there's gradual acceleration, and as the function moves forward, the rate of change increases. - **Ease Out**: Contrary to ease-in, ease-out makes the animation start quickly and then slow down towards the end. It begins with a faster rate of change and gradually decelerates. - **Ease In Out**: This function combines both ease-in and ease-out. The animation starts slowly, speeds up in the middle, and then slows down again towards the end. It offers a balance of acceleration and deceleration. The code samples below displays a custom Marker when the map is first clicked. When the map is clicked again, the Marker is animated to the clicked location using the default animation options. ```swift private var marker: Marker? ... mapView.on(Events.click) { [weak self] clickPayload in guard let self = self, let clickPayload = clickPayload else { return } if let existingMarker = self.marker { // Animate existing marker to new position self.mapView.markers.animateTo(marker: existingMarker, target: clickPayload.coordinate) } else { // Add new marker at clicked position let options = AddMarkerOptions( interactive: .True, placement: .single(.right) ) self.mapView.markers.add(target: clickPayload.coordinate, html: "Marker", options: options) { result in if case .success(let newMarker) = result { self.marker = newMarker } } } } ``` ## Enabling and Disabling Markers Markers can be dynamically enabled or disabled using MapView.updateState()/>). When a marker is disabled, it will be hidden from view but remain in the map's memory. This is useful for managing marker visibility based on conditions like zoom level or user interaction, without the overhead of repeatedly adding and removing markers. Use MapView.getState()/>) to check a marker's current state, which returns the marker's current properties including its enabled status. Here's an example on how to enable/disable markers on click: ```swift private var marker: Marker? ... mapView.on(Events.click) { [weak self] clickPayload in guard let self = self, let clickPayload = clickPayload else { return } if self.marker == nil { // First click - add marker print("Adding marker at: \(coordinate)") self.mapView.markers.add( target: clickPayload.coordinate, html: "
Enabled Marker!
" ) { result in if case .success(let newMarker) = result { self.marker = newMarker } } } else { // Get current state of marker self.mapView.getState(marker: self.marker!) { result in if case .success(let markerState) = result { print("Current Marker State: \(String(describing: markerState))") // Toggle enabled state let newState = !(markerState?.enabled ?? true) self.mapView.updateState(marker: self.marker!, state: MarkerUpdateState(enabled: newState)) print("Marker is now \(newState ? "enabled" : "disabled")") } } } } ``` :::tip A complete example demonstrating Markers can be found in the Mappedin iOS GitHub repo: MarkersDemoViewController.swift ::: ### Migration Guide # Migration Guide Mappedin SDK for iOS version 6.0 is a major release that includes a number of changes to the SDK. It uses a GeoJSON-based rendering engine, rebuilt from the ground up. It works in unison with MapLibre which enables outdoor base maps as well as data visualization features. By using GeoJSON as the core data format, v6 can integrate with existing external data sources. This guide explains the steps needed to migrate from version 5. ## Initialization Changes The options for getting a map have changed. `getVenue()` has been replaced by `getMapData()`. v5 Initialization ```swift // See Trial API key Terms and Conditions // https://developer.mappedin.com/api-keys/ mapView.loadVenue(options: MPIOptions.Init( clientId: "5eab30aa91b055001a68e996", clientSecret: "RJyRXKcryCMy4erZqqCbuB1NbR66QTGNXVE0x3Pg6oCIlUR1", venue: "mappedin-demo-mall" ), showVenueOptions: MPIOptions.ShowVenue(multiBufferRendering: true, outdoorView: MPIOptions.OutdoorView(enabled: true), shadingAndOutlines: true)) } ``` v6 Initialization ```swift // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps let options = GetMapDataWithCredentialsOptions( key: "5eab30aa91b055001a68e996", secret: "RJyRXKcryCMy4erZqqCbuB1NbR66QTGNXVE0x3Pg6oCIlUR1", mapId: "mappedin-demo-mall" ) // Load the map data. mapView.getMapData(options: options) { [weak self] r in guard let self = self else { return } if case .success = r { print("getMapData success") // Display the map. self.mapView.show3dMap(options: Show3DMapOptions()) { r2 in if case .success = r2 { print("show3dMap success - Map displayed") } else if case .failure(let error) = r2 { print("show3dMap error: \(error)") } } } else if case .failure(let error) = r { print("getMapData error: \(error)") } } ``` ## Component Access `MPIData.` has been replaced by `MapData.getByType()` For example: In v5 access nodes using `MPIData.nodes`, which contains an array of `Node` objects. In v6 access nodes using `mapData.getByType(MapDataType.Node)`, which returns an array of `Node` objects. `Mappedin.getCollectionItemById` is replaced by `mapData.getById(, id)` The following classes have been redesigned. Refer to the chart below for a mapping of similar classes. | v5 Component | v6 Component | | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | MPIPolygon | Space | | MPIMapGroup | FloorStack | | MPIMap | Floor | | MPICoordinates | Coordinate | | MPINode | Node | MPIState no longer exists. ## UI Components MPIFloatingLabelManager has been replaced with Labels. MPIFloatingLabelManager.labelAllLocations()/>) will be replaced with `MapView.auto()`. MPIMarkerManager has been replaced with Markers. Tooltips/>) have not been carried over from v5 and Markers should be used instead. ## Updating UI Components `setPolygonColor`, `clearPolygonColor` and other methods of changing the state of UI objects on the map is now performed using MapView.updateState()/>), which also supports `initial` for returning properties to their original state. MPICameraManager.animate()-3747r/>) has been renamed to Camera.animateTo()/>). MPIMarkerManager.animate()/>) has been renamed to Markers.animateTo()/>). ## Interactivity MapView.addInteractivePolygon()/>) has been replaced by updating the state of each space to be interactive as shown below. ```swift // Set all spaces to be interactive so they can be clicked mapView.mapData.getByType(.space) { [weak self] (result: Result<[Space], Error>) in guard let self = self else { return } if case .success(let spaces) = result { spaces.forEach { space in self.mapView.updateState(space: space, state: GeometryUpdateState(interactive: true)) } } } ``` Event names passed from `MapView.on()` have been changed: | v5 Event | v 6 Event | | --------------------------------------------------------------- | -------------------------- | | `E_SDK_EVENT.CLICK` | `Events.click` | | `E_SDK_EVENT.MAP_CHANGED , E_SDK_EVENT.MAP_CHANGED_WITH_REASON` | `Events.floorChange` | | `E_SDK_EVENT.OUTDOOR_VIEW_LOADED` | `Events.outdoorViewLoaded` | ## Stacked Maps Stacked Maps APIs have been replaced with Multi Floor View along with the ability to control the visibility and altitude of floors. Refer to the Multi Floor View and Stacked Maps guides for more information. ## Map Metadata Classes containing enterprise map metadata have been renamed. | v5 Component | v6 Component | | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | MPICategory | EnterpriseCategory | | MPILocation | EnterpriseLocation | | MPIVenue | EnterpriseVenue | ## Wayfinding & Directions MapData.getDirections()-642dy/>) only supports getting directions between two locations. For multi destination directions, use MapData.getDirectionsMultiDestination()/>). MPIJourneyManager is now Navigation. ## Camera v5 MPICameraManager.tilt in radians is replaced by v6 Camera.pitch/>) in degrees. `Camera.minPitch` and `Camera.maxPitch` are now available to set the minimum and maximum pitch values. v5 MPICameraManager.rotation in radians is replace in v6 by Camera.bearing/>) in degrees Clockwise rotation is now positive. v5 `Camera.zoom`, `Camera.minZoom` and `Camera.maxZoom` used meters from ground level. In v6 these now use mercator zoom level units. The following camera methods and accessors have been renamed. | v5 Method | v6 Method | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | MPICameraManager.position() | Camera.center()/>) | | MPICameraManager.setSafeAreaInsets()/>) | Camera.setScreenOffsets()/>) | | MPICameraManager.safeAreaInsets() | Camera.screenOffsets()/>) | | MPIMapViewDelegate.onCameraChanged()/>) | MapView.on(...)/>) | ## Search MPISearchManager has been changed to Search. The results returned in SearchResult are more structured: ```swift struct SearchResult { let enterpriseCategories: [SearchResultEnterpriseCategory]? let enterpriseLocations: [SearchResultEnterpriseLocations]? let places: [SearchResultPlaces] } ``` ## Multi Language The methods to work with maps in multiple languages have been changed. v6 ```swift // Specify a non default language when getting map data let options = GetMapDataWithCredentialsOptions( key: "key", secret: "secret", mapId: "mapId", language: "en" ) // Get supported languages mapView.mapData.getByType(.enterpriseVenue) { [weak self] (result: Result<[EnterpriseVenue], Error>) in guard let self = self else { return } if case .success(let enterpriseVenue) = result { enterpriseVenue.languages.forEach { language in print("Language: \(language)") } } } // Change language mapData.changeLanguage("es") { result in switch result { case .success: print("Map language changed to Spanish") case .failure(let error): print("Error: \(error)") } } ``` ## Blue Dot The Blue Dot system has been redesigned in v6. MPIBlueDotManager has been replaced by BlueDot, accessed through MapView.blueDot. The v6 API provides more granular control over the Blue Dot's appearance, a richer event system and asynchronous result callbacks. :::tip Complete examples demonstrating Blue Dot can be found in the Mappedin iOS GitHub repo: - v5: BlueDotVC.swift - v6: BlueDotDemoViewController.swift ::: ### Class and API Changes | v5 Component | v6 Component | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | MPIBlueDotManager | BlueDot | | MPIOptions.BlueDot | BlueDotOptions | | MPIPosition / MPICoordinates | BlueDotPositionUpdate | | `MPIBlueDotManager.setState(state:)` | BlueDot.follow()/>) with FollowMode | | MPIState.FOLLOW / MPIState.EXPLORE | FollowMode (`.positionOnly`, `.positionAndHeading`, `.positionAndPathDirection`, `nil`) | ### Enabling Blue Dot In v5, the Blue Dot is enabled through MPIBlueDotManager with minimal options. In v6, BlueDot.enable()/>) accepts BlueDotOptions to customize the appearance and provides an asynchronous result callback. v5: ```swift mapView?.blueDotManager.enable(options: MPIOptions.BlueDot()) ``` v6: ```swift let options = BlueDotOptions( accuracyRing: BlueDotOptions.AccuracyRing(color: "#2266ff", opacity: 0.25), color: "#2266ff", heading: BlueDotOptions.Heading(color: "#2266ff", opacity: 0.6), initialState: .inactive, radius: 12, watchDevicePosition: false ) mapView.blueDot.enable(options: options) { result in if case .success = result { print("BlueDot enabled") } } ``` ### Disabling Blue Dot v6 introduces an explicit BlueDot.disable()/>) method with a result callback. ```swift mapView.blueDot.disable { result in if case .success = result { print("BlueDot disabled") } } ``` ### Updating Blue Dot Position In v5, positions are provided through MPIPosition containing MPICoordinates. In v6, BlueDotPositionUpdate provides typed parameters for each field and supports partial updates. v5: ```swift let coords = MPICoordinates(latitude: 43.520124, longitude: -80.539517, accuracy: 2.0, floorLevel: 0) let position = MPIPosition(timestamp: 1.0, coords: coords) mapView?.blueDotManager.updatePosition(position: position) ``` v6: ```swift let position = BlueDotPositionUpdate( accuracy: .value(5.0), floorId: .id("floor_id"), heading: .value(90.0), latitude: .value(43.520124), longitude: .value(-80.539517) ) mapView.blueDot.update(position: position, options: BlueDotUpdateOptions(animate: true)) { result in if case .success = result { print("BlueDot position updated") } } ``` ### Events v5 relies on MPIMapViewDelegate methods for Blue Dot events. v6 uses an event-based system through BlueDotEvents, which provides more event types including click, error and follow change events. v5: ```swift func onBlueDotPositionUpdate(update: Mappedin.MPIBlueDotPositionUpdate) { print(update.position) } func onBlueDotStateChange(stateChange: Mappedin.MPIBlueDotStateChange) { print(stateChange.name) print(stateChange.reason) } ``` v6: ```swift mapView.blueDot.on(BlueDotEvents.positionUpdate) { payload in guard let payload = payload else { return } let floorName = payload.floor?.name ?? "nil" let heading = payload.heading.map { String(format: "%.0f°", $0) } ?? "nil" print("position-update: (\(payload.coordinate.latitude), \(payload.coordinate.longitude)) floor=\(floorName) heading=\(heading)") } mapView.blueDot.on(BlueDotEvents.statusChange) { payload in guard let payload = payload else { return } print("status-change: \(payload.status.rawValue) (action: \(payload.action.rawValue))") } ``` ### Camera Follow Mode In v5, camera follow mode is controlled through MPIState. In v6, MPIState no longer exists and follow mode is controlled directly through BlueDot.follow()/>) with FollowMode options that provide more granular control. v5: ```swift mapView?.blueDotManager.enable(options: MPIOptions.BlueDot(smoothing: true)) mapView?.blueDotManager.setState(state: MPIState.FOLLOW) ``` v6: ```swift mapView.blueDot.follow(FollowMode.positionOnly) ``` Refer to the Blue Dot guide for full documentation on v6 Blue Dot capabilities. ### Multi Floor View & Stacked Maps # Multi Floor View & Stacked Maps > ## Multi Floor View Multi Floor View is a feature that displays all floors in a building stacked vertically below the active floor, allowing users to see the building's vertical structure and navigate between floors. The active floor is fully rendered while lower floors appear as semi-transparent footprints. !MultiFloor View :::tip A complete example demonstrating Multi Floor View can be found in the Mappedin iOS GitHub repo: MultiFloorViewDemoViewController.swift ::: Multi Floor View is enabled by default. It can be controlled using the Show3DMapOptions.multiFloorView option when initializing the map view as shown in the code sample below. In this example, the map view is initialized with the `initialFloor` set to `m_952cd353abcb1a13`, which is ID of the 10th floor of the building. Other Multi Floor View options are defined in MultiFloorViewOptions and include the ability to set the gap between floors as well as whether the camera should move when the active floor changes. ### Code Sample ```swift // Display the map with multi-floor view enabled. let show3dMapOptions = Show3DMapOptions( multiFloorView: MultiFloorViewOptions( enabled: true, floorGap: 10.0, updateCameraElevationOnFloorChange: true ) ) self.mapView.show3dMap(options: show3dMapOptions) { r2 in if case .success = r2 { print("show3dMap success - Map displayed") } else if case .failure(let error) = r2 { print("show3dMap error: \(error)") } } ``` It is possible to set multiple floors to be visible at once, which can be useful for overlaying data within the building, such as the locations of security cameras or other assets. This can be done by setting the visible property to true for each floor that should be shown, passing FloorUpdateState into MapView.updateState()/) as shown in the code sample below. ```swift // Get all floors and find the ones to display. mapView.mapData.getByType(MapDataType.floor) { [weak self] (result: Result<[Floor], Error>) in guard let self = self else { return } switch result { case .success(let floors): // Set the current floor to the one with elevation 9. if let floor9 = floors.first(where: { $0.elevation == 9.0 }) { self.mapView.setFloor(floorId: floor9.id) { _ in print("Set floor to elevation 9: \(floor9.name)") } } // Show the 6th floor (elevation 6) as well. if let floor6 = floors.first(where: { $0.elevation == 6.0 }) { self.mapView.updateState( floor: floor6, state: FloorUpdateState( geometry: FloorUpdateState.Geometry(visible: true) ) ) { _ in print("Made floor with elevation 6 visible: \(floor6.name)") } } case .failure(let error): print("Failed to get floors: \(error)") } } ``` When displaying multiple floors, by default content such as labels, markers, paths and images are only visible on the active floor. These can be displayed on additional visible floors by enabling them for the floor passing FloorUpdateState into MapView.updateState()/) as shown below. ```swift mapView.updateState( floor: floor, state: FloorUpdateState( visible: true, altitude: 0.0, geometry: FloorUpdateState.Geometry(visible: true), images: FloorUpdateState.Images(visible: true), labels: FloorUpdateState.Labels(enabled: true), markers: FloorUpdateState.Markers(enabled: true), paths: FloorUpdateState.Paths(visible: true) ) ) { result in switch result { case .success: // Handle success break case .failure(let error): // Handle failure print("Error: \(error)") } } ``` ## Stacked Maps Stacked Maps consist of individual layers that represent different floors of a multi-story building. These layers are conceptually or digitally stacked to create a complete view of the structure, allowing users to visualize, navigate, and interact with multiple floors in an integrated way. !Stacked Maps :::tip A complete example demonstrating Stacked Maps can be found in the Mappedin iOS GitHub repo: StackedMapsDemoViewController.swift ::: Stacked Maps can be enabled by setting the visibility of floors that should appear in the stack to `true` and altering their altitude defined in FloorUpdateState, so that each floor is raised above the previous floor. There are two ways to modify the visibility and altitude of a floor: 1. MapView.animateState()/>) gradually moves the floor to the new altitude over a specified duration of time. 2. MapView.updateState()/>) instantly snaps the floor to the new altitude. Alternative visualization methods can also be created by using variations of this technique. For example, an app may wish to only show the top and bottom floors of a building, or only show the floors that are currently accessible to the user by setting the visibility of the floors to `true` or `false` based on the app's requirements. When displaying multiple floors, by default content such as labels, markers, paths and images are only visible on the active floor. These can be displayed on additional visible floors by enabling them for the floor passing FloorUpdateState into MapView.updateState()/) as shown below. ```swift mapView.updateState( floor: floor, state: FloorUpdateState( images: FloorUpdateState.Images(visible: true), labels: FloorUpdateState.Labels(enabled: true), markers: FloorUpdateState.Markers(enabled: true), paths: FloorUpdateState.Paths(visible: true) ) ) { result in switch result { case .success: // Handle success break case .failure(let error): // Handle failure print("Error: \(error)") } } ``` When using Stacked Maps with Dynamic Focus or Navigation, the `MapView.manualFloorVisibility` property should be set to `true` to ensure that the floors remain visible. Otherwise they could be hidden by the Dynamic Focus or Navigation features, which by default will hide all floors that are not the active floor. ### Stacked Maps Utility Class The following utility class can be used to expand and collapse the stacked maps view using the `animateState` or `updateState` methods. ` ```swift import Foundation import Mappedin /// Options for expanding floors in a stacked view. public struct ExpandOptions { /// The vertical spacing between floors in meters. Default: 10 public let distanceBetweenFloors: Double /// Whether to animate the floor expansion. Default: true public let animate: Bool /// The camera pan mode to use ("default" or "elevation"). Default: "elevation" public let cameraPanMode: String public init( distanceBetweenFloors: Double = 10.0, animate: Bool = true, cameraPanMode: String = "elevation" ) { self.distanceBetweenFloors = distanceBetweenFloors self.animate = animate self.cameraPanMode = cameraPanMode } } /// Options for collapsing floors back to their original positions. public struct CollapseOptions { /// Whether to animate the floor collapse. Default: true public let animate: Bool public init(animate: Bool = true) { self.animate = animate } } /// Utility class for managing stacked floor views. /// /// Provides functions to expand all floors vertically (stacked view) and collapse them back /// to a single floor view. This creates a 3D exploded view effect where all floors are visible /// at different altitudes. /// /// Example usage: /// ``` swift /// // Expand floors with default options /// StackedMapsUtils.expandFloors(mapView: mapView) /// /// // Expand floors with custom gap /// StackedMapsUtils.expandFloors(mapView: mapView, options: ExpandOptions(distanceBetweenFloors: 20.0)) /// /// // Collapse floors back /// StackedMapsUtils.collapseFloors(mapView: mapView) /// ``` public class StackedMapsUtils { /// Expands all floors vertically to create a stacked view. /// /// Each floor is positioned at an altitude based on its elevation multiplied by the /// distance between floors. This creates a 3D exploded view where all floors are visible. /// /// - Parameters: /// - mapView: The MapView instance /// - options: Options controlling the expansion behavior public static func expandFloors( mapView: MapView, options: ExpandOptions = ExpandOptions() ) { // Set camera pan mode to elevation for better navigation in stacked view mapView.camera.setPanMode(options.cameraPanMode) // Get the current floor ID to identify the active floor mapView.currentFloor { currentFloorResult in let currentFloorId: String? switch currentFloorResult { case .success(let floor): currentFloorId = floor?.id case .failure: currentFloorId = nil } // Get all floors mapView.mapData.getByType(MapDataType.floor) { (result: Result<[Floor], Error>) in switch result { case .success(let floors): for floor in floors { let newAltitude = floor.elevation * options.distanceBetweenFloors let isCurrentFloor = floor.id == currentFloorId // First, make sure the floor is visible mapView.getState(floor: floor) { stateResult in switch stateResult { case .success(let currentState): if let state = currentState, (!state.visible || !state.geometry.visible) { // Make the floor visible first with 0 opacity if not current mapView.updateState( floor: floor, state: FloorUpdateState( altitude: 0.0, visible: true, geometry: FloorUpdateState.Geometry( opacity: isCurrentFloor ? 1.0 : 0.0, visible: true ) ) ) } // Then animate or update to the new altitude if options.animate { mapView.animateState( floor: floor, state: FloorUpdateState( altitude: newAltitude, geometry: FloorUpdateState.Geometry( opacity: 1.0 ) ) ) } else { mapView.updateState( floor: floor, state: FloorUpdateState( altitude: newAltitude, visible: true, geometry: FloorUpdateState.Geometry( opacity: 1.0, visible: true ) ) ) } case .failure: break } } } case .failure: break } } } } /// Collapses all floors back to their original positions. /// /// Floors are returned to altitude 0, and only the current floor remains fully visible. /// Other floors are hidden to restore the standard single-floor view. /// /// - Parameters: /// - mapView: The MapView instance /// - options: Options controlling the collapse behavior public static func collapseFloors( mapView: MapView, options: CollapseOptions = CollapseOptions() ) { // Reset camera pan mode to default mapView.camera.setPanMode("default") // Get the current floor ID to identify the active floor mapView.currentFloor { currentFloorResult in let currentFloorId: String? switch currentFloorResult { case .success(let floor): currentFloorId = floor?.id case .failure: currentFloorId = nil } // Get all floors mapView.mapData.getByType(MapDataType.floor) { (result: Result<[Floor], Error>) in switch result { case .success(let floors): for floor in floors { let isCurrentFloor = floor.id == currentFloorId if options.animate { // Animate to altitude 0 and fade out non-current floors mapView.animateState( floor: floor, state: FloorUpdateState( altitude: 0.0, geometry: FloorUpdateState.Geometry( opacity: isCurrentFloor ? 1.0 : 0.0 ) ) ) // After animation, hide non-current floors if !isCurrentFloor { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { mapView.updateState( floor: floor, state: FloorUpdateState( altitude: 0.0, visible: false, geometry: FloorUpdateState.Geometry( opacity: 0.0, visible: false ) ) ) } } } else { mapView.updateState( floor: floor, state: FloorUpdateState( altitude: 0.0, visible: isCurrentFloor, geometry: FloorUpdateState.Geometry( opacity: isCurrentFloor ? 1.0 : 0.0, visible: isCurrentFloor ) ) ) } } case .failure: break } } } } } ``` ` :::tip A complete example demonstrating Stacked Maps can be found in the Mappedin iOS GitHub repo: StackedMapsDemoViewController.swift ::: ### Multi language # Multi language > Mappedin SDKs have the ability to support multiple languages. The particular languages supported will vary by venue. Each language must be enabled and text translated in the source map before a language can be used in the SDK. :::note Multi language is currently only supported for enterprise maps created in Mappedin CMS. ::: EnterpriseVenue.languages will contain an array of supported languages that have been configured in Mappedin CMS for the venue. To get the currently displayed language, use MapData.currentLanguage/). ```swift // Get supported languages mapView.mapData.getByType(.enterpriseVenue) { (result: Result<[EnterpriseVenue], Error>) in if case .success(let venues) = result { if let venue = venues.first { print("EnterpriseVenue: \(venue.name)") venue.languages.forEach { lang in print("Language: \(lang.name) (\(lang.code))") } } } } // Get current language mapView.mapData.currentLanguage { result in if case .success(let language) = result { print("Current language: \(language?.name ?? "unknown") (\(language?.code ?? "unknown"))") } } ``` The language can be changed by calling MapData.changeLanguage()/) and passing in a language code. ```swift mapView.mapData.changeLanguage("es") ``` Once a change language is complete, a MapData.on(MapDataEvents.LanguageChange)/) event is fired. At this point the map is ready to use the new language. Use this event to trigger things like re-creation of labels in the new language. ```swift mapView.mapData.on(MapDataEvents.languageChange) { payload in if let language = payload { print("Language changed to \(language.name) (\(language.code))") } } ``` ### Outdoor Map # Outdoor Map > The outdoor map shown around the Mappedin indoor map gives users context for their indoor navigation. ## Styles Styles can be applied to the outdoor map to change its colors. Mappedin provides five pre-built styles to choose from. :::info The URLs listed below can be used in Mappedin SDKs. They require authentication and cannot be used in other contexts. ::: - Classic: `https://tiles-cdn.mappedin.com/styles/mappedin/style.json` - Beach Please: `https://tiles-cdn.mappedin.com/styles/beachplease/style.json` - Honeycrisp: `https://tiles-cdn.mappedin.com/styles/honeycrisp/style.json` - Fresh Mint: `https://tiles-cdn.mappedin.com/styles/freshmint/style.json` - Night Blue: `https://tiles-cdn.mappedin.com/styles/midnightblue/style.json` - Starlight: `https://tiles-cdn.mappedin.com/styles/starlight/style.json` - Dark Roast: `https://tiles-cdn.mappedin.com/styles/darkroast/style.json` To change the style used by the Outdoor map, pass the URL of the style in the outdoorView object of Show3DMapOptions as shown below. ```swift self.mapView.show3dMap(options: Show3DMapOptions( outdoorView: Show3DMapOptions.OutdoorViewOptions(style: "https://tiles-cdn.mappedin.com/styles/darkroast/style.json"))) { r2 in if case .success = r2 { // Successfully initialized the map. } } ``` The outdoor style can also be changed at runtime using mapView.outdoor.setStyle()/). ```swift mapView.outdoor.setStyle(style: "https://tiles-cdn.mappedin.com/styles/midnightblue/style.json") ``` ### Points of Interest # Points of Interest > Points of Interest (POIs) are specific locations or features on a map that users find useful or informative. POIs serve as landmarks or markers, highlighting key places, services, or objects to enhance the map's utility and user experience. They are contained in the PointOfInterest class, which contains a coordinate, name, description, images, links and the floor the point of interest exists on. All of these elements are configured in Mappedin Maker. !Mappedin JS v6 POIs PointOfInterest.name could be used to create labels to show users helpful information. The following sample code demonstrates one possible use for PointOfInterest. It labels each PointOfInterest and uses Camera.animateTo()/) to create a flyby of each one. ```swift import UIKit import Mappedin import Foundation final class DisplayMapDemoViewController: UIViewController { private let mapView = MapView() private let loadingIndicator = UIActivityIndicatorView(style: .large) private let animationDuration = 4000 override func viewDidLoad() { super.viewDidLoad() title = "Display a Map" view.backgroundColor = .systemBackground let container = mapView.view container.translatesAutoresizingMaskIntoConstraints = false view.addSubview(container) // Add loading indicator loadingIndicator.translatesAutoresizingMaskIntoConstraints = false loadingIndicator.startAnimating() view.addSubview(loadingIndicator) NSLayoutConstraint.activate([ container.leadingAnchor.constraint(equalTo: view.leadingAnchor), container.trailingAnchor.constraint(equalTo: view.trailingAnchor), container.topAnchor.constraint(equalTo: view.topAnchor), container.bottomAnchor.constraint(equalTo: view.bottomAnchor), loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps let options = GetMapDataWithCredentialsOptions( key: "mik_yeBk0Vf0nNJtpesfu560e07e5", secret: "mis_2g9ST8ZcSFb5R9fPnsvYhrX3RyRwPtDGbMGweCYKEq385431022", mapId: "65c0ff7430b94e3fabd5bb8c" ) // Load the map data. mapView.getMapData(options: options) { [weak self] r in guard let self = self else { return } if case .success = r { print("getMapData success") // Display the map. self.mapView.show3dMap(options: Show3DMapOptions()) { r2 in if case .success = r2 { DispatchQueue.main.async { self.loadingIndicator.stopAnimating() } self.onMapReady() } else if case .failure(let error) = r2 { DispatchQueue.main.async { self.loadingIndicator.stopAnimating() } print("show3dMap error: \(error)") } } } else if case .failure(let error) = r { print("getMapData error: \(error)") } } } // Place your code to be called when the map is ready here. private func onMapReady() { print("show3dMap success - Map displayed") // Get the map center as the starting point for bearing calculations. mapView.mapData.mapCenter { [weak self] centerResult in guard let self = self else { return } if case .success(let mapCenter) = centerResult, let mapCenter = mapCenter { // Get all points of interest. self.mapView.mapData.getByType(.pointOfInterest) { [weak self] (result: Result<[PointOfInterest], Error>) in guard let self = self else { return } if case .success(let pois) = result { // Start iterating through POIs with initial position from map center. self.animateThroughPOIs( pois: pois, index: 0, startLat: mapCenter.latitude, startLon: mapCenter.longitude ) } else if case .failure(let error) = result { print("Failed to get POIs: \(error)") } } } else if case .failure(let error) = centerResult { print("Failed to get map center: \(error)") } } } /// Recursively animates through each point of interest. private func animateThroughPOIs( pois: [PointOfInterest], index: Int, startLat: Double, startLon: Double ) { guard index < pois.count else { print("Finished animating through all POIs") return } let poi = pois[index] // Label the point of interest. mapView.labels.add(target: poi.coordinate, text: poi.name) // Calculate the bearing between the current position and the POI. let bearing = calcBearing( startLat: startLat, startLng: startLon, destLat: poi.coordinate.latitude, destLng: poi.coordinate.longitude ) // Animate to the current point of interest. mapView.camera.animateTo( target: CameraTarget( bearing: bearing, center: poi.coordinate, pitch: 80.0, zoomLevel: 50.0 ), options: CameraAnimationOptions( duration: animationDuration, easing: .easeOut ) ) // Wait for the animation to complete before moving to the next POI. DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(animationDuration)) { [weak self] in self?.animateThroughPOIs( pois: pois, index: index + 1, startLat: poi.coordinate.latitude, startLon: poi.coordinate.longitude ) } } /// Calculate the bearing between two points. private func calcBearing(startLat: Double, startLng: Double, destLat: Double, destLng: Double) -> Double { let startLatRad = toRadians(startLat) let startLngRad = toRadians(startLng) let destLatRad = toRadians(destLat) let destLngRad = toRadians(destLng) let y = sin(destLngRad - startLngRad) * cos(destLatRad) let x = cos(startLatRad) * sin(destLatRad) - sin(startLatRad) * cos(destLatRad) * cos(destLngRad - startLngRad) var brng = atan2(y, x) brng = toDegrees(brng) return (brng + 360).truncatingRemainder(dividingBy: 360) } /// Converts from degrees to radians. private func toRadians(_ degrees: Double) -> Double { return degrees * .pi / 180 } /// Converts from radians to degrees. private func toDegrees(_ radians: Double) -> Double { return radians * 180 / .pi } } ``` ### Release Notes # Release Notes Mappedin SDK for iOS release notes are posted here and this page will be kept up-to-date as updates are released. { // Release notes template: // https://keepachangelog.com/en/1.0.0/#how // ## vXX.XX.XX - Month Day, Year // _ Added // _ Changed // _ Deprecated // _ Removed // _ Fixed // _ Security } ## 6.4.0 - June 5, 2026 ### Packages - `@mappedin/blue-dot`: 6.20.0-beta.0 - `@mappedin/dynamic-focus`: 6.20.0-beta.0 - `@mappedin/events`: 6.20.0-beta.0 - `@mappedin/icons`: 8.3.0 - `@mappedin/mappedin-js`: 6.20.0 ### Features - Upgraded to Mappedin JS v6.20.0 - Added the `Icons` API, accessible via `mapView.icons`, for working with Mappedin's CDN-backed icon library. The API mirrors the `@mappedin/icons` web package and is map-independent, so it can be used without a loaded map. It includes lookup methods (`getByName`, `getByType`, `getBySubtype`, `getByCategory`, `getByTags`, `getAll`, `getSmallIcon`), SVG fetching (`fetchSvg`), pre-fetching (`prefetch`, `prefetchByType`, `prefetchBySubtype`, `prefetchByCategory`), cache management (`isCached`, `getCachedSvg`, `clearCache`), and CDN region selection via `initialize`. New strongly typed models include `MappedinIcon`, `IconType`, `IconSubtype`, `IconCategory`, and `IconCdnRegion` ```swift // Look up an icon by name mapView.icons.getByName(name: "information") { result in if case .success(let icon) = result { print(icon.url) } } // Fetch the raw SVG markup for an icon mapView.icons.fetchSvg(name: "information") { result in if case .success(let svg) = result { // Render the SVG } } // Pre-fetch all icons in a category for offline use mapView.icons.prefetchByCategory(category: .foodAndDrink) { _ in } ``` ## 6.3.0 - May 20, 2026 - Upgraded to Mappedin JS v6.19.0 ### Features - Added `visible` property to `BlueDotOptions`, `AccuracyRing`, and `Heading` to control the visibility of Blue Dot components independently - Added `AutoZoomThresholds` enum (`.disabled`, `.enabled`, `.enabledWith(options:)`) to `DynamicFocusOptions` and `DynamicFocusState` for configuring `DynamicFocus` auto-zoom behavior ```swift let options = DynamicFocusOptions( autoZoomThresholds: .enabledWith(options: .init(debug: true)) ) ``` - Added `.floorStack(FloorStack)` case to the `FocusTarget` enum, with convenience `focusOn` and `getFocusOnTransform` overloads on `Camera` for focusing on a `FloorStack` ```swift mapView.camera.focusOn(.floorStack(floorStack)) ``` - Added `Events.cameraScreenOffsetsChange` event to listen for camera screen offset changes - Added `.pixels(String)` case to `Width` for pixel-based width values (e.g. `"20px"`) - Added `Navigation.trackCoordinate` method for path tethering and travelled-path tracking, with `TetheredOptions`, `TravelledOptions`, `TrackCoordinateOptions`, `TrackCoordinateResult`, `TrackingMode`, `CoordinateOutsideThresholdMode`, and `OutsideThresholdPathStyle` models ```swift let handle = mapView.navigation.trackCoordinate( origin: startCoordinate, destinations: [destinationSpace], options: TrackCoordinateOptions( mode: .tethered, tethered: TetheredOptions( outsideThresholdMode: .smoothTransition, outsideThresholdDistance: 5.0 ) ) ) { result in /* ... */ } mapView.blueDot.on(.dotPositionUpdate) { payload in handle?.update(coordinate: payload.coordinate) } ``` - Added `BlueDotEvents.dotPositionUpdate` event that emits smoothed `Coordinate` updates suitable for feeding into `Navigation.trackCoordinate` - Added `includeNonPublic` property to `GetDirectionsOptions` to allow routing through non-public spaces and connections - Added `interactive` property to `AddImageOptions` and `interactive`/`cursor` properties to `ImageState` and `ImageUpdateState`. `Image3DView` is now exposed in `ClickPayload` and `HoverPayload` `images` and `target` ### Deprecated - `BlueDot.update()` has been deprecated in favor of `BlueDot.forcePosition()` or `BlueDot.reportPosition()` ```swift // Before (deprecated) mapView.blueDot.update( position: BlueDotPositionUpdate( latitude: .value(lat), longitude: .value(lng), floorId: .id(floorId) ), options: BlueDotUpdateOptions(animate: true) ) { _ in } // After mapView.blueDot.reportPosition( options: ManualPositionOptions( latitude: lat, longitude: lng, floorLevel: floorLevel, confidence: 1.0 ) ) { _ in } ``` ## 6.2.0 - April 2, 2026 - Upgraded to Mappedin JS v6.17.1 - Added - `BlueDot.forcePosition` method to override all positioning sensors with a fixed position for a specified duration ```swift let target = BlueDot.ForcePositionTarget( latitude: 43.5186, longitude: -80.5394, heading: 90.0, floorLevel: 1 ) mapView.blueDot.forcePosition(position: target, durationMs: 30000) { result in // Position forced } ``` - Added - `BlueDot.reportPosition` method to feed a confidence-weighted position into the positioning fusion engine ```swift let options = ManualPositionOptions( latitude: 43.5186, longitude: -80.5394, accuracy: 5.0, confidence: 0.8 ) mapView.blueDot.reportPosition(options: options) { result in // Position reported } ``` - Added - Blue Dot sensor management methods: `enableSensor`, `disableSensor`, `checkSensorPermission`, `requestSensorPermission`, and `isSensorEnabled` for controlling individual positioning sensor sources - Added - `BlueDotEvents.anchorSet` and `BlueDotEvents.anchorExpired` events with `PositionAnchor` payload for monitoring anchor lifecycle ```swift mapView.blueDot.on(BlueDotEvents.anchorSet) { payload in guard let anchor = payload?.anchor else { return } print("Anchor set: \(anchor.sensorId) at \(anchor.latitude), \(anchor.longitude)") } ``` - Added - `floorGapMultiplier` and `floorGapFallback` properties to `MultiFloorViewOptions` for finer control over floor spacing in multi-floor view - Added - `animated` property to `AddMarkerOptions` to control whether `Marker`s animate when added - Added - `MaterialSide.auto` case for automatic material side selection based on opacity - Added - `EventsManager` API accessible via `mapView.mapData.eventsManager` for loading and querying CMS events. Includes `load()`, `getEvents()`, `getById()`, and `getByLocationId()` methods ```swift mapView.mapData.eventsManager.load { result in if case .success = result { mapView.mapData.eventsManager.getEvents { eventsResult in if case .success(let events) = eventsResult { for event in events { print("\(event.name): \(event.startDate) - \(event.endDate)") } } } } } ``` - Added - `position` property to `ShapeState` and `ShapeUpdateState`, enabling reading and updating the position of `Shape`s ```swift // Get the position of a shape mapView.getState(shape: shape) { result in if case .success(let state) = result { print("Shape position: \(state?.position.latitude), \(state?.position.longitude)") } } // Move a shape to a new position let newPosition = Coordinate(latitude: 43.46, longitude: -80.52) mapView.updateState(shape: shape, state: ShapeUpdateState(position: newPosition)) ``` - Added - `Models.all()` method to load all CMS-defined 3D `Model`s for a venue - Added - `MultiFloorViewEffectState` model and related APIs for configuring visual effects on floors visible through open-to-below `Space`s in multi-floor view. Access the current state via `GlobalState.multiFloorView` and update it at runtime via `GlobalStateUpdate.multiFloorView` ```swift // Configure visual effects at init time let options = Show3DMapOptions( multiFloorView: MultiFloorViewOptions( spacesOpenToBelowEnabled: true, spacesOpenToBelowVisualEffectEnabled: true, spacesOpenToBelowVisualEffectDarkenAmount: 0.3, spacesOpenToBelowVisualEffectDesaturateAmount: 0.7 ) ) // Update visual effects at runtime mapView.updateGlobalState(update: GlobalStateUpdate( multiFloorView: GlobalStateUpdate.MultiFloorViewEffectStateUpdate( spacesOpenToBelowVisualEffectDarkenAmount: 0.5 ) )) ``` - Added - `Events.globalStateChange` event emitted when `updateGlobalState()` is called - Fixed - an issue where facades could cover the map when calling `setMap` ## 6.2.0-beta.1 - February 13, 2026 - Upgraded to Mappedin JS v6.14.0 - Added - `Query.at` method to find all geometry objects (`Space`s, `MapObject`s, `Area`s, `Floor`s) at a given coordinate, with typed results returned as `QueryAtResult` ```swift mapView.mapData.query.at(coordinate: coordinate) { result in switch result { case .success(let results): results?.forEach { queryResult in switch queryResult { case .space(let space): mapView.updateState(space: space, state: GeometryUpdateState(color: "#FF6B35")) case .mapObject(let mapObject): print("MapObject at point: \(mapObject.name)") case .area(let area): print("Area at point: \(area.name)") case .floor(let floor): print("Floor at point: \(floor.name)") default: break } } case .failure(let error): print("Query.at failed: \(error)") } } ``` - Added - `FloorState.Images.collisionsEnabled` property to control whether floor images participate in collision detection - Added - `ModelUpdateState.clippingPlaneZOffset` property to control model clipping plane. Use `.infinity` to disable clipping ```swift // Disable clipping for a model let noClipState = ModelUpdateState(clippingPlaneZOffset: .infinity) // Set explicit clipping plane offset let clipState = ModelUpdateState(clippingPlaneZOffset: 5.0) ``` - Added - `DynamicFocus` methods for full API parity: - `preloadFloors()` - Preloads initial floors for improved rendering performance - `setIndoor()` and `setOutdoor()` - Forces indoor or outdoor view state regardless of zoom level - `setDefaultFloorForStack(floorStack:floor:)` and `resetDefaultFloorForStack(floorStack:)` - Manages default floors for `FloorStack`s - `getDefaultFloorForStack(floorStack:)` and `getCurrentFloorForStack(floorStack:)` - Gets default and current floors - `setCurrentFloorForStack(floorStack:floor:)` - Sets the current visible floor for a `FloorStack` - `exclude(floorStack:)` / `exclude(floorStacks:)` and `include(floorStack:)` / `include(floorStacks:)` - Excludes or includes `FloorStack`s from Dynamic Focus visibility changes - `destroy()` - Destroys the `DynamicFocus` instance and cleans up event listeners ```swift // Force indoor view dynamicFocus.setIndoor() // Set current floor for a floor stack dynamicFocus.setCurrentFloorForStack(floorStack: building.floorStack, floor: floor2) { _ in } // Exclude a building from dynamic focus visibility changes dynamicFocus.exclude(floorStack: parkingGarage.floorStack) // Clean up when done dynamicFocus.destroy() ``` - Added - `MapData.isEnterpriseMode` property to determine whether the loaded map uses CMS/Enterprise or Maker data. Runtime warnings are now logged when requesting data types that do not match the active data source ```swift mapView.mapData.isEnterpriseMode { result in switch result { case .success(let isEnterprise): if isEnterprise { mapView.mapData.getByType(.enterpriseLocation) { (locResult: Result<[EnterpriseLocation], Error>) in // Handle enterprise locations } } else { mapView.mapData.getByType(.locationProfile) { (locResult: Result<[LocationProfile], Error>) in // Handle maker locations } } case .failure(let error): print("Error: \(error)") } } ``` ## 6.2.0-beta.0 - February 2, 2026 - Upgraded to Mappedin JS v6.13.0 - Added - `BlueDot` support via `MapView.blueDot` extension with enable/disable/update/follow/getters and typed event subscriptions. Includes CoreLocation integration for native location and heading updates. New models include `BlueDotState`, `BlueDotOptions`, `BlueDotUpdateOptions`, `FollowMode`, and `FollowCameraOptions` - Added - `MapView.dynamicFocus` extension providing `enable`, `disable`, `getState`, `updateState`, and `focus` methods for Dynamic Focus functionality. Includes new models `DynamicFocusOptions`, `DynamicFocusState`, `DynamicFocusMode`, and `DynamicFocusAnimationOptions` - Added - Experimental `MapView.__EXPERIMENTAL__auto()` method for automatic label and marker setup - Added - `hydrateMapDataFromURL(url:options:)` method for hydrating map data directly from a URL - Added - Cache URL handling with `mappedin-cache://` scheme and FileManager helper to build cache URLs - Added - Support for loading maps from MVF filename for improved offline capabilities - Added - `LocationProfile.searchTags` property for accessing location search tags - Added - `allFloors` option to `Query.nearest()` to search across all floors instead of just the current floor - Added - `images3D` getter to `PointOfInterest`, `Space`, `Area`, and `MapObject` classes with new `Image3DView` model - Improved - Map loading performance by optimizing how map data is passed to the WebView ## 6.2.0-alpha.3 - January 16, 2026 - Upgraded to Mappedin JS v6.11.0 - Added - `MapData.on` method for subscribing to map data events. This enables listening for events such as language changes on the map data object. Use `MapData.changeLanguage` to change the current language and subscribe to changes using `MapDataEvents.languageChange` ```swift // Change the map language to Spanish mapView.mapData.changeLanguage("es") // Listen for language change events mapView.mapData.on(MapDataEvents.languageChange) { payload in if let language = payload { print("Language changed to \(language.name) (\(language.code))") } } ``` - Added - `FloorState.text3d` property to access 3D text state for a floor - Added - `LocationProfile.extra` property to access additional profile data - Added - `enterpriseLocation` property to `Space` and `MapObject`, providing direct access to enterprise location data when available - Added - `LocationProfile.logoImage` property as a replacement for the deprecated `logo` property - Added - Strongly typed `MapData.hydrateMapData` options parameter for better type safety when hydrating cached map data - Added - Support for caching map data in binary format for improved offline performance - Added - Experimental support for Spaces that are open to below in multi-floor view. When enabled, floors will render with cutouts for Spaces like atriums that span multiple levels, creating a more realistic visualization. Enable this feature using `MultiFloorViewOptions.spacesOpenToBelowEnabled` - Added - New `MultiFloorViewOptions` properties for customizing floor footprints in multi-floor view: - `footprintColor` - Sets the color of the floor footprint - `footprintOpacity` - Controls the opacity of the floor footprint - `footprintOutline` - Configures the outline appearance of the floor footprint - Changed - `MultiFloorViewOptions.floorGap` now supports both a numeric value or an `auto` setting for automatic gap calculation between floors - Fixed - Bridge serialization issue where `FloorStack.geoJSON` and other complex GeoJSON properties were not being passed correctly over the native bridge. This resolves issues when accessing GeoJSON data from floor stacks and related objects - Deprecated - `LocationProfile.logo` has been deprecated in favor of `LocationProfile.logoImage` ## 6.2.0-alpha.2 - January 8, 2026 - Upgraded to Mappedin JS v6.10.0 - Added - getDirections must also accept an array of NavigationTarget - Added - Coordinate.geoJSON - Fixed - mapView.updateState is not working on walls or doors - Fixed - getDirectionsMultiDestination's to method should use NavigationTarget not Any - Removed - Duplicated unused standalone classes of Point, LineString and MultiPolygon that are defined in Geometry ### ⚠️ Breaking Changes - Changed - Clean up remaining generic types - Changed - Strongly type .geoJSON properties - Changed - Shapes.add should be strongly typed ```swift // ❌ Before // Get the GeoJSON from the area guard let areaGeoJSON = area.geoJSON else { return } // Create a FeatureCollection containing the Feature of the Area var shapeFeatureCollection: [String: Any] = [ "type": "FeatureCollection", "features": [] ] var feature: [String: Any] = [ "type": areaGeoJSON["type"] as? String ?? "Feature" ] if let properties = areaGeoJSON["properties"] as? [String: Any] { feature["properties"] = properties } if let geometry = areaGeoJSON["geometry"] as? [String: Any] { feature["geometry"] = geometry } shapeFeatureCollection["features"] = [feature] // Draw the shape mapView.shapes.add( geometry: shapeFeatureCollection, style: PaintStyle(color: color, altitude: altitude, height: height, opacity: opacity) ) { _ in } // ✅ After // Get the GeoJSON Feature from the area guard let feature = area.geoJSON else { return } // Create a FeatureCollection containing the single Feature let featureCollection = FeatureCollection(features: [feature]) // Draw the shape using the typed API mapView.shapes.add( geometry: featureCollection, style: PaintStyle(color: color, altitude: altitude, height: height, opacity: opacity) ) { _ in } ``` ## 6.2.0-alpha.1 - December 30, 2025 - Upgraded to Mappedin JS v6.9.1. - Added - `Show3DMapOptions.preloadFloors` option to preload floors. - Added -`MultiFloorViewOptions.floorGap` option to set the gap between floors. - Fixed - `Show3DMapOptions.initialFloor` should accept only a string. ### ⚠️ Breaking Changes - Changed - Strongly type MapView.on parameters and return values ```swift // ❌ Before self.mapView.on("click") { [weak self] payload in guard let self = self, let clickPayload = payload as? ClickPayload, } // ❌ Before self.mapView.on(Events.click) { [weak self] payload in guard let self = self, let clickPayload = payload as? ClickPayload, } // ✅ After self.mapView.on(Events.click) { [weak self] clickPayload in guard let self = self } ``` ## 6.1.0-alpha.1 - December 19, 2025 - Added - LabelAppearance.iconScale property to control the scale of the icon. ### ⚠️ Breaking Changes - Fixed - Removed duplicate inline implementation of Interpolation and Width. ```kotlin // ❌ Before let opts = AddPathOptions(width: .fixed(1.0)) // ✅ After let opts = AddPathOptions(width: .value(1.0)) ``` - Fixed - `MapView.getState` and `MapView.updateState` should use data classes and not plain objects. ```swift // ❌ Before self.mapView.updateState(target: space, state: ["interactive": interactive]) { _ in } // ✅ After self.mapView.updateState(space: space, state: GeometryUpdateState(interactive: interactive)) ``` ## 6.0.0-alpha.0 - December 12, 2025 - Initial release of Mappedin SDK for iOS v6. ### Spaces # Spaces > A Space represents an area enclosed by walls, such as a hall or room. Spaces can be Interactive and have Labels and Markers added to them. Spaces can also be customized with a color and texture. ## Spaces Example The example below loops through all spaces and makes them interactive. Refer to the Textures & Colors Guide section for more information on how to set the texture or color of a space. ```swift // Set all spaces to be interactive so they can be clicked mapView.mapData.getByType(.space) { [weak self] (result: Result<[Space], Error>) in guard let self = self else { return } if case .success(let spaces) = result { spaces.forEach { space in self.mapView.updateState(target: space, state: ["interactive": true]) { _ in } } } } ``` ## Doors By default, a Door is not visible and drawn as a gap in a wall. An app can set the `visible` property to `true` to make the door visible. Other properties of a door can also be set, such as color, texture, interactivity and more. Refer to DoorsState for the complete list of properties. Refer to the Textures & Colors Guide section for more information on how to set the texture or color of a door. Doors are grouped into DOORS.Interior and DOORS.Exterior doors, based on whether they are on an interior or exterior wall. ```ts // Make interior doors visible and brown. mapView.updateState( target: Doors.interior.rawValue, state: [ "visible": true, "color": "#5C4033", "opacity": 0.6 ] ) { _ in } // Make exterior doors visible and black. mapView.updateState( target: Doors.exterior.rawValue, state: [ "visible": true, "color": "black", "opacity": 0.6 ] ) { _ in } ``` The screenshot below shows brown interior and black exterior doors with labels added to depict the status of the door. ### Using SwiftUI # Using SwiftUI > The Mappedin SDK for iOS v6 uses a MapView class that provides a `view` property (a `UIView`). To use it in SwiftUI, wrap it in a UIViewRepresentable. This allows a SwiftUI-based app to display and interact with indoor maps. :::tip A complete example demonstrating SwiftUI can be found in the Mappedin iOS GitHub repo: SwiftUI With Mappedin Project implemented in the MapViewRepresentable.swift file. ::: ## Setting Up the MapModel Mappedin SDK for iOS v6 uses an event-based API. Events like click handlers must be registered after the map finishes loading, and because `UIViewRepresentable` is a struct that SwiftUI may recreate, the MapView instance must be stored in a stable, long-lived object. An ObservableObject is the idiomatic SwiftUI approach. It holds the MapView and any `@Published` properties that drive the UI, such as alert state for click events. Since the MapView lives on the model, SDK methods can be called directly from SwiftUI buttons and controls. ```swift import SwiftUI import Mappedin class MapModel: ObservableObject { let mapView = MapView() @Published var showAlert = false @Published var alertTitle = "" @Published var alertMessage = "" } ``` ## Creating the MapViewRepresentable The `MapViewRepresentable` struct wraps the SDK's MapView and conforms to `UIViewRepresentable`. It receives the `MapModel` via `@ObservedObject` and accesses the MapView through `model.mapView`. Inside `makeUIView`, the map data is loaded with `getMapData` using a GetMapDataWithCredentialsOptions object containing an API key, secret, and map ID. Once the data loads successfully, `show3dMap` is called to render the 3D map. ```swift struct MapViewRepresentable: UIViewRepresentable { @ObservedObject var model: MapModel func makeUIView(context: Context) -> UIView { let options = GetMapDataWithCredentialsOptions( key: "5eab30aa91b055001a68e996", secret: "RJyRXKcryCMy4erZqqCbuB1NbR66QTGNXVE0x3Pg6oCIlUR1", mapId: "mappedin-demo-mall" ) model.mapView.getMapData(options: options) { result in if case .success = result { model.mapView.show3dMap(options: Show3DMapOptions()) { showResult in if case .success = showResult { onMapReady() } else if case .failure(let error) = showResult { print("show3dMap error: \(error)") } } } else if case .failure(let error) = result { print("getMapData error: \(error)") } } return model.mapView.view } func updateUIView(_ uiView: UIView, context: Context) {} } ``` The GetMapDataWithCredentialsOptions object contains a `key`, `secret`, and `mapId`. To get started, use the Mappedin demo API key and secret. To use your own venues you will need your own unique key and secret. ## Adding MapViewRepresentable to a View The `MapViewRepresentable` is added to a SwiftUI `ContentView`. The `MapModel` is created as a `@StateObject` so it persists across view re-renders. Because the MapView lives on the model, SDK methods can be called directly from SwiftUI buttons and controls. ```swift struct ContentView: View { @StateObject private var model = MapModel() var body: some View { VStack { MapViewRepresentable(model: model) Button("Remove Labels") { model.mapView.labels.removeAll() } } } } ``` ## Handling Click Events SDK v6 uses an event-based system for handling user interactions. Subscribe to `Events.click` using mapView.on()/>) to receive a ClickPayload whenever the user taps the map. The payload contains information about what was clicked, including labels, spaces, paths, floors, and the geographic coordinate. The click event listener must be registered after the map is ready (inside the `onMapReady` callback), because the SDK only accepts event subscriptions once the map has loaded. The listener updates `@Published` properties on the `MapModel`, which drives a SwiftUI `.alert` in `ContentView`. Because the event callback may run off the main thread, wrap state updates in `DispatchQueue.main.async`: ```swift // Inside MapViewRepresentable's onMapReady method: mapView.on(Events.click) { [model] clickPayload in guard let click = clickPayload else { return } DispatchQueue.main.async { model.alertTitle = click.floors?.first?.name ?? "Map Click" var message = "" if let labels = click.labels, !labels.isEmpty { message.append("Label Clicked: \(labels.first?.text ?? "")\n") } if let spaces = click.spaces, !spaces.isEmpty { message.append("Space Clicked: \(spaces.first?.name ?? "")\n") } if let paths = click.paths, !paths.isEmpty { message.append("You clicked a path.\n") } message.append("Coordinate Clicked:\nLatitude: \(click.coordinate.latitude)\nLongitude: \(click.coordinate.longitude)") model.alertMessage = message model.showAlert = true } } ``` In `ContentView`, bind the alert to the model's published properties: ```swift struct ContentView: View { @StateObject private var model = MapModel() var body: some View { MapViewRepresentable(model: model) .alert(model.alertTitle, isPresented: $model.showAlert) { Button("Close", role: .cancel) {} } message: { Text(model.alertMessage) } } } ``` ## Adding Interactive Labels Labels can be added to spaces on the map using Labels.add()/>). Query for spaces using `mapView.mapData.getByType(.space)`, then add a label to each named space. Setting `interactive: true` in AddLabelOptions allows labels to be included in click events. ```swift mapView.mapData.getByType(.space) { (result: Result<[Space], Error>) in if case .success(let spaces) = result { spaces.forEach { space in guard !space.name.isEmpty else { return } mapView.labels.add( target: space, text: space.name, options: AddLabelOptions(interactive: true) ) } } } ``` ## Making Spaces Interactive By default, spaces are not interactive. To enable click detection on spaces, update their state using MapView.updateState()/>) with `GeometryUpdateState(interactive: true)`: ```swift mapView.mapData.getByType(.space) { (result: Result<[Space], Error>) in if case .success(let spaces) = result { spaces.forEach { space in mapView.updateState(space: space, state: GeometryUpdateState(interactive: true)) } } } ``` ## Drawing Navigation Paths Navigation paths can be drawn between locations using the Navigation class. First, get directions between two locations, then draw the path: ```swift mapView.mapData.getByType(.enterpriseLocation) { (result: Result<[EnterpriseLocation], Error>) in if case .success(let locations) = result { let origin = locations.first(where: { $0.name == "Microsoft" }) let destination = locations.first(where: { $0.name == "Apple" }) if let origin = origin, let destination = destination { mapView.mapData.getDirections( from: .enterpriseLocation(origin), to: .enterpriseLocation(destination) ) { dirResult in if case .success(let directions?) = dirResult { let pathOptions = AddPathOptions(interactive: true) let navOptions = NavigationOptions(pathOptions: pathOptions) mapView.navigation.draw(directions: directions, options: navOptions) { _ in } } } } } } ``` :::tip A complete example demonstrating SwiftUI can be found in the Mappedin iOS GitHub repo: SwiftUI With Mappedin Project implemented in the MapViewRepresentable.swift file. ::: ### Views # Views > A `view` is a copy of a map that allows a map creator to toggle the visibility of a layer or individual items in a layer on and off and choose a map theme. It is similarly named but should not be confused with the `MapView` class, which is used to show a map. Views allow the same map to be used for many use cases, such as: - Public use for wayfinding - Use one of many light or dark themes - With safety annotations for safety and security personnel - Showing private areas for maintenance and custodial staff - Displaying only the floor plan for leasing prospects - Highlighting delivery doors for delivery drivers - and many more... ## Get a View ID A `viewId` is used by Mappedin SDKs to download specific view. To get a `viewId`: 1. Log into Mappedin Maker. 2. Click on the `Developers` tab. 3. Select the desired map from the map drop down. 4. Select the desired view from the view drop down. 5. The `viewId` will be shown in the code snippet on the page. ## Display a View To display a specific view of a map, specify the `viewId` of the view using the GetMapDataWithCredentialsOptions or GetMapDataWithAccessTokenOptions objects that is passed into the MapView.getMapData()/) method as shown below. ```swift // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps let options = GetMapDataWithCredentialsOptions( key: "mik_yeBk0Vf0nNJtpesfu560e07e5", secret: "mis_2g9ST8ZcSFb5R9fPnsvYhrX3RyRwPtDGbMGweCYKEq385431022", mapId: "660c0c3aae0596d87766f2da", viewId: "71-i" ) // Load the map data. mapView.getMapData(options: options) { [weak self] r in ``` ### Wayfinding # Wayfinding > Wayfinding is the process of navigating through environments using spatial cues and instructions to efficiently reach a destination. Mappedin SDKs provide the tools needed to guide users from one location to another. It allows drawing a path between points on the map as well as creation of textual turn-by-turn directions for navigation. !Mappedin v6 Navigation ## Getting Directions Directions form the core of wayfinding. Mappedin has path finding capabilities that allow it to generate directions from one target to another. These directions are used for drawing paths and providing the user with textual turn-by-turn directions. Generate directions by calling MapData.getDirections() and pass in targets for the origin and destination. These targets are wrapped in a NavigationTarget, which can contain various types of targets. MapData.getDirections() can accept a single NavigationTarget or an array of them for the origin and destination. If an array is used, Mappedin will choose the targets that are closest to each other. An example of where this could be used is when a user asks for directions to a washroom. There may be many Spaces named Washroom. The app can pass all washroom spaces to `getDirections` and receive directions to the nearest one. Directions for some maps may appear jagged or not smooth. This is due to the SDK attempting to find the shortest path through the map's geometry. Directions can be smoothed by setting GetDirectionsOptions.smoothing to enabled in the `GetDirectionsOptions` passed to MapData.getDirections(). Directions are smoothed by default for maps created using Mappedin Maker and disabled for maps created using Mappedin CMS. ## Drawing Navigation When a user needs to get from point A to point B, drawing a path on the map helps them to navigate to their destination. It can help them to visualize the route they'll need to take, like a good treasure map. Navigation is a helper class to display wayfinding easily on the map. Functionality of Navigation could be replicated by drawing the paths using Paths and adding well designed markers at connection points. :::tip Note that the MapView class instantiates the Navigation class and exposes it as MapView.navigation. Use MapView.navigation to utilize Navigation's methods. ::: Navigation.draw() allows for easily drawing multiple components that make up a wayfinding illustration. It shows a human figure to mark the start point, a path with animated directional arrows, pulses in the direction of travel and a pin to mark the destination. Each of these components can be customized to match an app's style. The following sample uses the default navigation settings to navigate from the Cafeteria to the Gymnasium. ```swift private func drawNavigation() { // Use previously generated directions. guard let directions = currentDirections else { return } let pathOptions = AddPathOptions(animateDrawing: true, color: "#4b90e2", displayArrowsOnPath: true) let navOptions = NavigationOptions( createMarkers: NavigationOptions.CreateMarkers.withCustomMarkers( connection: NavigationOptions.CreateMarkers.CustomMarker( template: getConnectionMarker(), options: AddMarkerOptions(interactive: .True, rank: .tier(.alwaysVisible)) ), departure: NavigationOptions.CreateMarkers.CustomMarker( template: getDepartureMarker(), options: AddMarkerOptions(interactive: .True, rank: .tier(.alwaysVisible)) ), destination: NavigationOptions.CreateMarkers.CustomMarker( template: getDestinationMarker(), options: AddMarkerOptions(interactive: .True, rank: .tier(.alwaysVisible)) ) ), pathOptions: pathOptions ) mapView.navigation.draw(directions: directions, options: navOptions) { _ in } } private func getDepartureMarker() -> String { return """ """ } private func getDestinationMarker() -> String { return """ """ } private func getConnectionMarker() -> String { return """ """ } ``` ## Highlighting a Portion of a Path The path drawn by `Navigation` or `Paths` can have a portion highlighted to draw attention to it. This could be used to show the progress of a user's journey. To highlight a portion of the path, use the Navigation.highlightPathSection/>) or Paths.highlightSection/>) methods. The following code demonstrates how to highlight a portion of a path, starting from the origin of directions and ending at a given coordinate. Note that the floor must be provided when using a coordinate to highlight a path section. ```swift let coordinate = mapView.createCoordinate( latitude: Double(position.latitude), longitude: Double(position.longitude), floorId: position.floorOrFloorId ) if let firstCoordinate = directions?.coordinates.first { // Highlight the traveled path. mapView.Navigation.highlightPathSection( from: firstCoordinate, to: coordinate, options: [ "animationDuration": 0, "color": "#9d9d9d", "widthMultiplier": 1.1 ] ) } ``` ## Navigating From a Click Event A navigation start or end point may originate from a user clicking on the map. It is possible to create a start and or end point using the click event and accessing its Events payload. :::tip A complete example demonstrating Navigation from a click event can be found in the Mappedin iOS GitHub repo: PathsDemoViewController.swift ::: The code shown below uses the first click position as a start point and the second click position as an end point to create directions between them. ```swift // Set all spaces to be interactive initially mapView.mapData.getByType(.space) { (result: Result<[Space], Error>) in if case .success(let spaces) = result { print("PathsDemo: Found \(spaces.count) spaces") for space in spaces { self.mapView.updateState(space: space, state: GeometryUpdateState(interactive: true)) } // Handle click events self.mapView.on(Events.click) { [weak self] clickPayload in guard let self = self, let clickPayload = clickPayload else { return } let spaces = clickPayload.spaces if spaces == nil || spaces?.isEmpty == true { // Click on non-space area when path exists - reset if self.path != nil { self.mapView.paths.removeAll() self.startSpace = nil self.path = nil self.setSpacesInteractive(interactive: true) self.instructionLabel.text = "1. Click on a space to select it as the starting point." } return } guard let clickedSpace = spaces?.first else { return } if self.startSpace == nil { // Step 1: Select starting space self.startSpace = clickedSpace self.instructionLabel.text = "2. Click on another space to select it as the end point." } else if self.path == nil { // Step 2: Select ending space and create path guard let start = self.startSpace else { return } self.mapView.mapData.getDirections( from: .space(start), to: .space(clickedSpace) ) { result in if case .success(let directions) = result, let directions = directions { let opts = AddPathOptions(color: "#1871fb", width: .value(1.0)) self.mapView.paths.add(coordinates: directions.coordinates, options: opts) { pathResult in if case .success(let createdPath) = pathResult { self.path = createdPath self.setSpacesInteractive(interactive: false) self.instructionLabel.text = "3. Click anywhere to remove the path." } } } } } else { // Step 3: Remove path and reset self.mapView.paths.removeAll() self.startSpace = nil self.path = nil self.setSpacesInteractive(interactive: true) self.instructionLabel.text = "1. Click on a space to select it as the starting point." } } } } ``` ## Multi-Floor Wayfinding Using Navigation, no additional work is needed to provide wayfinding between floors. Whenever a user needs to switch a floor, an interactive tooltip with an icon indicating the type of Connection (such as an elevator or stairs) will be drawn. By clicking or tapping the tooltip, the map view switches to the destination floor. ## Multi-Destination Wayfinding Multi-segment directions are directions that include multiple legs. They can be created by passing an array of destinations to MapData.getDirectionsMultiDestination()/>). ```swift let start = // ... get entrance space let destinations: [Any] = [ NavigationTarget.space(store1), NavigationTarget.space(store2), NavigationTarget.space(restaurant) ] mapData.getDirectionsMultiDestination( from: .space(start), to: destinations ) { result in switch result { case .success(let allDirections): allDirections?.enumerated().forEach { index, directions in print("Route \(index + 1): \(directions.distance)m") } case .failure(let error): print("Error: \(error)") } } ``` ## Wayfinding Using Accessible Routes When requesting directions, it is important to consider the needs of the user. Users with mobility restrictions may require a route that avoids stairs and escalators and instead uses ramps or elevators. The getDirections method accepts GetDirectionsOptions, which allows specifying whether an accessible route should be returned. By default, the shortest available route is chosen. The following code demonstrates how to request directions that make use of accessible routes. ```swift mapData.getDirections( from: .space(space1), to: .space(space2), options: GetDirectionsOptions(accessible: true) ) { result in switch result { case .success(let directions): // Use directions case .failure(let error): print("Error: \(error)") } } ``` ## Drawing a Path :::tip A complete example demonstrating drawing a path can be found in the Mappedin iOS GitHub repo: PathsDemoViewController.swift ::: While Navigation provides a complete start and end navigation illustration, it may be desired to draw just the path. This can be done using Paths. :::tip Note that the MapView class implements the Paths interface and exposes it as MapView.paths. Use MapView.paths to utilize Paths methods. ::: Paths can be drawn from one coordinate to another using Paths.add()/>). If using just two coordinates, the path will be drawn straight between the two points. This may work for some scenarios, but in most cases an app will need to show the user their walking path, going through doors and avoiding walls and other objects. Such a path of coordinates can be created by calling the MapData.getDirections() method, passing in a start and end NavigationTarget. Note that a Space requires an entrance to be used as a target. The width of the path is set using the `width` parameter. This value is in meters. Additional path styles are outlined later in this guide in the Path Styles section. ```swift self.mapView.mapData.getDirections( from: .space(start), to: .space(end) ) { result in if case .success(let directions) = result, let directions = directions { let opts = AddPathOptions(color: "#1871fb", width: .value(1.0)) self.mapView.paths.add(coordinates: directions.coordinates, options: opts) { pathResult in if case .success(let createdPath) = pathResult { // Store the path for later use. self.path = createdPath } } } } ``` ## Removing Paths There are two methods that can be used to remove paths. Paths.remove()/>) accepts a path to remove and is used to remove a single path. To remove all paths, Paths.removeAll()/>) can be used. ```ts // Remove a single path. mapView.paths.remove(path); ``` ```ts // Remove all paths mapView.paths.removeAll(); ``` ## Path Styles Mappedin SDKs offer many options to customise how paths appear to the user. Path animations, color, width and many more options can be set using AddPathOptions. ## Dynamic Routing When generating directions, Mappedin SDKs provide the most direct route possible between the origin and destination. There can be scenarios when this is not desired, such as when there there is an obstruction like a spill that requires cleaning or where maintenance crews are active. :::tip A complete example demonstrating dynamic routing can be found in the Mappedin iOS GitHub repo: AreaShapesDemoViewController.swift ::: The MapData.getDirections() method accepts a zones parameter that denotes areas that could be avoided. Zones are defined using DirectionZone and contain a `cost`, `floor` and `geometry`. `cost` represents the additional cost for navigation through the zone and can range from from 0 to Infinity. A cost of 0 will make the zone free to navigate through. A zone cost of Infinity will block passage through the zone. Multiple zones occupying the same location they are added together, along with the cost from the map. The SDK may route directions through a zone if all other routes have a higher cost. `floor` represents the floor for the zone. If not specified the zone blocks all floors. `geometry` is held within a Feature that contains a Geometry object. ## Turn-by-Turn Directions Turn-by-Turn directions are a set of text instructions describing the route the user needs to take. An example is "Turn right and go 10 meters". These could be shown to the user in a list to give them an overview with all steps they need to take, or the current instruction could be displayed along with a map showing the next leg of their journey. :::tip A complete example demonstrating turn-by-turn directions can be found in the Mappedin iOS GitHub repo: TurnByTurnDemoViewController.swift ::: The code sample assembles these actions together and: - Gets directions between the start and end NavigationTarget. - Draws a path using the directions' coordinates. - Creates a Marker with textual turn-by-turn instructions for each leg in the journey. Refer to the Marker Guide for more information on using Markers. ```swift private var currentDirections: Directions? private func onMapReady() { // Add labels to all named spaces mapView.mapData.getByType(.space) { [weak self] (spacesResult: Result<[Space], Error>) in guard let self = self else { return } if case .success(let spaces) = spacesResult { spaces.forEach { space in if !space.name.isEmpty { self.mapView.labels.add( target: space, text: space.name, options: AddLabelOptions(interactive: true) ) { _ in } } } // Find destination space let destination = spaces.first { $0.name == "Family Med Lab EN-09" } // Get origin object self.mapView.mapData.getByType(.mapObject) { [weak self] (objResult: Result<[MapObject], Error>) in guard let self = self else { return } if case .success(let objects) = objResult { let origin = objects.first { $0.name == "Lobby" } if let origin = origin, let destination = destination { self.getAndDisplayDirections(origin: origin, destination: destination) } } } } } } private func getAndDisplayDirections(origin: MapObject, destination: Space) { mapView.mapData.getDirections( from: .mapObject(origin), to: .space(destination) ) { [weak self] result in guard let self = self else { return } if case .success(let directions) = result, let directions = directions { self.currentDirections = directions // Focus on the first 3 steps in the journey let focusCoordinates = Array(directions.coordinates.prefix(3)).map { FocusTarget.coordinate($0) } let focusOptions = FocusOnOptions( screenOffsets: InsetPadding( bottom: 50, left: 50, right: 50, top: 50 ) ) self.mapView.camera.focusOn(targets: focusCoordinates, options: focusOptions) // Add markers for each direction instruction self.addInstructionMarkers(directions: directions) // Draw navigation by default self.drawNavigation() } } } private func addInstructionMarkers(directions: Directions) { let instructions = directions.instructions for i in 0..

\(markerText)

""" mapView.markers.add( target: instruction.coordinate, html: markerTemplate, options: AddMarkerOptions(rank: .tier(.alwaysVisible)) ) { _ in } } } private func drawNavigation() { guard let directions = currentDirections else { return } mapView.navigation.draw(directions: directions) { _ in } } ```