Custom user location marker with custom accuracy circle in MKMapView - ios

I'm implementing simple navigation and to display user location I'm using custom MKAnnotationView:
let reuseId = "userLocationPin"
userLocationViewAnnotation = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId) as? MKPinAnnotationView
if userLocationViewAnnotation == nil {
userLocationViewAnnotation = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
userLocationViewAnnotation!.canShowCallout = true
userLocationViewAnnotation!.centerOffset = CGPoint(x: 0.9, y: -2)
userLocationViewAnnotation!.image = UIImage(named: "User_location_red_moving_x1")
}
And this code is working fine. Next I need to add accuracy circle. I'm adding it by MKCircle overlay:
userLocationCircle = MKCircle(centerCoordinate: location.coordinate, radius: location.horizontalAccuracy)
map.addOverlay(userLocationCircle!)
The problem is that user location is updating more frequently and with animation by MKMap internally, but circle is updating after user location change (in my code) so it's jumping from one point to another.
Is it possible to add this circle to MKAnnotationView, or maybe do you have any other ideas?

Solution 1 (which I did before)
Do not set MKMapView.showsUserLocation to true. Which means that you cannot use mapView:didUpdateUserLocation: of the delgate any more. That means that fetching the user's location is less convenient. You will have to use Core Location for that. (Tons of tutorials around)
Then you have to set the map's center ans span accordingly but yourself. By doing so you can add any annotation that you want for the user's location. Just respond to mapView(_:viewForAnnotation:) and return the view that you want to be displayed.
Solution 2: (Which I guess works but I never tried it myself)
Just go along with MKMapView.showsUserLocation as you did before but respond to mapView(_:viewForAnnotation:) in any case. Debug it. On the first call to this method it is handed in the standard user annotation. (I just don't know its type/class name out of the top of my head. It is the first one. That's why I ask you to debug for it.)
Just don't return nil but return the view that you want to be displayed.
It is worth a try. It is less work than solution 1 if it works.
However, solution 1 gives you much more flexibilty and full control over the part of the world that is currently displayed in the Map.

Related

Finding the maximum zoom level of an MKMapView, and Knowing When I've Reached It

I've seen a heck of a lot of stuff about setting the maximum zoom level, but not how to get it.
Here's the deal. I'm doing a "drill down" UI for clustered annotations. While the map can be zoomed, tapping on a cluster simply centers the map on the cluster, and halves the span.
When it reaches the end, and there is still a cluster, however, I want to be able to show a popover/callout, denoting the contents of the remaining cluster.
I am not able to check the zoom, to find out whether or not the new region I'm planning to set, will "take."
I know that, once it reaches max zoom, mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) will no longer be called, but that means that I need to play with contexts, and do a fairly complex dance.
Is there a simple way to examine an MKMapView, and see if it is at its maximum zoom?
OK, so no takers, then.
Here's how I did it. Clunky, but it works.
First, I set up a private instance property as a canary:
private var _lastClusterTapped: Bool = false
Next, I set up a simple delegate callback that clears the canary:
func mapView(_: MKMapView, regionWillChangeAnimated: Bool) {
_lastClusterTapped = false
}
Then, in the code that handles a tap on the cluster, I did this:
func tappedOnClusterAnnotation(mapView inMapView: MKMapView, annotationView inAnnotationView: MKAnnotationView) {
// We attempt to zoom in by a certain amount. We do this by dividing the span.
if let coords = inAnnotationView.annotation?.coordinate,
var region = inMapView?.region {
// The new region will be half the size of the original map region.
region.span.latitudeDelta /= 2.0
region.span.longitudeDelta /= 2.0
region.center = coords // The new region will center on the annotation.
_lastClusterTapped = true
inMapView?.setRegion(region, animated: true)
// If the callback did not happen (canary is still alive), then we assume we are maxed out, and call the handler.
if _lastClusterTapped {
// Do whatever we do, when a "locked" annotation is tapped.
}
}
}
I hate canaries and semaphores, but it does work.
This has the significant disadvantage of not working, if the region callbacks happen in an asynchronous fashion. It is predicated on the callback being made inline, so the canary has been cleared after the region set is done.

