воскресенье, 3 января 2016 г.

38. Карты. MKMapView Part 2

Продолжаем разбираться в картах - MapKit.
Видео к этому уроку находится здесь.

Добавляем к всплывающему облачку над Pin справа кнопку информации. Для этого в метод

- (nullable MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation

добавляем

       UIButton *descriptionButton=[UIButton buttonWithType:UIButtonTypeDetailDisclosure];
        [descriptionButton addTarget:self action:@selector(actionDescription:) forControlEvents:UIControlEventTouchUpInside];

        pin.rightCalloutAccessoryView=descriptionButton;

Для того, чтобы узнать на каком Pin была нажата кнопка (для правильной обработки событий) применим ранее используемый метод с расширением категории UIView.
Создаем новую категорию для UIView с именем MKAnnotationView: 

File->New File->Objective C File 
выбираем из списка категория и вводим имя.

Для поиска владельца кнопки  создаем рекурсивную функцию в категории:

#import "UIView+MKAnnotationView.h"
#import <MapKit/MapKit.h>

@implementation UIView (MKAnnotationView)
- (MKAnnotationView*) superAnnotationView{
    
    if ([self isKindOfClass:[MKAnnotationView class]]) {
        return (MKAnnotationView*) self;
    }
    if (!self.subviews) {
        return nil;
    }
    return [self.superview superAnnotationView];

}


@end


Для получения адреса по координатам создаем проперти:

@property(nonatomic,strong) CLGeocoder *geoCoder;

и инициализируем его в viewDidLoad:

    self.geoCoder=[[CLGeocoder alloc] init];

Пишем обработчик кнопки:

-(void) actionDescription: (UIButton *) sender{

//определяем владельца кнопки с помощью нашей функции

    MKAnnotationView* annotationView = [sender superAnnotationView];
    
    if (!annotationView) {
        return;
    }
//сохраняем координаты аннотации    
    CLLocationCoordinate2D coordinate = annotationView.annotation.coordinate;

//используя широту и долготу инициализируем объект типа CLLocation
    CLLocation* location = [[CLLocation alloc] initWithLatitude:coordinate.latitude
                                                      longitude:coordinate.longitude];
//Если Геокодер что-то делает, то мы отменяем это действие    
    if ([self.geoCoder isGeocoding]) {
        [self.geoCoder cancelGeocode];
    }
//Делаем запрос к геокодеру по нашим координатам    
    [self.geoCoder
     reverseGeocodeLocation:location
     completionHandler:^(NSArray *placemarks, NSError *error) {
         
         NSString* message = nil;
//если произошла ошибка - выводим в сообщении         
         if (error) {
             
             message = [error localizedDescription];
             
         } else {
//если массив с адресом не пустой, выводим в сообщение             
             if ([placemarks count] > 0) {
                 
                 MKPlacemark* placeMark = [placemarks firstObject];
                 
                 message = [placeMark.addressDictionary description];
//если пустой массив, сообщаем об этом                 
             } else {
                 message = @"No Placemarks Found";
             }
         }
//Выводим сообщение с помощью AlerController (для этого создали функцию showAlertWithTitle)
         [self showAlertWithTitle:@"Location" andMessage:message];

    }];

}

UIAlertView используемый в данном уроке для вывода адреса или ошибки устарел Deprecated, поэтому используем UIAlertController:

-(void) showAlertWithTitle:(NSString*) title andMessage:(NSString*) message{
    
    UIAlertController* alert = [UIAlertController alertControllerWithTitle:title
                                                                   message:message
                                                            preferredStyle:UIAlertControllerStyleAlert];
    
    UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault
                                                          handler:^(UIAlertAction * action) {}];
    
    [alert addAction:defaultAction];
    [self presentViewController:alert animated:YES completion:nil];
    

}


