Статьи

Создание приложения с прогнозом погоды — Настройка проекта

В марте этого года компания Dark Sky и Forecast.io представили Forecast. Прогноз — это простой API погоды, который предоставляет как краткосрочные, так и долгосрочные данные о погоде. В этой серии я покажу вам, как создать красивое погодное приложение для iOS на базе Forecast.


Ранее в этом году компания Dark Sky представила Forecast — простой, но мощный API для прогнозирования погоды , который обеспечивает краткосрочные и долгосрочные прогнозы погоды. В этой серии мы создадим приложение для iOS на основе API Forecast. Прогноз бесплатен до тысячи вызовов API в день, поэтому не стесняйтесь зарегистрироваться в аккаунте разработчика и подписаться на меня.

Несмотря на то, что доступно несколько оболочек с открытым исходным кодом для API прогноза, в этой серии мы будем использовать библиотеку AFNetworking для запроса API прогноза. В первой части этой серии мы создадим основу проекта и реализуем базовый пользовательский интерфейс. Несмотря на то, что наше приложение простое по объему, оно будет поддерживать несколько мест, и именно на этом мы сосредоточимся в этой статье.


Запустите Xcode и создайте новый проект на основе шаблона Empty Application (рисунок 1). Назовите приложение Rain и включите автоматический подсчет ссылок (рисунок 2).

Создание приложений Weather для iOS с Forecast.io: Часть 1. Настройка проекта
Рисунок 1: Настройка проекта
Создание приложений погоды для iOS с Forecast.io: Часть 1. Настройка проекта
Рисунок 2: Настройка проекта

Для этого проекта мы будем использовать три библиотеки с открытым исходным кодом: SVProgressHUD, созданный Сэмом Верметтом , AFNetworking, созданный Мэттом Томпсоном , и ViewDeck, созданный Томом Адриаенсеном .

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

Закройте ваш проект , перейдите к корню проекта и создайте файл с именем Podfile . Откройте Podfile в вашем любимом текстовом редакторе и замените его содержимое фрагментом ниже. В файле модуля проекта мы указываем платформу, цель развертывания и модули, которые мы хотим включить в проект.

1
2
3
4
5
platform :ios, ‘6.0’
 
pod ‘ViewDeck’, ‘~> 2.2.11’
pod ‘AFNetworking’, ‘~> 1.2.1’
pod ‘SVProgressHUD’, ‘~> 0.9.0’

Откройте приложение Terminal , перейдите к корню проекта и установите библиотеки, выполнив pod install . В дополнение к установке трех модулей, которые мы указали в файле модуля проекта, CocoaPods уже создала для нас рабочее пространство Xcode. Откройте новое рабочее пространство, выполнив команду open Rain.xcworkspace из командной строки.

Прежде чем мы сможем продолжить, нам нужно связать наш проект с несколькими структурами. Библиотека AFNetworking зависит от платформ Mobile Core Services и System Configuration, а библиотека ViewDeck использует платформу Quartz Core . Выберите проект в Навигаторе проектов слева, выберите цель Rain в списке целей, откройте вкладку « Фазы сборки » вверху и разверните панель « Связать двоичные файлы с библиотеками» . Нажмите кнопку «плюс», чтобы связать ваш проект с вышеупомянутыми платформами. Чуть позже в этой статье мы будем использовать базовую платформу Location Location, поэтому сейчас самое время связать ваш проект с этой платформой (рисунок 3).

Создание приложений погоды для iOS с Forecast.io: Часть 1. Связывание проекта с горсткой фреймворков
Рисунок 3: Связывание проекта с горсткой фреймворков

Прежде чем мы начнем реализовывать базовую структуру нашего погодного приложения, рекомендуется отредактировать предварительно скомпилированный заголовочный файл нашего проекта. Добавьте оператор импорта для каждой из платформ, которые мы добавили в наш проект недавно, и сделайте то же самое для библиотек AFNetworking, SVProgressHUD и ViewDeck.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
#import <Availability.h>
 
#ifndef __IPHONE_3_0
#warning «This project uses features only available in iOS SDK 3.0 and later.»
#endif
 
#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    #import <QuartzCore/QuartzCore.h>
    #import <CoreLocation/CoreLocation.h>
    #import <MobileCoreServices/MobileCoreServices.h>
    #import <SystemConfiguration/SystemConfiguration.h>
 
    #import «AFNetworking.h»
    #import «SVProgressHUD.h»
    #import «IIViewDeckController.h»
