Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions platform/ios/MapLibre.docc/ActionJournalExample.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Enabling the action journal.
```swift
let options = MLNMapOptions()
options.actionJournalOptions.enabled = true
options.actionJournalOptions.renderingStatsReportInterval = 10
options.styleURL = AMERICANA_STYLE
mapView = MLNMapView(frame: view.bounds, options: options)
```
Expand Down
234 changes: 234 additions & 0 deletions platform/ios/MapLibre.docc/FeatureStateExample.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
# Set Feature State

This example demonstrates how to use feature state to create interactive maps with dynamic styling based on user interactions.

## Overview

[Feature state](https://maplibre.org/maplibre-style-spec/expressions/#feature-state) allows you to assign user-defined key-value pairs to features at runtime for styling purposes. This is useful for creating interactive maps where features change appearance based on user interactions like selection, highlighting, or other states.

@Video(
source: "FeatureState.mp4",
poster: "FeatureState.png",
alt: "A video showing how tapping on US states toggles their feature-state values."
)

## Key Concepts

- **Feature State**: User-defined key-value pairs assigned to features at runtime
- **Feature-State Expressions**: Style expressions that access feature state values
- **Interactive Styling**: Dynamic visual changes based on user interactions

## Example Implementation

### Setting Up the Map View

<!-- include-example(FeatureStateExampleSetup) -->

```swift
import MapLibre
import SwiftUI
import UIKit

class FeatureStateExampleUIKit: UIViewController, MLNMapViewDelegate {
private var mapView: MLNMapView!

override func viewDidLoad() {
super.viewDidLoad()

let mapView = MLNMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.delegate = self

// Set initial camera position to show US states
mapView.setCenter(CLLocationCoordinate2D(latitude: 42.619626, longitude: -103.523181), zoomLevel: 3, animated: false)
view.addSubview(mapView)

self.mapView = mapView
}
```

### Adding Data Source and Style Layers

<!-- include-example(FeatureStateExampleLayers) -->

```swift
func mapView(_ mapView: MLNMapView, didFinishLoading style: MLNStyle) {
// Add US states GeoJSON source
let statesURL = URL(string: "https://maplibre.org/maplibre-gl-js/docs/assets/us_states.geojson")!
let statesSource = MLNShapeSource(identifier: "states", url: statesURL)
style.addSource(statesSource)

// Add state fills layer with feature-state expressions for highlighting and selection effects
let stateFillsLayer = MLNFillStyleLayer(identifier: "state-fills", source: statesSource)

// Use feature-state expression to change color based on selection
let fillColorExpression = NSExpression(mglJSONObject: [
"case",
["boolean", ["feature-state", "selected"], false],
UIColor.red.withAlphaComponent(0.7), // Selected color
UIColor.blue.withAlphaComponent(0.5), // Default color
])
stateFillsLayer.fillColor = fillColorExpression

// Use feature-state expression to change opacity when highlighted
// This expression checks if the feature has a "highlighted" state set to true
let highlightedExpression = NSExpression(mglJSONObject: [
"case",
["boolean", ["feature-state", "highlighted"], false],
1.0,
0.5,
])
stateFillsLayer.fillOpacity = highlightedExpression

style.addLayer(stateFillsLayer)

// Add state borders layer with feature-state expressions for highlighting and selection effects
let stateBordersLayer = MLNLineStyleLayer(identifier: "state-borders", source: statesSource)

// Use feature-state expression to change border color based on selection
let borderColorExpression = NSExpression(mglJSONObject: [
"case",
["boolean", ["feature-state", "selected"], false],
UIColor.red, // Selected border color
UIColor.blue, // Default border color
])
stateBordersLayer.lineColor = borderColorExpression

// Use feature-state expression to change line width when highlighted
let borderWidthExpression = NSExpression(mglJSONObject: [
"case",
["boolean", ["feature-state", "highlighted"], false],
2.0,
1.0,
])
stateBordersLayer.lineWidth = borderWidthExpression

style.addLayer(stateBordersLayer)

// Add tap gesture recognizer to handle feature selection
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleMapTap(_:)))
mapView.addGestureRecognizer(tapGesture)
}
```

### Handling User Interactions

<!-- include-example(FeatureStateExampleInteraction) -->

```swift
@objc private func handleMapTap(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: mapView)
let features = mapView.visibleFeatures(at: location, styleLayerIdentifiers: ["state-fills"])

if let feature = features.first {
// Toggle selection state
let currentState = mapView.getFeatureState(feature, sourceID: "states")
let isSelected = currentState?["selected"] as? Bool ?? false

mapView.setFeatureState(feature, sourceID: "states", state: ["selected": !isSelected])

// Show alert with feature information
let stateName = feature.attributes["STATE_NAME"] as? String ?? "Unknown State"
let alert = UIAlertController(
title: "State Selected",
message: "\(stateName) is now \(!isSelected ? "selected" : "deselected")",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
```

### SwiftUI Integration

<!-- include-example(FeatureStateExampleSwiftUI) -->

```swift
}

struct FeatureStateExampleUIViewControllerRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = FeatureStateExampleUIKit

func makeUIViewController(context _: Context) -> FeatureStateExampleUIKit {
FeatureStateExampleUIKit()
}

func updateUIViewController(_: FeatureStateExampleUIKit, context _: Context) {}
}

// SwiftUI wrapper
struct FeatureStateExample: View {
var body: some View {
FeatureStateExampleUIViewControllerRepresentable()
.navigationTitle("Feature State")
.navigationBarTitleDisplayMode(.inline)
}
}
```

## Key Features Demonstrated

### 1. Setting Feature State
```swift
// Set feature state for a specific feature
mapView.setFeatureState(feature, sourceID: "states", state: ["selected": true])

// Set feature state using explicit identifiers
mapView.setFeatureState("states", sourceLayer: nil, featureID: "54", state: ["selected": true])
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method call is incorrect. The first parameter should be sourceID, but the method name suggests it should be setFeatureStateForSource. The correct call should be mapView.setFeatureStateForSource(\"states\", sourceLayer: nil, featureID: \"54\", state: [\"selected\": true]).

Suggested change
mapView.setFeatureState("states", sourceLayer: nil, featureID: "54", state: ["selected": true])
mapView.setFeatureStateForSource("states", sourceLayer: nil, featureID: "54", state: ["selected": true])

Copilot uses AI. Check for mistakes.
```

### 2. Getting Feature State
```swift
// Get current state of a feature
let currentState = mapView.getFeatureState(feature, sourceID: "states")
let isSelected = currentState?["selected"] as? Bool ?? false

// Get state using explicit identifiers
let state = mapView.getFeatureState("states", sourceLayer: nil, featureID: "54")
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method call is incorrect. The method name should be getFeatureStateForSource. The correct call should be mapView.getFeatureStateForSource(\"states\", sourceLayer: nil, featureID: \"54\").

Suggested change
let state = mapView.getFeatureState("states", sourceLayer: nil, featureID: "54")
let state = mapView.getFeatureStateForSource("states", sourceLayer: nil, featureID: "54")

Copilot uses AI. Check for mistakes.
```

### 3. Removing Feature State
```swift
// Remove specific state key
mapView.removeFeatureState(feature, sourceID: "states", stateKey: "selected")

// Remove all state for a feature
mapView.removeFeatureState(feature, sourceID: "states", stateKey: nil)
```

### 4. Feature-State Expressions
```swift
// Create expressions that respond to feature state
let colorExpression = NSExpression(mglJSONObject: [
"case",
["boolean", ["feature-state", "selected"], false],
UIColor.red, // Selected color
UIColor.blue // Default color
])
```

## API Reference

### MLNMapView Feature State Methods

- ``MLNMapView/setFeatureState:sourceID:state:`` - Set state for a feature
- ``MLNMapView/setFeatureState:sourceLayer:featureID:state:`` - Set state with explicit identifiers
- ``MLNMapView/getFeatureState:sourceID:`` - Get current state of a feature
- ``MLNMapView/getFeatureState:sourceLayer:featureID:`` - Get state with explicit identifiers
- ``MLNMapView/removeFeatureState:sourceID:stateKey:`` - Remove state from a feature
- ``MLNMapView/removeFeatureState:sourceLayer:featureID:stateKey:`` - Remove state with explicit identifiers
Comment on lines +216 to +220
Copy link

Copilot AI Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation references are incorrect for the explicit identifier methods. Lines 216, 218, and 220 should reference setFeatureStateForSource:sourceLayer:featureID:state:, getFeatureStateForSource:sourceLayer:featureID:, and removeFeatureStateForSource:sourceLayer:featureID:stateKey: respectively.

Suggested change
- ``MLNMapView/setFeatureState:sourceLayer:featureID:state:`` - Set state with explicit identifiers
- ``MLNMapView/getFeatureState:sourceID:`` - Get current state of a feature
- ``MLNMapView/getFeatureState:sourceLayer:featureID:`` - Get state with explicit identifiers
- ``MLNMapView/removeFeatureState:sourceID:stateKey:`` - Remove state from a feature
- ``MLNMapView/removeFeatureState:sourceLayer:featureID:stateKey:`` - Remove state with explicit identifiers
- ``MLNMapView/setFeatureStateForSource:sourceLayer:featureID:state:`` - Set state with explicit identifiers
- ``MLNMapView/getFeatureState:sourceID:`` - Get current state of a feature
- ``MLNMapView/getFeatureStateForSource:sourceLayer:featureID:`` - Get state with explicit identifiers
- ``MLNMapView/removeFeatureState:sourceID:stateKey:`` - Remove state from a feature
- ``MLNMapView/removeFeatureStateForSource:sourceLayer:featureID:stateKey:`` - Remove state with explicit identifiers

Copilot uses AI. Check for mistakes.

## Best Practices

1. **Use meaningful state keys**: Choose descriptive names like "selected", "highlighted", "touched"
2. **Keep state values simple**: Use basic JSON types (strings, numbers, booleans)
3. **Clear state when appropriate**: Remove state when features are no longer relevant
4. **Use feature-state expressions**: Create dynamic styling that responds to state changes
5. **Handle edge cases**: Always provide fallback values in expressions

## Related Examples

- [Predicates and Expressions](Predicates_and_Expressions.md) - Learn about style expressions
- [GeoJSON](GeoJSON.md) - Working with GeoJSON data sources
- [Gesture Recognizers](GestureRecognizers.md) - Handling user interactions
1 change: 1 addition & 0 deletions platform/ios/MapLibre.docc/MapLibre.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Powerful, free and open-source mapping toolkit with full control over data sourc
- <doc:POIAlongRouteExample>
- <doc:GeoJSON>
- <doc:PMTiles>
- <doc:FeatureStateExample>

### Map Interaction

Expand Down
4 changes: 1 addition & 3 deletions platform/ios/MapLibre.docc/ObserverExample.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ Observe frame rendering statistics with ``MLNMapViewDelegate/mapViewDidFinishRen
<!-- include-example(ObserverExampleRenderingStats) -->

```swift
func mapViewDidFinishRenderingFrame(_: MLNMapView, fullyRendered: Bool, renderingStats: MLNRenderingStats) {

}
func mapViewDidFinishRenderingFrame(_: MLNMapView, fullyRendered _: Bool, renderingStats _: MLNRenderingStats) {}
```

See also: ``MLNMapViewDelegate/mapViewDidFinishRenderingFrame:fullyRendered:`` and ``MLNMapViewDelegate/mapViewDidFinishRenderingFrame:fullyRendered:frameEncodingTime:frameRenderingTime:``
Expand Down
135 changes: 135 additions & 0 deletions platform/ios/app-swift/Sources/FeatureStateExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// #-example-code(FeatureStateExampleSetup)
import MapLibre
import SwiftUI
import UIKit

class FeatureStateExampleUIKit: UIViewController, MLNMapViewDelegate {
private var mapView: MLNMapView!

override func viewDidLoad() {
super.viewDidLoad()

let mapView = MLNMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.delegate = self

// Set initial camera position to show US states
mapView.setCenter(CLLocationCoordinate2D(latitude: 42.619626, longitude: -103.523181), zoomLevel: 3, animated: false)
view.addSubview(mapView)

self.mapView = mapView
}

// #-end-example-code

// #-example-code(FeatureStateExampleLayers)

func mapView(_ mapView: MLNMapView, didFinishLoading style: MLNStyle) {
// Add US states GeoJSON source
let statesURL = URL(string: "https://maplibre.org/maplibre-gl-js/docs/assets/us_states.geojson")!
let statesSource = MLNShapeSource(identifier: "states", url: statesURL)
style.addSource(statesSource)

// Add state fills layer with feature-state expressions for highlighting and selection effects
let stateFillsLayer = MLNFillStyleLayer(identifier: "state-fills", source: statesSource)

// Use feature-state expression to change color based on selection
let fillColorExpression = NSExpression(mglJSONObject: [
"case",
["boolean", ["feature-state", "selected"], false],
UIColor.red.withAlphaComponent(0.7), // Selected color
UIColor.blue.withAlphaComponent(0.5), // Default color
])
stateFillsLayer.fillColor = fillColorExpression

// Use feature-state expression to change opacity when highlighted
// This expression checks if the feature has a "highlighted" state set to true
let highlightedExpression = NSExpression(mglJSONObject: [
"case",
["boolean", ["feature-state", "highlighted"], false],
1.0,
0.5,
])
stateFillsLayer.fillOpacity = highlightedExpression

style.addLayer(stateFillsLayer)

// Add state borders layer with feature-state expressions for highlighting and selection effects
let stateBordersLayer = MLNLineStyleLayer(identifier: "state-borders", source: statesSource)

// Use feature-state expression to change border color based on selection
let borderColorExpression = NSExpression(mglJSONObject: [
"case",
["boolean", ["feature-state", "selected"], false],
UIColor.red, // Selected border color
UIColor.blue, // Default border color
])
stateBordersLayer.lineColor = borderColorExpression

// Use feature-state expression to change line width when highlighted
let borderWidthExpression = NSExpression(mglJSONObject: [
"case",
["boolean", ["feature-state", "highlighted"], false],
2.0,
1.0,
])
stateBordersLayer.lineWidth = borderWidthExpression

style.addLayer(stateBordersLayer)

// Add tap gesture recognizer to handle feature selection
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleMapTap(_:)))
mapView.addGestureRecognizer(tapGesture)
}

