質問

The end state I'm trying to achieve is to have a running Timer that refreshes a collection of "Vehicle" annotations. The annotation coordinates are successfully refreshing every 60 seconds using the Timer, but the user must invoke mapView:regionWillChangeAnimated and mapView:regionWillChangeAnimated delegates. These delegates are correctly working and moving the Vehicle annotations, but I want the annotations to move autonomously without having the user interact with the screen.

Here's my approach:

1) Start the Timer.. this works perfectly!

//|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#pragma mark Timers
//|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
dispatch_source_t CreateDispatchTimer(uint64_t interval,
                                   uint64_t leeway, dispatch_queue_t queue, dispatch_block_t block)
{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
if (timer)
{
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, interval), interval * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10);
    dispatch_source_set_event_handler(timer, block);
    dispatch_resume(timer);
}
return timer;
}

- (void)startTimer
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
double secondsToFire = 60.000f;
double secondsLeeway = 2.000f;

_timer = CreateDispatchTimer(secondsToFire, secondsLeeway, queue, ^{
    // Do something
    //Method Call to Refresh NSMutableArray of Vehicle Models
    [self RefreshVehicles];
    NSLog(@"TIMER TASK CALLED");
});
}

- (void)cancelTimer
{
if (_timer) {
    dispatch_source_cancel(_timer);        
    _timer = nil;
    }
}

The timer is used to fetch and load the latest Vehicles into an NSMutable Array by calling (void)RefreshVehicles, this will update the latest coordinates for each object that will be used to update the Vehicle annotation. I'm using an NSNotification to know when the Async Network Call and SQLite work to update the Vehicle records are complete. When the Notification fires, I'm removing any existing Vehicle annotations and then updating the local Vehicle NSMutable Array, by calling addVehiclesToMap to add the new annotations to the Map:

//|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#pragma mark Notification Listener
//|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||

- (void)RefreshVehicles:(NSNotification *)notif {

  NSLog(@"++++++++++++++++++++++++++++++++++++++++  Vehicles UPDATED!");

** POST SOLUTION REMARK:  MOVED REMOVE ANNOTATION LOGIC TO: (void)addVehiclesToMap
//** MOVED //If it already Exists, Remove it, Must redraw vehicles because they are moving.
 //** MOVED for (id <MKAnnotation> annotation in self.mapView.annotations)
 //** MOVED{
     //** MOVED//Only Remove Vehicles, Leave Stations, they are static
     //** MOVED if ([annotation isKindOfClass:[VehicleAnnotation class]])
     //** MOVED{
        //** MOVED [self.mapView removeAnnotation:annotation];
     //** MOVED}
 //** MOVED}

//Load Vehicle Collection
self.vehicleCollection = [[VehicleModelDataController defaultVehicleModelDataController] vehicleReturnAll];    

[self addVehiclesToMap];

}

Here's the Method for addVehiclesToMap: **POST SOLUTION REMARK: After implementing Anna's solution to update the map annotations on the Main Thread, I started to receive the following error: *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSArrayM: 0x16d94af0> was mutated while being enumerated.'

This is because I was removing the annotations from the timer refresh on a background thread. To fix this issue, I implemented [self.mapView removeAnnotation:annotation]; to the Main Thread as well.**

/*
 ----- VEHICLES -----
 */
- (void)addVehiclesToMap {

//If it already Exists, Remove it, Must redraw vehicles because they are moving.
for (id <MKAnnotation> annotation in self.mapView.annotations)
{
    //Only Remove Vehicles, Leave Stations, they are static
    if ([annotation isKindOfClass:[VehicleAnnotation class]])
    {
        //Remove Vehicle Annotation to MapView on the Main Thread
        dispatch_async(dispatch_get_main_queue(), ^{
           [self.mapView removeAnnotation:annotation];
        });
    }
}

//Loop through Vehicle Collection and generate annotation for each Vehicle Object
for (VehicleModel *vehicle in vehicleCollection) {

    //New Vehicle Annotation Instance
    VehicleAnnotation *myVehicleAnnotation = [[VehicleAnnotation alloc] init];

    myVehicleAnnotation.coordinate = CLLocationCoordinate2DMake([vehicle.vehicleLat doubleValue], [vehicle.vehicleLon doubleValue]);  
    myVehicleAnnotation.vehicleId = [vehicle.vehicleId stringValue];                
    myVehicleAnnotation.title = vehicle.vehicleLabel;                               
    myVehicleAnnotation.subtitle = vehicle.vehicleIsTrainDelayed;                   

    **POST SOLUTION REMARK: PER ANNA'S SOLUTION, MOVE addAnnodation TO MAIN THREAD:**
    //MODIFIED THIS:** [self.mapView addAnnotation:myVehicleAnnotation];

    **//TO THIS:**
    //Add Vehicle Annotation to MapView on the Main Thread
    dispatch_async(dispatch_get_main_queue(), ^{

        [self.mapView addAnnotation:myVehicleAnnotation];
    });**
}

}

Next the code for viewAnnotation Delegate:

//|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#pragma mark MKAnnotationView Delegate
//|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||