#endif

Концепция и структура приложения просты. Приложение управляет тремя представлениями: (1) представлением в центре, показывающим текущую погоду для определенного местоположения, представлением справа, показывающим погоду на следующие несколько дней, и представлением слева со списком местоположений. Основная структура будет иметь больше смысла, как только мы ее осуществим. Для создания этой структуры мы используем потрясающую библиотеку ViewDeck , созданную и поддерживаемую Томом Адрианссеном . Библиотека ViewDeck является одной из самых мощных реализаций шаблона проектирования скользящего представления, первоначально представленного в Facebook для iOS.

Прежде чем использовать библиотеку ViewDeck, нам нужно создать классы контроллера представления, которые будут управлять тремя представлениями, которые я упоминал в предыдущем абзаце. Создайте три подкласса UIViewController именами MTWeatherViewController , MTForecastViewController и MTLocationsViewController соответственно (рисунок 4). Не забудьте создать пользовательский интерфейс или файл XIB для каждого класса (рисунок 4).

Создание приложений Weather для iOS с Forecast.io: Часть 1. Создание трех классов
Рисунок 4: Создание трех классов контроллера основного представления

Откройте MTAppDelegate.m и импортируйте файл заголовка для трех подклассов UIViewController . Добавьте расширение класса и создайте свойство типа IIViewDeckController и назовите его viewDeckController . Причина этого станет ясна через мгновение.

01
02
03
04
05
06
07
08
09
10
11
#import «MTAppDelegate.h»
 
#import «MTWeatherViewController.h»
#import «MTForecastViewController.h»
#import «MTLocationsViewController.h»
 
@interface MTAppDelegate ()
 
@property (strong, nonatomic) IIViewDeckController *viewDeckController;
 
@end

В application:didFinishLaunchingWithOptions: мы начинаем с создания экземпляра каждого из трех подклассов UIViewController . Затем мы инициализируем экземпляр класса IIViewDeckController и передаем объекты контроллера представления в качестве аргументов initWithCenterViewController:leftViewController:rightViewController: Как указывает инициализатор, контроллер представления колоды, который мы создаем, управляет центральным, левым и правым контроллером представления. Остальная часть реализации application:didFinishLaunchingWithOptions: должна быть вам знакома. Мы инициализируем окно приложения и устанавливаем контроллер панели представления в качестве его корневого контроллера представления.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
— (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Initialize View Controllers
    MTLocationsViewController *leftViewController = [[MTLocationsViewController alloc] initWithNibName:@»MTLocationsViewController» bundle:nil];
    MTForecastViewController *rightViewController = [[MTForecastViewController alloc] initWithNibName:@»MTForecastViewController» bundle:nil];
    MTWeatherViewController *centerViewController = [[MTWeatherViewController alloc] initWithNibName:@»MTWeatherViewController» bundle:nil];
 
    // Initialize View Deck Controller
    self.viewDeckController = [[IIViewDeckController alloc] initWithCenterViewController:centerViewController leftViewController:leftViewController rightViewController:rightViewController];
 
    // Initialize Window
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
 
    // Configure Window
    [self.window setRootViewController:self.viewDeckController];
    [self.window makeKeyAndVisible];
 
    return YES;
}

Создайте и запустите приложение в iOS Simulator или на физическом устройстве, чтобы увидеть библиотеку ViewDeck в действии. Несмотря на то, что представления контроллеров представления пусты, фундаментальная структура приложения готова.


Как я уже упоминал во введении, в этом руководстве мы также добавим возможность добавлять местоположения в список местоположений, управляемых приложением. Класс MTLocationsViewController отвечает за управление списком местоположений и представление их в виде таблицы. Пользователь может добавить текущее местоположение в список местоположений, коснувшись первой строки представления таблицы, которая будет помечена как «Добавить текущее местоположение». Добавление нового местоположения с платформой Core Location является обязанностью класса MTWeatherViewController как мы увидим через несколько минут.

Это оставляет нас с проблемой. Как контроллер погодного представления уведомляется, когда пользователь коснулся первого ряда контроллера представления местоположений? Центр уведомлений? Делегирование — лучший выбор в этом контексте. Всякий раз, когда я сталкиваюсь с необходимостью односторонней связи между двумя объектами, я склоняюсь к делегированию в пользу уведомлений. Протокол делегата гораздо проще обернуть вокруг себя, а расширить его так же просто, как объявить другой метод.

