-
-
Notifications
You must be signed in to change notification settings - Fork 439
Add feature state functionality to iOS #3858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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]) | ||||||||||||||||||||||
| ``` | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ### 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") | ||||||||||||||||||||||
|
||||||||||||||||||||||
| let state = mapView.getFeatureState("states", sourceLayer: nil, featureID: "54") | |
| let state = mapView.getFeatureStateForSource("states", sourceLayer: nil, featureID: "54") |
Copilot
AI
Oct 4, 2025
There was a problem hiding this comment.
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.
| - ``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 |
| 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 |
There was a problem hiding this comment.
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 bemapView.setFeatureStateForSource(\"states\", sourceLayer: nil, featureID: \"54\", state: [\"selected\": true]).