- (MKAnnotationView *)mapView:(MKMapView *)theMapView viewForAnnotation:(id     <MKAnnotation>)annotation
{
    // if it's the user location, just return nil.
    if ([annotation isKindOfClass:[MKUserLocation class]])
    return nil;


// handle our two custom annotations
//
if ([annotation isKindOfClass:[VehicleAnnotation class]]) /// for Vehicles Only
 {

     //Important, can't use annotation, this lets the compiler know that the annotation is actually an StationAnnotation object.
     VehicleAnnotation *vehicleAnnotation = (VehicleAnnotation *)annotation;

     //Reuse existing Annotation
     NSString* AnnotationIdentifier = vehicleAnnotation.vehicleId.lowercaseString;
     MKPinAnnotationView* pinView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:AnnotationIdentifier];

    if (!pinView)
    {

        //Set unique annotation identifier  exp: 304 (The Vehicles's Unique Number)
        NSString* AnnotationIdentifier = vehicleAnnotation.vehicleId.lowercaseString;


        MKAnnotationView *annotationView = [[MKAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:AnnotationIdentifier];
        annotationView.canShowCallout = YES;


        NSString *vehicleFlagIcon = [@"map_train_green_" stringByAppendingString:vehicleAnnotation.vehicleId.lowercaseString];
        UIImage *flagImage = [UIImage imageNamed:vehicleFlagIcon];

        CGRect resizeRect;

        resizeRect.size = flagImage.size;
        CGSize maxSize = CGRectInset(self.view.bounds,
                                     [VehicleMapViewController annotationPadding],
                                     [VehicleMapViewController calloutHeight]).size;

        maxSize.height -= self.navigationController.navigationBar.frame.size.height + [VehicleMapViewController calloutHeight];
        if (resizeRect.size.width > maxSize.width)
            resizeRect.size = CGSizeMake(maxSize.width, resizeRect.size.height / resizeRect.size.width * maxSize.width);
        if (resizeRect.size.height > maxSize.height)
            resizeRect.size = CGSizeMake(resizeRect.size.width / resizeRect.size.height * maxSize.height, maxSize.height);

        resizeRect.origin = (CGPoint){0.0f, 0.0f};
        UIGraphicsBeginImageContext(resizeRect.size);
        [flagImage drawInRect:resizeRect];
        UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        annotationView.image = resizedImage;
        annotationView.opaque = NO;

        NSString *vehicleLogo = [@"map_train_green_" stringByAppendingString:vehicleAnnotation.vehicleId.lowercaseString];
        UIImageView *sfIconView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:vehicleLogo]];
        annotationView.leftCalloutAccessoryView = sfIconView;

        return annotationView;

    }
    else
    {
        pinView.annotation = annotation;
    }
    return pinView;
}    

return nil;
}

Next I do have logic to remove then re-add the annotations using the following delegates. These delegates work just fine, but require the user to interact with the screen. I'm trying to refresh the Map every X seconds instead. So far to see any changes to the annotation locations, I must touch the screen and move the map to invoke these deletes. I'm still unable to watch the vehicles move autonomously without having to perform interaction.

POST SOLUTION REMARKS: I REMOVED THESE DELEGATES BECAUSE THEY WERE PERFORMING UN-NECESSARY UPDATES TO THE VEHICLE ANNOTATION MAKING THE ICONS FLICKER WHEN THE MAP WAS TOUCHED AND MOVED... LETTING THE TIMER DO THE WORK

**// DELETED** -(void)mapView:(MKMapView *)theMapView regionWillChangeAnimated:(BOOL)animated { ...Annotation tear down and rebuild code here }
**//DELETED** -(void)mapView:(MKMapView *)theMapView didUpdateUserLocation:(MKUserLocation *)userLocation { ...Annotation tear down and rebuild code here }

Am I even close to a solution here? Thanks in advance...

役に立ちましたか?

解決

Try calling the addAnnotation on the main thread so the UI updates without that delay:

dispatch_async(dispatch_get_main_queue(), ^{
    [self.mapView addAnnotation:myVehicleAnnotation];
});



An unrelated suggestion:
Instead of removing and re-adding the vehicle annotations, you could simply update the coordinate properties of the existing vehicles and the map view will automatically move the annotation views. This can result in a slightly smoother effect though it's slightly harder to implement the logic (eg. you'll need to find the existing vehicle annotation in the map view annotations array and update it instead of creating a new VehicleAnnotation). You also have to account for new vehicles and deleting annotations for vehicles that no longer exist.


Another unrelated suggestion:
Instead of this roundabout and cryptic way to set the coordinate:

NSString *coord = [NSString stringWithFormat:@"{%@,%@}", 
   [NSString stringWithFormat:@"%@", vehicle.vehicleLat], 
   [NSString stringWithFormat:@"%@", vehicle.vehicleLon]];
CGPoint point = CGPointFromString(coord);
myVehicleAnnotation.coordinate = CLLocationCoordinate2DMake(point.x, point.y);  

I suggest this more direct and less cryptic approach:

myVehicleAnnotation.coordinate = CLLocationCoordinate2DMake(
    [vehicle.vehicleLat doubleValue], [vehicle.vehicleLon doubleValue]);
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top