Добавляем еще одну кнопку в облачко Pin для отображения пути от текущей геопозиции до Pin.   В функцию
- (nullable MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation

добавляем

        UIButton *wayButton=[UIButton buttonWithType:UIButtonTypeContactAdd];
        [descriptionButton addTarget:self action:@selector(actionWayDirection:) forControlEvents:UIControlEventTouchUpInside];

        pin.leftCalloutAccessoryView=wayButton;

Добавляем проперти  MKDirections, чтобы переменная не уничтожилась раньше времени 

@property (nonatomic,strong) MKDirections *directions;


Добавляем обработчик этой кнопки

-(void) actionWayDirection: (UIButton *) sender{

//Ищем аннотацию - владельца нажатой кнопки с помощью нашей рекурсивной функции    

    MKAnnotationView* annotationView = [sender superAnnotationView];
    
    if (!annotationView) {
        return;
    }

//Если происходит вычисление пути, отменяем вычисление

    if ([self.directions isCalculating]) {
        [self.directions cancel];
    }

// Получаем координаты из аннотации    
    CLLocationCoordinate2D coordinate = annotationView.annotation.coordinate;
    
// Создаем запрос     
    MKDirectionsRequest *request=[[MKDirectionsRequest alloc]init];

//Заполняем запрос  из текущего местоположения
    request.source = [MKMapItem mapItemForCurrentLocation];

//Создаем MKPlacemark  из координат аннотации  
    MKPlacemark *placemark=[[MKPlacemark alloc]initWithCoordinate:coordinate addressDictionary:nil];

//Создаем MKMapItem из полученной placemark
    MKMapItem *destination = [[MKMapItem alloc] initWithPlacemark:placemark];

//Заполняем запрос полученным destination
    request.destination = destination;

//Тип транспорта на котором можно передвигаться - автомобиль
    request.transportType= MKDirectionsTransportTypeAutomobile;

//Для показа альтернативных путей 
    request.requestsAlternateRoutes=YES;
//Инициализируем нашу property  c помощью сформированного запроса 
    self.directions = [[MKDirections alloc] initWithRequest:request];

//Запускаем на вычисление - поиски маршрутов
    [self.directions calculateDirectionsWithCompletionHandler:^(MKDirectionsResponse * _Nullable response, NSError * _Nullable error) {

//Если существует ошибка, выводим её
        if (error) {
            [self showAlertWithTitle:@"Error" andMessage:[error localizedDescription]];
        }
//Если направлений 0 - выводим сообщение об этом
        else if ([response.routes count]==0){
            [self showAlertWithTitle:@"Error" andMessage:@"No routes found"];
        }
        else{
//Удаляем старые нарисованные маршруты
            [self.mapView removeOverlays:[self.mapView overlays]];

//Формируем массив с маршрутами для прорисовки
            
            NSMutableArray *arrayRoute=[NSMutableArray array];
            for (MKRoute *route in response.routes) {
                [arrayRoute addObject:route.polyline];
            }
//Рисуем на карте наши маршруты            
            [self.mapView addOverlays:arrayRoute level:MKOverlayLevelAboveRoads];
            
        }
      
    }];

}


Исходный текст проекта находится здесь.

Домашнее задание к этому уроку:

По ходу выполнения домашнего задания завис на 5 пункте, т.к. картинки для Pin не отображались.
Оказывается с IOS 9  для класса MKPinAnnotationView  отображаются только стандартные картинки для Пина. Можно только менять цвета.
Пришлось для кастомных картинок использовать родительский класс MKAnnotationView  который не имеет свойств таких как анимированное падение и цвет пина.
Вот такая получилась функция для 5 задания:

- ( MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation{

    if ([annotation isKindOfClass:[MKUserLocation class]]) {
        return nil;
    }
    static NSString *pinIdentifier=@"AnnotationStudent";
    MKAnnotationView *pin=(MKAnnotationView *) [self.mapView dequeueReusableAnnotationViewWithIdentifier:pinIdentifier];

    if (!pin) {
        pin=[[MKAnnotationView alloc]initWithAnnotation:annotation reuseIdentifier:pinIdentifier];
        pin.canShowCallout=YES;
        pin.draggable=YES;
    }
    else{
        pin.annotation=annotation;
    }

    if ([annotation isKindOfClass:[TAStudent class]]) {
        TAStudent *student=(TAStudent*) annotation;

        NSString *imageName=student.isMen?@"man_pin":@"woman_pin";
        UIImage *imagePin=[UIImage imageNamed:imageName];
        pin.image = imagePin;

    }
  
    return pin;

}


Выполнил все задания кроме супермена (нужно следующие уроки разбирать).

Исходник с домашним заданием здесь.





Ученик. 

1. Создайте массив из 10 - 30 рандомных студентов, прямо как раньше, только в этот раз пусть у них наряду с именем и фамилией будет еще и координата. Можете использовать структуру координаты, а можете просто два дабла - лонгитюд и латитюд. 

2. Координату генерируйте так, установите центр например в вашем городе и просто генерируйте небольшие отрицательные либо положительные числа, чтобы рандомно получалась координата от центра в пределах установленного радиуса.

(Ну а если совсем не получается генерировать координату, то просто ставьте им заготовленные координаты - это не главное)

3. После того, как вы сгенерировали своих студентов, покажите их всех на карте, причем в титуле пусть будет Имя и Фамилия а в сабтитуле год рождения. Можете для каждого студента создать свою аннотацию, а можете студентов подписать на протокол аннотаций и добавить их на карту напрямую - как хотите :)

Студент.

4. Добавьте кнопочку, которая покажет всех студентов на экране.

5. Вместо пинов на карте используйте свои кастомные картинки, причем если студент мужского пола, то картинка должна быть одна, а для девушек другая.

Мастер

6. У каждого колаута (этого облачка над пином) сделайте кнопочку информации справа так, что когда я на нее нажимаю вылазит поповер, в котором в виде статической таблицы находится имя и фамилия студента, год рождения, пол и самое главное адрес.

7. В случае если это телефон, то вместо поповера контроллер должен вылазить модально.

Супермен

8. Создайте аннотацию для места встречи и показывайте его на карте новымой соответствующей картинкой

9. Место встречи можно перемещать по карте, а студентов нет

10. Когда место встречи бросается на карту, то вокруг него надо рисовать 3 круга, с радиусами 5 км, 10 км и 15 км (используйте оверлеи)

11. На какой-то полупрозрачной вьюхе в одном из углов вам надо показать сколько студентов попадают в какой круг. Суть такая, чем дальше студент живет, тем меньше вероятность что он придет на встречу. Расстояние от студента до места встречи рассчитывайте используя функцию для расчета расстояния между точками, поищите ее в фреймворке :)

