В марте этого года компания Dark Sky и Forecast.io представили Forecast. Прогноз — это простой API погоды, который предоставляет как краткосрочные, так и долгосрочные данные о погоде. В этой серии я покажу вам, как создать красивое погодное приложение для iOS на базе Forecast.
Вступление
Ранее в этом году компания Dark Sky представила Forecast — простой, но мощный API для прогнозирования погоды , который обеспечивает краткосрочные и долгосрочные прогнозы погоды. В этой серии мы создадим приложение для iOS на основе API Forecast. Прогноз бесплатен до тысячи вызовов API в день, поэтому не стесняйтесь зарегистрироваться в аккаунте разработчика и подписаться на меня.
Несмотря на то, что доступно несколько оболочек с открытым исходным кодом для API прогноза, в этой серии мы будем использовать библиотеку AFNetworking для запроса API прогноза. В первой части этой серии мы создадим основу проекта и реализуем базовый пользовательский интерфейс. Несмотря на то, что наше приложение простое по объему, оно будет поддерживать несколько мест, и именно на этом мы сосредоточимся в этой статье.
1. Настройка проекта
Шаг 1: Создание проекта
Запустите Xcode и создайте новый проект на основе шаблона Empty Application (рисунок 1). Назовите приложение Rain и включите автоматический подсчет ссылок (рисунок 2).
Шаг 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
из командной строки.
Шаг 3: Добавление зависимостей
Прежде чем мы сможем продолжить, нам нужно связать наш проект с несколькими структурами. Библиотека AFNetworking зависит от платформ Mobile Core Services и System Configuration, а библиотека ViewDeck использует платформу Quartz Core . Выберите проект в Навигаторе проектов слева, выберите цель Rain в списке целей, откройте вкладку « Фазы сборки » вверху и разверните панель « Связать двоичные файлы с библиотеками» . Нажмите кнопку «плюс», чтобы связать ваш проект с вышеупомянутыми платформами. Чуть позже в этой статье мы будем использовать базовую платформу Location Location, поэтому сейчас самое время связать ваш проект с этой платформой (рисунок 3).
Шаг 4. Редактирование файла скомпилированного заголовка
Прежде чем мы начнем реализовывать базовую структуру нашего погодного приложения, рекомендуется отредактировать предварительно скомпилированный заголовочный файл нашего проекта. Добавьте оператор импорта для каждой из платформ, которые мы добавили в наш проект недавно, и сделайте то же самое для библиотек 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
|
2. Укладка фундамента
Концепция и структура приложения просты. Приложение управляет тремя представлениями: (1) представлением в центре, показывающим текущую погоду для определенного местоположения, представлением справа, показывающим погоду на следующие несколько дней, и представлением слева со списком местоположений. Основная структура будет иметь больше смысла, как только мы ее осуществим. Для создания этой структуры мы используем потрясающую библиотеку ViewDeck , созданную и поддерживаемую Томом Адрианссеном . Библиотека ViewDeck является одной из самых мощных реализаций шаблона проектирования скользящего представления, первоначально представленного в Facebook для iOS.
Шаг 1: Просмотр контроллеров
Прежде чем использовать библиотеку ViewDeck, нам нужно создать классы контроллера представления, которые будут управлять тремя представлениями, которые я упоминал в предыдущем абзаце. Создайте три подкласса UIViewController
именами MTWeatherViewController
, MTForecastViewController
и MTLocationsViewController
соответственно (рисунок 4). Не забудьте создать пользовательский интерфейс или файл XIB для каждого класса (рисунок 4).
Шаг 2: Создание View Deck Controller
Откройте 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 в действии. Несмотря на то, что представления контроллеров представления пусты, фундаментальная структура приложения готова.
3. Добавление местоположений
Как я уже упоминал во введении, в этом руководстве мы также добавим возможность добавлять местоположения в список местоположений, управляемых приложением. Класс MTLocationsViewController
отвечает за управление списком местоположений и представление их в виде таблицы. Пользователь может добавить текущее местоположение в список местоположений, коснувшись первой строки представления таблицы, которая будет помечена как «Добавить текущее местоположение». Добавление нового местоположения с платформой Core Location является обязанностью класса MTWeatherViewController
как мы увидим через несколько минут.
Это оставляет нас с проблемой. Как контроллер погодного представления уведомляется, когда пользователь коснулся первого ряда контроллера представления местоположений? Центр уведомлений? Делегирование — лучший выбор в этом контексте. Всякий раз, когда я сталкиваюсь с необходимостью односторонней связи между двумя объектами, я склоняюсь к делегированию в пользу уведомлений. Протокол делегата гораздо проще обернуть вокруг себя, а расширить его так же просто, как объявить другой метод.
Другой вариант — передать ссылку на контроллер отображения погоды в контроллер отображения местоположения, но мне не нравится этот тип жесткой связи. Тесная связь делает код менее пригодным для повторного использования и приводит к излишне сложной иерархии объектов, когда база кода со временем увеличивается. Делегация — правильный выбор для этой проблемы.
Шаг 1: Объявление протокола делегата
Откройте 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
|
Шаг 2: Добавление представления таблицы
Пересмотрите 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).
Шаг 3: Заполнение табличного представления
Прежде чем мы реализуем протоколы 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:
мы сообщаем контроллеру деки представления закрыть левое представление, что означает, что центральное представление снова становится видимым.
Шаг 4: Ключи и Константы
Прежде чем мы продолжим заполнять табличное представление местоположений, мы должны обратить внимание на некоторые лучшие практики. В настоящее время мы используем строковые литералы для доступа к значениям словаря местоположения. Несмотря на то, что это прекрасно работает, для этой цели лучше и безопаснее использовать строковые константы. Чтобы сделать все это простым и понятным, мы объявляем строковые константы в центральном месте. Позвольте мне показать вам, как это работает.
Создайте подкласс 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]];
}
|
Шаг 5: Назначение делегата
Как я упоминал ранее, экземпляр 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__);
}
|
Шаг 6: выбор текущего местоположения
Наконец пришло время получить текущее местоположение устройства. Добавьте расширение класса в верхней части 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];
}
}
|
Шаг 7: Добавление метки
В этом уроке мы не будем тратить много времени на пользовательский интерфейс, но чтобы убедиться, что все работает так, как мы ожидаем, было бы неплохо получить некоторую визуальную обратную связь, добавив метку к виду контроллера погодных условий, отображающему выбранное местоположение. Откройте 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) мы обновляем метку новым местоположением, как показано ниже.
1
2
3
4
|
— (void)updateView {
// Update Location Label
[self.labelLocation setText:[self.location objectForKey:MTLocationKeyCity]];
}
|
Шаг 8: Реализация протокола делегата
Благодаря работе, которую мы проделали до настоящего времени, реализация двух методов протокола 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;
}
|
4. Последние штрихи
Прежде чем закончить первый выпуск этой серии, нам нужно добавить несколько последних штрихов. Когда пользователь запускает приложение, например, свойство 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), что значительно упрощает тестирование приложений на основе местоположения, таких как созданное нами.
Вывод
Хотя мы даже не касались API Forecast, в этой статье мы проделали немалую работу. Я надеюсь, что вы попробовали CocoaPods и убедились в его мощности и гибкости. В следующей части этой серии мы сосредоточимся на API Forecast и библиотеке AFNetworking.