Другой вариант — передать ссылку на контроллер отображения погоды в контроллер отображения местоположения, но мне не нравится этот тип жесткой связи. Тесная связь делает код менее пригодным для повторного использования и приводит к излишне сложной иерархии объектов, когда база кода со временем увеличивается. Делегация — правильный выбор для этой проблемы.

Откройте MTLocationsViewController.h и обновите файл заголовка, как показано ниже. Мы создаем свойство для делегата контроллера представления и объявляем протокол MTLocationsViewControllerDelegate . Протокол определяет два метода: (1) controllerShouldAddCurrentLocation: который вызывается при нажатии на первую строку в табличном представлении контроллера представления, и (2) controller:didSelectLocation: который вызывается, когда пользователь выбирает местоположение из списка местах.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
#import <UIKit/UIKit.h>
 
@protocol MTLocationsViewControllerDelegate;
 
@interface MTLocationsViewController : UIViewController
 
@property (weak, nonatomic) id<MTLocationsViewControllerDelegate> delegate;
 
@end
 
@protocol MTLocationsViewControllerDelegate <NSObject>
— (void)controllerShouldAddCurrentLocation:(MTLocationsViewController *)controller;
— (void)controller:(MTLocationsViewController *)controller didSelectLocation:(NSDictionary *)location;
@end

Пересмотрите MTLocationsViewController.h еще раз. Создайте выход для табличного представления контроллера представления и убедитесь, что класс MTLocationsViewController соответствует протоколам UITableViewDataSource и UITableViewDelegate .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
#import <UIKit/UIKit.h>
 
@protocol MTLocationsViewControllerDelegate;
 
@interface MTLocationsViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
 
@property (weak, nonatomic) id<MTLocationsViewControllerDelegate> delegate;
 
@property (weak, nonatomic) IBOutlet UITableView *tableView;
 
@end
 
@protocol MTLocationsViewControllerDelegate <NSObject>
— (void)controllerShouldAddCurrentLocation:(MTLocationsViewController *)controller;
— (void)controller:(MTLocationsViewController *)controller didSelectLocation:(NSDictionary *)location;
@end

Откройте MTLocationsViewController.xib , добавьте табличное представление к представлению контроллера представления и установите для dataSource табличного представления и delegate выходы объект- владелец файла . Выберите объект «Владелец файла» и соедините его выход tableView с представлением таблицы, которое мы добавили к представлению контроллера представления (рисунок 5).

Создание приложений для iOS с Forecast.io: Часть 1. Добавление табличного представления для получения списка местоположений
Рисунок 5: Добавление табличного представления к списку местоположений

Прежде чем мы реализуем протоколы UITableViewDataSource и UITableViewDelegate , нам нужно создать свойство, которое будет служить источником данных табличного представления. Создайте расширение класса в верхней части MTLocationsViewController.m и создайте свойство с именем locations типа NSMutableArray . Он будет хранить местоположения, управляемые нашим приложением.

1
2
3
4
5
6
7
#import «MTLocationsViewController.h»
 
@interface MTLocationsViewController ()
 
@property (strong, nonatomic) NSMutableArray *locations;
 
@end

Реализация протоколов UITableViewDataSource и UITableViewDelegate довольно проста. Мы начнем с объявления статической строковой константы для идентификатора повторного использования ячейки. В методе viewDidLoad контроллера представления мы вызываем setupView , вспомогательный метод, в котором мы настраиваем пользовательский интерфейс контроллера представления. В setupView мы говорим табличному представлению использовать класс UITableViewCell для создания новых ячеек табличного представления для идентификатора повторного использования, который мы объявили ранее.

1
static NSString *LocationCell = @»LocationCell»;
1
2
3
4
5
6
— (void)viewDidLoad {
    [super viewDidLoad];
 
    // Setup View
    [self setupView];
}
1
2
3
4
— (void)setupView {
    // Register Class for Cell Reuse
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:LocationCell];
}

Реализация протоколов UITableViewDataSource и UITableViewDelegate тривиальна, как вы можете видеть ниже. Две детали реализации требуют небольшого пояснения. Мы храним каждое местоположение как словарь с четырьмя ключами: (1) город, (2) страна, (3) широта и (4) долгота. Имея это в виду, реализация configureCell:atIndexPath: должна стать немного понятнее. Обратите внимание, что configureCell:atIndexPath: является не чем иным, как другим вспомогательным методом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}
 
