// // MapView.swift // // // Created by Morris Richman on 2/11/22. // import SwiftUI import MapKit import CoreLocation import UIKit import Combine import Foundation import SwiftUIX struct MapView: View { static var shared = MapView() @State var pinLatitude: Double = 47.6062 @State var pinLongitude: Double = -122.3321 @State var loadLatitude: Double = 47.6062 @State var loadLongitude: Double = -122.3321 @State var zoom: Double = 0.2 @State var searchLocation = "" @State var currentLoc = false @Environment(\.presentationMode) var presentationMode @State private var locManager = CLLocationManager() { didSet { if locManager.authorizationStatus == .authorizedWhenInUse || locManager.authorizationStatus == .authorizedAlways { searchLocation = "Current Location" }else { allowLocation = true } } } @State var reloadMap = false { didSet { DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(1)) { reloadMap = false } } } @State var shouldOpenPlacePicker = false @State var allowLocation = false var body: some View { GeometryReader { geo in VStack { Text("Drop a pin to select your location") .font(.headline) .padding() HStack { Button("Cancel") { presentationMode.wrappedValue.dismiss() pinLatitude = 0 pinLongitude = 0 } .foregroundColor(.red) .padding(.trailing) Button("Done") { print("pinCoordinate = lat: \(pinLatitude), long: \(pinLongitude)") presentationMode.wrappedValue.dismiss() } .padding(.leading) } if !reloadMap { RawMapView(lat: loadLatitude, long: loadLongitude, zoom: zoom) .overlay(alignment: .topTrailing, { HStack { Button(action: {shouldOpenPlacePicker = true}) { Image(systemName: "magnifyingglass") .foregroundColor(.primary) } if Utilities.currentLocationEnabled { Button(action: { locManager.requestWhenInUseAuthorization() print("locManager authStatus = \(locManager.authorizationStatus)") if locManager.authorizationStatus == .denied { currentLoc = false allowLocation = true }else { let coordinates = locManager.location?.coordinate if Utilities.appType == .Debug { if coordinates?.latitude == nil { loadLatitude = 47.68114 }else { loadLatitude = coordinates!.latitude } if coordinates?.longitude == nil { loadLongitude = -122.39898 }else { loadLongitude = coordinates!.longitude } }else { loadLatitude = coordinates!.latitude loadLongitude = coordinates!.longitude } zoom = 0.05 reloadMap = true } // if locManager.authorizationStatus == .authorizedWhenInUse || locManager.authorizationStatus == .authorizedAlways { // }else { // allowLocation = true // } }, label: { // if (locManager.authorizationStatus == .authorizedWhenInUse || locManager.authorizationStatus == .authorizedAlways) { // if loadLatitude == 47.68114 && loadLongitude == -122.39898 { // Image(systemName: "location.fill") // }else if loadLatitude == locManager.location?.coordinate.latitude && loadLongitude == locManager.location?.coordinate.longitude { // Image(systemName: "location.fill") // }else { // Image(systemName: "location") // } // }else { Image(systemName: "location") // } }) } // Text("Enter Location") } .sheet(isPresented: $shouldOpenPlacePicker, onDismiss: { // MapView.pinLatitude = let address = searchView.address let geoCoder = CLGeocoder() geoCoder.geocodeAddressString(address) { (placemarks, error) in guard let placemarks = placemarks, let location = placemarks.first?.location else { // handle no location found return } loadLatitude = location.coordinate.latitude loadLongitude = location.coordinate.longitude zoom = 0.1 reloadMap = true // Use your location } }, content: {searchView(locationSearchService: LocationSearchService())}) .descreteOpaqueBackground(color: .white, diameter: 2) .padding() }) }else { // Loading(selectedType: .semiCircleSpin, duration: 0.0) } } .ignoresSafeArea() .alert(isPresented: $allowLocation, content: { Alert(title: Text("Uh Oh"), message: Text("We're not able to find your location, please open settings to enable it."), primaryButton: .destructive(Text("Cancel"), action: { allowLocation = false }), secondaryButton: .default(Text("Open Settings"), action: { allowLocation = false if let appSettings = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(appSettings) } })) }) } } } struct MapView_Previews: PreviewProvider { static var previews: some View { MapView() } } struct searchView: View { @ObservedObject var locationSearchService: LocationSearchService @Environment(\.presentationMode) var presentationMode static var address = "" var body: some View { VStack { SearchBar(text: $locationSearchService.searchQuery) List(locationSearchService.completions) { completion in VStack(alignment: .leading) { Text(completion.title) Text(completion.subtitle) .font(.subheadline) .foregroundColor(.gray) } .onTapGesture { searchView.address = completion.subtitle presentationMode.wrappedValue.dismiss() } } } } } struct RawMapView: UIViewRepresentable { var lat: Double var long: Double var zoom: Double func updateUIView(_ mapView: MKMapView, context: Context) { let span = MKCoordinateSpan(latitudeDelta: zoom, longitudeDelta: zoom) var chicagoCoordinate = CLLocationCoordinate2D() chicagoCoordinate.latitude = lat chicagoCoordinate.longitude = long let region = MKCoordinateRegion(center: chicagoCoordinate, span: span) mapView.setRegion(region, animated: true) } func makeUIView(context: Context) -> MKMapView { let myMap = MKMapView(frame: .zero) let longPress = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(RawMapViewCoordinator.addAnnotation(gesture:))) longPress.minimumPressDuration = 0.5 myMap.addSwipeGestureRecognizer(for: [.down, .left, .right, .up], target: myMap, action: #selector(RawMapViewCoordinator.updateCoordinates(gesture:))) myMap.addGestureRecognizer(longPress) myMap.delegate = context.coordinator return myMap } func makeCoordinator() -> RawMapViewCoordinator { return RawMapViewCoordinator(self) } class RawMapViewCoordinator: NSObject, MKMapViewDelegate { var entireMapViewController: RawMapView init(_ control: RawMapView) { self.entireMapViewController = control } @objc func updateCoordinates(gesture: UIGestureRecognizer) { print("get map coordinates") if let mapView = gesture.view as? MKMapView { print("Map Coordinates = \(mapView.centerCoordinate.latitude), \(mapView.centerCoordinate.longitude)") MapView.shared.loadLatitude = mapView.centerCoordinate.latitude MapView.shared.loadLongitude = mapView.centerCoordinate.longitude } } @objc func addAnnotation(gesture: UIGestureRecognizer) { if gesture.state == .began { if let mapView = gesture.view as? MKMapView { for point in mapView.annotations { mapView.removeAnnotation(point) } let point = gesture.location(in: mapView) let coordinate = mapView.convert(point, toCoordinateFrom: mapView) let annotation = MKPointAnnotation() annotation.coordinate = coordinate mapView.addAnnotation(annotation) print("pinCoordinate = lat: \(coordinate.longitude), long: \(coordinate.longitude)") MapView.shared.pinLatitude = coordinate.latitude MapView.shared.pinLongitude = coordinate.longitude } } } } } class MapSearch : NSObject, ObservableObject { @Published var locationResults : [MKLocalSearchCompletion] = [] @Published var searchTerm = "" private var cancellables : Set<AnyCancellable> = [] private var searchCompleter = MKLocalSearchCompleter() private var currentPromise : ((Result<[MKLocalSearchCompletion], Error>) -> Void)? override init() { super.init() searchCompleter.delegate = self searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address]) $searchTerm .debounce(for: .seconds(0.2), scheduler: RunLoop.main) .removeDuplicates() .flatMap({ (currentSearchTerm) in self.searchTermToResults(searchTerm: currentSearchTerm) }) .sink(receiveCompletion: { (completion) in //handle error }, receiveValue: { (results) in self.locationResults = results.filter { $0.subtitle.contains("United States") } // This parses the subtitle to show only results that have United States as the country. You could change this text to be Germany or Brazil and only show results from those countries. }) // .store(in: &cancellables) } func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> { Future { promise in self.searchCompleter.queryFragment = searchTerm self.currentPromise = promise } } } extension MapSearch : MKLocalSearchCompleterDelegate { func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { currentPromise?(.success(completer.results)) } func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { //could deal with the error here, but beware that it will finish the Combine publisher stream //currentPromise?(.failure(error)) } } struct ReversedGeoLocation { let streetNumber: String // eg. 1 let streetName: String // eg. Infinite Loop let city: String // eg. Cupertino let state: String // eg. CA let zipCode: String // eg. 95014 let country: String // eg. United States let isoCountryCode: String // eg. US var formattedAddress: String { return """ \(streetNumber) \(streetName), \(city), \(state) \(zipCode) \(country) """ } // Handle optionals as needed init(with placemark: CLPlacemark) { self.streetName = placemark.thoroughfare ?? "" self.streetNumber = placemark.subThoroughfare ?? "" self.city = placemark.locality ?? "" self.state = placemark.administrativeArea ?? "" self.zipCode = placemark.postalCode ?? "" self.country = placemark.country ?? "" self.isoCountryCode = placemark.isoCountryCode ?? "" } } struct SearchBar: UIViewRepresentable { @Binding var text: String class Coordinator: NSObject, UISearchBarDelegate { @Binding var text: String init(text: Binding<String>) { _text = text } func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { text = searchText } } func makeCoordinator() -> SearchBar.Coordinator { return Coordinator(text: $text) } func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar { let searchBar = UISearchBar(frame: .zero) searchBar.delegate = context.coordinator searchBar.searchBarStyle = .minimal searchBar.becomeFirstResponder() return searchBar } func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) { uiView.text = text } } class LocationSearchService: NSObject, ObservableObject, MKLocalSearchCompleterDelegate { @Published var searchQuery = "" var completer: MKLocalSearchCompleter @Published var completions: [MKLocalSearchCompletion] = [] var cancellable: AnyCancellable? override init() { completer = MKLocalSearchCompleter() super.init() cancellable = $searchQuery.assign(to: \.queryFragment, on: self.completer) completer.delegate = self } func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { self.completions = completer.results } } extension MKLocalSearchCompletion: Identifiable {}