How do I determine the origin of a CGPath? - core-graphics

I'm drawing text within a CGPath, in order to do hit-testing on the text, I'm using CTFrameGetLineOrigins. Here's what the documentation says:
Each CGPoint is the origin of the corresponding line in the array of lines returned by CTFrameGetLines, relative to the origin of the frame's path.
How would I go about finding the origin of the frame's path? Examples I've found all save the origin of the path when the path is initially created. I have two problems with this:
The creation of my path is quite distant from the place where I'm doing the hit testing. I would need to make sure that I'm passing around a CGPoint in addition to the CGPath. Ugly, but not insurmountable.
What is the origin for a shape that isn't rectangular? What is the origin of a circular CGPath?

You can use CGPathApply to examine the full path:
typedef struct {
CGPoint origin;
BOOL found;
} MySearchData;
void MyApplierFunction (void* info, const CGPathElement* element) {
MySearchData* searchData = (MySearchData *) info;
if (! searchData->found) {
searchData->origin = element->points[0];
searchData->found = YES;
}
}
CGPoint GetPathOrigin (CGPathRef path) {
MySearchData searchData = { CGPointZero, NO };
CGPathApply(path, (void *) &searchData, &MyApplierFunction);
return searchData.origin;
}

Related

Obj-C and SpriteKit - Changing a sprite value that is created randomly