How do i restrict panning and restrict zooming out of a certain area of apple maps in MapKit?

I am currently making an app with a map that should focus on a certain location only. I would like the user to not be able to zoom out or pan out of this area so they can keep their focus on the image overlay that i have put over this area.
In order to get the app to start off from the location that i want and not some random map, I used a tutorial from Ray Wenderlich: https://www.raywenderlich.com/425-mapkit-tutorial-overlay-views
How would I acoomplish my task based on the code that is written in the tutorial above? I have completed the tutorial, so I am looking for help in adding any code and identifying where and what kind of code to put.
I found other tutorials on this topic unhelpful because they were for other map types like Google maps or MapBox. The apple website about MapKit and MaximumZ does not help me very much either.
I am a beginner in XCode and Swift, and have only had little bit of experience in Python previously. I was hoping limiting the zoom and user access to parts of the maps would be easier...
override func viewDidLoad() {
super.viewDidLoad()
let latDelta = park.overlayTopLeftCoordinate.latitude -
park.overlayBottomRightCoordinate.latitude
// Think of a span as a tv size, measure from one corner to another
let span = MKCoordinateSpanMake(fabs(latDelta), 0.0)
let region = MKCoordinateRegionMake(park.midCoordinate, span)
mapView.region = region
}
This is what I have so far for getting the app to startup on the location that I want, using a rectangle that bounds the area that I am looking to restrict the user to.
The MKMapView has a delegate MKMapViewDelegate. This protocol has a function called:
func mapViewDidChangeVisibleRegion(_:)
This method is called whenever the user scrolls or zooms the map. In this method you can specify the behavior of the map. You can, for instance set a specific region that you want the map to zoom into and specify the maximum level of zoom allowed.
In the function mapViewDidChangeVisibleRegion(_:) you can then check to what latitudeDelta and longitudeDelta the map can zoom into. If the delta's go below or above a certain level you can lock the zooming by setting the region with something like this:
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
if mapView.region.span.latitudeDelta < 0.4 {
regionSpan = MKCoordinateSpan(latitudeDelta: 0.4, longitudeDelta: 0.5)
let mapRegion = MKCoordinateRegion(center: self.centerCoordinate, span: self.regionSpan)
self.trackMapView.setRegion(mapRegion, animated: true)
}
}

Move objects around, with gesture recognizer for multiple Objects

I am trying to make an app where you can use Stickers like on Snapchat and Instagram. It fully worked to find a technique, that adds the images, but now I want that if you swipe the object around the object changes its position (I also want to make the scale / rotate function).
My code looks like this:
#objc func StickerLaden() {
for i in 0 ..< alleSticker.count {
let imageView = UIImageView(image: alleSticker[i])
imageView.frame = CGRect(x: StickerXScale[i], y:StickerYScale[i], width: StickerScale[i], height: StickerScale[i])
ImageViewsSticker.append(imageView)
ImageView.addSubview(imageView)
imageView.isUserInteractionEnabled = true
let aSelector : Selector = "SlideFunc"
let slideGesture = UISwipeGestureRecognizer(target: self, action: aSelector)
imageView.addGestureRecognizer(slideGesture)
}
}
func SlideFunc(fromPoint:CGPoint, toPoint: CGPoint) {
}
Here are the high-level steps you need to take:
Add one UIPanGestureRecognizer to the parent view that has the images on it.
Implement UIGestureRecognizerDelegate methods to keep track of the user touching and releasing the screen.
On first touch, loop through all your images and call image.frame.contains(touchPoint). Add all images that are under the touch point to an array.
Loop through the list of touched images and calculate the distance of the touch point to the center of the image. Chose the image whose center is closest to the touched point.
Move the chosen image to the top of the view stack. [You now have selected an image and made it visible.]
Next, when you receive pan events, change the frame of the chosen image accordingly.
Once the user releases the screen, reset any state variables you may have, so that you can start again when the next touch is done.
The above will give you a nicely working pan solution. It's a good amount of things you need to sort out, but it's not very difficult.
As I said in my comment, scale and rotate are very tricky. I advise you to forget that for a bit and first implement other parts of your app.

