I'm plotting over 500 points on a map using mapkit. Zooming is a little jittery compared to the native google map app. I've discovered what is causing the slowness. I'm adding custom annotations so that I can later add different pin colors and buttons for detail views:
- (MKAnnotationView *) mapView:(MKMapView *)mapView viewForAnnotation:(AddressNote *) annotation {
MKPinAnnotationView *annView=[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"currentlocation"];
annView.pinColor = MKPinAnnotationColorGreen;
annView.animatesDrop=TRUE;
annView.canShowCallout = YES;
annView.calloutOffset = CGPointMake(-5, 5);
return annView;
}
If I comment out the above code, everything works fine. Very smooth with zooming in/out. Should I be adding annotations differently to boost performance?
500 annotations is probably too many, but not so many that the performance suffers when you are only viewing some of them. However, you should be using dequeueing with your annotations to improve performance
- (MKAnnotationView *)mapView:(MKMapView *)mapView
viewForAnnotation:(id <MKAnnotation>)annotation
{
MKPinAnnotationView *view = nil;
if (annotation != mapView.userLocation) {
view = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:#"identifier"];
if (nil == view) {
view = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"identifier"] autorelease];
}
[view setPinColor:MKPinAnnotationColorGreen];
[view setCanShowCallout:YES];
[view setAnimatesDrop:NO];
}
else {
// Do something with the user location view
}
return view;
}
Also, with 500 pins, the drop animation takes a long time to finish. You should turn that off with [view setAnimatesDrop:NO]
I wrote an iPhone app for a client and we included a store locator for one of their store brands. It includes 501 locations and while the animation can be a bit jerky when you are zoomed out to view the entire United States, it's perfectly fine zoomed in to the state level, where only a handful of pins are visible.
The keys:
Zoom the user into their current location before you add your annotations.
Reuse annotation views using dequeueReusableAnnotationViewWithIdentifier:.
To achieve the first point, you need to turn on location updates and set a flag when you receive the first one. Animate the map to that location using a region span that makes sense for your app, then in mapView:regionDidChangeAnimated:, check to see if you still need to add your annotations and that the current location has been updated before you call addAnnotation:.
If you can't or don't want to zoom in to the user's location, can you filter the annotations shown at the highest level and only add additional annotations as the user zooms in?
By the way, I believe you have a memory leak in your code as posted above. Even if you don't want to use the dequeuing mechanism, the view you return from mapView:viewForAnnotation: should be autoreleased.
I think St3fan is correct. In theory you can just keep adding annotations to the map and let the view handle display of them, but the reality is that it's better if you keep the count down a bit, 500 on screen at once would be way too many to see anyway.
However there is one more step to try - make sure all of the annotation views have opaque set to YES like so:
annView.opaque = YES;
It sounds to me that adding 500 map points to a section of a map the size of the iPhone screen makes no sense. There is no way you can visualize that or even click on the right annotation.
Can't you look at the zoom level and center of the map and then decide to only add a subset of annotations to the map?
I think I've seen other apps do this too.
If you are not removing annotations not seen by user from view, that is one thing to do by means of MKMapViewDelegate.
If performance is degrading when user zoom-out to country level you may want to present aggregated info on zoom levels > some const value, e.g. instead of 30 annotations within 10 square miles show single annotation like [30 something].
Related
I have a UICollectionView which shows images retrieved from the web. They are downloaded asynchronous.
When user scrolls fast, they see placeholders until the cell loads. It seems UICollectionView only loads what is visible.
Is there a way to say "collection view, load 20 cells more above and below" so chance is higher that it loaded more cells while user was looking at content without scrolling?
The idea is to have the VC recognize when a remote load might be required and start it. The only tricky part is keeping the right state so you don't trigger too much.
Let's say your collection is vertical, the condition you want to know about is when:
BOOL topLoad = scrollView.contentOffset.y < M * scrollView.bounds.size.height
or when
BOOL bottomLoad = scrollView.contentOffset.y > scrollView.contentSize.height - M * scrollView.bounds.size.height
in other words, when we are M "pages" from the edge of the content. In practice though, this condition will be over-triggered, like when you're first loading, or if you're testing it on scrollViewDidScroll, you don't want to generate web requests for every pixel of user scrolling.
Getting it right, therefore, requires additional state in the view controller. The vc can have a pair of BOOLs, like topLoadEnabled, bottomLoadEnabled, that are NO until the view is ready. Then, scroll delegate code looks like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// compute topLoad and bottomLoad conditions
if (topLoad && self.topLoadEnabled) [self startTopLoad];
similarly for bottom. The load code looks like this, abstractly:
self.topLoadEnabled = NO; // don't trigger more loading until we're done
[self.model getMoreTopStuff:^(NSArray *newStuff, NSError *error) {
// do view inserts, e.g. tableView.beginUpdates
self.topLoadEnabled = YES;
}];
Same idea for bottom. We expect the model to fetch for itself (maybe the model has image urls) and cache the result (then the model has images). As the datasource for the view, the view controller gets called upon to configure view cells. I can just naively ask the model for images. The model should answer either fetched images or placeholders.
Hope that makes sense.
In my opinion you are making the wrong assumption: cells are just views so you shouldn't treat them as model objects. UICollectionView and UITableView are very efficient because they constantly recycle cells so you should think in therms of pre loading content in the business side of things. Create interactor or viewmodel objects and populate your data source with those, then you'll be able to ask those objects to preload images, if you still wish to do so.
A BOOL flag seldom is the answer. I'd rather go for estimating a reasonable page size and fetching images as needed from the cellForItemAtIndePath method.
Hi is that possible to display multiple User Pin's on MKMapView at same location Lat Long. see this Bellow Image:-
in This above image i can there is a Two user location pin but Because of same Lat long i can see only one pin. so my question is that is that possible to display multiple pin on same location.
if we can't drop Multiple Pin's on a Same location then it is possible to display multiple AccessoryView on one pin who contain same location user's info.
Please Guide me how to do this task.
You will need to create a custom MKAnnotation class to do this.
There is no meaning in dropping multiple pins at same position.
Instead you can show multiple AccessoryView in same one pin
For that you need to create UIView that list all your pin headers which you wish to show.
[customAnnotationView.contentView addSubview:_contentView];
customAnnotationView.contentWidth = _contentView.frame.size.width;
customAnnotationView.contentHeight = _contentView.frame.size.height;
UIView is what you design so you can create that
Something like this can help you.
First filter out the unique location records. Lets say you have 10 records to show on map and out of which 3 records have same locations and other 2 records have other same locations and rest 5 have different locations. So on map there should be total 1(for 3 same records)+1(for 2 same records)+5 = 7 pins on map.
Now in method
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
check current annotation has more than 1 records same as annotation location and based on that do
if (ary.count > 1)
{
[annotationView setCanShowCallout:NO];
}
else
{
[annotationView setCanShowCallout:YES];
}
then in method
-(void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
check view can show callout of not. do following
if (![view canShowCallout])
{
// means annotation has more than 1 records on same location
now get records of same location and display the way you want.
}
in my app I'm adding and removing annotations to my map view, but actually even if I first add the new ones and then remove the old ones there is a blink of the annotations
The new annotations are received from the internet and so I can't just remove them from the array of the new ones...
My question is how does it come to the blink behavior and how I can avoid this?
I don't want to iterate through the annotations and compare each's location...
So I tried to delay the remove and ended up at 0.2 seconds. With exactly this delay there is no visible blink, but this isn't desirable at all.
Thanks
NSMutableArray *annotationsToRemove = [[NSMutableArray alloc] initWithArray:[_mapView annotations]];
[annotationsToRemove removeObject:[_mapView userLocation]];
[_mapView addAnnotations:locations];
// [_mapView performSelector:#selector(removeAnnotations:) withObject:annotationsToRemove afterDelay:0.2];
[_mapView removeAnnotations:annotationsToRemove];
("locations" is my array of new annotations)
The blink always happens when adding an annotation just after removing one. If you have control of the web service, add a unique id, check if an annotation with that id already exists on your map and only add a new annotation if it doesn't. If you can't add an id, check using the latitude and longitude (which should probably be unique). Iterating through them won't be expensive.
I am apparently in some swirling UIView hell zone at the moment where up is down sibling is parent and my brain is completely fried.
Here's the deal. Really, really simple. I have a container view with N leaf node sibling subviews. No tricks here, dead simple. I do the following:
// occludedPageSet is the set of view tags corresponding to views that are off screen and // thus fully occluded. This was determined geometrically.
for (NSNumber *n in occludedPageSet) {
// Point to a view corresponding to this tage
UIView *v = [self.containerView viewWithTag:[n integerValue]];
// Hide this view
if (v.hidden == NO) {
NSLog(#"View %d is occluded. Hide it.", [n integerValue]);
v.hidden = YES;
} // if (v.hidden == NO)
} // for (occludedPageSet)
Pretty tame stuff. Unfortunately ALL sibling views vanish! What the?!? How is this possible?
Do I need a [retain]/[release] for v here. I'm stumped.
Baffled,
Doug
Am I missing something about the problem here? It's only natural that if you hide a view, any view it holds as a subview would be hidden as well. After all, you can't see the container view...
If you put ten things in a box and make the box invisible, wouldn't you expect that to mean you couldn't see the things in the box? Similarly an invisibility cloak would be of little use if only the cloak were invisible and not the person beneath...
If you need some things visible and some not, work on the specific items and not the container.
Apparently, all of your views are included in occludedPageSet, or all of your tags are the same n.
NSNumber *n in occludedPageSet
Or, one of the v views is the parent of the rest, so when you hide it, you hide them all.
Make sure self.containerView's tag is something completely different from any of the children's tags. Calling viewWithTag will return the receiver if it is the given tag, which will in turn hide all of your views. Either step through the iteration or print out the address that v points to so that you know you're occluding what you should be occluding.
UPDATE
It looks like this problem has been quietly fixed in iOS 4.3. Up to this point, the distance that was considered "far enough" for an annotation to be recycled seemed to be hundreds of miles, even when zoomed in very closely. When I build my app with the iOS 4.3 SDK, annotations are recycled based on more reasonable limits.
Has anyone else run into this problem? Here's the code:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(WWMapAnnotation *)annotation {
// Only return an Annotation view for the placemarks. Ignore for the current location--the iPhone SDK will place a blue ball there.
NSLog(#"Request for annotation view");
if ([annotation isKindOfClass:[WWMapAnnotation class]]){
MKPinAnnotationView *browse_map_annot_view = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:#"BrowseMapAnnot"];
if (!browse_map_annot_view) {
browse_map_annot_view = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:#"BrowseMapAnnot"] autorelease];
NSLog(#"Creating new annotation view");
} else {
NSLog(#"Recycling annotation view");
browse_map_annot_view.annotation = annotation;
}
...
As soon as the view is displayed, I get
2009-08-05 13:12:03.332 xxx[24308:20b] Request for annotation view
2009-08-05 13:12:03.333 xxx[24308:20b] Creating new annotation view
2009-08-05 13:12:03.333 xxx[24308:20b] Request for annotation view
2009-08-05 13:12:03.333 xxx[24308:20b] Creating new annotation view
and on and on, for every annotation (~60) I've added. The map (correctly) only displays the two annotations in the current rect. I am setting the region in viewDidLoad:
if (center_point.latitude == 0) {
center_point.latitude = 35.785098;
center_point.longitude = -78.669899;
}
if (map_span.latitudeDelta == 0) {
map_span.latitudeDelta = .001;
map_span.longitudeDelta = .001;
}
map_region.center = center_point;
map_region.span = map_span;
NSLog(#"Setting initial map center and region");
[browse_map_view setRegion:map_region animated:NO];
The log entry for the region being set is printed to the console before any annotation views are requested.
The problem here is that since all of the annotations are being requested at once, [mapView dequeueReusableAnnotationViewWithIdentifier] does nothing, since there are unique MKAnnotationViews for every annotation on the map. This is leading to memory problems for me.
One possible issue is that these annotations are clustered in a pretty small space (~1 mile radius). Although the map is zoomed in pretty tight in viewDidLoad (latitude and longitude delta .001), it still loads all of the annotation views at once.
Thanks...
What you would expect is some kind of "clipping" of the annotation views based on the current region the map displays.
This is NOT the way the dequeueReusableAnnotationViewWithIdentifier selector works.
From its documentation :
As annotation views move offscreen, the map view moves them to an internally managed reuse queue. As new annotations move onscreen, and your code is prompted to provide a corresponding annotation view, you should always attempt to dequeue an existing view before creating a new one.
So the reusable mechanism only makes sense when you invoke a sequence like :
//this will create 1000 annotation views
[theMap addAnnotations:my1000annotations];
//this will move them offscreen (but some annotation views may be kept internally for further reuse)
[theMap removeAnnotatios:theMap.annotations];
//when adding back again some annotations onscreen, some of the previous annotation views will be reused.
[theMap addAnnotations:someNew400annotations];
In you case, the way I would implement the clipping (to display only the annotation for the current displayed region) is :
Add a delegate to your mapView and implement the - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated method to get informed when the region has changed
iterate through all your objects to get the ones that match this region
addAnnotations to your map for only those objects (you can implement a kind of merge between the previously displayed annotations and the new one or simply restart from scratch , remove all annotations, and set the new ones
Of course, when the user zooms out quite a lot and that the scope of the region is too big (too many pins to be displayed), then you have to take a decision : do I displayed all the annotations views (and take the risk that the display on the map does not give much info) or do I set a message to the user saying "zoom in to get pins" or whatever. But this is another story... ;)
Not sure if this will help, but you mentioned memory issues due to load of ~60 objects. Is there a way to conditionally load each object based on the current map region center and current map region span?
// :)