MapKit Showing current location with SwiftUI 2 (not using UIRepresentable) - swiftui

Trying to get the map to center on the current location. It is showing "Updated Location" over and over again, it then doesn't change the map the current location. If you scroll to your current location, it shows the blue dot.
struct Home:View {
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 13.086, longitude: 80.2769), latitudinalMeters: 10000, longitudinalMeters: 10000)
#State var trackingMode: MapUserTrackingMode = .follow
#State var manager = CLLocationManager()
#StateObject var managerDelegate = LocationDelegate()
var body:some View {
VStack {
Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, userTrackingMode: $trackingMode)
}
.onAppear {
manager.delegate = managerDelegate
}
}
}
class LocationDelegate: NSObject, ObservableObject, CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if manager.authorizationStatus == .authorizedWhenInUse {
print("Authorized...")
manager.startUpdatingLocation()
} else {
print("Not Authorized...")
manager.requestWhenInUseAuthorization()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("Updated Location")
}
}

This appears to be the answer.
struct Home:View {
#State var trackingMode: MapUserTrackingMode = .follow
#State var manager = CLLocationManager()
#StateObject var managerDelegate = LocationDelegate()
var body:some View {
VStack {
Map(coordinateRegion: $managerDelegate.region, interactionModes: .all, showsUserLocation: true, userTrackingMode: $trackingMode)
}
.onAppear {
manager.delegate = managerDelegate
}
}
}
class LocationDelegate: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 13.086, longitude: 80.2769), latitudinalMeters: 10000, longitudinalMeters: 10000)
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if manager.authorizationStatus == .authorizedWhenInUse {
print("Authorized...")
manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
manager.startUpdatingLocation()
} else {
print("Not Authorized...")
manager.requestWhenInUseAuthorization()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
print("Updated Location")
region.center.latitude = (manager.location?.coordinate.latitude)!
region.center.longitude = (manager.location?.coordinate.longitude)!
}
}

Related

MapView does not show user current location

I was trying to display my current location into a Swiftui MapView. To do so, I created the following class:
import SwiftUI
import CoreLocation
import Combine
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var locationManager = CLLocationManager()
#Published var locationStatus: CLAuthorizationStatus?
#Published var lastLocation: CLLocation?
override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
private var statusString: String {
guard let status = locationStatus else {
return "unknown"
}
switch status {
case .notDetermined: return "notDetermined"
case .authorizedWhenInUse: return "authorizedWhenInUse"
case .authorizedAlways: return "authorizedAlways"
case .restricted: return "restricted"
case .denied: return "denied"
default: return "unknown"
}
}
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
locationStatus = status
print(#function, statusString)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.first else { return }
lastLocation = location
// fetchLocation(location: lastLocation)
self.locationManager.stopUpdatingLocation()
print(#function, location)
}
}
and the following view:
var body: some View {
VStack {
Text("Latitude: \(locationManager.lastLocation?.coordinate.latitude ?? 0), Longitude: \(locationManager.lastLocation?.coordinate.longitude ?? 0)")
.onAppear{
print("DEBUG: status 1 : \(locationManager.lastLocation?.coordinate.latitude ?? 0)")
}
MapView(lat: (locationManager.lastLocation?.coordinate.latitude ?? 0), lon: locationManager.lastLocation?.coordinate.longitude ?? 0, latDelta: 0.05, lonDelta: 0.05)
.frame(width: UIScreen.screenWidth - 36, height: UIScreen.screenWidth / 2)
.cornerRadius(10)
.onAppear{
print("DEBUG: status 2 : \(locationManager.locationStatus)")
print("DEBUG: lat: \(locationManager.lastLocation?.coordinate.latitude), lon: \(locationManager.lastLocation?.coordinate.longitude)")
}
}
}
So far, the Textfield does show the correct coordinates, but my Mapview shows NIL as the coordinates.
Adding my MapView here as well for completeness:
struct MapView: UIViewRepresentable {
#State var lat = 0.0
#State var lon = 0.0
#State var latDelta = 0.05
#State var lonDelta = 0.05
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon )
let span = MKCoordinateSpan(latitudeDelta: latDelta, longitudeDelta: lonDelta)
let region = MKCoordinateRegion(center: coordinate, span: span)
uiView.setRegion(region, animated: true)
}
}
Hope you could have a look and see if I have missed anything.
The answer is:
Remove all the #State from the MapView():
struct MapView: UIViewRepresentable {
var lat = 0.0
var lon = 0.0
var latDelta = 0.05
var lonDelta = 0.05
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon )
let span = MKCoordinateSpan(latitudeDelta: latDelta, longitudeDelta: lonDelta)
let region = MKCoordinateRegion(center: coordinate, span: span)
uiView.setRegion(region, animated: true)
}
}