— (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return ([self.locations count] + 1);
}
 
— (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LocationCell forIndexPath:indexPath];
 
    // Configure Cell
    [self configureCell:cell atIndexPath:indexPath];
 
    return cell;
}
 
— (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row == 0) {
        [cell.textLabel setText:@»Add Current Location»];
 
    } else {
        // Fetch Location
        NSDictionary *location = [self.locations objectAtIndex:(indexPath.row — 1)];
 
        // Configure Cell
        [cell.textLabel setText:[NSString stringWithFormat:@»%@, %@», location[@»city»], location[@»country»]]];
    }
}
 
— (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return NO;
}
 
— (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
    return NO;
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
— (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
 
    if (indexPath.row == 0) {
        // Notify Delegate
        [self.delegate controllerShouldAddCurrentLocation:self];
 
    } else {
        // Fetch Location
        NSDictionary *location = [self.locations objectAtIndex:(indexPath.row — 1)];
 
        // Notify Delegate
        [self.delegate controller:self didSelectLocation:location];
    }
 
    // Show Center View Controller
    [self.viewDeckController closeLeftViewAnimated:YES];
}

tableView:didSelectRowAtIndexPath: также требует краткого объяснения. Если пользователь касается первой строки, помеченной как « Добавить текущее местоположение» , делегат уведомляется о том, что текущее местоположение должно быть добавлено в список местоположений. Если controller:didSelectLocation: любой другой строки в табличном представлении, соответствующее местоположение передается в качестве второго аргумента controller:didSelectLocation: другой метод делегата протокола делегирования MTLocationsViewController . Это информирует делегата о том, что приложение должно установить новое местоположение, поскольку приложение не является местоположением по умолчанию, и данные о погоде для этого местоположения должны быть получены.

Последняя строка tableView:didSelectRowAtIndexPath: также стоит упомянуть. Экземпляр IIViewDeckController назначает себя контроллерам представления, IIViewDeckController он управляет. Свойство viewDeckController обеспечивает доступ к контроллеру viewDeckController представления контроллера вида, что удобно, если вам нужен доступ к деке контроллера изображения. Это работает очень похоже на свойство navigationController экземпляра контроллера представления. В последней строке tableView:didSelectRowAtIndexPath: мы сообщаем контроллеру деки представления закрыть левое представление, что означает, что центральное представление снова становится видимым.

Прежде чем мы продолжим заполнять табличное представление местоположений, мы должны обратить внимание на некоторые лучшие практики. В настоящее время мы используем строковые литералы для доступа к значениям словаря местоположения. Несмотря на то, что это прекрасно работает, для этой цели лучше и безопаснее использовать строковые константы. Чтобы сделать все это простым и понятным, мы объявляем строковые константы в центральном месте. Позвольте мне показать вам, как это работает.

Создайте подкласс NSObject и назовите его MTConstants . Замените содержимое MTConstants.h и MTConstants.m фрагментами, показанными ниже. Должно быть ясно, что MTConstants не является классом Objective-C. Это не более чем центральное место для хранения набора констант, характерных для нашего проекта.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
#pragma mark —
#pragma mark User Defaults
extern NSString * const MTRainUserDefaultsLocation;
extern NSString * const MTRainUserDefaultsLocations;
 
#pragma mark —
#pragma mark Notifications
extern NSString * const MTRainDidAddLocationNotification;
extern NSString * const MTRainLocationDidChangeNotification;
 
#pragma mark —
#pragma mark Location Keys
extern NSString * const MTLocationKeyCity;
extern NSString * const MTLocationKeyCountry;
extern NSString * const MTLocationKeyLatitude;
extern NSString * const MTLocationKeyLongitude;
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
#import «MTConstants.h»
 
#pragma mark —
#pragma mark User Defaults
NSString * const MTRainUserDefaultsLocation = @»location»;
NSString * const MTRainUserDefaultsLocations = @»locations»;
 
#pragma mark —
#pragma mark Notifications
NSString * const MTRainDidAddLocationNotification = @»com.mobileTuts.MTRainDidAddLocationNotification»;
NSString * const MTRainLocationDidChangeNotification = @»com.mobileTuts.MTRainLocationDidChangeNotification»;
 
#pragma mark —
#pragma mark Location Keys
NSString * const MTLocationKeyCity = @»city»;
NSString * const MTLocationKeyCountry = @»country»;
NSString * const MTLocationKeyLatitude = @»latitude»;
NSString * const MTLocationKeyLongitude = @»longitude»;

Чтобы сделать MTConstants действительно полезным, добавьте оператор импорта для MTConstants.h в предварительно скомпилированный заголовочный файл вашего проекта, чтобы константы, объявленные в MTConstants были доступны по всему проекту.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
#import <Availability.h>
 
#ifndef __IPHONE_3_0
#warning «This project uses features only available in iOS SDK 3.0 and later.»
#endif
 
#ifdef __OBJC__
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    #import <QuartzCore/QuartzCore.h>
    #import <CoreLocation/CoreLocation.h>
    #import <MobileCoreServices/MobileCoreServices.h>
    #import <SystemConfiguration/SystemConfiguration.h>
 
    #import «AFNetworking.h»
    #import «SVProgressHUD.h»
    #import «IIViewDeckController.h»
 
    #import «MTConstants.h»
#endif

Теперь мы можем обновить configureCell:atIndexPath: ( MTLocationsViewController.m ), как показано ниже. Мало того, что это даст завершение использования кода и очень, очень небольшой выигрыш в производительности, истинное преимущество этой лучшей практики заключается в том, что компилятор предупредит нас в случае опечаток. Я уверен, что мне не нужно говорить вам, что опечатки являются одной из наиболее распространенных причин ошибок в разработке программного обеспечения.

01
02
03
04
05
06
07
08
09
10
11
12
— (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row == 0) {
        [cell.textLabel setText:@»Add Current Location»];
 
    } else {
        // Fetch Location
        NSDictionary *location = [self.locations objectAtIndex:(indexPath.row — 1)];
 
        // Configure Cell
        [cell.textLabel setText:[NSString stringWithFormat:@»%@, %@», location[MTLocationKeyCity], location[MTLocationKeyCountry]]];
    }
}