12. Сделайте на навигейшине кнопочку, по нажатию на которую, от рандомных студентов до нее будут проложены маршруты (типо студенты идут на сходку), притом вероятности генератора разные, зависит от круга, в котором они находятся, если он близко, то 90%, а если далеко - то 10%

37. Карты. MKMapView Part 1

Большой перерыв получился с выполнения предыдущего урока в связи, с тем что я устроился разработчиком под IOS в крупную IT компанию.
Свободное время появилось только сейчас - в новогодние каникулы. Надеюсь пройти и закончить этот курс Алексея Скутаренко в ближайшее время. Так же планировал продолжить изучение разработки под IOS в новом курсе Алексея - Swift Development Course Beginner

Урок 37 и 38 посвящен библиотеке mapkit - картам Apple.
Видео 37 урока находится здесь.
Создается тестовый проект на базе Single View Application.
Для того, чтобы пользоваться картами - подключаем библиотеку в настройках проекта:
Linked FrameWorks and Libraries  добавляем (+) MapKit.framework.
В тех классах где будем применять карты необходимо так же  подключить главный загадочный файл MapKit.h (в нем подключаются все остальные заголовочные файлы фреймворка).

Создается Navigation controller и ViewController.
На ViewController помещаем MapView
Создаем аутлет MapView в заголочном файле.
Чтобы не было ошибки прописываем, что MKMapView будет объявлен (подключен) позже:

@class MKMapView;

