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(
online: onlineMap,
selection: $selectedMap
)
des12437 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down
45 changes: 33 additions & 12 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

/// Creates an `OfflineMapAreasView` with a given web map.
/// - Parameter map: The web map.
public init(map: Map) {
_mapViewModel = StateObject(wrappedValue: MapViewModel(map: map))
/// The currently selected map.
@Binding private var selectedMap: Map?

/// Creates a view with a given web map.
/// - Parameters:
/// - online: The web map to be taken offline.
/// - selection: A binding to the currently selected map.
public init(
online: Map,
selection: Binding<Map?>
) {
des12437 marked this conversation as resolved.
Show resolved Hide resolved
_mapViewModel = StateObject(wrappedValue: MapViewModel(map: online))
_selectedMap = selection
}

public var body: some View {
Expand Down Expand Up @@ -82,7 +91,10 @@ public struct OfflineMapAreasView: View {
List(models) { preplannedMapModel in
PreplannedListItemView(
model: preplannedMapModel
)
) { newMap in
selectedMap = newMap
dismiss()
}
des12437 marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
emptyPreplannedMapAreasView
Expand Down Expand Up @@ -114,12 +126,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(
online: Map(
item: PortalItem(
portal: .arcGISOnline(connection: .anonymous),
id: PortalItem.ID("acc027394bc84c2fb04d1ed317aac674")!
)
),
selection: $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,11 +17,14 @@ 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

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

var body: some View {
HStack(alignment: .center, spacing: 10) {
thumbnailView
VStack(alignment: .leading, spacing: 4) {
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 Down
19 changes: 17 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 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