Change coordinate of MKOverlay for an MKOverlayView - ios

I have an overlay on the map, and I would like to change its coordinates. To do this seamlessly I'm going to call the setNeedsDisplayInMapRect: method after the change has been made to the view.
I've tested this out by just changing the fillColor and it works fine:
overlayView.fillColor = [[UIColor greenColor] colorWithAlphaComponent:0.3];
[overlayView setNeedsDisplayInMapRect:mapView.visibleMapRect];
However I've seemingly hit a brick wall trying to also change the center coordinates of my overlay view (which is an MKCircleView with an MKCircle). There is a method in
MKAnnotation, which MKCircle conforms to, called setCoordinate: - which seems like what I need. Unfortunately though, the circle property in MKCircleView is readonly. Moreover the overlay property in MKOverlayView is also readonly.
Is there actually a way of changing the coordinates for an overlay, without resort to remove the overlay view and adding a new one (which would cause very noticeable flicker on the screen.) ?

same problem is occurred here , so i'm creating set of method and calling it according to requirement.
-(void)removeAllAnnotationFromMapView{
if ([[self.tmpMapView annotations] count]) {
[self.tmpMapView removeAnnotations:[self.tmpMapView annotations]];
}
}
-(void)removeAllOverlays{
if ([[self.tmpMapView overlays] count]) {
[self.tmpMapView removeOverlays:[self.tmpMapView overlays]];
}
}
-(void)removeOverlayWithTag:(int)tagValue{
for (MKOverlayView *oView in [self.tmpMapView overlays]) {
if (oView.tag == tagValue) {
[self.tmpMapView removeOverlay:oView];
}
}
}

Related

MKPolyline / MKPolylineRenderer changing color without remove it

I working around with map app, I want to ask how to change the polyline color without remove and add it again, I found this topic https://stackoverflow.com/questions/24226290/mkpolylinerenderer-change-color-without-removing-overlay in stackoverflow but this is not involve with my question, I did not touch the line, so no need to do with -[MKMapViewDelegate mapView:didSelectAnnotationView:]
So is it possible to do that?
EDIT: What I want to do is change the polyline color smoothly (by shading a color - sound like an animation) If you have any idea on how to animate this polyline please also tell me too. Thanks
Complex animations or shading/gradients will probably require creating a custom overlay renderer class.
These other answers give ideas about how to draw gradient polylines and animations will most like require a custom overlay renderer as well:
how to customize MKPolyLineView to draw different style lines
Gradient Polyline with MapKit ios
Draw CAGradient within MKPolyLineView
Apple's Breadcrumb sample app also has an example of a custom renderer which you may find useful.
However, if you just want to update the line's color (say from blue to red), then you may be able to do that as follows:
Get a reference to the MKPolyline you want to change.
Get a reference to the MKPolylineRenderer for the polyline obtained in step 1. This can be done by calling the map view's rendererForOverlay: instance method (not the same as the mapView:rendererForOverlay: delegate method.
Update the renderer's strokeColor.
Call invalidatePath on the renderer.
Not sure what you want but you may be able to "animate" the color going from blue to red by changing the color and calling invalidatePath gradually in timed steps.
Another important thing is to make sure the rendererForOverlay delegate method also uses the line's "current" color in case the map view calls the delegate method after you've changed the renderer's strokeColor directly.
Otherwise, after panning or zooming the map, the polyline's color will change back to whatever's set in the delegate method.
You could keep the line's current color in a class-level variable and use that in both the delegate method and the place where you want to change the line's color.
An alternative to a class-level variable (and probably better) is to either use the MKPolyline's title property to hold its color or a custom polyline overlay class (not renderer) with a color property.
Example:
#property (nonatomic, strong) UIColor *lineColor;
//If you need to keep track of multiple overlays,
//try using a NSMutableDictionary where the keys are the
//overlay titles and the value is the UIColor.
-(void)methodWhereYouOriginallyCreateAndAddTheOverlay
{
self.lineColor = [UIColor blueColor]; //line starts as blue
MKPolyline *pl = [MKPolyline polylineWithCoordinates:coordinates count:count];
pl.title = #"test";
[mapView addOverlay:pl];
}
-(void)methodWhereYouWantToChangeLineColor
{
self.lineColor = theNewColor;
//Get reference to MKPolyline (example assumes you have ONE overlay)...
MKPolyline *pl = [mapView.overlays objectAtIndex:0];
//Get reference to polyline's renderer...
MKPolylineRenderer *pr = (MKPolylineRenderer *)[mapView rendererForOverlay:pl];
pr.strokeColor = self.lineColor;
[pr invalidatePath];
}
-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay
{
if ([overlay isKindOfClass:[MKPolyline class]]) {
MKPolylineRenderer *pr = [[MKPolylineRenderer alloc] initWithPolyline:overlay];
pr.strokeColor = self.lineColor;
pr.lineWidth = 5;
return pr;
}
return nil;
}
Yous hould look at MKOverlayPathRenderer method
- invalidatePath.
From the doc, it says:
Call this method when a change in the path information would require
you to recreate the overlay’s path. This method sets the path property
to nil and tells the overlay renderer to redisplay its contents.
So, at this moment, you should be able to change your drawing color.