How to get map to move from location button to where user picks

I have a location button that gives the region of $viewModel.region and it is defined inside of the locationButton's code. Then if the user types something inside of the designated TextField, the function will make that into its own region. How do I make it so that the map will change if the user taps on the location button, but then decides to enter an address.
Go to typed address;
func goToTypedAddress() {
geocoder.geocodeAddressString(goToAddress, completionHandler: {(placemarks, error) -> Void in
if((error) != nil){
print("Error", error ?? "")
}
if let placemark = placemarks?.first {
let coordinates:CLLocationCoordinate2D = placemark.location!.coordinate
print("Lat: \(coordinates.latitude) -- Long: \(coordinates.longitude)")
//added code
result = "Lat: \(coordinates.latitude) -- Long: \(coordinates.longitude)"
lat = coordinates.latitude
long = coordinates.longitude
}
})
print("\(lat)")
print("\(long)")
var goToAddressRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: lat, longitude: long), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
}
Location Button;
final class ContentViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 40, longitude: 120), span: MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100))
let locationManager = CLLocationManager()
override init() {
super.init()
locationManager.delegate = self
}
func requestAllowOnceLocationPermission() {
locationManager.requestLocation()
}
func locationManager( _ _manager:CLLocationManager, didUpdateLocations locations: [CLLocation]){
guard let latestLocation = locations.first else {
// show an error
return
}
DispatchQueue.main.async{
self.region = MKCoordinateRegion(
center: latestLocation.coordinate,
span:MKCoordinateSpan(latitudeDelta:0.05, longitudeDelta:0.05))
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print(error.localizedDescription)
}
}
The Map;
Map(coordinateRegion: $viewModel.region, showsUserLocation: true)
.ignoresSafeArea()
LocationButton(.currentLocation) {
viewModel.requestAllowOnceLocationPermission()
}

How to transfer data from map annotation to fullscreencover popup

I'm trying to transfer data after user click MapAnnotation. Currently after user click MapAnnotation it add data to selectedCourse and prints it before going if let course = selectedCourse. But some some reason selectedCourse is empty inside .fullScreenCover if statement
import SwiftUI
import MapKit
struct CourseMapView: View {
#ObservedObject var viewModel: CourseSearchViewModel
#State var isShowSheet = false
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 60.480960,
longitude: 22.239808),
span: MKCoordinateSpan(latitudeDelta: 0.1,
longitudeDelta: 0.1))
#State var selectedCourse: Course? = nil
func setCurrentLocation() {
region = MKCoordinateRegion(center: viewModel.location?.coordinate ?? CLLocationCoordinate2D(latitude: 60.480960, longitude: 22.239808), span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
}
var body: some View {
ZStack {
if viewModel.location != nil {
Map(coordinateRegion: $region, interactionModes: .all, showsUserLocation: true, userTrackingMode: nil, annotationItems: viewModel.courses) { course in
MapAnnotation(coordinate: .init(latitude: course.location.latitude, longitude: course.location.longitude)) {
Image(systemName: "person")
.frame(width: 44, height: 44)
.onTapGesture(count: 1, perform: {
print("PRINT: \(course.name)")
selectedCourse = course
if selectedCourse != nil {
isShowSheet.toggle()
}
print("\(selectedCourse)")
})
}
}
.ignoresSafeArea()
} else {
Text("locating user location")
}
}
.fullScreenCover(isPresented: $isShowSheet, content: {
if let course = selectedCourse {
LocationInfoView(viewModel: LocationInfoViewModel(course: course))
.environment(\.showingSheet, self.$isShowSheet)
}
})
.alert(item: $viewModel.alertItem, content: { alertItem in
Alert(title: alertItem.title, message: alertItem.message, dismissButton: alertItem.dismissButton)
})
.onAppear {
setCurrentLocation()
}
}
}
You should use an other signature of .fullScreenCover :
func fullScreenCover<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)? = nil, #ViewBuilder content: #escaping (Item) -> Content)
In this way :
.fullScreenCover(item: $selectedCourse) { course in
// content
}
In your example, item: will be a Course?. When you pass a Course the fullScreenCover is presented ; nil and it's closed.
So you could use a #Binding in your LocationInfoView to dismiss the fullScreenCover. But it looks like you prefer to use an EnvironmentKey. You must therefore modify it:
private struct SelectedCourseKey: EnvironmentKey {
static let defaultValue: Binding<Course?> = .constant(nil)
}
extension EnvironmentValues {
var selectedCourse: Binding<Course?> {
get { self[SelectedCourseKey.self] }
set { self[SelectedCourseKey.self] = newValue }
}
}
In your LocationInfoView:
struct LocationInfoView: View {
#Environment(\.selectedCourse) var selectedCourse: Binding<Course?>
var viewModel: LocationInfoViewModel
var body: some View {
VStack {
Button("close") {
selectedCourse.wrappedValue = nil
}
// some code
}
}
}
And finally, in your CourseMapView :
.fullScreenCover(item: $selectedCourse) { course in
if let course = course {
LocationInfoView(viewModel: LocationInfoViewModel(course: course))
.environment(\.selectedCourse, self.$selectedCourse)
}
}