I'm making a game using SpriteKit and Objective-C.
I have four different texture drops (Blue, Green, Orange and Red) that falls down on screen randomly.
In my ANBDropNode class I have this method:
+(instancetype)dropOfType:(ANBDropType)type {
ANBDropsNode *drop;
if (type == ANBDropTypeBlue) {
drop = [self spriteNodeWithImageNamed:#"bluedrop"];
} else if (type == ANBDropTypeGreen) {
drop = [self spriteNodeWithImageNamed:#"greendrop"];
} else if (type == ANBDropTypeOrange) {
drop = [self spriteNodeWithImageNamed:#"orangedrop"];
} else if (type == ANBDropTypeRed){
drop = [self spriteNodeWithImageNamed:#"reddrop"];
}
[drop setupPhysicsBody];
return drop;
}
And in my GamePlayScene these two:
-(void)addDrops {
NSUInteger randomDrop = [ANBUtil randomWithMin:0 max:4];
self.drop = [ANBDropsNode dropOfType:randomDrop];
float y = self.frame.size.height + self.drop.size.height;
float x = [ANBUtil randomWithMin:10 + self.drop.size.width
max:self.frame.size.width - self.drop.size.width - 10];
self.drop.position = CGPointMake(x, y);
[self addChild:self.drop];
}
-(void)update:(NSTimeInterval)currentTime {
if (self.lastUpdateTimeInterval) {
self.timeSinceDropAdded += currentTime - self.lastUpdateTimeInterval;
}
if (self.timeSinceDropAdded > 1) {
[self addDrops];
self.timeSinceDropAdded = 0;
}
self.lastUpdateTimeInterval = currentTime;
}
The question is (and it may sound a little dumber, I know): before the drop hits the ground it has already changed it value. If ANBDropNode *drop is a bluedrop, before it hits the ground the method randomly create another drop and change it value for greendrop, for example. But I don't want this behavior. I want the drop to continue with its value until it reaches the ground so I can detect its color in my didBeginContact method.
Sorry in advance for any english mistakes, as I'm not an native english speaker.
From your question I understand that the reason you are keeping a reference to the drop (self.drop) is to check what is its colour when it hits the ground.
So you could just delete that, and create a new SKSpriteNode object each time, instead of just changing the reference of the current property.
If you have any other reason for keeping a reference to that drop, just still keep a reference.
Note that doing any of the above, will not affect the code below.
I think that you were in the right direction (when asking about didBeginContact) but took the wrong approach/mindset, since there is no need to keep a reference when using didBeginContact, because you can get the nodes in contact from this method.
Anyway, here is the code + explanations
// MyScene.m
// First, conform to SKPhysicsContactDelegate, so you can get didBeginContact 'calls'
#interface MyScene () <SKPhysicsContactDelegate>
#end
#implementation MyScene
// Now add the following constant, that you'll use as the physics body category bit masks
static const uint32_t groundCategory = 0x01 << 0;
static const uint32_t dropsCategory = 0x01 << 1;
// Somewhere in your initialisation, set yourself to be the physicsWorld
// contact delegate, so you'll receive the didBeginContact 'calls',
// And also call setupGround method, that we will create in here as well
-(id)initSceneWithSize:(CGSize)size {
...
...
self.physicsWorld.contactDelegate = self;
[self setupGround];
...
}
// Here we will create the ground node, so we can detect when a drop
// Hits the ground.
// The reason that, in the below code, I am setting just the ground,
// and not the whole borders of the screen, is because the drops
// are added above the screen 'borders', and if we would make
// a 'frame' node, and not a 'ground' node, we will also receive
// contact delegate calls, when the nodes first enter the scene
// and hits the top 'border' of the frame
-(void)setupGround {
SKNode *groundNode = [SKNode node];
groundNode.strokeColor = [SKColor clearColor];
groundNode.fillColor = [SKColor clearColor];
// Not sure if the above two are mandatory, but better be safe than sorry...
// Here we set the physics body to start at the bottom left edge of the screen
// and be the width of the screen, and the size of 1 points
// Then, we also set its category bit mask
CGRect groundRect = CGRectMake(0, 0, self.size.width, 1);
groundNode.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:groundRect];
groundNode.physicsBody.categoryBitMask = groundCategory;
[self addChild:groundNode];
}
// Next we will modify your current method of creating drops, to also have
// their name property to holds the corresponding colour name
+(instancetype)dropOfType:(ANBDropType)type {
ANBDropsNode *drop;
if (type == ANBDropTypeBlue) {
drop = [self spriteNodeWithImageNamed:#"bluedrop"];
drop.name = #"Blue";
} else if (type == ANBDropTypeGreen) {
drop = [self spriteNodeWithImageNamed:#"greendrop"];
drop.name = #"Green";
} else if (type == ANBDropTypeOrange) {
drop = [self spriteNodeWithImageNamed:#"orangedrop"];
drop.name = #"Orange";
} else if (type == ANBDropTypeRed){
drop = [self spriteNodeWithImageNamed:#"reddrop"];
drop.name = #"Red";
}
[drop setupPhysicsBody];
return drop;
}
// In your setupPhysicsBody method of the drop, add the following to define
// the drop's bit mask, contact test, and collision.
// Make sure you are adding them AFTER setting the physics body, and not before.
// Since you revealed no code regarding this method, I will assume 'self' is
// referring to the drop, since you call this method on the drop.
-(void) setupPhysicsBody {
...
...
...
self.physicsBody.categoryBitMask = dropsCategory;
self.physicsBody.contactTestBitMask = groundCategory;
self.physicsBody.collisionBitMask = 0;
// The above code sets the drop category bit mask, sets its contactTestBitMask
// to be of the groundCategory, so whenever an object with drop category, will
// 'touch' and object with groundCategory, our didBeginContact delegate will
// get called.
// Also, we've set the collision bit mask to be 0, since we only want to
// be notified when a contact begins, but we don't actually want them both to
// 'collide', and therefore, have the drops 'lying' on the ground.
...
...
...
}
// Now we can set the didBeginContact: delegate method.
// Note that, as the name of the method suggests, this method gets called when a
// Contact is began, meaning, the drop is still visible on screen.
// If you would like to do whatever you want to do, when the drop leaves the screen,
// just call the didEndContact: delegate method
-(void)didBeginContact:(SKPhysicsContact *)contact {
// SKPhysicsContact have two properties, bodyA and bodyB, which represents
// the two nodes that contacted each other.
// Since there is no certain way to know which body will always be our drop,
// We will check the bodies category bit masks, to determine which is which
ANBDropsNode *drop = (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) ? (ANBDropsNode *)contact.bodyB.node : (ANBDropsNode *)contact.bodyA.node;
// In the above we are checking the category bit masks,
// Note that we set groundCategory to be 1, and dropsCategory to be 2,
// So we are checking which is higher.
// If bodyA bit mask is lower than bodyB bit mask, that means the bodyA is
// the ground, and bodyB is the drop, so we set the drop to be bodyB's node
// Else, we set it to be bodyA's node.
// Now you can easily get the drop colour from the name property we've set in
// the beginning. you can do some sort of if-else statement, that check
// 'if isEqualToString'. Here I just NSLog the colour
NSLog(#"%#", drop.name);
}
Good luck mate.
You can simply associate a property with the ANBDropsNode class which can be set when the drop is instantiated.
In the ANBDropsNode.h file,
#interface ANBDropsNode
#property (strong, nonatomic) NSString *dropColor; //This property will hold the value associated with the color.
Then in the dropOfType class method:
+(instancetype)dropOfType:(ANBDropType)type {
NSString *strDropColor;
if (type == ANBDropTypeBlue) {
strDropColor = #"bluedrop";
} else if (type == ANBDropTypeGreen) {
strDropColor = #"greendrop";
} else if (type == ANBDropTypeOrange) {
strDropColor = #"orangedrop";
} else if (type == ANBDropTypeRed){
strDropColor = #"reddrop";
}
ANBDropsNode *drop = [self spriteNodeWithImageNamed:strDropColor];
drop.dropColor = strDropColor;
[drop setupPhysicsBody];
return drop;
}
Now, in your collision detection delegate method, you can find out the color of the node by simply referring the dropColor property.

How to get pixel coordinates when CTRunDelegate callbacks are called

I have dynamic text drawn into a custom UIImageView. Text can contain combinations of characters like :-) or ;-), which I'd like to replace with PNG images.
I apologize for bunch of codes below.
Code that creates CTRunDelegate follows:
CTRunDelegateCallbacks callbacks;
callbacks.version = kCTRunDelegateVersion1;
callbacks.dealloc = emoticonDeallocationCallback;
callbacks.getAscent = emoticonGetAscentCallback;
callbacks.getDescent = emoticonGetDescentCallback;
callbacks.getWidth = emoticonGetWidthCallback;
// Functions: emoticonDeallocationCallback, emoticonGetAscentCallback, emoticonGetDescentCallback, emoticonGetWidthCallback are properly defined callback functions
CTRunDelegateRef ctrun_delegate = CTRunDelegateCreate(&callbacks, self);
// self is what delegate will be using as void*refCon parameter
Code for creating attributed string is:
NSMutableAttributedString* attString = [[NSMutableAttributedString alloc] initWithString:self.data attributes:attrs];
// self.data is string containing text
// attrs is just setting for font type and color
I've then added CTRunDelegate to this string:
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attString, range, kCTRunDelegateAttributeName, ctrun_delegate);
// where range is for one single emoticon location in text (eg. location=5, length = 2)
// ctrun_delegate is previously created delegate for certain type of emoticon
Callback functions are defined like:
void emoticonDeallocationCallback(void*refCon)
{
// dealloc code goes here
}
CGFloat emoticonGetAscentCallback(void * refCon)
{
return 10.0;
}
CGFloat emoticonGetDescentCallback(void * refCon)
{
return 4.0;
}
CGFloat emoticonGetWidthCallback(void * refCon)
{
return 30.0;
}
Now all this works fine - I get callback functions called, and I can see that width, ascent and descent affect how text before and after detected "emoticon char combo" is drawn.
Now I'd like to draw an image at the spot where this "hole" is made, however I can't find any documentation that can guide me how do I get pixel (or some other) coordinates in each callback.
Can anyone guide me how to read these?
Thanks in advance!
P.S.
As far as I've seen, callbacks are called when CTFramesetterCreateWithAttributedString is called. So basically there's no drawing going on yet. I couldn't find any example showing how to match emoticon location to a place in drawn text. Can it be done?
I've found a solution!
To recap: issue is to draw text using CoreText into UIImageView, and this text, aside from obvious font type and color formatting, needs to have parts of the text replaced with small images, inserted where replaced sub-text was (eg. :-) will become a smiley face).
Here's how:
1) Search provided string for all supported emoticons (eg. search for :-) substring)
NSRange found = [self.rawtext rangeOfString:emoticonString options:NSCaseInsensitiveSearch range:searchRange];
If occurrence found, store it in CFRange:
CFRange cf_found = CFRangeMake(found.location, found.length);
If you're searching for multiple different emoticons (eg. :) :-) ;-) ;) etc.), sort all found occurrences in ascending order of it's location.
2) Replace all emoticon substrings (eg. :-)) you will want to replace with an image, with an empty space. After this, you must also update found locations to match these new spaces. It's not as complicated as it sounds.
3) Use CTRunDelegateCreate for each emoticon to add callback to newly created string (the one that does not have :-) but [SPACE] instead).
4) Callback functions should obviously return correct emoticon width based on image size you will use.
5) As soon as you will execute CTFramesetterCreateWithAttributedString, these callbacks will be executed as well, giving framesetter data which will be later used in creating glyphs for drawing in given frame path.
6) Now comes the part I missed: once you create frame for framesetter using CTFramesetterCreateFrame, cycle through all found emoticons and do following:
Get num of lines from frame and get origin of the first line:
CFArrayRef lines = CTFrameGetLines(frame);
int linenum = CFArrayGetCount(lines);
CGPoint origins[linenum];
CTFrameGetLineOrigins(frame, CFRangeMake(0, linenum), origins);
Cycle through all lines, for each emoticon, looking for glyph that contains it (based on the range.location for each emoticon, and number of characters in each glyph):
(Inspiration came from here: CTRunGetImageBounds returning inaccurate results)
int eloc = emoticon.range.location; // emoticon's location in text
for( int i = 0; i<linenum; i++ )
{
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
CFArrayRef gruns = CTLineGetGlyphRuns(line);
int grunnum = CFArrayGetCount(gruns);
for( int j = 0; j<grunnum; j++ )
{
CTRunRef grun = (CTRunRef) CFArrayGetValueAtIndex(gruns, j);
int glyphnum = CTRunGetGlyphCount(grun);
if( eloc > glyphnum )
{
eloc -= glyphnum;
}
else
{
CFRange runRange = CTRunGetStringRange(grun);
CGRect runBounds;
CGFloat ascent,descent;
runBounds.size.width = CTRunGetTypographicBounds(grun, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, runRange.location, NULL);
runBounds.origin.x = origins[i].x + xOffset;
runBounds.origin.y = origins[i].y;
runBounds.origin.y -= descent;
emoticon.location = CGPointMake(runBounds.origin.x + runBounds.size.width, runBounds.origin.y);
emoticon.size = CGPointMake([emoticon EmoticonWidth] ,runBounds.size.height);
break;
}
}
}
Please do not take this code as copy-paste-and-will-work as I had to strip lots of other stuff - so this is just to explain what I did, not for you to use it as is.
7) Finally I can create context and draw both text and emoticons at correct place:
if(currentContext)
{
CGContextSaveGState(currentContext);
{
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
CTFrameDraw(frame, currentContext);
}
CGContextRestoreGState(currentContext);
if( foundEmoticons != nil )
{
for( FoundEmoticon *emoticon in foundEmoticons )
{
[emoticon DrawInContext:currentContext];
}
}
}
And function that draws emoticon (I just made it to draw it's border and pivot point):
-(void) DrawInContext:(CGContext*)currentContext
{
CGFloat R = round(10.0 * [self randomFloat] ) * 0.1;
CGFloat G = round(10.0 * [self randomFloat] ) * 0.1;
CGFloat B = round(10.0 * [self randomFloat] ) * 0.1;
CGContextSetRGBStrokeColor(currentContext,R,G,B,1.0);
CGFloat pivotSize = 8.0;
CGContextBeginPath(currentContext);
CGContextMoveToPoint(currentContext, self.location.x, self.location.y - pivotSize);
CGContextAddLineToPoint(currentContext, self.location.x, self.location.y + pivotSize);
CGContextMoveToPoint(currentContext, self.location.x - pivotSize, self.location.y);
CGContextAddLineToPoint(currentContext, self.location.x + pivotSize, self.location.y);
CGContextDrawPath(currentContext, kCGPathStroke);
CGContextBeginPath(currentContext);
CGContextMoveToPoint(currentContext, self.location.x, self.location.y);
CGContextAddLineToPoint(currentContext, self.location.x + self.size.x, self.location.y);
CGContextAddLineToPoint(currentContext, self.location.x + self.size.x, self.location.y + self.size.y);
CGContextAddLineToPoint(currentContext, self.location.x, self.location.y + self.size.y);
CGContextAddLineToPoint(currentContext, self.location.x, self.location.y);
CGContextDrawPath(currentContext, kCGPathStroke);
}
Resulting image: http://i57.tinypic.com/rigis5.png
:-)))
P.S.
Here is result image with multiple lines: http://i61.tinypic.com/2pyce83.png
P.P.S.
Here is result image with multiple lines and with PNG image for emoticon:
http://i61.tinypic.com/23ixr1y.png
Are you drawing the text in a UITextView object? If so, then you can ask it's layout manager where the emoticon is drawn, specifically the -[NSLayoutManager boundingRectForGlyphRange:inTextContainer: method (also grab the text container of the text view).
Note that it expects the glyph range, not a character range. Multiple characters can make up a single glyph, so you will need to convert between them. Again, NSLayoutManager has methods to convert between character ranges and glyph ranges.
Alternatively, if you're not drawing inside a text view, you should create your own layout manager and text container, so you can do the same.
A text container describes a region on the screen where text will be drawn, typically it's a rectangle but it can be any shape:
A layout manager figures out how to fit the text within whatever shape the text container describes.
Which brings me to the other approach you could take. You can modify the text container object, adding a blank space where no text can be rendered, and put a UIImageView inside that blank space. Use the layout manager to figure out where the blank spaces should be.
Under iOS 7 and later, you can do this by adding "exclusion paths" to the text container, which is just an array of paths (rectangles probably) where each image is. For earlier versions of iOS you need to subclass NSTextContainer and override lineFragmentRectForProposedRect:atIndex:writingDirection:remainingRect:.