"Animating" circle overlay with circle renderer on map view flickers

I need to change radius of my MKCircle continuously as user pinches on the screen. As its radius property is read-only, I am removing and recreating the circle and the renderer continuously, which, I believe, causes "flickering" effect when user pinches. It continuously appears/disappears when "animating" which looks really bad visually, creating a crappy UX. Here is my code:
//this method may be called many times a second.
-(void)refreshRadius{ //called when user pinches after updating to correct radius.
if(radiusCircle){
[self.mapView removeOverlay:radiusCircle];
}
radiusCircle = [MKCircle circleWithCenterCoordinate:userCoordinates radius:radius];
[self.mapView addOverlay:radiusCircle level:MKOverlayLevelAboveLabels];
}
-(MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay{
if(overlay == radiusCircle){
MKCircleRenderer *renderer = [[MKCircleRenderer alloc] initWithCircle:radiusCircle];
renderer.strokeColor = [UIColor colorWithWhite:1 alpha:0.5];
renderer.lineWidth = 0.8;
renderer.fillColor = [UIColor colorWithRed:0.3 green:0.36 blue:0.7 alpha:0.2];
return renderer;
}else{
return nil;
}
}
How can I "animate" the radius smoothly on scale?
After a bit experimenting, I've luckily found a solution. However, while it works perfectly now, it's prone to future internal changes in iOS SDK. I've first set the read-only property "radius" using KVC. Then, I've invalidated my renderer's path, causing it to redraw itself immediately using the new radius. It is not the most-smooth-60FPS-animation-ever but it works really nicely. Here is the code:
[radiusCircle setValue:#(radius) forKey:#"radius"];
[renderer invalidatePath];
radiusCircle is my MKCircle instance, and renderer is my circle renderer. It works.

How to remove an annotation from a map view gently?

I'm adding and removing annotations to a map view. When I remove one it disappears abruptly and looks a bit startling, I'd rather it faded away gracefully.
I tried removing it with UIView:animateWithDuration: but it wasn't an animatable attribute.
If there's no other easy solution I was thinking I could get the annotation view to fade its alpha and then remove its annotation from the map. However the problem with this is it doesn't seem like an annotation view has a reference to its map view? Adding one starts to get a bit messy.
Is there some easy quick solution to removing an annotation gracefully?
Using animateWithDuration should work fine. To fade the removal of an annotation, one can:
MKAnnotationView *view = [self.mapView viewForAnnotation:annotation];
if (view) {
[UIView animateWithDuration:0.5 delay:0.0 options:0 animations:^{
view.alpha = 0.0;
} completion:^(BOOL finished) {
[self.mapView removeAnnotation:annotation];
view.alpha = 1.0; // remember to set alpha back to 1.0 because annotation view can be reused later
}];
} else {
[self.mapView removeAnnotation:annotation];
}
I think your proposed solution is the correct one. Set up an animation to opacity = 0, then, upon completion, remove the annotation from the MKMapView. The code that starts the animation doesn't have to be in the view itself; a better location for the code may be the view controller. Consider using NSNotificationCenter to notify the view controller that an annotation is requesting fade-out-and-remove.

Adding multiple tappable shapes to UIView

I have a floor plan with many exhibitor stands. When a UIView loads, a UIImage is displayed with the floor plan and a database is checked for a list of exhibitors and their locations. The locations are loaded into an array and a UIButton is created for each exhibitor and placed over the floor plan where their stand is. When tapped, this button will show information about that exhibitor.
Here is a screenshot of the floor plan with boxes where the buttons are rendered.
This works fine as it is BUT I need the buttons to be irregular shapes (triangles, pentagons, circles etc). So I need a way of drawing these shapes and having them clickable in the same way the buttons were.
I have created a test class which generates a UIView which contains the shape and added it to my original UIView. I get the feeling this may not be the correct way to do this as I will need to have many buttons on the screen and this would mean many views stacked on each other. I don't know how I could check which shape was tapped as the UIViews would overlap each other.
Can all the shapes be drawn on one view and then the view added? What is the best approach to this?
In terms of UI the cleanest thing to do is to use actual buttons. Set their type to custom, and set their image property to the image you want to display. That way the buttons handle highlighting correctly, and manage IBActions just like regular buttons. You can set all the buttons to point to the same action, and use tag values to figure out which button is which.
You can create these buttons either from code or in IB - whichever fits your design better.
You could also do this with custom views or a single view that has drawing on it. IF you use views for each booth, you would need to attach a tap gesture recognizer to each view, and set it's userInteractionEnabled flag to YES.
If you want to use a drawing for the entire floor plan, you would need to add a tap gesture recognizer to the drawing view, and then interpret the coordinates of the tap to figure out which image it lands on.
override
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
method of your map representer view. So you will be able to compare current touch location with all of your shapes, and calculate in which shape your touch point is laying. There different approach for different shapes (e.g is point inside a rectangle?)
ole has a great project: OBShapedButtons. He achieves it by checking the alpha value of the touched pixel and overwriting -pointInside:withEvent:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
// Return NO if even super returns NO (i.e., if point lies outside our bounds)
BOOL superResult = [super pointInside:point withEvent:event];
if (!superResult) {
return superResult;
}
// Don't check again if we just queried the same point
// (because pointInside:withEvent: gets often called multiple times)
if (CGPointEqualToPoint(point, self.previousTouchPoint)) {
return self.previousTouchHitTestResponse;
} else {
self.previousTouchPoint = point;
}
BOOL response = NO;
if (self.buttonImage == nil && self.buttonBackground == nil) {
response = YES;
}
else if (self.buttonImage != nil && self.buttonBackground == nil) {
response = [self isAlphaVisibleAtPoint:point forImage:self.buttonImage];
}
else if (self.buttonImage == nil && self.buttonBackground != nil) {
response = [self isAlphaVisibleAtPoint:point forImage:self.buttonBackground];
}
else {
if ([self isAlphaVisibleAtPoint:point forImage:self.buttonImage]) {
response = YES;
} else {
response = [self isAlphaVisibleAtPoint:point forImage:self.buttonBackground];
}
}
self.previousTouchHitTestResponse = response;
return response;
}
Another sample code that test if the point is within a layer mask, maybe you can adapt that more easily:
#implementation MyView
//
// ...
//
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint p = [self convertPoint:point toView:[self superview]];
if(self.layer.mask){
if (CGPathContainsPoint([(CAShapeLayer *)self.layer.mask path], NULL, p, YES) )
return YES;
}else {
if(CGRectContainsPoint(self.layer.frame, p))
return YES;
}
return NO;
}
#end
The full article: http://blog.vikingosegundo.de/2013/10/01/hittesting-done-right/
In the end this is what I did:
Looped through all of the stand shapes I needed to have and created a dictionary with UIBezierpath of their coordinates on the floorplan image and the standNo for the shape.
I then added these dictionaries to an array.
When the screen was tapped I would note the X and Y of the tap position and looped through the array of dictionaries and it checked if the UIBezierpath contained a point made up of the X and Y tapped coordinates.
If a shape was found that had the X and Y coordinates within its bounds I would draw a CAShapelayer using the UIBezierpath and adding a fillColor so it showed up on the map. An alertView was then displayed which showed more information about the exhibitor.
This method seemed WAY more efficient than actually drawing out hundreds of UIButtons or even CAShaplayers and even with 300+ areas on the floorplan to check through the process appears instant.