// #-end-example-code

// #-example-code(FeatureStateExampleInteraction)

@objc private func handleMapTap(_ gesture: UITapGestureRecognizer) {
let location = gesture.location(in: mapView)
let features = mapView.visibleFeatures(at: location, styleLayerIdentifiers: ["state-fills"])

if let feature = features.first {
// Toggle selection state
let currentState = mapView.getFeatureState(feature, sourceID: "states")
let isSelected = currentState?["selected"] as? Bool ?? false

mapView.setFeatureState(feature, sourceID: "states", state: ["selected": !isSelected])

// Show alert with feature information
let stateName = feature.attributes["STATE_NAME"] as? String ?? "Unknown State"
let alert = UIAlertController(
title: "State Selected",
message: "\(stateName) is now \(!isSelected ? "selected" : "deselected")",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
// #-end-example-code

// #-example-code(FeatureStateExampleSwiftUI)
}

struct FeatureStateExampleUIViewControllerRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = FeatureStateExampleUIKit

func makeUIViewController(context _: Context) -> FeatureStateExampleUIKit {
FeatureStateExampleUIKit()
}

func updateUIViewController(_: FeatureStateExampleUIKit, context _: Context) {}
}

// SwiftUI wrapper
struct FeatureStateExample: View {
var body: some View {
FeatureStateExampleUIViewControllerRepresentable()
.navigationTitle("Feature State")
.navigationBarTitleDisplayMode(.inline)
}
}

// #-end-example-code
Loading