На данный момент свойство locations пусто, как и табличное представление. В initWithNibName:bundle: мы вызываем loadLocations , вспомогательный метод, который загружает массив расположений. В loadLocations загрузите массив местоположений, который хранится в пользовательской базе данных приложения по умолчанию. Обратите внимание, что мы используем другую строковую константу, которую мы объявили в MTConstants.h .

01
02
03
04
05
06
07
08
09
10
— (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
 
    if (self) {
        // Load Locations
        [self loadLocations];
    }
 
    return self;
}
1
2
3
— (void)loadLocations {
    self.locations = [NSMutableArray arrayWithArray:[[NSUserDefaults standardUserDefaults] objectForKey:MTRainUserDefaultsLocations]];
}

Как я упоминал ранее, экземпляр MTWeatherViewController будет служить делегатом контроллера представления местоположений. Повторно посетите MTAppDelegate.m и обновите application:didFinishLaunchingWithOptions: как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
— (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Initialize View Controllers
    MTLocationsViewController *leftViewController = [[MTLocationsViewController alloc] initWithNibName:@»MTLocationsViewController» bundle:nil];
    MTForecastViewController *rightViewController = [[MTForecastViewController alloc] initWithNibName:@»MTForecastViewController» bundle:nil];
    MTWeatherViewController *centerViewController = [[MTWeatherViewController alloc] initWithNibName:@»MTWeatherViewController» bundle:nil];
 
    // Configure Locations View Controller
    [leftViewController setDelegate:centerViewController];
 
    // Initialize View Deck Controller
    self.viewDeckController = [[IIViewDeckController alloc] initWithCenterViewController:centerViewController leftViewController:leftViewController rightViewController:rightViewController];
 
    // Initialize Window
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
 
    // Configure Window
    [self.window setRootViewController:self.viewDeckController];
    [self.window makeKeyAndVisible];
 
    return YES;
}

После внесения этого изменения должно немедленно MTWeatherViewController предупреждение о том, что MTWeatherViewController не соответствует протоколу MTLocationsViewControllerDelegate . Компилятор прав, так что давайте исправим это.

Откройте MTWeatherViewController.h , импортируйте файл заголовка MTLocationsViewController и MTWeatherViewController соответствие MTWeatherViewController с протоколом MTLocationsViewControllerDelegate .