Smooth resizing of MKCircle

How can I achieve a smooth resizing of a MKCircleView on a UIMapView when adjusting an NSSlider? Apple has managed to do it in the Find my friends app when creating geofences (http://reviewznow.com/wp-content/uploads/2013/03/find-my-friends-location-alerts-01.jpg), so I guess it's possible in some way. So far I've tried the following solutions, but with a very "flickery" result:
First attempt
I added a new MKCircleView with an updated radius and immediately after removing the one that was (as suggested here MKOverlay not resizing smoothly) when the slider changed value. I also tried the other way around: first removing the overlay then adding a new one, but with the same "flickery" result.
- (void)sliderChanged:(UISlider*)sender
{
double radius = (sender.value * 100);
[self addCircleWithRadius:radius];
[mapView removeOverlays:[self.mapView.overlays firstObject]];
}
Second attempt
In the linked SO answer, he suggests that NSOperation could be used to "help you create the MKCircle objects faster" and thus making the resizing smoother using the above method of adding/removing overlays when the slides changes value. I did a implementation where I start a new thread whenever the slider changes. In each thread I remove all old overlays and add a new one with the updated scale. Perhaps he has some other kind of implementation in mind, because the way I did it I still get the same flicker when changing the slider.
- (void)sliderChanged:(UISlider*)sender
{
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(updateOverlayWithScale:)
object:[NSNumber numberWithFloat:sender.scale]];
[self.queue addOperation:operation];
}
The method that runs i each thread:
- (void)updateOverlayWithScale:(NSNumber *)scale
{
MKCircle *circle = [MKCircle circleWithCenterCoordinate:self.currentMapPin.coordinate
radius:100*[scale floatValue]];
[self.mapView performSelectorOnMainThread:#selector(removeOverlays:) withObject:self.mapView.overlays waitUntilDone:NO];
[self.mapView performSelectorOnMainThread:#selector(addOverlay:) withObject:circle waitUntilDone:NO];
}
Third attempt
I also tried implementing my own subclass of MKOverlayView that draws itself based on its scale property. Whenever the slider changes I call setNeedsDisplay and let it redraw itself, but I get the same flicker.
- (void)sliderChanged:(UISlider*)sender
{
self.currentOverlayView.scale = sender.scale
[self.currentOverlayView setNeedsDisplay];
}
And in my custom overlay view I implement drawMapRect:zoomScale:inContext:(CGContextRef)context like this
- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)context
{
double radius = [(MKCircle *)[self overlay] radius];
radius *= self.scale;
// ... Create a rect using the updated radius and draw a circle inside it using CGContextAddEllipseInRect ...
}
So, do you have any ideas? Thanks in advance!
A few months ago I stumbled upon this animated MKCircleView. It also has a demo on git. So I guess you should give it a go!
Check it out as you might be able to tweak it to your needs with a slider etc.
Credits go to yickhong for providing this YHAnimatedCircleView

Resources