Skip to content
Open
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
56dff8a
initial commit
chriswebb09 Jun 15, 2026
a4dee50
update sample
chriswebb09 Jun 15, 2026
7f82f45
update readme
chriswebb09 Jun 15, 2026
29c72c1
update readme
chriswebb09 Jun 15, 2026
12b2a93
update
chriswebb09 Jun 15, 2026
5f5135e
update
chriswebb09 Jun 15, 2026
c6bb865
update for script error
chriswebb09 Jun 15, 2026
262df01
update
chriswebb09 Jun 15, 2026
025101a
add in comma
chriswebb09 Jun 15, 2026
90662b0
update
chriswebb09 Jun 15, 2026
3f82dfc
update
chriswebb09 Jun 15, 2026
7994419
update
chriswebb09 Jun 15, 2026
028fc69
Apply suggestions from code review
chriswebb09 Jun 15, 2026
2816732
fix optional error
chriswebb09 Jun 15, 2026
0314c69
Apply suggestions from code review
chriswebb09 Jun 15, 2026
eefe293
update
chriswebb09 Jun 15, 2026
ed6ad2d
update
chriswebb09 Jun 15, 2026
f6c315f
Apply suggestions from code review
chriswebb09 Jun 15, 2026
c2cb7e9
Apply suggestions from code review
chriswebb09 Jun 15, 2026
8eb53ac
update indentation
chriswebb09 Jun 15, 2026
3d16479
update formatting
chriswebb09 Jun 15, 2026
86e16a2
Potential fix for pull request finding
chriswebb09 Jun 15, 2026
92129d9
update remove textfield
chriswebb09 Jun 15, 2026
823c370
update
chriswebb09 Jun 15, 2026
9932321
update
chriswebb09 Jun 15, 2026
306038d
cleanup
chriswebb09 Jun 15, 2026
e49a67d
Apply suggestions from code review
chriswebb09 Jun 16, 2026
caaa93a
fix error
chriswebb09 Jun 16, 2026
a75a2c1
cleanup
chriswebb09 Jun 16, 2026
a43a17f
update
chriswebb09 Jun 16, 2026
f3a118c
cleanup
chriswebb09 Jun 16, 2026
6759ded
Apply suggestions from code review
chriswebb09 Jun 16, 2026
9d70469
Merge branch 'v.next' into chrisw/navigate-and-interact-with-keyboard
chriswebb09 Jun 16, 2026
a45d9e0
Apply suggestions from code review
chriswebb09 Jun 16, 2026
c3fcd46
make method names more idiomatic, add in comments, make envelope reap…
chriswebb09 Jun 17, 2026
b8d2ed3
Apply suggestions from code review
chriswebb09 Jun 18, 2026
fbccaeb
simplify code
chriswebb09 Jun 22, 2026
9b7f48d
update tipkit initialization
chriswebb09 Jun 22, 2026
7148266
update
chriswebb09 Jun 22, 2026
63ebd9a
fix formatting
chriswebb09 Jun 22, 2026
5ef82e6
Apply suggestions from code review
chriswebb09 Jun 22, 2026
02f2e40
revert change to README
chriswebb09 Jun 23, 2026
8b715d9
add bac additional info section
chriswebb09 Jun 23, 2026
d85ecd4
Merge remote-tracking branch 'refs/remotes/origin/chrisw/navigate-and…
chriswebb09 Jun 23, 2026
e555b4a
fix ofr linting error in README
chriswebb09 Jun 23, 2026
c16eb60
update envelope logic
chriswebb09 Jun 24, 2026
49a79b7
update halo wddwxr logic
chriswebb09 Jun 24, 2026
5879ba0
hide and show keyboard tip depending when keyboard is active
chriswebb09 Jun 24, 2026
f0e8d34
update
chriswebb09 Jun 24, 2026
d8e399e
cleanup
chriswebb09 Jun 25, 2026
9cad02e
Potential fix for pull request finding
chriswebb09 Jun 25, 2026
939730b
update map scale and only show keyboard tip when keyboard is active
chriswebb09 Jun 26, 2026
6f07fd9
Apply suggestions from code review
chriswebb09 Jun 26, 2026
c4e515b
update README url for swift
chriswebb09 Jun 26, 2026
c8e6a28
add changes for review
chriswebb09 Jun 26, 2026
30d9c7f
Apply suggestions from code review
chriswebb09 Jun 29, 2026
89bfc9e
Apply suggestions from code review
chriswebb09 Jun 29, 2026
3853780
stash
chriswebb09 Jun 30, 2026
779f23d
stash 2
chriswebb09 Jun 30, 2026
7431fe4
update keyboard actions
chriswebb09 Jun 30, 2026
3d47da4
update tips text
chriswebb09 Jun 30, 2026
5511dfb
remove unused code
chriswebb09 Jun 30, 2026
210a9a2
Apply suggestions from code review
chriswebb09 Jun 30, 2026
4f9563c
Apply suggestions from code review
chriswebb09 Jun 30, 2026
a9b5ded
update keyboard functionality
chriswebb09 Jul 1, 2026
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
20 changes: 20 additions & 0 deletions Samples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -343,9 +343,13 @@
95321B302E554B31002E9DE2 /* ApplyRenderersToSceneLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95321B2F2E554B20002E9DE2 /* ApplyRenderersToSceneLayerView.swift */; };
95321B322E554B88002E9DE2 /* ApplyRenderersToSceneLayerView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 95321B2F2E554B20002E9DE2 /* ApplyRenderersToSceneLayerView.swift */; };
9537AFD72C220EF0000923C5 /* ExchangeSetwithoutUpdates in Resources */ = {isa = PBXBuildFile; fileRef = 9537AFD62C220EF0000923C5 /* ExchangeSetwithoutUpdates */; settings = {ASSET_TAGS = (ConfigureElectronicNavigationalCharts, ); }; };
95398E6F2FDFAF1600E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95398E6E2FDFAF1400E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.swift */; };
95398E712FDFAF6300E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 95398E6E2FDFAF1400E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.swift */; };
95398E732FE0C78B00E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95398E722FE0C78800E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.Model.swift */; };
9547085C2C3C719800CA8579 /* EditFeatureAttachmentsView.Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9547085B2C3C719800CA8579 /* EditFeatureAttachmentsView.Model.swift */; };
954AEDEE2C01332600265114 /* SelectFeaturesInSceneLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 954AEDED2C01332600265114 /* SelectFeaturesInSceneLayerView.swift */; };
955271612C0E6749009B1ED4 /* AddRasterFromServiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955271602C0E6749009B1ED4 /* AddRasterFromServiceView.swift */; };
95565A4D2FE0C82D00DAEEBF /* NavigateMapAndIdentifyFeaturesWithKeyboardView.Model.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 95398E722FE0C78800E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.Model.swift */; };
955AFAC42C10FD6F009C8FE5 /* ApplyMosaicRuleToRastersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 955AFAC32C10FD6F009C8FE5 /* ApplyMosaicRuleToRastersView.swift */; };
955AFAC62C110B8A009C8FE5 /* ApplyMosaicRuleToRastersView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = 955AFAC32C10FD6F009C8FE5 /* ApplyMosaicRuleToRastersView.swift */; };
956332482E3455DB0091C877 /* SimplifyGeometryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 956332472E3455D70091C877 /* SimplifyGeometryView.swift */; };
Expand Down Expand Up @@ -777,6 +781,8 @@
dstPath = "";
dstSubfolder = Resources;
files = (
95565A4D2FE0C82D00DAEEBF /* NavigateMapAndIdentifyFeaturesWithKeyboardView.Model.swift in Copy Source Code Files */,
95398E712FDFAF6300E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.swift in Copy Source Code Files */,
95DF31822FD37D99005D06E7 /* UpdateBasemapForContrastAccessibilityView.swift in Copy Source Code Files */,
D7CDCBAC2F86C3CE00B6C6AE /* ShowLineOfSightAnalysisInMapView.swift in Copy Source Code Files */,
D73169B42F7D990600AB132D /* ShowInteractiveViewshedWithAnalysisOverlayView.swift in Copy Source Code Files */,
Expand Down Expand Up @@ -1279,6 +1285,8 @@
9520B2B42E135AD800B3BEF9 /* ShowExploratoryLineOfSightBetweenGeoelementsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowExploratoryLineOfSightBetweenGeoelementsView.swift; sourceTree = "<group>"; };
95321B2F2E554B20002E9DE2 /* ApplyRenderersToSceneLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplyRenderersToSceneLayerView.swift; sourceTree = "<group>"; };
9537AFD62C220EF0000923C5 /* ExchangeSetwithoutUpdates */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ExchangeSetwithoutUpdates; sourceTree = "<group>"; };
95398E6E2FDFAF1400E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigateMapAndIdentifyFeaturesWithKeyboardView.swift; sourceTree = "<group>"; };
95398E722FE0C78800E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigateMapAndIdentifyFeaturesWithKeyboardView.Model.swift; sourceTree = "<group>"; };
9547085B2C3C719800CA8579 /* EditFeatureAttachmentsView.Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFeatureAttachmentsView.Model.swift; sourceTree = "<group>"; };
954AEDED2C01332600265114 /* SelectFeaturesInSceneLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectFeaturesInSceneLayerView.swift; sourceTree = "<group>"; };
955271602C0E6749009B1ED4 /* AddRasterFromServiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRasterFromServiceView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1841,6 +1849,7 @@
D7352F8D2BD992C40013FFEF /* Monitor changes to draw status */,
D71371782BD88ECC00EB2F86 /* Monitor changes to layer view state */,
D752D9422A3A6EB8003EB25E /* Monitor changes to map load status */,
95398E702FDFAF2300E30CE8 /* Navigate map view and identify features with keyboard */,
75DD739029D38B1B0010229D /* Navigate route */,
D7588F5B2B7D8DAA008B75E2 /* Navigate route with rerouting */,
D76929F32B4F78340047205E /* Orbit camera around object */,
Expand Down Expand Up @@ -2947,6 +2956,15 @@
path = 9d2987a825c646468b3ce7512fb76e2d;
sourceTree = "<group>";
};
95398E702FDFAF2300E30CE8 /* Navigate map view and identify features with keyboard */ = {
isa = PBXGroup;
children = (
95398E6E2FDFAF1400E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.swift */,
95398E722FE0C78800E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.Model.swift */,
);
path = "Navigate map view and identify features with keyboard";
sourceTree = "<group>";
};
954AEDEF2C01332F00265114 /* Select features in scene layer */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4889,6 +4907,7 @@
007079212DE0E7E800E06300 /* ListGeodatabaseVersionsView.swift in Sources */,
0086F40128E3770A00974721 /* ShowExploratoryViewshedFromPointInSceneView.swift in Sources */,
0044289229C90C0B00160767 /* GetElevationAtPointOnSurfaceView.swift in Sources */,
95398E6F2FDFAF1600E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.swift in Sources */,
00E1D90B2BC0AF97001AEB6A /* SnapGeometryEditsView.SnapSettingsView.swift in Sources */,
D7E440D72A1ECE7D005D74DE /* CreateBuffersAroundPointsView.swift in Sources */,
00D4EF802863842100B9CC30 /* AddFeatureLayersView.swift in Sources */,
Expand Down Expand Up @@ -4920,6 +4939,7 @@
00FA4E682DCA87A9008A34CF /* ApplyRGBRendererView.RangeSlider.swift in Sources */,
D75101812A2E493600B8FA48 /* ShowLabelsOnLayerView.swift in Sources */,
1C3B7DCB2A5F64FC00907443 /* AnalyzeNetworkWithSubnetworkTraceView.swift in Sources */,
95398E732FE0C78B00E30CE8 /* NavigateMapAndIdentifyFeaturesWithKeyboardView.Model.swift in Sources */,
00B042E8282EDC690072E1B4 /* SetBasemapView.swift in Sources */,
E004A6E62846A61F002A1FE6 /* StyleGraphicsWithSymbolsView.swift in Sources */,
0000FB6E2BBDB17600845921 /* Add3DTilesLayerView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Copyright 2026 Esri
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import ArcGIS
import Foundation
import SwiftUI
import UIKit.UIColor

extension NavigateMapAndIdentifyFeaturesWithKeyboardView {
/// The model for the sample.
@MainActor
@Observable
final class Model {
/// The maximum number of features that can be identified with number keys.
private static let maximumNumberedFeatures = 9

/// Buffer distance around the selection geometry to account for edge cases.
private static let selectionBufferDistance = LinearUnit.meters.convert(to: .meters, value: 60)
Comment thread
chriswebb09 marked this conversation as resolved.
Outdated

/// Attribute used to title and label each feature.
private static let nameAttribute = "name"

/// Colors used for the marker, selection halo, and label.
private static let markerFillColor = UIColor(red: 11 / 255, green: 79 / 255, blue: 138 / 255, alpha: 1)
static let selectionHaloColor = Color(red: 190 / 255, green: 24 / 255, blue: 93 / 255)
private static let labelTextColor = UIColor(red: 31 / 255, green: 35 / 255, blue: 40 / 255, alpha: 1)

/// The map displayed in the map view.
let map: Map

/// The feature table of restaurants in Redlands.
private let restaurantsTable = ServiceFeatureTable(url: .redlandsRestaurants)

/// The feature layer holding the restaurants displayed and identified by the sample.
private let restaurantsLayer: FeatureLayer

/// The overlay for the numbered 1-9 labels.
let labelOverlay = GraphicsOverlay()

/// Features currently in the area of interest, indexed by the matching number key.
private(set) var numberedFeatures: [Feature] = []

/// A Boolean value indicating whether more than nine features are selected.
private(set) var hasMoreThanNineSelectedFeatures = false

init() {
restaurantsLayer = FeatureLayer(featureTable: restaurantsTable)
restaurantsLayer.renderer = SimpleRenderer(symbol: Self.restaurantSymbol)

let map = Map(basemapStyle: .arcGISLightGray)
map.initialViewpoint = Viewpoint(
center: Point(x: -117.1825, y: 34.0556, spatialReference: .wgs84),
scale: 4_000
)
map.addOperationalLayer(restaurantsLayer)
self.map = map
}

/// Ensures the feature table is fully loaded before querying.
func ensureLayerLoaded() async throws {
// Load the feature table to ensure metadata and features are available
try await restaurantsTable.load()
}
Comment thread
chriswebb09 marked this conversation as resolved.

func selectFeatures(
intersecting selectionGeometry: Geometry?,
containsScreenPoint: (CGPoint) -> Bool,
screenPointFor: (Point) -> CGPoint?
) async throws {
clearSelection()

guard let selectionGeometry else { return }

let unorderedFeatures = try await makeFeatures(intersecting: selectionGeometry)
.filter { orderedFeature in
guard let screenPoint = screenPointFor(orderedFeature.anchor) else { return false }
return containsScreenPoint(screenPoint)
}
let orderedFeatures = unorderedFeatures.sorted { lhs, rhs in
let lhsScreenPoint = screenPointFor(lhs.anchor)
let rhsScreenPoint = screenPointFor(rhs.anchor)

switch (lhsScreenPoint, rhsScreenPoint) {
case let (l?, r?):
if l.y != r.y { return l.y < r.y }
if l.x != r.x { return l.x < r.x }
return Self.isEarlierInGeographicReadingOrder(lhs.anchor, than: rhs.anchor)
case (nil, nil):
return Self.isEarlierInGeographicReadingOrder(lhs.anchor, than: rhs.anchor)
case (nil, _?):
return false
case (_?, nil):
return true
}
}
Comment thread
chriswebb09 marked this conversation as resolved.
Outdated

hasMoreThanNineSelectedFeatures = orderedFeatures.count > Self.maximumNumberedFeatures

// Select all intersecting features; only the first 9 are numbered/labeled.
restaurantsLayer.selectFeatures(orderedFeatures.map(\.feature))

addNumberedLabels(for: orderedFeatures)
}
Comment thread
chriswebb09 marked this conversation as resolved.

/// Clears selected features, label graphics, and numbered feature state.
func clearSelection() {
restaurantsLayer.clearSelection()
labelOverlay.removeAllGraphics()
numberedFeatures.removeAll()
hasMoreThanNineSelectedFeatures = false
}

/// Reads the feature's name attribute, returning the fallback when it is missing or blank.
/// - Parameters:
/// - feature: The feature whose name is read.
/// - fallback: The value to return when the feature has no name.
/// - Returns: The feature's name, or the fallback if no name exists.
func name(for feature: Feature, fallback: String?) -> String? {
guard let name = feature.attributes[Self.nameAttribute] as? String,
!name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return fallback
}
return name
}

/// Queries restaurant features intersecting the given geometry.
/// - Parameter geometry: The geometry used to query restaurant features.
/// - Returns: The restaurant features with their map positions.
private func makeFeatures(intersecting geometry: Geometry) async throws -> [OrderedFeature] {
// Add a buffer to catch features near the rectangle edges.
// This accounts for projection distortions and rendering tolerances.
let queryGeometry = GeometryEngine.buffer(around: geometry, distance: Self.selectionBufferDistance) ?? geometry

let queryParameters = QueryParameters()
queryParameters.geometry = queryGeometry
queryParameters.spatialRelationship = .intersects
queryParameters.maxFeatures = 1000

let queryResult = try await restaurantsTable.queryFeatures(using: queryParameters)
let allFeatures = Array(queryResult.features())

return allFeatures
.compactMap { feature -> OrderedFeature? in
guard let anchor = feature.geometry as? Point else { return nil }
return OrderedFeature(feature: feature, anchor: anchor)
}
}

/// Returns whether a map point is earlier in north-to-south, west-to-east order.
private static func isEarlierInGeographicReadingOrder(_ lhs: Point, than rhs: Point) -> Bool {
if lhs.y != rhs.y { return lhs.y > rhs.y }
return lhs.x < rhs.x
}

/// Adds numbered text labels for the first restaurant features.
/// - Parameter orderedFeatures: The ordered restaurant features to label.
private func addNumberedLabels(for orderedFeatures: [OrderedFeature]) {
let numberedOrderedFeatures = orderedFeatures.prefix(Self.maximumNumberedFeatures)

for (offset, orderedFeature) in numberedOrderedFeatures.enumerated() {
let number = offset + 1
let text = name(for: orderedFeature.feature, fallback: nil).map { "\(number): \($0)" } ?? "\(number)"
let labelSymbol = TextSymbol(
text: text,
color: Self.labelTextColor,
size: 15,
horizontalAlignment: .center,
verticalAlignment: .top
)
labelSymbol.haloColor = .white
labelSymbol.haloWidth = 2
labelSymbol.offsetY = -14
labelOverlay.addGraphic(Graphic(geometry: orderedFeature.anchor, symbol: labelSymbol))
numberedFeatures.append(orderedFeature.feature)
}
}

/// The restaurant marker symbol.
private static var restaurantSymbol: SimpleMarkerSymbol {
let symbol = SimpleMarkerSymbol(style: .circle, color: markerFillColor, size: 12)
symbol.outline = SimpleLineSymbol(style: .solid, color: .white, width: 1.5)
return symbol
}

/// A feature and its map location.
private struct OrderedFeature {
let feature: Feature
let anchor: Point
}
}
}

private extension URL {
/// The URL of the Redlands restaurants feature service.
static var redlandsRestaurants: URL {
URL(string: "https://services2.arcgis.com/ZQgQTuoyBrtmoGdP/arcgis/rest/services/redlands_food/FeatureServer/0")!
}
}
Loading
Loading