1
2
3
4
5
6
7
#import <UIKit/UIKit.h>
 
#import «MTLocationsViewController.h»
 
@interface MTWeatherViewController : UIViewController <MTLocationsViewControllerDelegate>
 
@end

Подождите. Еще одно предупреждение? Мы еще не реализовали два обязательных метода протокола делегата, отсюда и предупреждение. Откройте MTWeatherViewController.m и добавьте реализацию-заглушку для каждого из методов делегата.

1
2
3
4
5
6
7
— (void)controllerShouldAddCurrentLocation:(MTLocationsViewController *)controller {
    NSLog(@»%s», __PRETTY_FUNCTION__);
}
 
— (void)controller:(MTLocationsViewController *)controller didSelectLocation:(NSDictionary *)location {
    NSLog(@»%s», __PRETTY_FUNCTION__);
}

Наконец пришло время получить текущее местоположение устройства. Добавьте расширение класса в верхней части MTWeatherViewController.m и объявите два свойства: (1) location ( NSDictionary ) для хранения местоположения приложения по умолчанию и (2) locationManager ( CLLocationManager ), который мы будем использовать для извлечения местоположения устройства. MTWeatherViewController класс CLLocationManagerDelegate протоколом CLLocationManagerDelegate и объявите переменную экземпляра с именем _locationFound типа BOOL . Цель _locationFound станет ясна через несколько минут.

01
02
03
04
05
06
07
08
09
10
11
#import «MTWeatherViewController.h»
 
@interface MTWeatherViewController () <CLLocationManagerDelegate> {
    BOOL _locationFound;
}
 
@property (strong, nonatomic) NSDictionary *location;
 
@property (strong, nonatomic) CLLocationManager *locationManager;
 
@end

В указанном инициализаторе класса initWithNibName:bundle: мы инициализируем и настраиваем менеджер местоположений. Мы назначаем контроллер представления в качестве делегата диспетчера местоположений и устанавливаем свойство accuracy диспетчера местоположений в kCLLocationAccuracyKilometer . Нет необходимости в большей точности, поскольку нам нужно только местоположение для данных о погоде.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
— (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
 
    if (self) {
        // Initialize Location Manager
        self.locationManager = [[CLLocationManager alloc] init];
 
        // Configure Location Manager
        [self.locationManager setDelegate:self];
        [self.locationManager setDesiredAccuracy:kCLLocationAccuracyKilometer];
    }
 
    return self;
}

Следующая часть головоломки реализует, locationManager:didUpdateLocations: один из методов протокола CLLocationManagerDelegate , который вызывается каждый раз, когда менеджер местоположений обновляет местоположение устройства. Второй аргумент locationManager:didUpdateLocations: массив экземпляров CLLocation . Реализация locationManager:didUpdateLocations: также раскрывает назначение _locationFound . Несмотря на то, что мы сообщаем менеджеру местоположения прекратить обновление местоположения, как только вызывается locationManager:didUpdateLocations: нередко другое обновление местоположения вызывает locationManager:didUpdateLocations: снова, даже после отправки менеджеру местоположения сообщения stopUpdatingLocation , Если это произойдет, то одно и то же место будет добавлено дважды в список мест. Простое решение — использовать вспомогательную переменную _locationFound , которая отслеживает состояние, в котором мы находимся.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
— (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
    if (![locations count] || _locationFound) return;
 
    // Stop Updating Location
    _locationFound = YES;
    [manager stopUpdatingLocation];
 
    // Current Location
    CLLocation *currentLocation = [locations objectAtIndex:0];
 
    // Reverse Geocode
    CLGeocoder *geocoder = [[CLGeocoder alloc] init];
    [geocoder reverseGeocodeLocation:currentLocation completionHandler:^(NSArray *placemarks, NSError *error) {
        if ([placemarks count]) {
            _locationFound = NO;
            [self processPlacemark:[placemarks objectAtIndex:0]];
        }
    }];
}

