From 8f60fa987f93b91c3b09be47cebb17b9775a3ab0 Mon Sep 17 00:00:00 2001 From: Brad Leege Date: Tue, 3 Mar 2026 20:44:35 -0600 Subject: [PATCH 1/3] macOS Tahoe --- MacMaps.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MacMaps.xcodeproj/project.pbxproj b/MacMaps.xcodeproj/project.pbxproj index 10edef1..ac84a4a 100644 --- a/MacMaps.xcodeproj/project.pbxproj +++ b/MacMaps.xcodeproj/project.pbxproj @@ -436,6 +436,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.leege.MacMaps; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -469,6 +470,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.leege.MacMaps; PRODUCT_NAME = "$(TARGET_NAME)"; From 65c02c5fcb28c51eb937b79a03fb865ca6aa8747 Mon Sep 17 00:00:00 2001 From: Brad Leege Date: Tue, 3 Mar 2026 20:56:33 -0600 Subject: [PATCH 2/3] Preview Macros --- MacMaps/ContentView.swift | 6 ++---- MacMaps/MapContentView.swift | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/MacMaps/ContentView.swift b/MacMaps/ContentView.swift index 7acb54b..9e74060 100644 --- a/MacMaps/ContentView.swift +++ b/MacMaps/ContentView.swift @@ -17,8 +17,6 @@ struct ContentView: View { } } -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } +#Preview { + ContentView() } diff --git a/MacMaps/MapContentView.swift b/MacMaps/MapContentView.swift index 9ce6477..95f654b 100644 --- a/MacMaps/MapContentView.swift +++ b/MacMaps/MapContentView.swift @@ -153,8 +153,6 @@ struct MapContentView: View { } -struct AppleMapsView_Previews: PreviewProvider { - static var previews: some View { - MapContentView() - } +#Preview { + MapContentView() } From ef51adac90cf7f7314c300ab3644c15a3e64d9a6 Mon Sep 17 00:00:00 2001 From: Brad Leege Date: Tue, 3 Mar 2026 23:40:54 -0600 Subject: [PATCH 3/3] Converting To Observable --- MacMaps/Extensions/MKMapType+Extensions.swift | 9 + MacMaps/MapContentView.swift | 242 ++++++++++-------- MacMaps/MapContentViewModel.swift | 30 +-- 3 files changed, 152 insertions(+), 129 deletions(-) diff --git a/MacMaps/Extensions/MKMapType+Extensions.swift b/MacMaps/Extensions/MKMapType+Extensions.swift index 8d2f711..6e7f306 100644 --- a/MacMaps/Extensions/MKMapType+Extensions.swift +++ b/MacMaps/Extensions/MKMapType+Extensions.swift @@ -8,6 +8,15 @@ import Foundation import MapKit +extension MKCoordinateRegion: @retroactive Equatable { + public static func == (lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool { + lhs.center.latitude == rhs.center.latitude + && lhs.center.longitude == rhs.center.longitude + && lhs.span.latitudeDelta == rhs.span.latitudeDelta + && lhs.span.longitudeDelta == rhs.span.longitudeDelta + } +} + extension MKMapType: Identifiable { // Identifiable diff --git a/MacMaps/MapContentView.swift b/MacMaps/MapContentView.swift index 95f654b..c630d20 100644 --- a/MacMaps/MapContentView.swift +++ b/MacMaps/MapContentView.swift @@ -11,7 +11,7 @@ import SwiftUI struct MapContentView: View { - @ObservedObject + @State private var viewModel = MapContentViewModel() // Map Vendors @@ -22,135 +22,157 @@ struct MapContentView: View { @Environment(\.isSearching) private var isSearching: Bool var body: some View { - HStack { - switch viewModel.mapVendor { - case .appleMaps: - appleMapView - case .mapbox: - mapboxMapView - case .googleMaps: - googleMapsView + mapContent + .toolbar { + toolbarContent(Bindable(viewModel)) } - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - VStack { - Picker("Map Vendor", selection: $viewModel.mapVendor) { - ForEach(MapContentViewModel.MapVendor.allCases, id: \.rawValue) { vendor in - Text(vendor.rawValue).tag(vendor) - } - } + .searchable(text: $viewModel.searchQuery, + prompt: "Search...", + suggestions: { + ForEach(viewModel.searchSuggestions, id: \.self) { suggestion in + Text(suggestion.name ?? "") + .searchCompletion(suggestion.name ?? "") } + }) + .onSubmit(of: .search) { + viewModel.searchForLocation() } - ToolbarItem(placement: .primaryAction) { - VStack { - switch viewModel.mapVendor { - case .appleMaps: - Picker("Apple Styles", selection: $viewModel.selectedAppleMapType) { - ForEach(MapContentViewModel.AppleMapTypes.allCases) { mapType in - Text(mapType.rawValue).tag(mapType.type) - } - } - case .mapbox: - Picker("Mapbox Styles", selection: $viewModel.selectedMapboxMapStyle) { - ForEach(MapContentViewModel.MapboxStyles.allCases) { mapStyle in - Text(mapStyle.rawValue).tag(mapStyle) - } - } - case .googleMaps: - Picker("Google Styles", selection: $viewModel.selectedGoogleMapStyle) { - ForEach(MapContentViewModel.GoogleMapStyles.allCases) { mapStyle in - Text(mapStyle.rawValue).tag(mapStyle) - } - } - } + .onChange(of: viewModel.searchQuery) { _, newValue in + if newValue.isEmpty && !isSearching { + print("Search is cancelled") + viewModel.searchCancelledPublisher.send(true) } } - ToolbarItem(placement: .primaryAction) { - Button(action: { - viewModel.toggleLocationMonitoring() - }, label: { - Image(systemName: viewModel.locationButtonImageName) - }) + .onChange(of: viewModel.mapRegion) { _, newValue in + handleMapRegionChange(newValue) } - } - .searchable(text: $viewModel.searchQuery, - prompt: "Search...", - suggestions: { - ForEach(viewModel.searchSuggestions, id: \.self) { suggestion in - Text(suggestion.name ?? "") - .searchCompletion(suggestion.name ?? "") + .onChange(of: viewModel.selectedAppleMapType, handleAppleMapTypeChange) + .onChange(of: viewModel.selectedMapboxMapStyle, handleMapboxStyleChange) + .onChange(of: viewModel.selectedGoogleMapStyle, handleGoogleStyleChange) + .onChange(of: viewModel.showUserLocation, handleUserLocationChange) + .onChange(of: viewModel.searchResultPlacemark, handleSearchResultChange) + .onReceive(viewModel.searchCancelledPublisher) { _ in + handleSearchCancelled() } - }) - .onSubmit(of: .search) { - viewModel.searchForLocation() + } + + @ViewBuilder + private var mapContent: some View { + switch viewModel.mapVendor { + case .appleMaps: + appleMapView + case .mapbox: + mapboxMapView + case .googleMaps: + googleMapsView } - .onChange(of: viewModel.searchQuery) { _ in - if viewModel.searchQuery.isEmpty && !isSearching { - print("Search is cancelled") - viewModel.searchCancelledPublisher.send(true) + } + + private func handleMapRegionChange(_ newValue: MKCoordinateRegion) { + switch viewModel.mapVendor { + case .appleMaps: + appleMapView.mapView.region = newValue + case .mapbox: + mapboxMapView.setCenter(newValue.center) + case .googleMaps: + googleMapsView.setCenter(newValue.center) + } + } + + private func handleAppleMapTypeChange(_: MKMapType, _ newValue: MKMapType) { + appleMapView.mapView.mapType = newValue + } + + private func handleMapboxStyleChange(_: MapContentViewModel.MapboxStyles, _ newValue: MapContentViewModel.MapboxStyles) { + mapboxMapView.changeMapStyle(newValue) + } + + private func handleGoogleStyleChange(_: MapContentViewModel.GoogleMapStyles, _ newValue: MapContentViewModel.GoogleMapStyles) { + googleMapsView.changeMapStyle(newValue) + } + + private func handleUserLocationChange(_: Bool, _ newValue: Bool) { + switch viewModel.mapVendor { + case .appleMaps: + appleMapView.mapView.showsUserLocation = newValue + case .mapbox: + if newValue { + mapboxMapView.showUserLocation(viewModel.mapRegion.center) + } else { + mapboxMapView.hideUserLocation() + } + case .googleMaps: + if newValue { + googleMapsView.showUserLocation(viewModel.mapRegion.center) + } else { + googleMapsView.hideUserLocation() } } - .onReceive(viewModel.$mapRegion, perform: { region in - switch viewModel.mapVendor { - case .appleMaps: - appleMapView.mapView.region = region - case .mapbox: - mapboxMapView.setCenter(region.center) - case .googleMaps: - googleMapsView.setCenter(region.center) + } + + private func handleSearchResultChange(_: CLPlacemark?, _ newValue: CLPlacemark?) { + guard let placemark = newValue else { return } + switch viewModel.mapVendor { + case .appleMaps: + appleMapView.showMarker(placemark) + case .mapbox: + mapboxMapView.showMarker(placemark) + case .googleMaps: + googleMapsView.showMarker(placemark) + } + } + + private func handleSearchCancelled() { + switch viewModel.mapVendor { + case .appleMaps: + appleMapView.clearMarker() + case .mapbox: + mapboxMapView.clearMarker() + case .googleMaps: + googleMapsView.clearMarker() + } + } + + @ToolbarContentBuilder + private func toolbarContent(_ bindable: Bindable) -> some ToolbarContent { + ToolbarItem(placement: .primaryAction) { + Picker("Map Vendor", selection: bindable.mapVendor) { + ForEach(MapContentViewModel.MapVendor.allCases, id: \.rawValue) { vendor in + Text(vendor.rawValue).tag(vendor) + } } - }) - .onReceive(viewModel.$selectedAppleMapType, perform: { mapType in - appleMapView.mapView.mapType = mapType - }) - .onReceive(viewModel.$selectedMapboxMapStyle, perform: { mapStyle in - mapboxMapView.changeMapStyle(mapStyle) - }) - .onReceive(viewModel.$selectedGoogleMapStyle, perform: { mapStyle in - googleMapsView.changeMapStyle(mapStyle) - }) - .onReceive(viewModel.$showUserLocation, perform: { showUserLocation in + } + ToolbarItem(placement: .primaryAction) { switch viewModel.mapVendor { case .appleMaps: - appleMapView.mapView.showsUserLocation = showUserLocation + Picker("Apple Styles", selection: bindable.selectedAppleMapType) { + ForEach(MapContentViewModel.AppleMapTypes.allCases) { mapType in + Text(mapType.rawValue).tag(mapType.type) + } + } case .mapbox: - if showUserLocation { - mapboxMapView.showUserLocation(viewModel.mapRegion.center) - } else { - mapboxMapView.hideUserLocation() + Picker("Mapbox Styles", selection: bindable.selectedMapboxMapStyle) { + ForEach(MapContentViewModel.MapboxStyles.allCases) { mapStyle in + Text(mapStyle.rawValue).tag(mapStyle) + } } case .googleMaps: - if showUserLocation { - googleMapsView.showUserLocation(viewModel.mapRegion.center) - } else { - googleMapsView.hideUserLocation() + Picker("Google Styles", selection: bindable.selectedGoogleMapStyle) { + ForEach(MapContentViewModel.GoogleMapStyles.allCases) { mapStyle in + Text(mapStyle.rawValue).tag(mapStyle) + } } } - }) - .onReceive(viewModel.$searchResultPlacemark, perform: { placemark in - guard let placemark = placemark else { return } - switch viewModel.mapVendor { - case .appleMaps: - appleMapView.showMarker(placemark) - case .mapbox: - mapboxMapView.showMarker(placemark) - case .googleMaps: - googleMapsView.showMarker(placemark) - } - }) - .onReceive(viewModel.searchCancelledPublisher, perform: { _ in - switch viewModel.mapVendor { - case .appleMaps: - appleMapView.clearMarker() - case .mapbox: - mapboxMapView.clearMarker() - case .googleMaps: - googleMapsView.clearMarker() + } + ToolbarItem(placement: .primaryAction) { + Button { + viewModel.toggleLocationMonitoring() + } label: { + Image(systemName: viewModel.locationButtonImageName) } - }) + } } - + } #Preview { diff --git a/MacMaps/MapContentViewModel.swift b/MacMaps/MapContentViewModel.swift index 77e05b8..9839977 100644 --- a/MacMaps/MapContentViewModel.swift +++ b/MacMaps/MapContentViewModel.swift @@ -11,8 +11,10 @@ import MapKit import GeoJSON import Intents import Contacts +import SwiftUI -class MapContentViewModel: ObservableObject { +@Observable +class MapContentViewModel { enum MapVendor: String, CaseIterable, Identifiable { var id: Self { self } @@ -111,35 +113,25 @@ class MapContentViewModel: ObservableObject { } - @Published var mapVendor: MapVendor = .mapbox - - @Published + var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 43.07472, longitude: -89.38421), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)) - - @Published + var selectedAppleMapType = MKMapType.standard - - @Published + var selectedMapboxMapStyle: MapboxStyles = .streets - - @Published + var selectedGoogleMapStyle: GoogleMapStyles = .roadmap - - @Published + var locationButtonImageName = "location.fill" - - @Published + var showUserLocation = true - - @Published + var searchQuery = "" - - @Published + var searchSuggestions = [CLPlacemark]() - @Published var searchResultPlacemark: CLPlacemark? let searchCancelledPublisher = PassthroughSubject()