Skip to content

Commit 10b7416

Browse files
committed
Refactor view state with MapState enum with SwiftUI previews.
1 parent d481ccd commit 10b7416

File tree

2 files changed

+135
-76
lines changed

2 files changed

+135
-76
lines changed

WooCommerce/Classes/ViewModels/Order Details/OrderDetailsShippingAddressMapView.swift

Lines changed: 97 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,106 @@ struct OrderDetailsShippingAddressMapView: View {
77

88
var body: some View {
99
VStack(spacing: 0) {
10-
if viewModel.isValidAddress {
11-
Group {
12-
if let coordinate = viewModel.coordinate {
13-
Map(position: .constant(viewModel.cameraPosition)) {
14-
Annotation(viewModel.shippingAddress?.fullNameWithCompany ?? "Address", coordinate: coordinate) {
15-
Image(systemName: "mappin.circle.fill")
16-
.foregroundColor(.red)
17-
.font(.title)
18-
.background(Color.white.clipShape(Circle()))
19-
}
20-
}
21-
.mapStyle(.standard)
22-
.mapControlVisibility(.hidden)
23-
.disabled(true) // Disable user interaction (scrolling, zooming)
24-
.frame(height: viewModel.mapHeight)
25-
.clipShape(RoundedRectangle(cornerRadius: 8))
26-
.contentShape(Rectangle())
27-
.onTapGesture {
28-
viewModel.onMapTapped?()
29-
}
30-
} else if viewModel.isGeocoding {
31-
RoundedRectangle(cornerRadius: 8)
32-
.fill(Color.gray.opacity(0.3))
33-
.frame(height: viewModel.mapHeight)
34-
.overlay(
35-
ProgressView()
36-
.progressViewStyle(CircularProgressViewStyle())
37-
)
38-
} else {
39-
// Empty state or failed geocoding
40-
RoundedRectangle(cornerRadius: 8)
41-
.fill(Color.gray.opacity(0.2))
42-
.frame(height: viewModel.mapHeight)
43-
.overlay(
44-
Image(systemName: "map")
45-
.foregroundColor(.gray)
46-
.font(.title2)
47-
)
48-
.contentShape(Rectangle())
49-
.onTapGesture {
50-
viewModel.onMapTapped?()
51-
}
10+
switch viewModel.mapState {
11+
case let .loaded(coordinate, cameraPosition):
12+
Map(position: .constant(cameraPosition)) {
13+
Annotation("", coordinate: coordinate) {
14+
Image(systemName: "mappin.circle.fill")
15+
.foregroundColor(.red)
16+
.font(.title)
17+
.background(Color.white.clipShape(Circle()))
5218
}
5319
}
20+
.mapStyle(.standard)
21+
.mapControlVisibility(.hidden)
22+
.disabled(true) // Disable user interaction (scrolling, zooming)
23+
.clipShape(RoundedRectangle(cornerRadius: Layout.cornerRadius))
24+
.contentShape(Rectangle())
25+
.onTapGesture {
26+
viewModel.onMapTapped?()
27+
}
28+
case .loading:
29+
RoundedRectangle(cornerRadius: Layout.cornerRadius)
30+
.fill(Color.gray.opacity(0.3))
31+
.overlay(
32+
ProgressView()
33+
.progressViewStyle(CircularProgressViewStyle())
34+
)
35+
case .failed, .none:
36+
RoundedRectangle(cornerRadius: Layout.cornerRadius)
37+
.fill(Color.gray.opacity(0.2))
38+
.overlay(
39+
Image(systemName: "map")
40+
.foregroundColor(.gray)
41+
.font(.title2)
42+
)
43+
.contentShape(Rectangle())
44+
.onTapGesture {
45+
viewModel.onMapTapped?()
46+
}
5447
}
5548
}
49+
.frame(height: viewModel.mapHeight)
50+
.renderedIf(viewModel.isValidAddress)
5651
}
5752
}
53+
54+
@available(iOS 17.0, *)
55+
private extension OrderDetailsShippingAddressMapView {
56+
enum Layout {
57+
static let cornerRadius: CGFloat = 8
58+
}
59+
}
60+
61+
#if DEBUG
62+
63+
import struct Yosemite.Address
64+
65+
@available(iOS 17.0, *)
66+
#Preview {
67+
let sampleAddress = Address(
68+
firstName: "",
69+
lastName: "",
70+
company: "",
71+
address1: "60 29th Street #343",
72+
address2: "Suite 100",
73+
city: "San Francisco",
74+
state: "CA",
75+
postcode: "94102",
76+
country: "US",
77+
phone: "+1-555-0123",
78+
79+
)
80+
let viewModel = OrderDetailsShippingAddressMapViewModel(shippingAddress: sampleAddress)
81+
return OrderDetailsShippingAddressMapView(viewModel: viewModel)
82+
.padding()
83+
}
84+
85+
@available(iOS 17.0, *)
86+
#Preview("Invalid address") {
87+
let sampleAddress = Address(
88+
firstName: "",
89+
lastName: "",
90+
company: "",
91+
address1: "",
92+
address2: "",
93+
city: "ZZ",
94+
state: "",
95+
postcode: "",
96+
country: "US",
97+
phone: "+1-555-0123",
98+
99+
)
100+
let viewModel = OrderDetailsShippingAddressMapViewModel(shippingAddress: sampleAddress)
101+
return OrderDetailsShippingAddressMapView(viewModel: viewModel)
102+
.padding()
103+
}
104+
105+
@available(iOS 17.0, *)
106+
#Preview("No address") {
107+
let viewModel = OrderDetailsShippingAddressMapViewModel(shippingAddress: nil)
108+
return OrderDetailsShippingAddressMapView(viewModel: viewModel)
109+
.padding()
110+
}
111+
112+
#endif

