I'm using Apple's KMLViewer to load a KML file and display it in a MapView. There are over 50,000 lines of coordinates in the KML file which, of course, causes it to load slowly. In an attempt to speed things up, I'm trying to perform the parsing in another thread using GCD.
I have it working reasonably well as far as it is displaying properly and the speed is acceptable. However, I'm getting intermittent runtime errors when loading the map. I suspect it is because the way I have things laid out, the UI is being updated within the GCD block. Everything I'm reading says the UI should be updated in the main thread or else runtime errors can occur which are intermittent and hard to track down. Well, that's what I'm seeing.
The problem is, I can't figure out how to update the UI in the main thread. I'm still new to iOS programming so I'm just throwing things against the wall to see what works. Here is my code, which is basically Apple's KMLViewerViewController.m with some modifications:
#import "KMLViewerViewController.h"
#implementation KMLViewerViewController
- (void)viewDidLoad
{
[super viewDidLoad];
activityIndicator.hidden = TRUE;
dispatch_queue_t myQueue = dispatch_queue_create("My Queue",NULL);
dispatch_async(myQueue, ^{
// Locate the path to the route.kml file in the application's bundle
// and parse it with the KMLParser.
NSString *path = [[NSBundle mainBundle] pathForResource:#"BigMap" ofType:#"kml"];
NSURL *url = [NSURL fileURLWithPath:path];
kmlParser = [[KMLParser alloc] initWithURL:url];
[kmlParser parseKML];
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI
// Add all of the MKOverlay objects parsed from the KML file to the map.
NSArray *overlays = [kmlParser overlays];
[map addOverlays:overlays];
// Add all of the MKAnnotation objects parsed from the KML file to the map.
NSArray *annotations = [kmlParser points];
[map addAnnotations:annotations];
// Walk the list of overlays and annotations and create a MKMapRect that
// bounds all of them and store it into flyTo.
MKMapRect flyTo = MKMapRectNull;
for (id <MKOverlay> overlay in overlays) {
if (MKMapRectIsNull(flyTo)) {
flyTo = [overlay boundingMapRect];
} else {
flyTo = MKMapRectUnion(flyTo, [overlay boundingMapRect]);
}
}
for (id <MKAnnotation> annotation in annotations) {
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0, 0);
if (MKMapRectIsNull(flyTo)) {
flyTo = pointRect;
} else {
flyTo = MKMapRectUnion(flyTo, pointRect);
}
}
// Position the map so that all overlays and annotations are visible on screen.
map.visibleMapRect = flyTo;
});
});
}
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
activityIndicator.hidden = FALSE;
[activityIndicator startAnimating];
}
#pragma mark MKMapViewDelegate
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay
{
return [kmlParser viewForOverlay:overlay];
}
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
return [kmlParser viewForAnnotation:annotation];
}
- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView
{
[activityIndicator stopAnimating];
activityIndicator.hidden = TRUE;
}
#end
Suggestions?
Related
I have developed a project, where a user draws a image on a canvas, I store it in the file using CoreData, I have one-to-many relationship called folder-to-files. So here all are images. I retrive the images from files , resize according to my table cell height and show it on a table. Once it is shown, I want to cache the images.
I also have some labels on the folder cell, which give me some info regarding my files, which I update on fly.
I also swipe the cells to mark it complete and move it to the bottom the cell.
I also show same file images in different Views depending on how user queries it.
I want to know the best method for this, I read through the web, their are many methods, GCD, NSOperationQueues and many more.
Which method will be best suited for me.
I want to show some code
- (UITableViewCell *)tableView:(FMMoveTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
FolderCell *tableCell = (FolderCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (tableCell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"FolderCell" owner:self options:nil];
tableCell = [nib objectAtIndex:0];
}
NSMutableArray *categoryArray = [[self.controller fetchedObjects]mutableCopy];
Folder *category = [categoryArray objectAtIndex:[indexPath row]];
[tableCell configureCellWithNote:category]; //This function is written in my FolderCell.m function
}
return tableCell;
}
-(void)configureCellWithNote:(Folder *)category
{
self.category = category;
UIImage *image1 = [UIImage imageWithData:category.noteImage];
CGSize newSize;
if(image1.size.width == 620 && image1.size.height == 200)
{
newSize = CGSizeMake(300, 97);
}
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image1 drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
self.notesImage.image = newImage;
}
So what is happening here is that configureCellWithNote is taking lot of time, because it is resizing images. Please help me out in deciding how can this performance issue be solved.
Regards
Rajit
If you simply want to shuffle the resize operation to a background thread, you could do something like this:
- (void)configureCellWithNote:(Folder *)category
{
self.category = category;
UIImage *image1 = [UIImage imageWithData:category.noteImage];
CGSize newSize;
if(image1.size.width == 620 && image1.size.height == 200)
{
newSize = CGSizeMake(300, 97);
}
dispatch_async(dispatch_get_global_queue(0,0), ^{
UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0);
[image1 drawInRect:CGRectMake(0,0,newSize.width,newSize.height)];
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.notesImage.image = newImage;
});
});
}
If you want to cache the results, then the trick will be to come up with a good cache key. Unfortunately, it's hard to tell from what you've posted what would make a good cache key. Certainly it will need to include the size, but it'll also need to include something that ties it back to the category. I suppose if nothing else you could use the NSManagedObjectID for the category, but I think that'll be specific to each managed object context you have. Assuming there was a property on Folder called uniqueName a caching implementation might look like this:
- (UIImage*)imageForCategory: (Folder*)category atSize: (CGSize)size
{
// A shared (i.e. global, but scoped to this function) cache
static NSCache* imageCache = nil;
// The following initializes the cache once, and only once
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
imageCache = [[NSCache alloc] init];
});
// Generate a cache key sufficient to uniquely identify the image we're looking for
NSString* cacheKey = [NSString stringWithFormat: #"%#|%#", category.uniqueName, NSStringFromSize((NSSize)size)];
// Try fetching any existing image for that key from the cache.
UIImage* img = [imageCache objectForKey: cacheKey];
// If we don't find a pre-existing one, create one
if (!img)
{
// Your original code for creating a resized image...
UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
UIImage* image1 = [UIImage imageWithData:category.noteImage];
[image1 drawInRect:CGRectMake(0,0,size.width,size.height)];
img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Now add the newly-created image to the cache
[imageCache setObject: img forKey: cacheKey];
}
// Return the image
return img;
}
Sure this is something simple as I'm just starting with the maps. I already have a map showing one location, but when I've added a second anotation the map stays zoomed all the way out rather than going to my locations. The pins are there when I zoom in, so I know that bit's working.
Code snippets:
- (void)viewDidLoad
{
...
...
...
// Set coordinates for our position
CLLocationCoordinate2D location;
location.latitude = [self.lat doubleValue];
location.longitude = [self.lon doubleValue];
// Add the annotation to our map view
MapViewAnnotation *newAnnotation = [[MapViewAnnotation alloc]
initWithTitle:self.placename
andSubtitle:self.subtitle
andCoordinate:location];
[self.mapView addAnnotation:newAnnotation];
[newAnnotation release];
// Set coordinates for our second position
CLLocationCoordinate2D amenitylocation;
amenitylocation.latitude = self.latitude;
amenitylocation.longitude = self.longitude;
// Add the annotation to our map view
MapViewAnnotation *amenityAnnotation = [[MapViewAnnotation alloc]
initWithTitle:self.callouttitle
andSubtitle:self.calloutsubtitle
andCoordinate:amenitylocation];
[self.mapView addAnnotation:amenityAnnotation];
[amenityAnnotation release];
[super viewDidLoad];
}
#pragma mark - MKMapView Delegates
// When a map annotation point is added, zoom to it (1500 range)
- (void)mapView:(MKMapView *)mv didAddAnnotationViews:(NSArray *)views
{
MKAnnotationView *annotationView = [views objectAtIndex:0];
id <MKAnnotation> mp = [annotationView annotation];
MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance([mp coordinate], 1500, 1500);
[mv setRegion:region animated:YES];
[mv selectAnnotation:mp animated:YES];
}
- (MKAnnotationView *)mapView:(MKMapView *)mv viewForAnnotation:(id<MKAnnotation>)annotation
{
if(mapView.userLocation==annotation)
{
return nil;
}
NSString *identifier = #"IDENTIFIER";
MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if(annotationView==nil)
{
annotationView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier]autorelease];
annotationView.pinColor=MKPinAnnotationColorPurple;
annotationView.canShowCallout=YES;
}
return annotationView;
}
I'd appreciate any pointers.
Also, am I right in thinking I'll have to make custom callouts if I want more than one to appear on the map at the same time?
Sorry, found the answer - I didn't have the MKMapView delegate linked to File's Owner in IB, although I do have in my header file. Linked that up and it's working.
I have the following code but can't compile it because I have a "Type name requires a specifier or qualifier" error" for (self).
How to fix this error? I have compared it with the original code and there are no differences, so I don't know what's going on.
#import "CurrentTimeViewController.h"
#implementation CurrentTimeViewController
{
// Call the superclass's designated initializer
self = [super initWithNibName:#"CurrentTimeViewController"
bundle:nil];
if (self) {
// Get the tab bar item
UITabBarItem *tbi = [self tabBarItem];
// Give it a label
[tbi setTitle:#"Time"];
}
return self;
}
Here is the code from the mirror file HynososViewController.h, and which I cut, pasted and modified:
#import "HypnososViewController.h"
#implementation HypnososViewController
- (id) init
{
// Call the superclass's designated initializer
self = [super initWithNibName:nil
bundle:nil];
if (self) {
// Get the tab bar item
UITabBarItem *tbi = [self tabBarItem];
// Give it a label
[tbi setTitle:#"Hypnosis"];
}
return self;
}
- (id) initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle
{
// Disregard parameters - nib name is an implementation detail
return [self init];
}
// This method gets called automatically when the view is created
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"Loaded the view for HypnosisViewController");
// Set the background color of the view so we can see it
[[self view] setBackgroundColor:[UIColor orangeColor]];
}
#end
Here is the complete code for CurrentTimeViewController.h:
#import "CurrentTimeViewController.h"
#implementation CurrentTimeViewController
{
// Call the superclass's designated initializer
self = [super initWithNibName:#"CurrentTimeViewController"
bundle:nil];
if (self) {
// Get the tab bar item
UITabBarItem *tbi = [self tabBarItem];
// Give it a label
[tbi setTitle:#"Time"];
}
return self;
}
- (id) initWithNibName:(NSString *)nibName bundle:(NSBundle *)Bundle
{
// Disregard parameters - nib name is an implementation detail
return [self init];
}
// This method gets called automatically when the view is created
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"Loaded the view for CurrentTimeViewController");
// Set the background color of the view so we can see it
[[self view] setBackgroundColor:[UIColor greenColor]];
}
#end
Is the code above a cut-and-paste of EXACTLY what you are trying to compile? If so, I think you are missing something very important that would make that code block a method implementation:
-(id)init // This, or something like it, is missing???
{
...
}
Check your code, here, and in your project. :-)
i am working with Mapkit and i am on SDK 4.2, i am having a strange bug here, in fact i have 3 annotation types, "blue.png", red.png,black.png. I am loading these by a flux and depending on the type its will select these annotation types. Everything works fine when the map is loaded i have the the different annotation view, but when i move , zoom in or zoom out the annotation view changes i.e where it was supposed to be blue.png it becomes black.png.
I am actually testing it on device.
Thank you very much :)
Hey veer the problem is that this method is called if the user pans the map to view another location and then comes back to the place where the annotations are plotted.
- (MKAnnotationView *)mapView:(MKMapView *)mapview viewForAnnotation:(id <MKAnnotation>)annotation
I have seen many sample code for map application and this in what most of the people are using.
- (MKAnnotationView *)mapView:(MKMapView *)mapview viewForAnnotation:(id <MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MKUserLocation class]])
return nil;
static NSString* AnnotationIdentifier = #"AnnotationIdentifier";
MKAnnotationView *annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:AnnotationIdentifier];
if(annotationView)
return annotationView;
else
{
MKPinAnnotationView* pinView = [[[MKPinAnnotationView alloc]
initWithAnnotation:annotation reuseIdentifier:AnnotationIdentifier] autorelease];
pinView.animatesDrop=YES;
pinView.canShowCallout=YES;
pinView.draggable = YES;
pinView.pinColor = MKPinAnnotationColorGreen;
return pinView;
}
return nil;
}
i found the solution - in fact i am using a custom annotation view and having 3 diff types of images :
Soln:
- (AnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
AnnotationView *annotationView = nil;
// determine the type of annotation, and produce the correct type of annotation view for it.
AnnotationDetails* myAnnotation = (AnnotationDetails *)annotation;
if(myAnnotation.annotationType == AnnotationTypeGeo)
{
// annotation for your current position
NSString* identifier = #"geo";
AnnotationView *newAnnotationView = (AnnotationView *)[self.mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if(nil == newAnnotationView)
{
newAnnotationView = [[[AnnotationView alloc] initWithAnnotation:myAnnotation reuseIdentifier:identifier] autorelease];
}
annotationView = newAnnotationView;
}
else if(myAnnotation.annotationType == AnnotationTypeMyfriends)
{
NSString* identifier = #"friends";
AnnotationView *newAnnotationView = (AnnotationView *)[self.mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if(nil == newAnnotationView)
{
newAnnotationView = [[[AnnotationView alloc] initWithAnnotation:myAnnotation reuseIdentifier:identifier] autorelease];
}
annotationView = newAnnotationView;
}
}
I am trying to capture tap event on my MKMapView, this way I can drop a MKPinAnnotation on the point where user tapped. Basically I have a map overlayed with MKOverlayViews (an overlay showing a building) and I would like to give user more information about that Overlay when they tap on it by dropping a MKPinAnnotaion and showing more information in the callout.
Thank you.
You can use a UIGestureRecognizer to detect touches on the map view.
Instead of a single tap, however, I would suggest looking for a double tap (UITapGestureRecognizer) or a long press (UILongPressGestureRecognizer). A single tap might interfere with the user trying to single tap on the pin or callout itself.
In the place where you setup the map view (in viewDidLoad for example), attach the gesture recognizer to the map view:
UITapGestureRecognizer *tgr = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(handleGesture:)];
tgr.numberOfTapsRequired = 2;
tgr.numberOfTouchesRequired = 1;
[mapView addGestureRecognizer:tgr];
[tgr release];
or to use a long press:
UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleGesture:)];
lpgr.minimumPressDuration = 2.0; //user must press for 2 seconds
[mapView addGestureRecognizer:lpgr];
[lpgr release];
In the handleGesture: method:
- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer.state != UIGestureRecognizerStateEnded)
return;
CGPoint touchPoint = [gestureRecognizer locationInView:mapView];
CLLocationCoordinate2D touchMapCoordinate =
[mapView convertPoint:touchPoint toCoordinateFromView:mapView];
MKPointAnnotation *pa = [[MKPointAnnotation alloc] init];
pa.coordinate = touchMapCoordinate;
pa.title = #"Hello";
[mapView addAnnotation:pa];
[pa release];
}
I setup a long press (UILongPressGestureRecognizer) in viewDidLoad: but it just detect the only one touch from the first.
Where can i setup a long press to detect all touch? (it means the map ready everytime waiting user touch to screen to push a pin)
The viewDidLoad: method!
- (void)viewDidLoad {
[super viewDidLoad];mapView.mapType = MKMapTypeStandard;
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleLongPressGesture:)];
[self.mapView addGestureRecognizer:longPressGesture];
[longPressGesture release];
mapAnnotations = [[NSMutableArray alloc] init];
MyLocation *location = [[MyLocation alloc] init];
[mapAnnotations addObject:location];
[self gotoLocation];
[self.mapView addAnnotations:self.mapAnnotations];
}
and the handleLongPressGesture method:
-(void)handleLongPressGesture:(UIGestureRecognizer*)sender {
// This is important if you only want to receive one tap and hold event
if (sender.state == UIGestureRecognizerStateEnded)
{NSLog(#"Released!");
[self.mapView removeGestureRecognizer:sender];
}
else
{
// Here we get the CGPoint for the touch and convert it to latitude and longitude coordinates to display on the map
CGPoint point = [sender locationInView:self.mapView];
CLLocationCoordinate2D locCoord = [self.mapView convertPoint:point toCoordinateFromView:self.mapView];
// Then all you have to do is create the annotation and add it to the map
MyLocation *dropPin = [[MyLocation alloc] init];
dropPin.latitude = [NSNumber numberWithDouble:locCoord.latitude];
dropPin.longitude = [NSNumber numberWithDouble:locCoord.longitude];
// [self.mapView addAnnotation:dropPin];
[mapAnnotations addObject:dropPin];
[dropPin release];
NSLog(#"Hold!!");
NSLog(#"Count: %d", [mapAnnotations count]);
}
}
If you want to use a single click/tap in the map view, here's a snippet of code I'm using. (Cocoa and Swift)
let gr = NSClickGestureRecognizer(target: self, action: "createPoint:")
gr.numberOfClicksRequired = 1
gr.delaysPrimaryMouseButtonEvents = false // allows +/- button press
gr.delegate = self
map.addGestureRecognizer(gr)
in the gesture delegate method, a simple test to prefer the double-tap gesture …
func gestureRecognizer(gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOfGestureRecognizer otherGestureRecognizer: NSGestureRecognizer) -> Bool {
let other = otherGestureRecognizer as? NSClickGestureRecognizer
if (other?.numberOfClicksRequired > 1) {
return true; // allows double click
}
return false
}
you could also filter the gesture in other delegate methods if you wanted the Map to be in various "states", one of which allowed the single tap/click
For some reason, the UIGestureRecognizer just didn't work for me in Swift. When I use the UIGestureRecognizer way. When I used the touchesEnded method, it returns a MKNewAnnotationContainerView. It seems that this MKNewAnnotationContainerView blocked my MKMapView. Fortunately enough, it's a subview of MKMapView. So I looped through MKNewAnnotationContainerView's superviews till self.view to get the MKMapView. And I managed to pin the mapView by tapping.
Swift 4.1
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let t = touches.first
print(t?.location(in: self.view) as Any)
print(t?.view?.superview?.superview.self as Any)
print(mapView.self as Any)
var tempView = t?.view
while tempView != self.view {
if tempView != mapView {
tempView = tempView?.superview!
}else if tempView == mapView{
break
}
}
let convertedCoor = mapView.convert((t?.location(in: mapView))!, toCoordinateFrom: mapView)
let pin = MKPointAnnotation()
pin.coordinate = convertedCoor
mapView.addAnnotation(pin)
}