I am trying to make a custom "user location" pin, with heading rotation based on users location heading.
I used this answer: https://stackoverflow.com/a/58363556/894671 as a base and managed to get up and running a custom pin, that rotates based on the heading.
The problem:
While testing on a device, it seems, that the transformation using the provided heading is not correct.
Only at 0/360 degrees it shows up correctly, but If I rotate around, I am seeing default MKMapKit shown heading to be correctly rotating, while my custom icon manages to rotate twice in that same time.
Please see the attached video:
https://i.imgur.com/3PEm2MS.mp4
Demo uploaded here:
https://github.com/GuntisTreulands/Demo123
But for all intents and purposes, here is AnnotationView:
class AnnotationView : MKAnnotationView, HeadingDelegate {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
}
func headingChanged(_ heading: CLLocationDirection) {
// For simplicity the affine transform is done on the view itself
UIView.animate(withDuration: 0.1, animations: { [unowned self] in
self.transform = CGAffineTransform(rotationAngle: CGFloat(heading * .pi / 180.0 ))
})
}
}
and heading is forwarded from here:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
if let lastLocation = locations.last {
userLocationAnnotation.coordinate = lastLocation.coordinate
}
}
I can't figure out, why my location heading is acting so weird.
Okay, I managed to kinda solve this.
Here is the new result: https://imgur.com/a/YJ1H683
What I did was:
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
if let heading = mapView.userLocation.heading {
userLocationAnnotation.heading = -mapView.camera.heading + (heading.trueHeading > 0 ? heading.trueHeading : heading.magneticHeading)
} else {
userLocationAnnotation.heading = -mapView.camera.heading + (newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading)
}
}
So - in case I am using userlocation with trackingmode == .followWithHeading, this is working great. My icon is on top of the original icon.
To hide original user icon and show yours instead - return a custom annotation view (with nothing to render) for userlocation:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !(annotation is MKUserLocation) else {
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "AnnotationView")
if annotationView == nil {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: "AnnotationView")
}
return annotationView
}
if let annotation = annotation as? Annotation {
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: NSStringFromClass(Annotation.self))
if (annotationView == nil) {
annotationView = AnnotationView(annotation: annotation as MKAnnotation, reuseIdentifier: NSStringFromClass(Annotation.self))
} else {
annotationView!.annotation = annotation as MKAnnotation
}
annotation.headingDelegate = annotationView as? HeadingDelegate
annotationView!.image = UIImage.init(named: "user_pin")
return annotationView
}
return nil
}
Thus the result is:
1.) Get original apple maps user location + followWithHeading, but with a custom pin and correct location.
2.) I can adjust my custom location pin location to some different coordinates, but keep in mind, that followWithHeading will rotate around the real userLocation coordinates.
3.) I have also noticed, that without userlocation, the original calculation:
userLocationAnnotation.heading = -mapView.camera.heading + (newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading)
Is also .. kinda okay, if I move and rotate. It is faulty, if I just rotate in place. Still cannot solve that one.
I also managed to make a custom followWithHeading, with this function:
func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
mapView.centerCoordinate = userLocationAnnotation.coordinate
mapView.camera.heading = (newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading)
userLocationAnnotation.heading = -mapView.camera.heading + (newHeading.trueHeading > 0 ? newHeading.trueHeading : newHeading.magneticHeading)
}
But the result is not as fluid as the original apple map rotation.
If I rotate fast, then it is fine. But if I slowly turn my device, then the rotation is visibly not smooth.
See the result: https://imgur.com/a/SusV61a
Related
I am trying to implement directions with clLocationManager in a project.
Everything is working ok, but the didEnterrRegion function is very slow to fire.
When testing, I enter the region but only 2-3 minutes after exiting the region I get the callback. Does anyone have any suggestion on how to improve this?
This is the locationManager:
private lazy var locationManager: CLLocationManager = {
let locationManager = CLLocationManager()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
locationManager.distanceFilter = kCLDistanceFilterNone
handleAuthorizationStatus(locationManager: locationManager)
} else {
//TODO: Handle error
}
return locationManager
}()
This is which regions I am tracking, here I am also drawing each region to easier see when I enter specific region:
private func getRouteSteps(_ mapView: MKMapView, route: MKRoute) {
for monitoredRegion in locationManager.monitoredRegions {
locationManager.stopMonitoring(for: monitoredRegion)
}
let steps = route.steps
self.steps = steps
for i in 0..<steps.count {
let step = steps[i]
let region = CLCircularRegion(center: step.polyline.coordinate, radius: 30, identifier: "\(i)")
let circle = MKCircle(center: region.center, radius: region.radius)
mapView.addOverlay(circle)
locationManager.startMonitoring(for: region)
}
stepCounter += 1
let initialMessage = "Om \(Int(steps[stepCounter].distance)) meter \(steps[stepCounter].instructions.lowercased())"
directionMessage = initialMessage
}
This is the locationManager-function:
extension MapViewModel: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// manager.stopUpdatingLocation()
if directionsViewState != .isShowingRoute {
if let location = locations.last {
self.didUpdateRegion = "Updated region with accuracy: \(location.horizontalAccuracy)"
let center = location.coordinate
setNewRegionForMapView(center: center)
isCenteringUserLocation = true
}
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
handleAuthorizationStatus(locationManager: locationManager)
}
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
self.didEnterregion = "Entered region: \(region.identifier)"
stepCounter += 1
if stepCounter < steps.count {
let message = "Om \(Int(steps[stepCounter].distance)) meter \(steps[stepCounter].instructions.lowercased())"
directionMessage = message
let speechUtterance = AVSpeechUtterance(string: message)
speechSynthesizer.speak(speechUtterance)
} else {
directionMessage = "You have arrived at your destination!"
stepCounter = 0
let speechUtterance = AVSpeechUtterance(string: directionMessage)
speechSynthesizer.speak(speechUtterance)
for monitoredRegion in locationManager.monitoredRegions {
locationManager.stopMonitoring(for: monitoredRegion)
}
}
}
}
I am also calling locationManager.startUpdatingLocations in the init-method.
Suppose that you are using locationManager.requestWhenInUseAuthorization().
So, I have some suggestions for you:
[METHOD 1] Apply background location updating:
Enable background mode to your target
Then add these lines of code:
locationManager.allowsBackgroundLocationUpdates = true
locationManager.pausesLocationUpdatesAutomatically = false
The line allowsBackgroundLocationUpdates = true will allow your app to run in the background to receive new location event (ex: lock screen, use another app,...).
And, pausesLocationUpdatesAutomatically = false will tell the system to not pause location updating, system can pause background location updating to save battery.
Call your locationManager.startUpdatingLocation() to start listening for new location change.
Next [METHOD 2], if above method doesn't work, you can switch to use locationManager.requestAlwaysAuthorization(). Its description is Requests the user’s permission to use location services regardless of whether the app is in use. System can wake your app to run in background to handle new location events. Remember to add the permission description for requestAlwaysAuthorization in Info.plist file.
Next [METHOD 3], try to increase your CLCircularRegion's radius to higher value, ex: 50 meters. Or you can try to increase the distanceFilter to 2 meters, distanceFilter = none isn't a best option.
Finally [METHOD 4], Make your customized region monitoring logic by calculating distance from user's location to region's center whenever we get a new location event, use this one func distance(from location: CLLocation) -> CLLocationDistance. If distance is <= your region's radius, that means user already crossed the boundary.
Hope that you can solve your problem.
How can I calculate the total distance traveled use CoreLocation in Swift
I haven't been able to so far find any resources for how to do this in Swift for iOS 8,
How would you calculate the total distance moved since you began tracking your location?
From what I've read so far, I need to save location of a points, then calculate the distance between current point, and last point, then add that distance to a totalDistance variable
Objective-C is extremely unfamiliar to me, so I haven't been able to work out the swift syntax
Here is what I've worked out so far, not sure if I'm doing it right. Though the distanceFromLocationmethod is returning all 0.0 so obviously something is wrong
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
var newLocation: CLLocation = locations[0] as CLLocation
oldLocationArray.append(newLocation)
var totalDistance = CLLocationDistance()
var oldLocation = oldLocationArray.last
var distanceTraveled = newLocation.distanceFromLocation(oldLocation)
totalDistance += distanceTraveled
println(distanceTraveled)
}
update: Xcode 8.3.2 • Swift 3.1
The problem there is because you are always getting the same location over and over again. Try like this:
import UIKit
import MapKit
class ViewController: UIViewController, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
let locationManager = CLLocationManager()
var startLocation: CLLocation!
var lastLocation: CLLocation!
var startDate: Date!
var traveledDistance: Double = 0
override func viewDidLoad() {
super.viewDidLoad()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
locationManager.startMonitoringSignificantLocationChanges()
locationManager.distanceFilter = 10
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if startDate == nil {
startDate = Date()
} else {
print("elapsedTime:", String(format: "%.0fs", Date().timeIntervalSince(startDate)))
}
if startLocation == nil {
startLocation = locations.first
} else if let location = locations.last {
traveledDistance += lastLocation.distance(from: location)
print("Traveled Distance:", traveledDistance)
print("Straight Distance:", startLocation.distance(from: locations.last!))
}
lastLocation = locations.last
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
if (error as? CLError)?.code == .denied {
manager.stopUpdatingLocation()
manager.stopMonitoringSignificantLocationChanges()
}
}
}
Sample Project
If you want to calculate the route distance between two points you need to use MKDirectionsRequest, this will return you one, or many, routes from point A to point B with the step by step instruction:
class func caculateDistance(){
var directionRequest = MKDirectionsRequest()
var sourceCoord = CLLocationCoordinate2D(latitude: -36.7346287, longitude: 174.6991812)
var destinationCoord = CLLocationCoordinate2D(latitude: -36.850587, longitude: 174.7391745)
var mkPlacemarkOrigen = MKPlacemark(coordinate: sourceCoord, addressDictionary: nil)
var mkPlacemarkDestination = MKPlacemark(coordinate: destinationCoord, addressDictionary: nil)
var source:MKMapItem = MKMapItem(placemark: mkPlacemarkOrigen)
var destination:MKMapItem = MKMapItem(placemark: mkPlacemarkDestination)
directionRequest.setSource(source)
directionRequest.setDestination(destination)
var directions = MKDirections(request: directionRequest)
directions.calculateDirectionsWithCompletionHandler {
(response, error) -> Void in
if error != nil { println("Error calculating direction - \(error.localizedDescription)") }
else {
for route in response.routes{
println("Distance = \(route.distance)")
for step in route.steps!{
println(step.instructions)
}
}
}
}
}
This example code will return you this:
Distance
Distance = 16800.0
Step by Step instructions
Start on the route
At the end of the road, turn left onto Bush Road
Turn right onto Albany Expressway
At the roundabout, take the first exit onto Greville Road toward 1, Auckland
At the roundabout, take the third exit to merge onto 1 toward Auckland
Keep left
Take exit 423 onto Shelly Beach Road
Continue onto Shelly Beach Road
At the end of the road, turn right onto Jervois Road
Turn left onto Islington Street
Keep right on Islington Street
Arrive at the destination
The function can be easily modified to receive two locations and return the distance and any other needed information.
I hope that helps you!
Leo Dabus method could be used to calculate the geographical distance between your actual location and start one.
In order to obtain the precise traveled distance, you have to update "traveledDistance" using the difference between the last position and the old one.
This is my implementation:
var startLocation:CLLocation!
var lastLocation: CLLocation!
var traveledDistance:Double = 0
func locationManager(manager: CLLocationManager!, didUpdateLocations locations: [AnyObject]!) {
if startLocation == nil {
startLocation = locations.first as! CLLocation
} else {
let lastLocation = locations.last as! CLLocation
let distance = startLocation.distanceFromLocation(lastLocation)
startLocation = lastLocation
traveledDistance += distance
}
}
I am trying to make an Uber clone in which there are a lot of annotation for passengers and driver. I want to only load and show the annotation which belong to the map region currently displayed, so that all the annotations are not added to the mapview and does not consume much memory.
I have two types of annotations: driver and passenger. I have different images for their annotation.
Here is how I am trying to do it:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let annotation = annotation as? DriverAnnotation {
guard let dequeuedDriverAnnotation = mapView.dequeueReusableAnnotationView(withIdentifier: "driver") else {
let driverAnnotation = MKAnnotationView(annotation: annotation, reuseIdentifier: "driver")
driverAnnotation.image = UIImage(named: "driverAnnotation")
return driverAnnotation
}
dequeuedDriverAnnotation.annotation = annotation
return dequeuedDriverAnnotation
} else if let annotation = annotation as? PassengerAnnotation {
guard let dequeuedPassengerAnnotation = mapView.dequeueReusableAnnotationView(withIdentifier: "passenger") else {
let passengerAnnotation = MKAnnotationView(annotation: annotation, reuseIdentifier: "passenger")
passengerAnnotation.image = UIImage(named: "currentLocationAnnotation")
return passengerAnnotation
}
dequeuedPassengerAnnotation.annotation = annotation
return dequeuedPassengerAnnotation
}
return nil
}
}
How about defining function to check if annotation belong to the map region currently displayed, and try returning nil if it is out of region.
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if !visibleMapRegion(annotation) {
return nil
}
...
To define this function see also:
How to check if MKCoordinateRegion contains CLLocationCoordinate2D without using MKMapView?
How do I determine if the current user location is inside of my MKCoordinateRegion?
Is there any way that annotations might show other images then it should supose ? Is it a way to fix that ?
I've got a werid problem. After my clusters going to evolve to the single annotations then after updating ( zoom out - zoom in ) they images going to be diferent then it should be. After debuging it says that the color of the pin view image is the one which I suposed. Where I could hook up yet to see the exact data ? Maybe something after viewFor which get invoked.
Here is a logic how am I doing this.
The dictionary to knows witch model is on the top of the added annotations.
var dicAnnotations = [FBAnnotation : MyModel]()
Through the loop of [MyModel] i am appending data to the array of the [FBAnnotation] which is needed for saving them via FBClusteringManager. Then in the same time while the new annotation is added to the [FBAnnotation] I am adding
self.dicAnnotations[fbAnnotation] = objModel
to know which annotation have which object.
So as we know how I am soring the data of annotations, how Am I indexing them, lets have a look on the delegate of the MKMapView.
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
DispatchQueue.global(qos: .userInitiated).async {
let mapBoundsWidth = Double(self.mapView.bounds.size.width)
let mapRectWidth = self.mapView.visibleMapRect.size.width
let scale = mapBoundsWidth / mapRectWidth
let annotationArray = self.clusterManager.clusteredAnnotations(withinMapRect: self.mapView.visibleMapRect, zoomScale:scale)
for ann in annotationArray {
if ann is FBAnnotation {
let a = ann as! FBAnnotation
print("Here u are , my color is : \(self.dicAnnotations[a]!.lineColor)")
}
}
DispatchQueue.main.async {
self.clusterManager.display(annotations: annotationArray, onMapView: self.mapView)
}
}
}
The for loop is for checking if the annotation is the one which should be - and it is!
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
var reuseId = ""
if annotation is FBAnnotationCluster {
reuseId = "Cluster"
var clusterView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
if clusterView == nil {
clusterView = FBAnnotationClusterView(annotation: annotation, reuseIdentifier: reuseId, configuration: FBAnnotationClusterViewConfiguration.default())
} else {
clusterView?.annotation = annotation
}
return clusterView
}
else {
let ann = annotation as! FBAnnotation
var pinView = mapView.dequeueReusableAnnotationView(withIdentifier: "Pin")
pinView = MKAnnotationView(annotation: ann, reuseIdentifier: "Pin")
var modelType = dicAnnotations[ann]?.modelType
modelType = modelType?.modelTypeToString(modelType: modelType!)
let modelColor = dicAnnotations[ann]?.lineColor
let modelDirection = dicAnnotations[ann]?.withHeading360
print("\(modelType!)_\(modelColor!)")
pinView?.image = UIImage(named: "\(modelType!)_\(modelColor!)")?.rotated(by: Measurement(value: modelDirection!, unit: .degrees))
return pinView
}
}
How it looks in the stack call which I am able get :
Here u are , my color is : yellow
(lldb) po dicAnnotations[ann]
▿ Optional<MyModel>
▿ some : <Project.MyModel: 0x101b656f0>
(lldb) po dicAnnotations[ann]?.lineColor
▿ Optional<String>
- some : "yellow"
From :
// viewFor
... else { pinView?.annotation = ann } ...
Any ideas ? It shows me would say "random colored image".
Thanks in advance!
Solution
I've edited the delegate viewFor. Deleted the if pinView == null condition and else as well. Somehow it was not working like it suposed to.
I have a map and on this map, I have 10 custom annotation pins. All pins have the same custom image. When I click on a pin, I need to change all the other 9 annotation's images. it's possible to change the clicked pin's image but I need to keep as it is and I need to change all other pins images.
I tried to get all annotations with Map mapView.annotations and tried to find selected annotations and change other images but couldn't manage it. And idea how to do it?
Thanks in advance.
Conform to MKMapViewDelegate protocol and then:
func mapView(mapView: MKMapView!, didSelectAnnotationView view: MKAnnotationView!) {
let selectedAnnotation = view.annotation
for annotation in mapView.annotations {
if let annotation = annotation as? MKAnnotation where !annotation.isEqual(selectedAnnotation) {
// do some actions on non-selected annotations in 'annotation' var
}
}
Also you can save the selected annotation for later use here, if you want to process all annotations in another moment.
finally managed :) solved the problem little bit hard way but working smooth :) thank you for the tip rshev ;)
i used a bool for tap recognize
func mapView(mapView: MKMapView!, viewForAnnotation annotation: MKAnnotation!) -> MKAnnotationView! {
if annotation is CustomAnnotation {
var pin = mapView.dequeueReusableAnnotationViewWithIdentifier(customAnnotationViewIdentifier)
pin = MKPinAnnotationView(annotation: annotation, reuseIdentifier: customAnnotationViewIdentifier)
if tapControl {
pin.image = UIImage(named: "MapAnnotationIcon")
} else {
pin.image = UIImage(named: "SelectedMapAnnotationIcon")
}
if pin == nil {
pin.canShowCallout = false
} else {
pin.annotation = annotation
}
return pin
and when pin tapped ->
if let annotation = view.annotation as? CustomAnnotation {
tapControl = !tapControl
for annotation in mapView.annotations {
if let annotation = annotation as? MKAnnotation where !annotation.isEqual(selectedAnnotation) {
mapView.removeAnnotation(annotation)
}
}
addAnnotations()
println("tapped")
i removed all pins without selected pin, and then draw them back but this time tapcontrol is false so other pins are redrawed with another imageview, so thats what i exactly want to do.
You just have to overrride isSelected property inside your MKAnnotationView subclass.
override var isSelected: Bool {
didSet {
if isSelected {
// do stuff where annotation is selected
} else {
// do opposite
}
}
}