Clustering annotation with swiftUI

My goal is clustering annotation on map with show number of items in cluster, I have no experience in UIKit and try to avoid it. Is it possible to do it using swiftUI only? If not how to reduce intervention of UIKit?
This is how it should look like
import SwiftUI
import MapKit
struct ContentView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 43.64422936785126, longitude: 142.39329541313924),
span: MKCoordinateSpan(latitudeDelta: 1.5, longitudeDelta: 2)
)
var body: some View {
Map(coordinateRegion: $region, annotationItems: data) { annotation in
MapAnnotation(coordinate: annotation.coordinate) {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(Color.purple)
}
}
.edgesIgnoringSafeArea(.all)
}
}
struct SampleData: Identifiable {
var id = UUID()
var latitude: Double
var longitude: Double
var coordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude)
}
}
var data = [
SampleData(latitude: 43.70564024126748, longitude: 142.37968945214223),
SampleData(latitude: 43.81257464206404, longitude: 142.82112322464369),
SampleData(latitude: 43.38416585162576, longitude: 141.7252598737476),
SampleData(latitude: 45.29168643283501, longitude: 141.95286751470724),
SampleData(latitude: 45.49261392585982, longitude: 141.9343973160499),
SampleData(latitude: 44.69825427301145, longitude: 141.91227845284203)
]
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I find the way to cluster annotations with MapKit, but reuse map like a view for easy swiftUI. Looks like that https://i.stack.imgur.com/u3hKR.jpg
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
var forDisplay = data
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 43.64422936785126, longitude: 142.39329541313924),
span: MKCoordinateSpan(latitudeDelta: 1.5, longitudeDelta: 2)
)
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
/// showing annotation on the map
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard let annotation = annotation as? LandmarkAnnotation else { return nil }
return AnnotationView(annotation: annotation, reuseIdentifier: AnnotationView.ReuseID)
}
}
func makeCoordinator() -> Coordinator {
MapView.Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
/// creating a map
let view = MKMapView()
/// connecting delegate with the map
view.delegate = context.coordinator
view.setRegion(region, animated: false)
view.mapType = .standard
for points in forDisplay {
let annotation = LandmarkAnnotation(coordinate: points.coordinate)
view.addAnnotation(annotation)
}
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
}
struct SampleData: Identifiable {
var id = UUID()
var latitude: Double
var longitude: Double
var coordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude)
}
}
var data = [
SampleData(latitude: 43.70564024126748, longitude: 142.37968945214223),
SampleData(latitude: 43.81257464206404, longitude: 142.82112322464369),
SampleData(latitude: 43.38416585162576, longitude: 141.7252598737476),
SampleData(latitude: 45.29168643283501, longitude: 141.95286751470724),
SampleData(latitude: 45.49261392585982, longitude: 141.9343973160499),
SampleData(latitude: 44.69825427301145, longitude: 141.91227845284203)
]
class LandmarkAnnotation: NSObject, MKAnnotation {
let coordinate: CLLocationCoordinate2D
init(
coordinate: CLLocationCoordinate2D
) {
self.coordinate = coordinate
super.init()
}
}
/// here posible to customize annotation view
let clusterID = "clustering"
class AnnotationView: MKMarkerAnnotationView {
static let ReuseID = "cultureAnnotation"
/// setting the key for clustering annotations
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
clusteringIdentifier = clusterID
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepareForDisplay() {
super.prepareForDisplay()
displayPriority = .defaultLow
}
}
And use that map like a default view
import SwiftUI
struct ContentView: View {
var body: some View {
MapView()
.edgesIgnoringSafeArea(.all)
}
}
For solving a problem I used next resources:
https://www.hackingwithswift.com/books/ios-swiftui/communicating-with-a-mapkit-coordinator
https://www.hackingwithswift.com/books/ios-swiftui/advanced-mkmapview-with-swiftui
https://developer.apple.com/videos/play/wwdc2017/237/
https://www.youtube.com/watch?v=QuYA7gQjTt4