How do I check if a CGPoint has been initialised?

I would like to initially set a CGPoint property to a particular point (middle of screen). Other methods may subsequently wish to change this property. My thoughts were to initialise it if empty in the getter, but I get the message invalid argument type 'struct CGPoint' to unary expression. I also tried using if property == nil or 0 but no joy.
Any thoughts?
-(CGPoint)graphOrigin
{
// initialise to centre of screen if has not been set
if(!_graphOrigin) // this expression is causing the problem
{
CGPoint origin = CGPointMake(self.bounds.origin.x + self.bounds.size.width / 2, self.bounds.origin.y + self.bounds.size.height / 2);
_graphOrigin = origin;
}
return _graphOrigin;
}
A CGPoint is a struct, so you can't set it to nil or NULL (it's not a pointer). In a sense, there's really no "uninitialized" state. Perhaps you could use {0.0, 0.0} to designate an unset CGPoint, but that's also a valid coordinate. Or you could use negative x and y values to flag an "uninitialized" point, since negative values can't be valid drawing points, but that's a bit of a hack, too.
Probably your best bet is to do one of two things:
Store the property as a pointer to a CGPoint. This value can be set to NULL when uninitialized. Of course, you have to worry about mallocing and freeing the value.
Store the CGPoint alongside a BOOL called pointInitialized or somesuch, initially set to NO, but set to YES once the point has been initialized. You can even wrap that up in a struct:
struct {
CGPoint point;
BOOL initialized;
} pointData;
An easier way would be to initialize _graphOrigin to CGRectZero and change your if statement for this:
if (!CGPointEqualToPoint(_graphOrigin, CGPointZero)) {
}
CGPoint does not have an uninitialized state. However, if you consider the point (0, 0) as uninitialized, you could use
if (_graphOrigin.x == 0 && _graphOrigin.y == 0)
{
...
This works because when an Objective-C instance is initialized, all its ivar are cleared to bits of zero, which in the CGFloat representation is 0.0.
(Note: The == is fine here even if the operands are CGFloat because we want to compare with the an exact bit pattern (ignoring the issue of -0))
Since CGPointZero (0,0) and any other value you give a point may exist in your context
you may want to initialize an NSValue with your point using:
NSValue *pointVal = [NSValue valueWithCGPoint:point];
You could do this based on some condition and then later test the NSValue for nil.
NSValue can also be added to an array which would allow you to have an array of points should you need.
To get the point later simply use:
CGPoint point = [pointVal CGPointValue];
static CGPoint kInvalidPoint = {.x = NSIntegerMax, .y = NSIntegerMax};
#implementation MyClass
- init()
{
self = [super init];
if (self) {
_oldPoint = kInvalidPoint;
}
return self;
}
- (void)foo
{
if (CGPointEqualToPoint(self.oldPoint, kInvalidPoint)) {
// Invalid point.
return;
}
}
#end
Create two CGPoint properties, that way they are both "uninitialized". Set one of them and use the second one to check whether or not they are equal.
#interface ClassName ()
#property (nonatomic) CGPoint point1;
#property (nonatomic) CGPoint point2;
#end
#implementation ClassName
self.point1 = CGPointMake(69.0f, 180.0f); //arbitrary numbers
//if not equal, then if statement proceeds
if (!CGPointEqualToPoint(self.point1, self.point2) {
//your code here
}
#end
Idk if you'd consider this way hackish though. And I know your question was already answered, but I had kinda the same dilemma till I thought of this.

Why do I get extra location coordinate points when my iPhone app first launches?

I am working an iPhone app which is using CLLocationManager. When a user goes for a run, it shows the run path on a mapView. I am drawing the running path on mapView using following code:
double leastDistanceToRecord = 0.0000905;
- (void) locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
if (newLocation.horizontalAccuracy >= 0) {
if (!runoPath)
{
NSLog(#"in !runoPath if");
// This is the first time we're getting a location update, so create
// the RunoPath and add it to the map.
runoPath = [[RunoPath alloc] initWithCenterCoordinate:newLocation.coordinate];
[map addOverlay:runoPath];
self.currentRunData = [[RunData alloc] init];
[currentRunData startPointLocation:newLocation];
// On the first location update, zoom map to user location
MKCoordinateRegion region =
MKCoordinateRegionMakeWithDistance(newLocation.coordinate, 1000, 1000);
[map setRegion:region animated: NO];
}
else
{
// This is a subsequent location update.
// If the runoPath MKOverlay model object determines that the current location has moved
// far enough from the previous location, use the returned updateRect to redraw just
// the changed area.
double latitudeChange = fabs(newLocation.coordinate.latitude - oldLocation.coordinate.latitude);
double longitudeChange = fabs(newLocation.coordinate.latitude - oldLocation.coordinate.longitude);
if (latitudeChange > leastDistanceToRecord || longitudeChange > leastDistanceToRecord) {
MKMapRect updateRect = [runoPath addCoordinate:newLocation.coordinate];
if (!MKMapRectIsNull(updateRect))
{
// There is a non null update rect.
// Compute the currently visible map zoom scale
MKZoomScale currentZoomScale = map.bounds.size.width / map.visibleMapRect.size.width;
// Find out the line width at this zoom scale and outset the updateRect by that amount
CGFloat lineWidth = MKRoadWidthAtZoomScale(currentZoomScale);
updateRect = MKMapRectInset(updateRect, -lineWidth, -lineWidth);
// Ask the overlay view to update just the changed area.
[runoPathView setNeedsDisplayInMapRect:updateRect];
}
// [currentRunData updateLocation:oldLocation toNewLocation: newLocation];
}
[currentRunData updateLocation:oldLocation toNewLocation: newLocation];
// }
}
}
}
The problem is that when I start a run, I get some extra points and then because of those points I get an extraneous line on mapView that does not reflect the actual run. It even happens when I install the app on my iPhone and run it for the first time. I don't know why it's adding those extra points. Can anyone help me with that? Thanks in advance.
The first location you get is usually a cached location and is old. You can check the age of the location and if it is old (>60 seconds or whatever) then ignore that location update. See this answer here.
--EDIT-- If you are still having problems, put this code in didUpdateToLocation: and show us the actual output from NSLog (you can edit your question and add the output):
NSTimeInterval age = -[newLocation.timestamp timeIntervalSinceNow];
NSLog(#"age: %0.3f sec, lat=%0.2f, lon=%0.2f, hAcc=%1.0f",
age, newLocation.coordinate.latitude, newLocation.coordinate.longitude,
newLocation.horizontalAccuracy);

How to delete all Annotations on a MKMapView

Is there a simple way to delete all the annotations on a map without iterating through all the displayed annotations in Objective-c?
Yes, here is how
[mapView removeAnnotations:mapView.annotations]
However the previous line of code will remove all map annotations "PINS" from
the map, including the user location pin "Blue Pin". To remove all map
annotations and keep the user location pin on the map, there are two
possible ways to do that
Example 1, retain the user location annotation, remove all pins, add
the user location pin back, but there is a flaw with this approach, it
will cause the user location pin to blink on the map, due to removing
the pin then adding it back
- (void)removeAllPinsButUserLocation1
{
id userLocation = [mapView userLocation];
[mapView removeAnnotations:[mapView annotations]];
if ( userLocation != nil ) {
[mapView addAnnotation:userLocation]; // will cause user location pin to blink
}
}
Example 2, I personally prefer to avoid removing the location user pin
in the first place,
- (void)removeAllPinsButUserLocation2
{
id userLocation = [mapView userLocation];
NSMutableArray *pins = [[NSMutableArray alloc] initWithArray:[mapView annotations]];
if ( userLocation != nil ) {
[pins removeObject:userLocation]; // avoid removing user location off the map
}
[mapView removeAnnotations:pins];
[pins release];
pins = nil;
}
Here is the simplest way to do that:
-(void)removeAllAnnotations
{
//Get the current user location annotation.
id userAnnotation=mapView.userLocation;
//Remove all added annotations
[mapView removeAnnotations:mapView.annotations];
// Add the current user location annotation again.
if(userAnnotation!=nil)
[mapView addAnnotation:userAnnotation];
}
Here's how to remove all annotations except the user location, written out explicitly because I imagine I will come looking for this answer again:
NSMutableArray *locs = [[NSMutableArray alloc] init];
for (id <MKAnnotation> annot in [mapView annotations])
{
if ( [annot isKindOfClass:[ MKUserLocation class]] ) {
}
else {
[locs addObject:annot];
}
}
[mapView removeAnnotations:locs];
[locs release];
locs = nil;
This is very similar to Sandip's answer, except that it doesn't re-add the user location so the blue dot doesn't blink on and off again.
-(void)removeAllAnnotations
{
id userAnnotation = self.mapView.userLocation;
NSMutableArray *annotations = [NSMutableArray arrayWithArray:self.mapView.annotations];
[annotations removeObject:userAnnotation];
[self.mapView removeAnnotations:annotations];
}
You do not need to save any reference to user location. All that is needed is:
[mapView removeAnnotations:mapView.annotations];
And as long as you have mapView.showsUserLocation set to YES, you will still have user location on the map. Settings this property to YES basically asks the map view to start updating and fetching user location, to to show it on the map. From the MKMapView.h comments:
// Set to YES to add the user location annotation to the map and start updating its location
Swift version:
func removeAllAnnotations() {
let annotations = mapView.annotations.filter {
$0 !== self.mapView.userLocation
}
mapView.removeAnnotations(annotations)
}
Swift 2.0
Simple and the best:
mapView.removeAnnotations(mapView.annotations)
Swift 3
if let annotations = self.mapView.annotations {
self.mapView.removeAnnotations(annotations)
}
To remove one type of subclass you can do
mapView.removeAnnotations(mapView.annotations.filter({$0 is PlacesAnnotation}))
where PlacesAnnotation is a subclass of MKAnnotation
Here is the function to remove all markers as well as all routes (if any) from MKMapView:
func removeAppleMapOverlays() {
let overlays = self.appleMapView.overlays
self.appleMapView.removeOverlays(overlays)
let annotations = self.appleMapView.annotations.filter {
$0 !== self.appleMapView.userLocation
}
self.appleMapView.removeAnnotations(annotations)
}
Cheers