Skip to content

Commit 14d084e

Browse files
bfoss765claude
andcommitted
fix: map centering and distance calculations in merchant search
- Fix map centering to use GPS location (Ashby, MA) on initial load - Fix map to preserve panned location when returning from merchant detail - Fix distance calculations to use map center instead of GPS when panning - Add searchCenterCoordinate tracking throughout the merchant flow - Implement region stabilization to prevent MKMapView auto-adjustments - Pass search center to cells for accurate distance display - Clear search center only when actually leaving the screen πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 483e115 commit 14d084e

12 files changed

Lines changed: 359 additions & 40 deletions

β€ŽDashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/POIDetailsViewController.swiftβ€Ž

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class POIDetailsViewController: UIViewController {
2525
internal var pointOfUse: ExplorePointOfUse
2626
internal let isShowAllHidden: Bool
2727
private let searchRadius: Double?
28+
private let searchCenterCoordinate: CLLocationCoordinate2D?
2829
internal let currentFilters: PointOfUseListFilters?
2930

3031
@objc public var payWithDashHandler: (()->())?
@@ -35,10 +36,11 @@ class POIDetailsViewController: UIViewController {
3536
private var mapView: ExploreMapView!
3637
private let defaultBottomSheetHeight: CGFloat = 450
3738

38-
public init(pointOfUse: ExplorePointOfUse, isShowAllHidden: Bool = true, searchRadius: Double? = nil, currentFilters: PointOfUseListFilters? = nil) {
39+
public init(pointOfUse: ExplorePointOfUse, isShowAllHidden: Bool = true, searchRadius: Double? = nil, searchCenterCoordinate: CLLocationCoordinate2D? = nil, currentFilters: PointOfUseListFilters? = nil) {
3940
self.pointOfUse = pointOfUse
4041
self.isShowAllHidden = isShowAllHidden
4142
self.searchRadius = searchRadius
43+
self.searchCenterCoordinate = searchCenterCoordinate
4244
self.currentFilters = currentFilters
4345

4446
super.init(nibName: nil, bundle: nil)
@@ -172,7 +174,7 @@ extension POIDetailsViewController {
172174
effectiveRadius = searchRadius ?? kDefaultRadius
173175
}
174176

175-
var detailsView = POIDetailsView(merchant: pointOfUse, isShowAllHidden: isShowAllHidden, searchRadius: effectiveRadius)
177+
var detailsView = POIDetailsView(merchant: pointOfUse, isShowAllHidden: isShowAllHidden, searchRadius: effectiveRadius, searchCenterCoordinate: searchCenterCoordinate)
176178
detailsView.payWithDashHandler = payWithDashHandler
177179
detailsView.sellDashHandler = sellDashHandler
178180
detailsView.showAllLocationsActionBlock = { [weak self] in
@@ -185,7 +187,7 @@ extension POIDetailsViewController {
185187
// For other tabs (Online, Nearby), use the current radius filtering
186188
let searchRadiusToUse = isFromAllTab ? Double.greatestFiniteMagnitude : effectiveRadius
187189

188-
let vc = AllMerchantLocationsViewController(pointOfUse: wSelf.pointOfUse, searchRadius: searchRadiusToUse, currentFilters: wSelf.currentFilters)
190+
let vc = AllMerchantLocationsViewController(pointOfUse: wSelf.pointOfUse, searchRadius: searchRadiusToUse, searchCenterCoordinate: wSelf.searchCenterCoordinate, currentFilters: wSelf.currentFilters)
189191
vc.payWithDashHandler = wSelf.payWithDashHandler
190192
vc.sellDashHandler = wSelf.sellDashHandler
191193
wSelf.navigationController?.pushViewController(vc, animated: true)

β€ŽDashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsView.swiftβ€Ž

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,26 @@ import SDWebImageSwiftUI
2121

2222
struct POIDetailsView: View {
2323
@StateObject private var viewModel: POIDetailsViewModel
24-
24+
2525
let merchant: ExplorePointOfUse
2626
let isShowAllHidden: Bool
2727
let searchRadius: Double?
28-
28+
let searchCenterCoordinate: CLLocationCoordinate2D?
29+
2930
// Action handlers
3031
var payWithDashHandler: (() -> Void)?
3132
var sellDashHandler: (() -> Void)?
3233
var dashSpendAuthHandler: ((GiftCardProvider) -> Void)?
3334
var buyGiftCardHandler: ((GiftCardProvider) -> Void)?
3435
var showAllLocationsActionBlock: (() -> Void)?
35-
36-
init(merchant: ExplorePointOfUse, isShowAllHidden: Bool = false, searchRadius: Double? = nil) {
36+
37+
init(merchant: ExplorePointOfUse, isShowAllHidden: Bool = false, searchRadius: Double? = nil, searchCenterCoordinate: CLLocationCoordinate2D? = nil) {
3738
self.merchant = merchant
3839
self.isShowAllHidden = isShowAllHidden
3940
self.searchRadius = searchRadius
41+
self.searchCenterCoordinate = searchCenterCoordinate
4042

41-
self._viewModel = StateObject(wrappedValue: POIDetailsViewModel(merchant: merchant, searchRadius: searchRadius))
43+
self._viewModel = StateObject(wrappedValue: POIDetailsViewModel(merchant: merchant, searchRadius: searchRadius, searchCenterCoordinate: searchCenterCoordinate))
4244
}
4345

4446
var body: some View {

β€ŽDashWallet/Sources/UI/Explore Dash/Merchants & ATMs/Details/Views/POIDetailsViewModel.swiftβ€Ž

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ class POIDetailsViewModel: ObservableObject, SyncingActivityMonitorObserver, Net
3737
private let syncMonitor = SyncingActivityMonitor.shared
3838
private let merchant: ExplorePointOfUse
3939
private var currentSearchRadius: Double = kDefaultRadius
40-
40+
private var searchCenterCoordinate: CLLocationCoordinate2D?
41+
4142
// NetworkReachabilityHandling requirements
4243
var networkStatusDidChange: ((NetworkStatus) -> ())?
4344
var reachabilityObserver: Any!
@@ -57,8 +58,9 @@ class POIDetailsViewModel: ObservableObject, SyncingActivityMonitorObserver, Net
5758
return formatPhoneNumber(phone)
5859
}
5960

60-
init(merchant: ExplorePointOfUse, searchRadius: Double? = nil) {
61+
init(merchant: ExplorePointOfUse, searchRadius: Double? = nil, searchCenterCoordinate: CLLocationCoordinate2D? = nil) {
6162
self.merchant = merchant
63+
self.searchCenterCoordinate = searchCenterCoordinate
6264

6365
if let radius = searchRadius {
6466
self.currentSearchRadius = radius
@@ -203,12 +205,20 @@ class POIDetailsViewModel: ObservableObject, SyncingActivityMonitorObserver, Net
203205
}
204206

205207
// For other tabs: Apply radius filtering as before
206-
guard let currentLocation = DWLocationManager.shared.currentLocation else {
208+
// Use search center if available (when user panned the map), otherwise use GPS location
209+
let locationToUse: CLLocation?
210+
if let searchCenter = searchCenterCoordinate {
211+
locationToUse = CLLocation(latitude: searchCenter.latitude, longitude: searchCenter.longitude)
212+
} else {
213+
locationToUse = DWLocationManager.shared.currentLocation
214+
}
215+
216+
guard let currentLocation = locationToUse else {
207217
locationCount = 0
208218
return
209219
}
210220

211-
// Create bounds using current search radius around current location
221+
// Create bounds using current search radius around the search location
212222
let bounds = ExploreMapBounds(rect: MKCircle(center: currentLocation.coordinate, radius: currentSearchRadius).boundingMapRect)
213223

214224

β€ŽDashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/AllMerchantLocationsViewController.swiftβ€Ž

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ import UIKit
2424
class AllMerchantLocationsViewController: ExplorePointOfUseListViewController {
2525
private let pointOfUse: ExplorePointOfUse
2626
private let searchRadius: Double
27+
private let searchCenterCoordinate: CLLocationCoordinate2D?
2728
private let currentFilters: PointOfUseListFilters?
2829

29-
init(pointOfUse: ExplorePointOfUse, searchRadius: Double = kDefaultRadius, currentFilters: PointOfUseListFilters? = nil) {
30+
init(pointOfUse: ExplorePointOfUse, searchRadius: Double = kDefaultRadius, searchCenterCoordinate: CLLocationCoordinate2D? = nil, currentFilters: PointOfUseListFilters? = nil) {
3031
self.pointOfUse = pointOfUse
3132
self.searchRadius = searchRadius
33+
self.searchCenterCoordinate = searchCenterCoordinate
3234
self.currentFilters = currentFilters
3335
super.init(nibName: nil, bundle: nil)
3436
}
@@ -70,6 +72,9 @@ class AllMerchantLocationsViewController: ExplorePointOfUseListViewController {
7072
dataProvider: AllMerchantLocationsDataProvider(pointOfUse: pointOfUse),
7173
filterGroups: [], territoriesDataSource: nil, sortOptions: [.name, .distance, .discount])])
7274

75+
// Set the search center coordinate if provided (from panned map)
76+
model.searchCenterCoordinate = searchCenterCoordinate
77+
7378
// Apply the current filters from parent screen if available
7479
if let filters = currentFilters {
7580

β€ŽDashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/MerchantItemCell.swiftβ€Ž

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,27 @@ class MerchantItemCell: PointOfUseItemCell {
3131

3232
if merchant.type == .online {
3333
subLabel.isHidden = true
34-
} else if let currentLocation = DWLocationManager.shared.currentLocation,
35-
DWLocationManager.shared.isAuthorized {
36-
subLabel.isHidden = false
37-
let distance = CLLocation(latitude: pointOfUse.latitude!, longitude: pointOfUse.longitude!)
38-
.distance(from: currentLocation)
39-
let distanceText: String = ExploreDash.distanceFormatter
40-
.string(from: Measurement(value: floor(distance), unit: UnitLength.meters))
41-
subLabel.text = distanceText
4234
} else {
43-
subLabel.isHidden = true
35+
// Use search center coordinate if available (when map is panned), otherwise use GPS
36+
let locationForDistance: CLLocation?
37+
if let searchCenter = searchCenterCoordinate {
38+
locationForDistance = CLLocation(latitude: searchCenter.latitude, longitude: searchCenter.longitude)
39+
} else if DWLocationManager.shared.isAuthorized {
40+
locationForDistance = DWLocationManager.shared.currentLocation
41+
} else {
42+
locationForDistance = nil
43+
}
44+
45+
if let currentLocation = locationForDistance {
46+
subLabel.isHidden = false
47+
let distance = CLLocation(latitude: pointOfUse.latitude!, longitude: pointOfUse.longitude!)
48+
.distance(from: currentLocation)
49+
let distanceText: String = ExploreDash.distanceFormatter
50+
.string(from: Measurement(value: floor(distance), unit: UnitLength.meters))
51+
subLabel.text = distanceText
52+
} else {
53+
subLabel.isHidden = true
54+
}
4455
}
4556

4657
let isGiftCard = merchant.paymentMethod == .giftCard

β€ŽDashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/Cells/PointOfUseItemCell.swiftβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class PointOfUseItemCell: UITableViewCell {
2727
private var nameLabel: UILabel!
2828
var subLabel: UILabel!
2929
var mainStackView: UIStackView!
30+
var searchCenterCoordinate: CLLocationCoordinate2D?
3031

3132
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
3233
super.init(style: style, reuseIdentifier: reuseIdentifier)

β€ŽDashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/ExplorePointOfUseListViewController.swiftβ€Ž

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,18 +282,31 @@ extension ExplorePointOfUseListViewController {
282282

283283
extension ExplorePointOfUseListViewController: DWLocationObserver {
284284
func locationManagerDidChangeCurrentLocation(_ manager: DWLocationManager, location: CLLocation) {
285+
print("πŸ” LOCATION: GPS location changed to \(location.coordinate.latitude), \(location.coordinate.longitude)")
286+
285287
// Set the map center first
286288
mapView.setCenter(location, animated: false)
289+
print("πŸ” LOCATION: Set map center to GPS location")
290+
291+
// Clear search center to use actual GPS location
292+
model.searchCenterCoordinate = nil
293+
print("πŸ” LOCATION: Cleared searchCenterCoordinate - will use GPS")
287294

288295
// Update the model's map bounds to match the new center
289296
if model.showMap {
290297
let radiusToUse = model.filters?.currentRadius ?? kDefaultRadius
298+
299+
// Update the map view's search radius
300+
mapView.searchRadius = radiusToUse
301+
291302
let newBounds = mapView.mapBounds(with: radiusToUse)
292303
model.currentMapBounds = newBounds
304+
print("πŸ” LOCATION: Updated map bounds with radius \(radiusToUse)")
293305
}
294306

295307
// If we're on the nearby tab and the model shows map, refresh the search with the new location
296308
if currentSegment.tag == MerchantsListSegment.nearby.rawValue && model.showMap {
309+
print("πŸ” LOCATION: Fetching merchants for new GPS location")
297310
model.fetch(query: nil)
298311
}
299312
}
@@ -680,9 +693,24 @@ extension ExplorePointOfUseListViewController: ExploreMapViewDelegate {
680693
return
681694
}
682695

696+
print("πŸ” MAP: Map region changed")
697+
print("πŸ” MAP: Center = \(mapView.centerCoordinate.latitude), \(mapView.centerCoordinate.longitude)")
698+
print("πŸ” MAP: Current radius = \(model.currentRadius) meters")
699+
683700
refreshFilterCell()
701+
702+
// Update the search radius on the map view to match current filter
703+
mapView.searchRadius = model.currentRadius
704+
705+
// Update the search center to the map center (not the device GPS location)
706+
model.searchCenterCoordinate = mapView.centerCoordinate
707+
708+
// Get bounds based on the current filter radius and new center location
684709
let newBounds = mapView.mapBounds(with: model.currentRadius)
710+
print("πŸ” MAP: New bounds NE=(\(newBounds.neCoordinate.latitude), \(newBounds.neCoordinate.longitude)), SW=(\(newBounds.swCoordinate.latitude), \(newBounds.swCoordinate.longitude))")
711+
685712
model.currentMapBounds = newBounds
713+
print("πŸ” MAP: Calling refreshItems()")
686714
model.refreshItems()
687715
}
688716

@@ -759,6 +787,8 @@ extension ExplorePointOfUseListViewController: UITableViewDelegate, UITableViewD
759787
let itemCell: PointOfUseItemCell = tableView
760788
.dequeueReusableCell(withIdentifier: PointOfUseItemCell.reuseIdentifier,
761789
for: indexPath) as! PointOfUseItemCell
790+
// Pass search center coordinate to cell for accurate distance calculations
791+
itemCell.searchCenterCoordinate = model.searchCenterCoordinate
762792
itemCell.update(with: merchant)
763793
cell = itemCell;
764794
case .nextPage:
@@ -863,6 +893,10 @@ extension ExplorePointOfUseListViewController: UITableViewDelegate, UITableViewD
863893
extension ExplorePointOfUseListViewController: PointOfUseListFiltersViewControllerDelegate {
864894
func apply(filters: PointOfUseListFilters?) {
865895
let radiusToUse = filters?.currentRadius ?? kDefaultRadius
896+
897+
// Update the map view's search radius
898+
mapView.searchRadius = radiusToUse
899+
866900
let newBounds = mapView.mapBounds(with: radiusToUse)
867901
model.currentMapBounds = newBounds
868902

β€ŽDashWallet/Sources/UI/Explore Dash/Merchants & ATMs/List/MerchantListViewController.swiftβ€Ž

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ class MerchantListViewController: ExplorePointOfUseListViewController {
194194
let isAllTab = currentSegment.tag == 2
195195
let searchRadius: Double? = isAllTab ? Double.greatestFiniteMagnitude : model.filters?.currentRadius
196196

197-
let vc = POIDetailsViewController(pointOfUse: pointOfUse, isShowAllHidden: merchant.type == .online, searchRadius: searchRadius, currentFilters: model.filters)
197+
let vc = POIDetailsViewController(pointOfUse: pointOfUse, isShowAllHidden: merchant.type == .online, searchRadius: searchRadius, searchCenterCoordinate: model.searchCenterCoordinate, currentFilters: model.filters)
198198
vc.payWithDashHandler = payWithDashHandler
199199
vc.onGiftCardPurchased = onGiftCardPurchased
200200
navigationController?.pushViewController(vc, animated: true)
@@ -256,16 +256,68 @@ class MerchantListViewController: ExplorePointOfUseListViewController {
256256
denominationType: .both // Default to both fixed and flexible
257257
)
258258

259-
model.apply(filters: defaultFilters)
259+
// Set filters but don't fetch yet - wait for map bounds to be set
260+
model.filters = defaultFilters
260261
}
261262

262263
override func viewWillAppear(_ animated: Bool) {
263264
super.viewWillAppear(animated)
264265

266+
print("πŸ” VIEW: viewWillAppear called")
267+
print("πŸ” VIEW: Current GPS location = \(DWLocationManager.shared.currentLocation?.coordinate.latitude ?? 0), \(DWLocationManager.shared.currentLocation?.coordinate.longitude ?? 0)")
268+
print("πŸ” VIEW: Current searchCenterCoordinate = \(model.searchCenterCoordinate?.latitude ?? 0), \(model.searchCenterCoordinate?.longitude ?? 0)")
269+
270+
// Check if we're coming from within the same screen (e.g., returning from merchant detail)
271+
// vs. entering the screen fresh (e.g., switching from home screen or another tab)
272+
let isReturningFromWithinFlow = model.searchCenterCoordinate != nil
273+
274+
// Only recenter to GPS if entering the screen fresh
275+
if !isReturningFromWithinFlow {
276+
print("πŸ” VIEW: No search center set, will recenter to GPS")
277+
// If we have a GPS location and are on Nearby tab, update map center and fetch
278+
if let gpsLocation = DWLocationManager.shared.currentLocation,
279+
currentSegment.tag == MerchantsListSegment.nearby.rawValue,
280+
model.showMap {
281+
print("πŸ” VIEW: Setting map center to GPS location: \(gpsLocation.coordinate.latitude), \(gpsLocation.coordinate.longitude)")
282+
283+
// Set initialCenterLocation so mapViewDidFinishLoadingMap will use the correct location
284+
mapView.initialCenterLocation = gpsLocation
285+
mapView.setCenter(gpsLocation, animated: false)
286+
287+
print("πŸ” VIEW: After setCenter, mapView.centerCoordinate = \(mapView.centerCoordinate.latitude), \(mapView.centerCoordinate.longitude)")
288+
289+
let radiusToUse = model.filters?.currentRadius ?? kDefaultRadius
290+
mapView.searchRadius = radiusToUse
291+
292+
// Use GPS location directly instead of relying on mapView.centerCoordinate
293+
let newBounds = ExploreMapBounds(rect: MKCircle(center: gpsLocation.coordinate, radius: radiusToUse).boundingMapRect)
294+
print("πŸ” VIEW: Created bounds from GPS location - NE=(\(newBounds.neCoordinate.latitude), \(newBounds.neCoordinate.longitude)), SW=(\(newBounds.swCoordinate.latitude), \(newBounds.swCoordinate.longitude))")
295+
model.currentMapBounds = newBounds
296+
297+
print("πŸ” VIEW: Refreshing items with GPS location")
298+
model.fetch(query: nil)
299+
}
300+
} else {
301+
print("πŸ” VIEW: Returning from within flow, preserving panned location at (\(model.searchCenterCoordinate!.latitude), \(model.searchCenterCoordinate!.longitude))")
302+
}
303+
265304
// Ensure filter status is visible on initial load
266305
updateAppliedFiltersView()
267306
}
268307

308+
override func viewWillDisappear(_ animated: Bool) {
309+
super.viewWillDisappear(animated)
310+
311+
// Clear search center coordinate when actually leaving the screen
312+
// (not when navigating to a child view like merchant detail)
313+
if isMovingFromParent || isBeingDismissed {
314+
print("πŸ” VIEW: viewWillDisappear - leaving screen, clearing searchCenterCoordinate")
315+
model.searchCenterCoordinate = nil
316+
} else {
317+
print("πŸ” VIEW: viewWillDisappear - navigating to child view, preserving searchCenterCoordinate")
318+
}
319+
}
320+
269321
override func configureHierarchy() {
270322
title = NSLocalizedString("Where to Spend", comment: "");
271323

0 commit comments

Comments
Β (0)