Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions MacMaps.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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)";
Expand Down Expand Up @@ -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)";
Expand Down
6 changes: 2 additions & 4 deletions MacMaps/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ struct ContentView: View {
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
#Preview {
ContentView()
}
9 changes: 9 additions & 0 deletions MacMaps/Extensions/MKMapType+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
248 changes: 134 additions & 114 deletions MacMaps/MapContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import SwiftUI

struct MapContentView: View {

@ObservedObject
@State
private var viewModel = MapContentViewModel()

// Map Vendors
Expand All @@ -22,139 +22,159 @@ 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<MapContentViewModel>) -> 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)
}
})
}
}

}

struct AppleMapsView_Previews: PreviewProvider {
static var previews: some View {
MapContentView()
}
#Preview {
MapContentView()
}
30 changes: 11 additions & 19 deletions MacMaps/MapContentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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<Bool, Never>()
Expand Down
Loading