Moving a view in viewDidAppear?

I've got a view that I'm wanting to have in a specific area of the screen based on a value.
I'm trying to keep the view's location the same when a user comes back to the screen after leaving, so I'm passing around a variable of "currentSearch" , and in viewDidAppear I'm checking that value and trying to set the location accordingly.
if currentSearch == "new" {
self.menuBar.center.x = self.new.center.x
} else ...... //checking my other variables and doing relative location moves.
I set a breakpoint and it's going into this code and getting to the center.x bit, but the view isn't moving.
I tried animating it with a duration of 0 or .1, the view didn't move. I also tried something I saw someone else mention that looked like
let f = menuBar.frame
let menubar.center.x = self.new.center.x
let menuBar.frame = f
that also did not work.
Any ideas why the view is just staying at where it's set in the storyboard?

Custom user location dot in Google maps for iOS (GMSMapview)

Is there an official way to set a custom user location dot in Google maps for iOS (GMSMapView)?
Is there a known way to "hack" it? Like iterating through all subviews and layers and fish the blue dot?
Even if you can't customise its appearance, can you control its z order index? When you have many markers, the little blue dot becomes hidden, and sometimes you want it to be visible at all times.
Thanks
You can try to find the image on:
GoogleMaps.framework > Resources > GoogleMaps.bundle
OR
GoogleMaps.framework > Resources > GoogleMaps.bundle > GMSCoreResources.bundle
I did a quick search on those and the only associated file I found with that blue dot is GMSSprites-0-1x.
Please read the google maps terms and conditions because this might not be legal.
You can set the maps myLocationEnabled to NO. That will hide the default location dot. Then use an instance of CLLocationManager to give you your position. Inside CLLocationManager didUpdateLocations method you can set a custom GMSMarker. Set its icon property to whatever you want your dot to look like using [UIImage imageNamed:]. This will allow you to achieve the desired effect.
Swift 4
Disable the default Google Map current location marker (it's disabled by default):
mapView.isMyLocationEnabled = false
Create a marker as an instance property of the view controller (because a delegate will need access to this):
let currentLocationMarker = GMSMarker()
The GMSMarker initializer allows for a UIImage or a UIView as a custom graphic, not a UIImageView unfortunately. If you want more control over the graphic, use a UIView. In your loadView or viewDidLoad (wherever you configured the map), configure the marker and add it to the map:
// configure custom view
let currentLocationMarkerView = UIView()
currentLocationMarkerView.frame.size = CGSize(width: 40, height: 40)
currentLocationMarkerView.layer.cornerRadius = 40 / 4
currentLocationMarkerView.clipsToBounds = true
let currentLocationMarkerImageView = UIImageView(frame: currentLocationMarkerView.bounds)
currentLocationMarkerImageView.contentMode = .scaleAspectFill
currentLocationMarkerImageView.image = UIImage(named: "masterAvatar")
currentLocationMarkerView.addSubview(currentLocationMarkerImageView)
// add custom view to marker
currentLocationMarker.iconView = currentLocationMarkerView
// add marker to map
currentLocationMarker.map = mapView
All that remains is giving the marker a coordinate (initially and every time the user's location changes), which you do through the CLLocationManagerDelegate delegate.
extension MapViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let lastLocation = locations.last!
// update current location marker
currentLocationMarker.position = CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)
}
}
The first few locations that the location manager produces may not be very accurate (although sometimes it is), so expect your custom marker to jump around a bit at first. You can wait until the location manager has gathered a few coordinates before applying it to your custom marker by waiting until locations.count > someNumber but I don't find this approach very attractive.

Resources