Problem of resizing Map using SwiftUI Mapkit

I would like to zoom in my map on the application, I tried both using latitudinalMeters: 300, longitudinalMeters: 300 or spin with latitudeDelta: 0.001. Both of them did not work at all.
I also chose (0, 0) as my center, but every time I run on the simulator, I have (37.326010,-122.026056) as my center. Apparently, none of the default settings of center and region that I had set in my location manager works in ContentView.
Here is my code of LocationManager.swift:
import Foundation
import CoreLocation
import MapKit
class LocationManager: NSObject, ObservableObject{
let locationManager = CLLocationManager()
#Published var location: CLLocation?
#Published var region: MKCoordinateRegion
override init(){
self.region = MKCoordinateRegion(center: CLLocationCoordinate2D.init(latitude: 0,longitude: 0),latitudinalMeters: 300, longitudinalMeters: 300)
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
}
extension LocationManager : CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]){
guard let location = locations.last else { return }
self.region = MKCoordinateRegion(center: location.coordinate, latitudinalMeters: 300, longitudinalMeters: 300)
self.location = location
}
}
Here is my ContentView:
struct ContentView: View {
var body: some View {
MapView2()
}
}
struct MapView2: View {
#ObservedObject var locationManager = LocationManager()
var body: some View {
let coord = locationManager.location?.coordinate
let lat = coord?.latitude ?? 0
let lon = coord?.longitude ?? 0
return VStack {
Map(coordinateRegion: $locationManager.region,
interactionModes: .all,
showsUserLocation: true, userTrackingMode: .constant(.follow))
}
}
}
As for SwiftUI using MapKit, I would not use the CoreLocation framework. You can use the .onChange modifier to perform zoom changes to your View. You can use the #State var zoom with a SwiftUI gesture to perform them if you want, or anything that can do those changes live. I added two buttons within a slider to zoom in or out for the example.
import SwiftUI
import MapKit
struct ContentView: View {
var body: some View {
MapsView()
}
}
struct MapsView: View {
#State var zoom: CGFloat = 15
#State var mapCoordinate = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 38.989202809314854,
longitude: -76.93626224283602),
span: MKCoordinateSpan(
latitudeDelta: .zero,
longitudeDelta: .zero))
var body: some View {
VStack(spacing: 16) {
Map(coordinateRegion: $mapCoordinate)
.ignoresSafeArea(edges: .all)
// You can see the changes being operating by the .onChange modifier.
Slider(value: $zoom,
in: 0.01...50,
minimumValueLabel: Image(systemName: "plus.circle"),
maximumValueLabel: Image(systemName: "minus.circle"), label: {})
.padding(.horizontal)
.onChange(of: zoom) { value in
mapCoordinate.span.latitudeDelta = CLLocationDegrees(value)
mapCoordinate.span.longitudeDelta = CLLocationDegrees(value)
}
}
.font(.title)
}
}