Skip to content

Commit e1997ef

Browse files
authored
Merge pull request #465 from Esri/Caleb/New-CreateDynamicBasemapGallery
[New] Create dynamic basemap gallery
2 parents 97ef908 + d1f79c5 commit e1997ef

File tree

6 files changed

+370
-0
lines changed

6 files changed

+370
-0
lines changed

Samples.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,10 @@
374374
D77D9C012BB2439400B38A6C /* AugmentRealityToShowHiddenInfrastructureView.ARSceneView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D77D9BFF2BB2438200B38A6C /* AugmentRealityToShowHiddenInfrastructureView.ARSceneView.swift */; };
375375
D78666AD2A2161F100C60110 /* FindNearestVertexView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78666AC2A2161F100C60110 /* FindNearestVertexView.swift */; };
376376
D78666AE2A21629200C60110 /* FindNearestVertexView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D78666AC2A2161F100C60110 /* FindNearestVertexView.swift */; };
377+
D78FA4942C3C88880079313E /* CreateDynamicBasemapGalleryView.Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78FA4932C3C88880079313E /* CreateDynamicBasemapGalleryView.Views.swift */; };
378+
D78FA4952C3C8E8A0079313E /* CreateDynamicBasemapGalleryView.Views.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D78FA4932C3C88880079313E /* CreateDynamicBasemapGalleryView.Views.swift */; };
379+
D79482D42C35D872006521CD /* CreateDynamicBasemapGalleryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79482D02C35D872006521CD /* CreateDynamicBasemapGalleryView.swift */; };
380+
D79482D72C35D8A3006521CD /* CreateDynamicBasemapGalleryView.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D79482D02C35D872006521CD /* CreateDynamicBasemapGalleryView.swift */; };
377381
D79EE76E2A4CEA5D005A52AE /* SetUpLocationDrivenGeotriggersView.Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79EE76D2A4CEA5D005A52AE /* SetUpLocationDrivenGeotriggersView.Model.swift */; };
378382
D79EE76F2A4CEA7F005A52AE /* SetUpLocationDrivenGeotriggersView.Model.swift in Copy Source Code Files */ = {isa = PBXBuildFile; fileRef = D79EE76D2A4CEA5D005A52AE /* SetUpLocationDrivenGeotriggersView.Model.swift */; };
379383
D7A737E02BABB9FE00B7C7FC /* AugmentRealityToShowHiddenInfrastructureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A737DC2BABB9FE00B7C7FC /* AugmentRealityToShowHiddenInfrastructureView.swift */; };
@@ -523,6 +527,8 @@
523527
dstPath = "";
524528
dstSubfolderSpec = 7;
525529
files = (
530+
D78FA4952C3C8E8A0079313E /* CreateDynamicBasemapGalleryView.Views.swift in Copy Source Code Files */,
531+
D79482D72C35D8A3006521CD /* CreateDynamicBasemapGalleryView.swift in Copy Source Code Files */,
526532
003B36F92C5042BA00A75F66 /* ShowServiceAreaView.swift in Copy Source Code Files */,
527533
95ADF34F2C3CBAE800566FF6 /* EditFeatureAttachmentsView.Model.swift in Copy Source Code Files */,
528534
9579FCEC2C33616B00FC8A1D /* EditFeatureAttachmentsView.swift in Copy Source Code Files */,
@@ -932,6 +938,8 @@
932938
D77BC5362B59A2D3007B49B6 /* StylePointWithDistanceCompositeSceneSymbolView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StylePointWithDistanceCompositeSceneSymbolView.swift; sourceTree = "<group>"; };
933939
D77D9BFF2BB2438200B38A6C /* AugmentRealityToShowHiddenInfrastructureView.ARSceneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AugmentRealityToShowHiddenInfrastructureView.ARSceneView.swift; sourceTree = "<group>"; };
934940
D78666AC2A2161F100C60110 /* FindNearestVertexView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindNearestVertexView.swift; sourceTree = "<group>"; };
941+
D78FA4932C3C88880079313E /* CreateDynamicBasemapGalleryView.Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateDynamicBasemapGalleryView.Views.swift; sourceTree = "<group>"; };
942+
D79482D02C35D872006521CD /* CreateDynamicBasemapGalleryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateDynamicBasemapGalleryView.swift; sourceTree = "<group>"; };
935943
D79EE76D2A4CEA5D005A52AE /* SetUpLocationDrivenGeotriggersView.Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetUpLocationDrivenGeotriggersView.Model.swift; sourceTree = "<group>"; };
936944
D7A737DC2BABB9FE00B7C7FC /* AugmentRealityToShowHiddenInfrastructureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AugmentRealityToShowHiddenInfrastructureView.swift; sourceTree = "<group>"; };
937945
D7ABA2F82A32579C0021822B /* MeasureDistanceInSceneView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeasureDistanceInSceneView.swift; sourceTree = "<group>"; };
@@ -1152,6 +1160,7 @@
11521160
D7E440D12A1ECBC2005D74DE /* Create buffers around points */,
11531161
D7B3C5C02A43B71E001DA4D8 /* Create convex hull around geometries */,
11541162
D744FD132A2112360084A66C /* Create convex hull around points */,
1163+
D79482D32C35D872006521CD /* Create dynamic basemap gallery */,
11551164
1C19B4EA2A578E46001D2506 /* Create load report */,
11561165
D73FABE82AD4A0370048EC70 /* Create mobile geodatabase */,
11571166
E004A6EB28495538002A1FE6 /* Create planar and geodetic buffers */,
@@ -2341,6 +2350,15 @@
23412350
path = "Find nearest vertex";
23422351
sourceTree = "<group>";
23432352
};
2353+
D79482D32C35D872006521CD /* Create dynamic basemap gallery */ = {
2354+
isa = PBXGroup;
2355+
children = (
2356+
D79482D02C35D872006521CD /* CreateDynamicBasemapGalleryView.swift */,
2357+
D78FA4932C3C88880079313E /* CreateDynamicBasemapGalleryView.Views.swift */,
2358+
);
2359+
path = "Create dynamic basemap gallery";
2360+
sourceTree = "<group>";
2361+
};
23442362
D7A737DF2BABB9FE00B7C7FC /* Augment reality to show hidden infrastructure */ = {
23452363
isa = PBXGroup;
23462364
children = (
@@ -3075,9 +3093,11 @@
30753093
004A2BA22BED456500C297CE /* ApplyScheduledUpdatesToPreplannedMapAreaView.swift in Sources */,
30763094
1CAB8D4B2A3CEAB0002AA649 /* RunValveIsolationTraceView.Model.swift in Sources */,
30773095
E070A0A3286F3B6000F2B606 /* DownloadPreplannedMapAreaView.swift in Sources */,
3096+
D79482D42C35D872006521CD /* CreateDynamicBasemapGalleryView.swift in Sources */,
30783097
D77570C02A2942F800F490CD /* AnimateImagesWithImageOverlayView.swift in Sources */,
30793098
D7054AE92ACCCB6C007235BA /* Animate3DGraphicView.SettingsView.swift in Sources */,
30803099
D7BA8C442B2A4DAA00018633 /* Array+RawRepresentable.swift in Sources */,
3100+
D78FA4942C3C88880079313E /* CreateDynamicBasemapGalleryView.Views.swift in Sources */,
30813101
E0EA0B772866390E00C9621D /* ProjectGeometryView.swift in Sources */,
30823102
D74C8BFE2ABA5605007C76B8 /* StyleSymbolsFromMobileStyleFileView.swift in Sources */,
30833103
D7E7D0812AEB39D5003AAD02 /* FindRouteInTransportNetworkView.swift in Sources */,
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright 2024 Esri
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import ArcGIS
16+
import SwiftUI
17+
18+
extension CreateDynamicBasemapGalleryView {
19+
/// A view for selecting a basemap.
20+
struct BasemapGallery: View {
21+
/// The view model for the sample.
22+
@ObservedObject var model: Model
23+
24+
var body: some View {
25+
Form {
26+
Picker("Style", selection: $model.basemapStyle) {
27+
ForEach(model.stylesInfo, id: \.style) { styleInfo in
28+
HStack {
29+
if let image = styleInfo.thumbnail?.image {
30+
Image(uiImage: image)
31+
.resizable()
32+
.scaledToFit()
33+
.frame(height: 50)
34+
}
35+
36+
Text(styleInfo.styleName)
37+
}
38+
.tag(styleInfo.style)
39+
}
40+
}
41+
.pickerStyle(.navigationLink)
42+
43+
Picker("Language", selection: $model.basemapStyleLanguage) {
44+
Section("Strategy") {
45+
ForEach(model.languageStrategies, id: \.self) { strategy in
46+
Text(strategy.label)
47+
.tag(BasemapStyleLanguage.strategic(strategy))
48+
}
49+
}
50+
51+
Section("Specific") {
52+
ForEach(model.languages, id: \.self) { language in
53+
Text(language.label ?? "Unknown")
54+
.tag(BasemapStyleLanguage.specific(language))
55+
}
56+
}
57+
}
58+
.disabled(model.basemapStyleInfo?.languageStrategies.isEmpty ?? true)
59+
60+
Picker("Worldview", selection: $model.worldviewCode) {
61+
ForEach(model.worldviews, id: \.?.code) { worldview in
62+
Text(worldview?.displayName ?? "")
63+
.tag(worldview?.code)
64+
}
65+
}
66+
.disabled(model.basemapStyleInfo?.worldviews.isEmpty ?? true)
67+
}
68+
}
69+
}
70+
}
71+
72+
extension BasemapStyleLanguage: Hashable {
73+
public func hash(into hasher: inout Hasher) {
74+
switch self {
75+
case .strategic(let strategy):
76+
hasher.combine(strategy)
77+
case .specific(let language):
78+
hasher.combine(language)
79+
@unknown default:
80+
fatalError("Unknown basemap style language.")
81+
}
82+
}
83+
}
84+
85+
private extension BasemapStyleLanguage {
86+
/// A human-readable label for the basemap style language.
87+
var label: String? {
88+
switch self {
89+
case .strategic(let strategy):
90+
strategy.label
91+
case .specific(let language):
92+
language.label
93+
@unknown default:
94+
fatalError("Unknown basemap style language.")
95+
}
96+
}
97+
}
98+
99+
private extension BasemapStyleLanguageStrategy {
100+
/// A human-readable label for the basemap style language strategy.
101+
var label: String {
102+
switch self {
103+
case .default: "Default"
104+
case .global: "Global"
105+
case .local: "Local"
106+
case .applicationLocale: "System Locale"
107+
@unknown default:
108+
fatalError("Unknown basemap style language strategy.")
109+
}
110+
}
111+
}
112+
113+
private extension Locale.Language {
114+
/// A human-readable label for the language.
115+
var label: String? {
116+
Locale.current.localizedString(forIdentifier: self.maximalIdentifier)
117+
}
118+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright 2024 Esri
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import ArcGIS
16+
import SwiftUI
17+
18+
struct CreateDynamicBasemapGalleryView: View {
19+
/// The view model for the sample.
20+
@StateObject private var model = Model()
21+
22+
/// A Boolean value indicating whether the basemap gallery is showing.
23+
@State private var isShowingBasemapGallery = false
24+
25+
/// The error shown in the error alert.
26+
@State private var error: Error?
27+
28+
var body: some View {
29+
MapView(map: model.map)
30+
.toolbar {
31+
ToolbarItemGroup(placement: .bottomBar) {
32+
Button("Basemap") {
33+
isShowingBasemapGallery.toggle()
34+
}
35+
.popover(isPresented: $isShowingBasemapGallery) {
36+
NavigationStack {
37+
BasemapGallery(model: model)
38+
.navigationTitle("Basemap")
39+
.navigationBarTitleDisplayMode(.inline)
40+
.toolbar {
41+
ToolbarItem(placement: .confirmationAction) {
42+
Button("Done") { isShowingBasemapGallery = false }
43+
}
44+
}
45+
}
46+
.presentationDetents([.fraction(0.5)])
47+
.frame(idealWidth: 320, idealHeight: 380)
48+
}
49+
}
50+
}
51+
.task {
52+
do {
53+
try await model.loadStylesInfo()
54+
} catch {
55+
self.error = error
56+
}
57+
}
58+
.errorAlert(presentingError: $error)
59+
}
60+
}
61+
62+
extension CreateDynamicBasemapGalleryView {
63+
/// The view model for the sample.
64+
@MainActor
65+
final class Model: ObservableObject {
66+
/// The map shown in the map view.
67+
let map = Map()
68+
69+
/// The basemap style of the map's basemap.
70+
@Published var basemapStyle = Basemap.Style.arcGISNavigation {
71+
didSet {
72+
guard basemapStyle != oldValue else { return }
73+
74+
basemapStyleLanguage = .strategic(.default)
75+
worldviewCode = nil
76+
basemapStyleInfo = stylesInfo.first { $0.style == basemapStyle }
77+
78+
updateBasemap()
79+
}
80+
}
81+
82+
/// The basemap style language of the map's basemap.
83+
@Published var basemapStyleLanguage = BasemapStyleLanguage.strategic(.default) {
84+
didSet {
85+
guard basemapStyleLanguage != oldValue else { return }
86+
updateBasemap()
87+
}
88+
}
89+
90+
/// The worldview code of the map's basemap.
91+
@Published var worldviewCode: String? {
92+
didSet {
93+
guard worldviewCode != oldValue else { return }
94+
updateBasemap()
95+
}
96+
}
97+
98+
/// The basemap styles info from the basemap styles service.
99+
@Published private(set) var stylesInfo: [BasemapStyleInfo] = []
100+
101+
/// The basemap style info for the basemap style.
102+
@Published private(set) var basemapStyleInfo: BasemapStyleInfo?
103+
104+
/// The basemap style language strategy options for the basemap style info.
105+
var languageStrategies: [BasemapStyleLanguageStrategy] {
106+
return [.default] + (basemapStyleInfo?.languageStrategies ?? [])
107+
}
108+
109+
/// The language options for the basemap style info.
110+
var languages: [Locale.Language] {
111+
basemapStyleInfo?.languages ?? []
112+
}
113+
114+
/// The worldview options for the basemap style info.
115+
var worldviews: [Worldview?] {
116+
return [nil] + (basemapStyleInfo?.worldviews ?? [])
117+
}
118+
119+
init() {
120+
map.basemap = Basemap(style: basemapStyle)
121+
map.initialViewpoint = Viewpoint(latitude: 52.3433, longitude: -1.5796, scale: 25e5)
122+
}
123+
124+
/// Loads the styles info from the basemap styles service.
125+
func loadStylesInfo() async throws {
126+
let service = BasemapStylesService()
127+
try await service.load()
128+
129+
guard let info = service.info else { return }
130+
stylesInfo = info.stylesInfo
131+
basemapStyleInfo = stylesInfo.first { $0.style == basemapStyle }
132+
133+
// Loads the styles info thumbnails.
134+
await stylesInfo.compactMap(\.thumbnail).load()
135+
}
136+
137+
/// Updates the map's basemap with the `basemapStyle`, `basemapStyleLanguage` and `worldviewCode`.
138+
private func updateBasemap() {
139+
let basemapStyleParameters = BasemapStyleParameters(language: basemapStyleLanguage)
140+
basemapStyleParameters.worldview = if let worldviewCode {
141+
Worldview(code: worldviewCode)
142+
} else {
143+
nil
144+
}
145+
146+
map.basemap = Basemap(style: basemapStyle, parameters: basemapStyleParameters)
147+
}
148+
}
149+
}
150+
151+
#Preview {
152+
NavigationStack {
153+
CreateDynamicBasemapGalleryView()
154+
}
155+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Create dynamic basemap gallery
2+
3+
Implement a basemap gallery that automatically retrieves the latest customization options from the basemap styles service.
4+
5+
![Screenshot of Create dynamic basemap gallery sample](create-dynamic-basemap-gallery.png)
6+
7+
## Use case
8+
9+
Multi-use and/or international applications benefit from the ability to change a basemap's style or localize the basemap. For example, an application used for ecological surveys might include navigation functionality to guide an ecologist to a location and functionality for inputting data. When traveling, a user is likely to benefit from a map with a style that emphasizes the transport infrastructure (e.g., `ArcGIS Navigation`). However, during surveys a user is likely to benefit from a map with a style that highlights features in the terrain (e.g. `ArcGIS Terrain`). Implementing a basemap gallery with customization options in an application gives a user the freedom to select a basemap with a style and features (e.g., the language of labels) suitable for the task they are undertaking. Making the basemap gallery dynamic ensures the latest customization options are automatically included.
10+
11+
## How to use the sample
12+
13+
Press "Basemap" to display a gallery of all styles available in the basemap styles service. Select a style using the "Style" picker. Select a language or language strategy using the "Language" picker. Optionally selected a worldview using the "Worldview" picker. Disabled pickers indicate that the customization cannot be applied to the selected style.
14+
15+
## How it works
16+
17+
* Create and load a `BasemapStylesService` object.
18+
* Get the `BasemapStylesServiceInfo` object from `BasemapStylesService.info`.
19+
* Access the list of `BasemapStyleInfo` objects using `BasemapStylesServiceInfo.stylesInfo`. These `BasemapStyleInfo` objects contain up-to-date information about each of the styles supported by the Maps SDK, including:
20+
* `languageStrategies`: A list of `BasemapStyleLanguageStrategy` enumeration values that can be used with the style.
21+
* `languages`: A list of `Locale.Language` objects that can be used to customize labels on the style.
22+
* `styleName`: The human-readable name of the style.
23+
* `style`: The `Basemap.Style` enumeration value representing this style in the Maps SDK.
24+
* `thumbnail`: An image that can be used to display a preview of the style.
25+
* `worldview`: A list of `Worldview` objects, which provide information about each representation of a disputed boundary that can be used to customize boundaries on the style.
26+
* The information contained in the list of `BasemapStyleInfo` objects can be used as the data model for a basemap gallery UI component.
27+
28+
## Relevant API
29+
30+
* BasemapStyleInfo
31+
* BasemapStyleLanguageInfo
32+
* BasemapStyleParameters
33+
* BasemapStylesService
34+
* BasemapStylesServiceInfo
35+
* Worldview
36+
37+
## Additional information
38+
39+
This sample demonstrates how to implement a basemap gallery using the Maps SDK. The styles and associated customization options used for the gallery are retrieved from the [basemap styles service](https://developers.arcgis.com/rest/basemap-styles/). A ready-made basemap gallery component is also available in the toolkits provided with each SDK. To see how the ready-made basemap gallery toolkit component can be integrated into a Maps SDK application, refer to the `Set Basemap` sample.
40+
41+
## Tags
42+
43+
basemap, languages, service, style

0 commit comments

Comments
 (0)