WooCommerce/Classes/ViewModels/Order Details/OrderDetailsShippingAddressMapViewModel.swift

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,19 @@ import Foundation
22
import SwiftUI
33
import MapKit
44
import CoreLocation
5-
import NetworkingCore
5+
import struct Yosemite.Address
66

77
@available(iOS 17.0, *)
88
@Observable
99
final class OrderDetailsShippingAddressMapViewModel {
10-
let shippingAddress: Address?
11-
12-
private(set) var coordinate: CLLocationCoordinate2D?
13-
private(set) var isGeocoding: Bool = false
14-
var cameraPosition: MapCameraPosition = .automatic
15-
16-
private let geocoder = CLGeocoder()
10+
enum MapState {
11+
case loading
12+
case loaded(coordinate: CLLocationCoordinate2D, cameraPosition: MapCameraPosition)
13+
case failed
14+
}
15+
private(set) var mapState: MapState?
1716

18-
/// The height of the map view - 150px if valid address, 0 if invalid
17+
/// The height of the map view - 150px if valid address, 0 if invalid.
1918
var mapHeight: CGFloat {
2019
isValidAddress ? 150 : 0
2120
}
@@ -34,19 +33,28 @@ final class OrderDetailsShippingAddressMapViewModel {
3433
/// Action handler for when the map is tapped
3534
var onMapTapped: (() -> Void)?
3635

36+
private let shippingAddress: Address?
37+
private let geocoder = CLGeocoder()
38+
3739
init(shippingAddress: Address?, onMapTapped: (() -> Void)? = nil) {
3840
self.shippingAddress = shippingAddress
3941
self.onMapTapped = onMapTapped
4042

4143
if isValidAddress {
42-
geocodeAddress()
44+
Task {
45+
await geocodeAddress()
46+
}
4347
}
4448
}
49+
}
4550

46-
private func geocodeAddress() {
51+
@available(iOS 17.0, *)
52+
private extension OrderDetailsShippingAddressMapViewModel {
53+
@MainActor
54+
func geocodeAddress() async {
4755
guard let address = shippingAddress else { return }
4856

49-
isGeocoding = true
57+
mapState = .loading
5058

5159
let addressString = [
5260
address.address1,
@@ -57,29 +65,25 @@ final class OrderDetailsShippingAddressMapViewModel {
5765
address.country
5866
].compactMap { $0?.isEmpty == false ? $0 : nil }.joined(separator: ", ")
5967

60-
geocoder.geocodeAddressString(addressString) { [weak self] placemarks, error in
61-
DispatchQueue.main.async {
62-
self?.isGeocoding = false
63-
64-
guard let placemark = placemarks?.first,
65-
let location = placemark.location else {
66-
// Fallback to a default coordinate if geocoding fails
67-
self?.coordinate = CLLocationCoordinate2D(latitude: 0, longitude: 0)
68-
self?.cameraPosition = .region(MKCoordinateRegion(
69-
center: CLLocationCoordinate2D(latitude: 0, longitude: 0),
70-
latitudinalMeters: 10000,
71-
longitudinalMeters: 10000
72-
))
73-
return
74-
}
75-
let coordinate = location.coordinate
76-
self?.coordinate = coordinate
77-
self?.cameraPosition = .region(MKCoordinateRegion(
78-
center: coordinate,
79-
latitudinalMeters: 1000,
80-
longitudinalMeters: 1000
81-
))
68+
do {
69+
let placemarks = try await geocoder.geocodeAddressString(addressString)
70+
71+
guard let placemark = placemarks.first,
72+
let location = placemark.location else {
73+
mapState = .failed
74+
return
8275
}
76+
77+
let coordinate = location.coordinate
78+
let cameraPosition = MapCameraPosition.region(MKCoordinateRegion(
79+
center: coordinate,
80+
latitudinalMeters: 1000,
81+
longitudinalMeters: 1000
82+
))
83+
84+
mapState = .loaded(coordinate: coordinate, cameraPosition: cameraPosition)
85+
} catch {
86+
mapState = .failed
8387
}
8488
}
8589
}

0 commit comments

Comments
 (0)