Assignment #5 Task #5

Please note, this blog entry is from a previous course. You might want to check out the current one.

Anywhere in your application where a list of photos or places appears, give the user an option (via some UI of your choice) to view information in the list on a map instead. Each annotation on the map should have a callout which displays the following:
a. The title of the photo or the name of the place.
b. In the case of a photo, its description (at least the first few words of it, if any) and a thumbnail image (FlickrFetcherPhotoFormatSquare) of the photo. It is okay if the callout shows up initially without the thumbnail, but as soon as you are able to retrieve it from Flickr (assuming the callout is still on screen at that point), the thumbnail should appear. Beware Required Task #1.
c. A disclosure button which brings up the full image of the chosen photo (exactly as if the user had chosen the photo from a table) or which brings up a list of photos in that place (again, exactly as if the user had chosen that place from a table).

First we start by adding a button to the navigation toolbar to allow displaying a map and a second one for displaying the table again when the map is on screen. Because we will need this functionality in all tables we will put this code into the common-table-view-controller class:

// CommonTableViewController.h
@property (nonatomic, strong) UIBarButtonItem *mapButton;
@property (nonatomic, strong) UIBarButtonItem *tableButton;

// CommonTableViewController.m
@synthesize mapButton = _mapButton;
@synthesize tableButton = _tableButton;
  ...
- (void)viewDidLoad
{    
    self.mapButton = [[UIBarButtonItem alloc] 
        initWithTitle:@"Map" 
                style:UIBarButtonItemStyleBordered 
               target:self 
               action:@selector(showMapView:)];
    self.tableButton = [[UIBarButtonItem alloc] 
        initWithTitle:@"Table" 
                style:UIBarButtonItemStyleBordered 
               target:self 
               action:@selector(showMapView:)];
}

We will place the buttons when we discard the spinner in the navigation tool bar by replacing

   [self.spinner startAnimating];
   [self.spinner startAnimating];

in the table controller classes by two new functions

   [self startSpinner];
   [self startSpinner];

To know which button (table or map) we have to put up, we use a private boolean:

// CommonTableViewController.h
- (void)startSpinner;
- (void)stopSpinner;

// CommonTableViewController.h
@property (nonatomic) BOOL showMap;
   ...
@synthesize showMap = _showMap;
   ...
- (void)startSpinner
{
    [self.spinner startAnimating];
    self.navigationItem.rightBarButtonItem = 
        [[UIBarButtonItem alloc] initWithCustomView:self.spinner];
}

- (void)stopSpinner
{
    [self.spinner stopAnimating];
    if (self.showMap)
        self.navigationItem.rightBarButtonItem = self.tableButton;
    else
        self.navigationItem.rightBarButtonItem = self.mapButton;
}

The buttons call two new methods. Where showTableView: just sets the map button, hides the map view and sets the above mentioned boolean, showMapView: is a little bit more tricky. First we have to scroll the table to its top because the map view will be a child view of it and will be placed there. In addition the annotations will be set and finally the map view is brought to the top. The last step is necessary as the table view tends to put the section headers on top and thus shows them on top of the map.

- (IBAction)showMapView:(id)sender 
{
    [self.tableView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:NO];    
    self.navigationItem.rightBarButtonItem = self.tableButton;
    self.annotations = [self mapAnnotations];
    self.mapView.hidden = NO;
    self.showMap = YES;
    [self.tableView bringSubviewToFront:self.mapView];
}

- (IBAction)showTableView:(id)sender 
{
    self.navigationItem.rightBarButtonItem = self.mapButton;
    self.mapView.hidden = YES;
    self.showMap = NO;
}

To ensure that the map stays on top we repeat this last step when the view reappears on screen:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    if (self.showMap) [self.tableView bringSubviewToFront:self.mapView];
}

When the map is called the first time it is instantiated lazily as sub view of the table view:

- (MKMapView *)mapView
{
    if (!_mapView) {
        _mapView = [[MKMapView alloc] initWithFrame:self.tableView.frame];
        _mapView.autoresizingMask = self.tableView.autoresizingMask;
        _mapView.delegate = self;
        [self.tableView addSubview:_mapView];
    }
    return _mapView;
}

Setting up the the annotations is basically the same code shown for the Shutterbug project in the demo of lecture #11.

- (void)updateMapView
{
    if (self.mapView.annotations) 
        [self.mapView removeAnnotations:self.mapView.annotations];
    if (self.annotations) {
        [self.mapView addAnnotations:self.annotations];    
    }
}

- (void)setAnnotations:(NSArray *)annotations
{
    if (_annotations == annotations) return;
    _annotations = annotations;
    [self updateMapView];
}

- (NSArray *)mapAnnotations
{
    NSMutableArray *annotations = 
        [NSMutableArray arrayWithCapacity:[self.data count]];
    for (NSDictionary *item in self.data) 
        [annotations addObject:[FlickrAnnotation annotationForData:item]];
    return annotations;
}

which uses a new FlickrAnnotation class:

// FlickrAnnotation.h
@property (nonatomic, strong) NSDictionary *data;
+ (FlickrAnnotation *)annotationForData:(NSDictionary *)data;

// FlickrAnnotation.m
@synthesize data = _data;
   ...
+ (FlickrAnnotation *)annotationForData:(NSDictionary *)data
{
    FlickrAnnotation *annotation = [[FlickrAnnotation alloc] init];
    annotation.data = data;
    return annotation;
}
   ...
