Skip to content
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

Offline: Open a preplanned map area #841

Merged
merged 15 commits into from
Aug 27, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ import SwiftUI
@MainActor
struct OfflineMapAreasExampleView: View {
/// The map of the Naperville water network.
@State private var map = Map(item: PortalItem.naperville())
@State private var onlineMap = Map(item: PortalItem.naperville())

/// The selected map.
@State private var selectedMap: Map?

/// A Boolean value indicating whether the offline map ares view should be presented.
@State private var isShowingOfflineMapAreasView = false

var body: some View {
MapView(map: map)
MapView(map: selectedMap ?? onlineMap)
.toolbar {
ToolbarItem(placement: .bottomBar) {
Button("Offline Maps") {
Expand All @@ -34,7 +37,10 @@ struct OfflineMapAreasExampleView: View {
}
}
.sheet(isPresented: $isShowingOfflineMapAreasView) {
OfflineMapAreasView(map: map)
OfflineMapAreasView(
onlineMap: onlineMap,
selectedMap: $selectedMap
)
}
}
}
Expand Down
42 changes: 32 additions & 10 deletions Sources/ArcGISToolkit/Components/Offline/OfflineMapAreasView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,19 @@ public struct OfflineMapAreasView: View {
/// A Boolean value indicating whether the preplanned map areas are being reloaded.
@State private var isReloadingPreplannedMapAreas = false

/// The currently selected map.
private var selectedMap: Binding<Map?>
des12437 marked this conversation as resolved.
Show resolved Hide resolved

/// Creates an `OfflineMapAreasView` with a given web map.
des12437 marked this conversation as resolved.
Show resolved Hide resolved
/// - Parameter map: The web map.
public init(map: Map) {
_mapViewModel = StateObject(wrappedValue: MapViewModel(map: map))
/// - Parameters:
/// - onlineMap: The web map.
/// - selectedMap: A binding to the currently selected map.
public init(
onlineMap: Map,
selectedMap: Binding<Map?>
des12437 marked this conversation as resolved.
Show resolved Hide resolved
) {
_mapViewModel = StateObject(wrappedValue: MapViewModel(map: onlineMap))
des12437 marked this conversation as resolved.
Show resolved Hide resolved
self.selectedMap = selectedMap
}

public var body: some View {
Expand Down Expand Up @@ -83,6 +92,10 @@ public struct OfflineMapAreasView: View {
PreplannedListItemView(
model: preplannedMapModel
)
.onMapSelectionChanged { newMap in
selectedMap.wrappedValue = newMap
dismiss()
}
}
} else {
emptyPreplannedMapAreasView
Expand Down Expand Up @@ -114,12 +127,21 @@ public struct OfflineMapAreasView: View {
}

#Preview {
OfflineMapAreasView(
map: Map(
item: PortalItem(
portal: .arcGISOnline(connection: .anonymous),
id: PortalItem.ID("acc027394bc84c2fb04d1ed317aac674")!
@MainActor
struct OfflineMapAreasViewPreview: View {
@State private var map: Map?

var body: some View {
OfflineMapAreasView(
onlineMap: Map(
item: PortalItem(
portal: .arcGISOnline(connection: .anonymous),
id: PortalItem.ID("acc027394bc84c2fb04d1ed317aac674")!
)
),
selectedMap: $map
)
)
)
}
}
return OfflineMapAreasViewPreview()
}
des12437 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ import ArcGIS

@MainActor
@preconcurrency
public struct PreplannedListItemView: View {
des12437 marked this conversation as resolved.
Show resolved Hide resolved
struct PreplannedListItemView: View {
/// The view model for the preplanned map.
@ObservedObject var model: PreplannedMapModel

/// The closure to perform when the map selection changes.
var onMapSelectionChanged: ((Map) -> Void)?
yo1995 marked this conversation as resolved.
Show resolved Hide resolved

public var body: some View {
HStack(alignment: .center, spacing: 10) {
thumbnailView
Expand Down Expand Up @@ -55,8 +58,21 @@ public struct PreplannedListItemView: View {
@ViewBuilder private var downloadButton: some View {
switch model.status {
case .downloaded:
Image(systemName: "checkmark.circle")
.foregroundStyle(.secondary)
Button {
Task {
if let map = await model.loadMobileMapPackage() {
onMapSelectionChanged?(map)
}
}
} label: {
Text("Open")
philium marked this conversation as resolved.
Show resolved Hide resolved
.font(.system(.headline, weight: .bold))
.foregroundStyle(Color.accentColor)
.frame(width: 76, height: 32)
.background(Color(.tertiarySystemFill))
.clipShape(.rect(cornerRadius: 16))
}
.buttonStyle(.plain)
case .downloading:
if let job = model.job {
ProgressView(job.progress)
Expand All @@ -73,7 +89,7 @@ public struct PreplannedListItemView: View {
}
.buttonStyle(.plain)
.disabled(!model.status.allowsDownload)
.foregroundColor(.accentColor)
.foregroundStyle(Color.accentColor)
}
}

Expand All @@ -95,7 +111,7 @@ public struct PreplannedListItemView: View {
switch model.status {
case .notLoaded, .loading:
Text("Loading")
case .loadFailure:
case .loadFailure, .mmpkLoadFailure:
Image(systemName: "exclamationmark.circle")
Text("Loading failed")
case .packaging:
Expand All @@ -118,6 +134,15 @@ public struct PreplannedListItemView: View {
.font(.caption2)
.foregroundStyle(.tertiary)
}

/// Sets the closure to call when the map selection changes.
public func onMapSelectionChanged(
perform action: @escaping (_ newMap: Map) -> Void
) -> Self {
var copy = self
copy.onMapSelectionChanged = action
return copy
}
des12437 marked this conversation as resolved.
Show resolved Hide resolved
}

#Preview {
Expand Down
39 changes: 37 additions & 2 deletions Sources/ArcGISToolkit/Components/Offline/PreplannedMapModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,19 @@ class PreplannedMapModel: ObservableObject, Identifiable {
return MobileMapPackage.init(fileURL: fileURL)
}

/// Loads the mobile map package and updates the status.
/// - Returns: The mobile map package map.
func loadMobileMapPackage() async -> Map? {
guard let mobileMapPackage else { return nil }

do {
try await mobileMapPackage.load()
} catch {
status = .mmpkLoadFailure(error)
}
return mobileMapPackage.maps.first
}

/// Downloads the preplanned map area.
/// - Precondition: `canDownload`
func downloadPreplannedMapArea() async {
Expand Down Expand Up @@ -186,12 +199,14 @@ extension PreplannedMapModel {
case downloaded
/// Preplanned map area failed to download.
case downloadFailure(Error)
/// Downloaded mobile map package failed to load.
case mmpkLoadFailure(Error)

/// A Boolean value indicating whether the model is in a state
/// where it needs to be loaded or reloaded.
var needsToBeLoaded: Bool {
switch self {
case .loading, .packaging, .packaged, .downloading, .downloaded:
case .loading, .packaging, .packaged, .downloading, .downloaded, .mmpkLoadFailure:
false
default:
true
Expand All @@ -202,7 +217,7 @@ extension PreplannedMapModel {
var allowsDownload: Bool {
switch self {
case .notLoaded, .loading, .loadFailure, .packaging, .packageFailure,
.downloading, .downloaded:
.downloading, .downloaded, .mmpkLoadFailure:
false
case .packaged, .downloadFailure:
true
Expand All @@ -211,6 +226,26 @@ extension PreplannedMapModel {
}
}

extension PreplannedMapModel.Status: Equatable {
des12437 marked this conversation as resolved.
Show resolved Hide resolved
static func == (lhs: PreplannedMapModel.Status, rhs: PreplannedMapModel.Status) -> Bool {
return switch (lhs, rhs) {
case (.notLoaded, .notLoaded),
(.loading, .loading),
(.loadFailure, .loadFailure),
(.packaged, .packaged),
(.packaging, .packaging),
(.packageFailure, .packageFailure),
(.downloading, .downloading),
(.downloaded, .downloaded),
(.downloadFailure, .downloadFailure),
(.mmpkLoadFailure, .mmpkLoadFailure):
true
default:
false
}
}
}

private extension PreplannedMapModel.Status {
init(packagingStatus: PreplannedMapArea.PackagingStatus) {
self = switch packagingStatus {
Expand Down
40 changes: 22 additions & 18 deletions Tests/ArcGISToolkitTests/PreplannedMapModelStatusTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,31 @@ import ArcGIS
@testable import ArcGISToolkit

class PreplannedMapModelStatusTests: XCTestCase {
private typealias Status = PreplannedMapModel.Status

func testNeedsToBeLoaded() {
XCTAssertFalse(PreplannedMapModel.Status.loading.needsToBeLoaded)
XCTAssertFalse(PreplannedMapModel.Status.packaging.needsToBeLoaded)
XCTAssertFalse(PreplannedMapModel.Status.packaged.needsToBeLoaded)
XCTAssertFalse(PreplannedMapModel.Status.downloading.needsToBeLoaded)
XCTAssertFalse(PreplannedMapModel.Status.downloaded.needsToBeLoaded)
XCTAssertTrue(PreplannedMapModel.Status.notLoaded.needsToBeLoaded)
XCTAssertTrue(PreplannedMapModel.Status.downloadFailure(NSError()).needsToBeLoaded)
XCTAssertTrue(PreplannedMapModel.Status.loadFailure(NSError()).needsToBeLoaded)
XCTAssertTrue(PreplannedMapModel.Status.packageFailure.needsToBeLoaded)
XCTAssertFalse(Status.loading.needsToBeLoaded)
XCTAssertFalse(Status.packaging.needsToBeLoaded)
XCTAssertFalse(Status.packaged.needsToBeLoaded)
XCTAssertFalse(Status.downloading.needsToBeLoaded)
XCTAssertFalse(Status.downloaded.needsToBeLoaded)
XCTAssertFalse(Status.mmpkLoadFailure(CancellationError()).needsToBeLoaded)
XCTAssertTrue(Status.notLoaded.needsToBeLoaded)
XCTAssertTrue(Status.downloadFailure(CancellationError()).needsToBeLoaded)
XCTAssertTrue(Status.loadFailure(CancellationError()).needsToBeLoaded)
XCTAssertTrue(Status.packageFailure.needsToBeLoaded)
}

func testAllowsDownload() {
XCTAssertFalse(PreplannedMapModel.Status.notLoaded.allowsDownload)
XCTAssertFalse(PreplannedMapModel.Status.loading.allowsDownload)
XCTAssertFalse(PreplannedMapModel.Status.loadFailure(NSError()).allowsDownload)
XCTAssertFalse(PreplannedMapModel.Status.packaging.allowsDownload)
XCTAssertTrue(PreplannedMapModel.Status.packaged.allowsDownload)
XCTAssertFalse(PreplannedMapModel.Status.packageFailure.allowsDownload)
XCTAssertFalse(PreplannedMapModel.Status.downloading.allowsDownload)
XCTAssertFalse(PreplannedMapModel.Status.downloaded.allowsDownload)
XCTAssertTrue(PreplannedMapModel.Status.downloadFailure(NSError()).allowsDownload)
XCTAssertFalse(Status.notLoaded.allowsDownload)
XCTAssertFalse(Status.loading.allowsDownload)
XCTAssertFalse(Status.loadFailure(CancellationError()).allowsDownload)
XCTAssertFalse(Status.packaging.allowsDownload)
XCTAssertTrue(Status.packaged.allowsDownload)
XCTAssertFalse(Status.packageFailure.allowsDownload)
XCTAssertFalse(Status.downloading.allowsDownload)
XCTAssertFalse(Status.downloaded.allowsDownload)
XCTAssertTrue(Status.downloadFailure(CancellationError()).allowsDownload)
XCTAssertFalse(Status.mmpkLoadFailure(CancellationError()).allowsDownload)
}
}
Loading