Мы извлекаем первое местоположение из массива местоположений и используем класс CLGeocoder для обратного геокодирования этого местоположения. Обратное геокодирование просто означает выяснение названия (ближайшего) города и страны местонахождения. Обработчик завершения reverseGeocodeLocation: возвращает массив меток. Объект метки — это не что иное, как контейнер для хранения данных о местоположении для координаты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
— (void)processPlacemark:(CLPlacemark *)placemark {
    // Extract Data
    NSString *city = [placemark locality];
    NSString *country = [placemark country];
    CLLocationDegrees lat = placemark.location.coordinate.latitude;
    CLLocationDegrees lon = placemark.location.coordinate.longitude;
 
    // Create Location Dictionary
    NSDictionary *currentLocation = @{ MTLocationKeyCity : city,
                                       MTLocationKeyCountry : country,
                                       MTLocationKeyLatitude : @(lat),
                                       MTLocationKeyLongitude : @(lon) };
 
    // Add to Locations
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    NSMutableArray *locations = [NSMutableArray arrayWithArray:[ud objectForKey:MTRainUserDefaultsLocations]];
    [locations addObject:currentLocation];
    [locations sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:MTLocationKeyCity ascending:YES]]];
    [ud setObject:locations forKey:MTRainUserDefaultsLocations];
 
    // Synchronize
    [ud synchronize];
 
    // Update Current Location
    self.location = currentLocation;
 
    // Post Notifications
    NSNotification *notification2 = [NSNotification notificationWithName:MTRainDidAddLocationNotification object:self userInfo:currentLocation];
    [[NSNotificationCenter defaultCenter] postNotification:notification2];
}

В processPlacemark: мы извлекаем processPlacemark: город, страну, широту и долготу, сохраняем их в словаре и обновляем массив местоположений в базе данных по умолчанию для пользователя приложения. Обратите внимание, что мы сортируем массив местоположений перед обновлением пользовательской базы данных по умолчанию. Свойство location контроллера представления обновляется новым местоположением, и отправляется уведомление, чтобы уведомить любой объект, заинтересованный в этом событии.

Это еще не все. Я также переопределил установщик свойства location контроллера представления. Поскольку класс MTWeatherViewController отвечает за добавление новых расположений, мы можем делегировать несколько дополнительных обязанностей этому классу, например, обновить расположение по умолчанию в базе данных пользователя по умолчанию. Поскольку другим частям приложения также необходимо знать об изменении местоположения, MTRainLocationDidChangeNotification уведомление с именем MTRainLocationDidChangeNotification . Мы также вызываем updateView , еще один вспомогательный метод, который мы вскоре реализуем.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
— (void)setLocation:(NSDictionary *)location {
    if (_location != location) {
        _location = location;
 
        // Update User Defaults
        NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
        [ud setObject:location forKey:MTRainUserDefaultsLocation];
        [ud synchronize];
 
        // Post Notification
        NSNotification *notification1 = [NSNotification notificationWithName:MTRainLocationDidChangeNotification object:self userInfo:location];
        [[NSNotificationCenter defaultCenter] postNotification:notification1];
 
        // Update View
        [self updateView];
    }
}

В этом уроке мы не будем тратить много времени на пользовательский интерфейс, но чтобы убедиться, что все работает так, как мы ожидаем, было бы неплохо получить некоторую визуальную обратную связь, добавив метку к виду контроллера погодных условий, отображающему выбранное местоположение. Откройте MTWeatherViewController.h и создайте выход типа UILabel и назовите его labelLocation .

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>
 
#import «MTLocationsViewController.h»
 
@interface MTWeatherViewController : UIViewController <MTLocationsViewControllerDelegate>
 
@property (weak, nonatomic) IBOutlet UILabel *labelLocation;
 
@end

Откройте MTWeatherViewController.xib , добавьте метку в представление контроллера представления и подключите розетку с вновь добавленной меткой (рисунок 6). В updateView ( MTWeatherViewController.m) мы обновляем метку новым местоположением, как показано ниже.

Создание приложений погоды для iOS с Forecast.io: Часть 1. Добавление метки местоположения в контроллер представления погоды
Рисунок 6: Добавление метки расположения к контроллеру Weather View
1
2
3
4
— (void)updateView {
    // Update Location Label
    [self.labelLocation setText:[self.location objectForKey:MTLocationKeyCity]];
}

Благодаря работе, которую мы проделали до настоящего времени, реализация двух методов протокола MTLocationsViewControllerDelegate простой. В controllerShouldAddCurrentLocation: мы сообщаем менеджеру местоположения начать обновление местоположения. В controller:didSelectLocation: мы устанавливаем свойство location контроллера представления в местоположение, выбранное пользователем в контроллере представления местоположений, что, в свою очередь, вызывает метод сеттера, который мы переопределили чуть ранее.