- (NSString *)title
{
    NSString *place = [self.data objectForKey:FLICKR_PLACE_NAME];
    if (place) return [FlickrData titleOfPlace:self.data];
    return [FlickrData titleOfPhoto:self.data];
}
   ...
- (NSString *)subtitle
{
    NSString *place = [self.data objectForKey:FLICKR_PLACE_NAME];
    if (place) return [FlickrData subtitleOfPlace:self.data];
    return [FlickrData subtitleOfPhoto:self.data];
}
   ...
- (CLLocationCoordinate2D)coordinate
{
    CLLocationCoordinate2D coordinate;
    coordinate.latitude = [[self.data objectForKey:FLICKR_LATITUDE] doubleValue];
    coordinate.longitude = [[self.data objectForKey:FLICKR_LONGITUDE] doubleValue];
    return coordinate;
}

Note that we are using a generic data model. Thus we have to check if its content is a place or photo. The data model itself is set up in the respective setters of each table view controller:

// CommonTableViewController.h
@property (nonatomic, strong) NSArray *data;

// CommonTableViewController.h
@synthesize data = _data;

// TopPlacesTableViewController.m
- (void)setPlaces:(NSArray *)places
{
    ....
    self.data = _places;
}

// TopPhotosTableViewController.m & RecentPhotosTableViewController.m
- (void)setPhotos:(NSArray *)places
{
    ....
    self.data = _photos;
}

The annotation view can also be setup in the common-table-view-controller class but has to will differ when used for places or photos:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation
{
    MKAnnotationView *aView = [mapView dequeueReusableAnnotationViewWithIdentifier:@"MapVC"];
    if (!aView) {
        aView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation 
                                                reuseIdentifier:@"MapVC"];
        aView.canShowCallout = YES;
        aView.leftCalloutAccessoryView = 
            [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
        aView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
    }
    aView.annotation = annotation;
    FlickrAnnotation *fa = annotation;
    if ([FlickrData titleOfPlace:fa.data]) {
        aView.leftCalloutAccessoryView = nil;
    } else {
        [(UIImageView *)aView.leftCalloutAccessoryView setImage:nil];
    }    
    return aView;
}

To fetch the preview images, also check first, if we have actually a photo, and then load and cache it like we did for the photo view itself.

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{ 
    if (!view.leftCalloutAccessoryView) return;
    FlickrAnnotation *fa = view.annotation;
    NSDictionary *photo = [fa.data copy];
    NSNumber *photoID = [photo objectForKey:FLICKR_PHOTO_ID];
    if (!photoID) return;    
    dispatch_queue_t queue = dispatch_queue_create("Flickr Thumbnails", NULL);
    dispatch_async(queue, ^{
        FlickrCache *cache = [FlickrCache cacheFor:@"photos"];
        NSURL *url = [cache urlForCachedPhoto:photo 
                                       format:FlickrPhotoFormatSquare];
        if (!url) url = [FlickrFetcher urlForPhoto:photo
                                            format:FlickrPhotoFormatSquare];
        NSData *data = [NSData dataWithContentsOfURL:url];
        [cache cacheData:data ofPhoto:photo format:FlickrPhotoFormatSquare];
        if ([[fa.data objectForKey:FLICKR_PHOTO_ID] 
             isEqualToString:[photo objectForKey:FLICKR_PHOTO_ID]]) 
            dispatch_async(dispatch_get_main_queue(), ^{
                UIImage *image = [UIImage imageWithData:data];        
                [(UIImageView *)view.leftCalloutAccessoryView setImage:image];
                [view setNeedsDisplay];                
            });
    });
    dispatch_release(queue);    
}

When the right disclosure button has been tapped to code for the places table is slightly different than for the photos tables, thus we add this part of the code inside the respective classes.

For the places table we have a segue for both iPhone and iPad thus we just perform that segue and provide the places data as sender:

- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view 
                      calloutAccessoryControlTapped:(UIControl *)control
{
    FlickrAnnotation *fa = view.annotation;
    [self performSegueWithIdentifier:@"Show Photos from Place" sender:fa.data];
}

As the sender now differs from the “normal” segue call we have to adjust for that:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    ...
        if ([sender isKindOfClass:[NSDictionary class]]) {
            [segue.destinationViewController setPlace:sender];
        } else {
            ....
        }
    }
}

For the photo view controllers we have to check if there is a split view controller (for the iPad). If so, we just send the photo object of the photo view controller. Otherwise we perform the segue like above:

- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view  
                      calloutAccessoryControlTapped:(UIControl *)control
{
    FlickrAnnotation *fa = view.annotation;
    id vc = [self.splitViewController.viewControllers lastObject];
    if ([vc isKindOfClass:[PhotoViewController class]])
        [vc setPhoto:fa.data];
    else 
        [self performSegueWithIdentifier:@"Show Photo" sender:fa.data];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    ...
        if ([sender isKindOfClass:[NSDictionary class]]) {
            [segue.destinationViewController setPhoto:sender];
        } else {
            ....
        }
    }
}

The complete code for this task is available at github.

FacebooktwitterredditpinterestlinkedintumblrmailFacebooktwitterredditpinterestlinkedintumblrmail

Leave a Reply

Your email address will not be published.