Импорт лучше переносить внутрь реализации.
В .m файле подключаем MapKit.h:

#import <MapKit/MapKit.h>

Делаем наш ViewController делегатом MapView:

@interface ViewController () <MKMapViewDelegate>

Так же в сторибоард перетягиваем с MapView на ViewController связь и отмечаем Delegate.

Определяем функции делегата, для отображения когда какая запускается:

#pragma mark - MKMapViewDelegate


- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated{
    NSLog(@"regionWillChangeAnimated");
    
}
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated{
        NSLog(@"regionDidChangeAnimated");
}

- (void)mapViewWillStartLoadingMap:(MKMapView *)mapView{
        NSLog(@"mapViewWillStartLoadingMap");
}
- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView{
        NSLog(@"mapViewDidFinishLoadingMap");
}
- (void)mapViewDidFailLoadingMap:(MKMapView *)mapView withError:(NSError *)error{
        NSLog(@"mapViewDidFailLoadingMap");
}

- (void)mapViewWillStartRenderingMap:(MKMapView *)mapView{
    NSLog(@"mapViewWillStartRenderingMap");
}
- (void)mapViewDidFinishRenderingMap:(MKMapView *)mapView fullyRendered:(BOOL)fullyRendered{
    NSLog(@"mapViewDidFinishRenderingMap");

}

 При приближении / удалении срабатывает функция regionWillChangeAnimated, которая сообщает что регион изменяется.
Регион в классе MKMapView - описывается переменной

@property (nonatomic) MKCoordinateRegion region

В свою очередь MKCoordinateRegion,  это структура

typedef struct {
CLLocationCoordinate2D center;
MKCoordinateSpan span;

} MKCoordinateRegion;

Которая определяется центром - center и радиусом/отклонением от центра span.

Координаты центра заданы типом - CLLocationCoordinate2D, который тоже является структурой состоящей из широты и долготы в градусах:

typedef struct {
CLLocationDegrees latitude;
CLLocationDegrees longitude;

} CLLocationCoordinate2D;

CLLocationDegrees - псевдоним  для типа double.

typedef double CLLocationDegrees;

span имеет тип  MKCoordinateSpan. Который определяется дельтой широты и долготы

typedef struct {
    CLLocationDegrees latitudeDelta;
    CLLocationDegrees longitudeDelta;

} MKCoordinateSpan;


Широта на экваторе - 0. На северном полюсе - 90 градусов. На южном полюсе - 90 градусов.
Долгота от 0 до 180 градусов если идти от нулевого меридиана против часовой стрелки. и 0 до - 180 если по часовой стрелке (смотря с северного полюса).
Широта горизонтальный срез. Долгота - вертикальный срез.

Создается кнопка Add  - добавить аннотацию на NavigationBar:

    UIBarButtonItem *addButton=[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(actionAddButton:)];
    self.navigationItem.rightBarButtonItem=addButton;

Для того, чтобы создавать аннотацию мы унаследуем новый класс TAMapAnnotation от NSObject и укажем, что он поддерживает протокол MKAnnotation:

@interface TAMapAnnotation : NSObject <MKAnnotation>

Добавляем обязательные и опциональные переменные содержащиеся в протоколе:

@property (nonatomic, assign) CLLocationCoordinate2D coordinate;
@property (nonatomiccopy, nullable) NSString *title;
@property (nonatomiccopy, nullable) NSString *subtitle;

Убираем из них свойство  - readonly (это минимальное требование).

Теперь добавляем метод - обработчик нажатия кнопки Add на NavigationBar:

#pragma mark - action
-(void) actionAddButton :(UIBarButtonItem *) sender{

    TAMapAnnotation *annotation=[[TAMapAnnotation alloc]init];
    annotation.coordinate=self.mapView.region.center;
    annotation.title=@"Test Title";
    annotation.subtitle=@"SubTitle";
    [self.mapView addAnnotation:annotation];
}

Для того чтобы Пин с аннотацией падали с анимацией, определим метод:


- (nullable MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation{
    if ([annotation isKindOfClass:[MKUserLocation class]]){
        return nil;
    }
    static NSString *strPinIdentifier=@"Annotation";
    MKPinAnnotationView *pin =(MKPinAnnotationView*) [self.mapView dequeueReusableAnnotationViewWithIdentifier:strPinIdentifier];
    if (!pin) {
        pin=[[MKPinAnnotationView alloc]initWithAnnotation:annotation reuseIdentifier:strPinIdentifier];
        pin.pinTintColor=[UIColor greenColor];
        pin.animatesDrop=YES;
        pin.canShowCallout=YES;
        pin.draggable=YES;
    }
    else{
        pin.annotation=annotation;
    }

    return pin;
}

По аналогии с ячейками таблицы создаем пин.

Для того, чтобы была возможность перетаскивать пин, необходимо  определить метод: 

- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view didChangeDragState:(MKAnnotationViewDragState)newState
   fromOldState:(MKAnnotationViewDragState)oldState{
}


Добавим еще одну кнопку и метод обработчик для неё, который выводит все пины на экране (масштабирует и сдвигает карту для этого):

-(void) actionShowAll :(UIBarButtonItem *) sender{
//Создается пустой прямоугольник
    MKMapRect zoomRect= MKMapRectNull;

//в цикле проходим по всем аннотациям    
    for (id<MKAnnotation> annotation in self.mapView.annotations) {
//сохраняем координаты текущей аннотации
        CLLocationCoordinate2D location =annotation.coordinate;
//переводим координаты текущей аннотации в 2д формат координат
        MKMapPoint center=MKMapPointForCoordinate(location);
//определяем дельту для квадрата с аннотацией.
        static double delta = 20000;
//создаем вокруг аннотации квадрат +-дельта        
        MKMapRect rect=MKMapRectMake(center.x-delta, center.y-delta, delta*2, delta*2);
//объединяем в один прямоугольник
        zoomRect =MKMapRectUnion(zoomRect, rect);
    }

//исправляем полученный прямоугольник для вывода его на экран
    
    zoomRect=[self.mapView mapRectThatFits:zoomRect];
//выводим на экран получившийся прямоугольник с отступами 50 со всех сторон    
    [self.mapView setVisibleMapRect:zoomRect edgePadding:UIEdgeInsetsMake(50, 50, 50, 50) animated:YES];

}

Изменим метод для перемещения пина, таким образом, чтобы после завершения перемещения пина выводилась его координаты:

- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view didChangeDragState:(MKAnnotationViewDragState)newState
   fromOldState:(MKAnnotationViewDragState)oldState{
//если состояние = конец перемещния
    if (newState==MKAnnotationViewDragStateEnding) {

//получаем координаты в градусах из аннотации
        CLLocationCoordinate2D location=view.annotation.coordinate;
//преобразуем в плоские координаты
        MKMapPoint point = MKMapPointForCoordinate(location);
//выводим координаты в градусах и в MKMapPoint
        NSLog(@"\nLocation={%@, %@}\nPoint = %@",@(location.latitude),@(location.longitude),MKStringFromMapPoint(point));
    }

}



Исходный текст этого урока здесь.
Продолжение в следующем уроке


Дополнение:

Для того, чтобы выводился запрос на показ локации необходимо
Объявить переменную:

@property (nonatomic,strong) CLLocationManager *locationManager;

Подписать ViewController на еще один протокол CLLocationManagerDelegate:

@interface ViewController () <MKMapViewDelegate,CLLocationManagerDelegate>

вставить следующий код в viewDidLoad:

    self.mapView.showsUserLocation=YES;
    self.locationManager = [[CLLocationManager alloc] init];
    self.locationManager.delegate = self;
    self.mapView.delegate = self;

    [self.locationManager requestWhenInUseAuthorization];
Так же в info.plist необходимо добавить новое поле типа String:
NSLocationWhenInUseUsageDescription с приветствием которое будет выводиться в запросе геопозиции, например: MegaMap would like to use your location.