1
2
3
4
5
6
7
8
9
— (void)controllerShouldAddCurrentLocation:(MTLocationsViewController *)controller {
    // Start Updating Location
    [self.locationManager startUpdatingLocation];
}
 
— (void)controller:(MTLocationsViewController *)controller didSelectLocation:(NSDictionary *)location {
    // Update Location
    self.location = location;
}

Прежде чем закончить первый выпуск этой серии, нам нужно добавить несколько последних штрихов. Когда пользователь запускает приложение, например, свойство MTWeatherViewController класса MTWeatherViewController должно быть установлено в местоположение, сохраненное в пользовательских настройках приложения по умолчанию. Кроме того, когда пользователь запускает наше приложение в первый раз, местоположение по умолчанию еще не установлено в пользовательских настройках приложения. Это не большая проблема, но для обеспечения хорошего взаимодействия с пользователем было бы лучше автоматически выбирать текущее местоположение пользователя при первом запуске приложения.

Мы можем внести оба изменения, внеся изменения в viewDidLoad контроллера viewDidLoad как показано ниже. Свойство location контроллера представления установлено на местоположение, сохраненное в базе данных пользователя по умолчанию. Если местоположение не найдено, то есть self.location равно nil , мы сообщаем менеджеру местоположения начать обновление местоположения. Другими словами, когда приложение запускается в первый раз, текущее местоположение автоматически извлекается и сохраняется.

01
02
03
04
05
06
07
08
09
10
— (void)viewDidLoad {
    [super viewDidLoad];
 
    // Load Location
    self.location = [[NSUserDefaults standardUserDefaults] objectForKey:MTRainUserDefaultsLocation];
 
    if (!self.location) {
        [self.locationManager startUpdatingLocation];
    }
}

Есть еще один свободный конец, который мы должны связать. Когда новое местоположение добавляется в массив местоположений, контроллер представления погоды публикует уведомление. Нам нужно обновить класс MTLocationsViewController чтобы он добавлялся в качестве наблюдателя для этих уведомлений. Таким образом, контроллер представления местоположений может обновлять свое табличное представление всякий раз, когда добавляется новое местоположение.

Повторно посетите MTLocationsViewController.m и обновите initWithNibName:bundle: как показано ниже. Мы добавляем контроллер представления в качестве наблюдателя для уведомлений с именем MTRainDidAddLocationNotification . Реализация didAddLocation: проста, то есть мы добавляем новое местоположение в массив местоположений, сортируем массив по городам и перезагружаем табличное представление.

01
02
03
04
05
06
07
08
09
10
11
12
13
— (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
 
    if (self) {
        // Load Locations
        [self loadLocations];
 
        // Add Observer
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didAddLocation:) name:MTRainDidAddLocationNotification object:nil];
    }
 
    return self;
}
1
2
3
4
5
6
— (void)didAddLocation:(NSNotification *)notification {
    NSDictionary *location = [notification userInfo];
    [self.locations addObject:location];
    [self.locations sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:MTLocationKeyCity ascending:YES]]];
    [self.tableView reloadData];
}

Не забудьте удалить контроллер представления в качестве наблюдателя в методе dealloc контроллера представления. Хорошей практикой также является присвоение свойству delegate контроллера delegate nil в dealloc .

1
2
3
4
5
6
7
8
— (void)dealloc {
    // Remove Observer
    [[NSNotificationCenter defaultCenter] removeObserver:self];
 
    if (_delegate) {
        _delegate = nil;
    }
}

Создайте и запустите приложение, чтобы увидеть, как все это работает вместе. Возможно, вы захотите запустить приложение в симуляторе iOS, а не на физическом устройстве, потому что симулятор iOS поддерживает симуляцию местоположения (рисунок 7), что значительно упрощает тестирование приложений на основе местоположения, таких как созданное нами.

Создание приложений для iOS с Forecast.io: Часть 1. Моделирование местоположений с помощью симулятора iOS
Рисунок 7: Имитация местоположений с помощью iOS Simulator

Хотя мы даже не касались API Forecast, в этой статье мы проделали немалую работу. Я надеюсь, что вы попробовали CocoaPods и убедились в его мощности и гибкости. В следующей части этой серии мы сосредоточимся на API Forecast и библиотеке AFNetworking.