15.0. Введение
Уведомления — это объекты, несущие определенную информацию, которая может передаваться множеству получателей методом широковещания. Уведомления очень удобны для разделения работы на относительно самостоятельные фрагменты кода, но при злоупотреблении ими ситуация легко может выйти из-под контроля. Следует понимать границы возможностей при работе с уведомлениями. В этой главе мы подробно поговорим об использовании уведомлений и узнаем, когда лучше обходиться без них.
В iOS доступны уведомления трех типов.
• Обычное уведомление (экземпляр класса NSNotification). Это обычное уведомление. Программа может широковещательно передавать его любым получателям в рамках приложения. iOS также широковещательно направляет вашему приложению уведомления такого типа, пока приложение находится в приоритетном режиме. Таким образом приложение получает информацию о различных системных событиях, происходящих во время его работы, например о выводе виртуальной клавиатуры на экран и ее уходе с экрана. Эти уведомления хорошо подходят для ослабления связанности кода и позволяют аккуратно отделять друг от друга различные компоненты сложного iOS-приложения.
Локальное уведомление (экземпляр класса UILocalNotification). Это уведомление, которое должно быть доставлено вашему приложению в определенный момент времени. Приложение сможет его получить, даже если находится в фоновом режиме или не работает вообще. Если приложение не работало, но получило такое уведомление, то оно запускается. Как правило, вы назначаете локальное уведомление, если хотите гарантированно разбудить приложение (предполагается, что пользователь разрешил вам такое действие, подробнее об этом — в дальнейшем) в определенный момент дня.
Пуш-уведомление. Такое уведомление отсылается на устройство iOS с сервера. Это уведомление выполняется по инициативе сервера, поэтому приложению не приходится опрашивать сервер на наличие таких уведомлений. iOS поддерживает постоянное соединение с серверами APNS (службы Apple для обеспечения пуш-уведомлений). Как только появляется новое пуш-уведомление, iOS обрабатывает сообщение и отсылает его тому приложению, которому это уведомление предназначалось.
Далее мы будем называть обычные уведомления просто уведомлениями. Слово «обычный» в данном контексте избыточно.
Особенность локальных уведомлений заключается в том, что они видны пользователю и пользователь может совершать над ними те или иные действия. iOS зафиксирует действие пользователя, после чего прикажет вашему приложению обработать это действие. В то же время уведомления являются невидимыми элементами. В приложении их можно распространять широковещательным способом, и приложение обязано обрабатывать эти уведомления. Пользователь не обязательно должен быть непосредственно вовлечен в этот процесс, если вы сами не потребуете от него каких-либо действий в результате получения и обработки уведомления. Например, приложение может послать уведомление в другую часть этого же уведомления. По получении такого уведомления в другой части вашего приложения генерируется диалоговое окно с предупреждением. После этого уже требуется вмешательство пользователя: он должен ознакомиться с этим окном и, например, нажать в нем кнопку ОК, чтобы закрыть его. Такое непрямое вовлечение пользователя очень отличается от его непосредственного участия, которое требуется при работе с обычными уведомлениями.
Уведомления — важнейшая составляющая операционных систем iOS и OS X. iOS выдает уведомления, действующие в масштабах всей системы. Эти уведомления адресуются всем приложениям в системе, которые их слушают, и сами приложения также могут отсылать уведомления. Такое уведомление, действующее в масштабах всей системы (также называемое распределенным), может выдаваться только самой системой iOS.
Уведомление — это простая сущность, представленная в iOS SDK классом NSNotification. Уведомление отсылается объектом и может нести информацию. Объект, отсылающий уведомление, «представляет себя» центру уведомления в момент самой отправки уведомления. Затем получатель уведомления может «справиться» об отправителе по его имени класса для получения более подробной информации об этом объекте. Объект-отправитель называется объектом уведомления. Кроме того, уведомление может включать в себя словарь с пользовательской информацией. Это словарная структура данных, которая может нести дополнительную информацию об уведомлении. Если словарь не предоставляется, то этот параметр равен nil.
15.1. Отправка уведомлений
Постановка задачи
Требуется разграничить части вашего приложения и отправить уведомление, которое может быть подхвачено другим компонентом приложения.
Решение
Создайте экземпляр класса NSNotification и широковещательно передайте его вашему приложению, воспользовавшись методом класса postNotification:. Вы можете получить экземпляр центра уведомлений, воспользовавшись его методом класса defaultCenter, вот так:
#import «AppDelegate.h»
NSString *const kNotificationName = @"NotificationNameGoesHere";
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
NSNotification *notification = [NSNotification
notificationWithName: kNotificationName
object: self
userInfo:@{@"Key 1": @"Value 1",
@"Key 2": @2}];
[[NSNotificationCenter defaultCenter] postNotification: notification];
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Обсуждение
Объект уведомления инкапсулируется в экземпляр класса NSNotification. Сам по себе объект уведомления практически ничего не представляет. Чтобы он был полезен, его нужно послать приложению с помощью центра уведомлений. Объект уведомления имеет три важных свойства.
• Имя. Это строка. Когда получатель начинает слушать уведомления, он должен указать имя интересующего его уведомления, как будет показано далее в этой главе. Если вы отправляете уведомление в созданный вами класс, убедитесь, что имя уведомления хорошо документировано. Еще лучше импортировать символ этой строки в файл заголовка. Подобный пример мы рассмотрим чуть позже в этом разделе.
Объект-отправитель. По желанию вы можете указать объект, являющийся отправителем уведомления. Обычно в таком качестве задается self. Но зачем же нужно указывать отправитель уведомления? Эта информация полезна для тех компонентов приложения, которые слушают уведомления. Допустим, в одном из ваших классов вы отправляете уведомление с именем MyNotification, а другой класс приложения отсылает уведомление с точно таким же именем. Когда элемент начинает слушать уведомление с именем MyNotification, получатель может указать, из какого источника ожидается интересующее его уведомление. Так, получатель может указать, что ему требуются все уведомления с именем MyNotification, поступающие от конкретного объекта, но не интересуют одноименные уведомления, приходящие от другого объекта. Таким образом, получатель действительно контролирует ситуацию. Хотя вы даже и можете при отправке уведомления указать вместо объекта-получателя nil, гораздо целесообразнее задавать данному свойству self, то есть имя объекта, отправляющего уведомление.
Словарь с пользовательской информацией. Это словарный объект, который вы можете прикреплять к объекту уведомления. Затем получатель может считывать этот словарь, когда получает уведомление. Можно сказать, что в этом параметре удобно передавать получателям вашего уведомления дополнительную информацию.
См. также
Раздел 15.0.
15.2. Слушание уведомлений и реагирование на них
Постановка задачи
Требуется отреагировать на уведомление, посылаемое либо вашим приложением, либо системой.
Решение
Слушайте интересующее вас уведомление путем вызова метода addObserver: selector: name: object: стандартного центра уведомлений. Этот метод имеет следующие параметры:
• addObserver — объект, который должен отслеживать заданное уведомление. Поэтому, если речь идет о текущем классе, задайте здесь self, чтобы указать на актуальный экземпляр вашего класса;
• selector — селектор, который будет получать уведомление. Этот селектор должен иметь один параметр типа NSNotification;
• name — имя уведомления, которое вы хотите слушать;
• object — объект, который должен прислать вам уведомление. Например, если одноименные уведомления поступают сразу от двух объектов, то вы можете сузить круг интересующих вас уведомлений и слушать только те из них, которые приходят от объекта A, игнорируя при этом приходящие от объекта B.
Если вы больше не хотите получать уведомления, выполните метод экземпляра removeObserver:, относящийся к классу NSNotificationCenter. Это должно делаться лишь при условии, что центр уведомлений удерживает экземпляры объектов-слушателей. Если центр уведомлений продолжает удерживать экземпляр вашего класса после того, как он был высвобожден, могут возникать утечки памяти и ошибки. Поэтому убедитесь в том, что своевременно удаляете объект из списка наблюдателей.
Обсуждение
Вся эта теория станет значительно более понятной, если пояснить на примере. Мы собираемся создать класс Person и добавить к нему два свойства: имя и фамилию. Оба этих свойства будут относиться к типу NSString. Затем в делегате нашего приложения мы собираемся инстанцировать объект типа Person. Но не будем задавать имя и фамилию этой персоны, а отошлем в центр уведомлений само уведомление и его пользовательский словарь. В этом пользовательском словаре уведомления запишем имя и фамилию как элементы типа NSString. В методе инициализации класса Person мы собираемся слушать уведомление, которое приходит от делегата приложения. Затем извлечем имя и фамилию из пользовательского словаря и зададим эти значения для соответствующих свойств объекта-персоны.
Вот заголовочный файл делегата нашего приложения:
#import
/* Имя уведомления, которое мы собираемся послать */
extern NSString *const kSetPersonInfoNotification;
/* Ключ имени в словаре пользовательской информации уведомления */
extern NSString *const kSetPersonInfoKeyFirstName;
/* Ключ фамилии в словаре пользовательской информации уведомления */
extern NSString *const kSetPersonInfoKeyLastName;
@interface AppDelegate: UIResponder
@property (nonatomic, strong) UIWindow *window;
@end
А вот реализация делегата нашего приложения:
#import «AppDelegate.h»
#import «Person.h»
NSString *const kSetPersonInfoNotification = @"SetPersonInfoNotification";
NSString *const kSetPersonInfoKeyFirstName = @"firstName";
NSString *const kSetPersonInfoKeyLastName = @"lastName";
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
Person *steveJobs = [[Person alloc] init];
NSNotification *notification =
[NSNotification
notificationWithName: kSetPersonInfoNotification
object: self
userInfo:@{kSetPersonInfoKeyFirstName: @"Steve",
kSetPersonInfoKeyLastName: @"Jobs"}];
/* В настоящее время класс person слушает это уведомление. Этот класс
извлечет из уведомления информацию об имени и фамилии и задаст
собственные имя и фамилию, основываясь на информации, полученной
из пользовательского словаря уведомления. */
[[NSNotificationCenter defaultCenter] postNotification: notification];
/* Вот доказательство */
NSLog(@"Person's first name = %@", steveJobs.firstName);
NSLog(@"Person's last name = %@", steveJobs.lastName);
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Важнее всего в данном случае будет реализация класса Person (Person.m):
#import «Person.h»
#import «AppDelegate.h»
@implementation Person
— (void) handleSetPersonInfoNotification:(NSNotification *)paramNotification{
self.firstName = paramNotification.userInfo[kSetPersonInfoKeyFirstName];
self.lastName = paramNotification.userInfo[kSetPersonInfoKeyLastName];
}
— (instancetype) init{
self = [super init];
if (self!= nil){
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver: self
selector:@selector(handleSetPersonInfoNotification:)
name: kSetPersonInfoNotification
object: [[UIApplication sharedApplication] delegate]];
}
return self;
}
— (void) dealloc{
[[NSNotificationCenter defaultCenter] removeObserver: self];
}
@end
Значение, указываемое для параметра object метода addObs erver: selector: name: object:, — это объект, от которого, как предполагается, должно поступать уведомление. Если какой-то другой объект пошлет уведомление с таким же именем, то ваш слушатель не должен будет обрабатывать это уведомление. Как правило, вы указываете такой объект в тех случаях, когда точно знаете, какой именно объект должен посылать интересующее вас уведомление. Такая возможность есть не всегда. Например, бывают очень сложные приложения, где один контроллер вида, расположенный на конкретной вкладке, должен слушать уведомления, поступающие от другого контроллера вида, находящегося на другой вкладке. При этом у слушателя может и не быть ссылки на тот экземпляр контроллера вида, откуда будет исходить уведомление. В таком случае можно передать nil параметру parameter вышеупомянутого метода.
Запустив это приложение, вы увидите на консоли следующий вывод:
Person's first name = Steve
Person's last name = Jobs
Итак, и отправка, и получение этого уведомления произошли внутри нашей программы. А что насчет системных уведомлений? Мы подробнее поговорим о них в дальнейшем. Сейчас, пока вы находитесь в Xcode, нажмите комбинацию клавиш Command+Shift+O (O — это Open (Открыть)), после чего введите UIWindow.h. Открыв этот файл заголовка, найдите в нем объект UIKeyboardWillShowNotification, а в этом объекте — такой блок кода:
// Каждое уведомление включает в себя объект nil и словарь userInfo,
// содержащий (в координатах экрана) начало и конец области, которую
// занимает на дисплее виртуальная клавиатура.
// Пользуйтесь различными возможностями convertRect, относящимися к UIView и UIWindow, чтобы получить контур клавиатуры в желаемой системе
// координат. Анимационные пары «ключ/значение» доступны лишь для
// уведомлений, относящихся к семейству «will»
UIKIT_EXTERN NSString *const UIKeyboardWillShowNotification;
UIKIT_EXTERN NSString *const UIKeyboardDidShowNotification;
UIKIT_EXTERN NSString *const UIKeyboardWillHideNotification;
UIKIT_EXTERN NSString *const UIKeyboardDidHideNotification;
Это код Apple. Мы написали наш код точно таким же образом. Apple предоставляет уведомления, отправляемые системой, а затем документирует их. Вам понадобится сделать нечто подобное. При создании уведомлений, посылаемых компонентами, находящимися внутри вашего приложения, обязательно документируйте эти уведомления и объясняйте другим программистам (в частности, коллегам, работающим с вами в одной команде), каковы типичные значения, содержащиеся в пользовательском словаре такого уведомления, а также сообщайте им всю прочую важную информацию о ваших уведомлениях.
15.3. Слушание уведомлений, поступающих с клавиатуры, и реагирование на них
Постановка задачи
Мы позволяем пользователю вводить какой-либо текст в нашем графическом интерфейсе. Для этого применяется определенный компонент, например текстовое поле или текстовый вид, требующий наличия клавиатуры. Тем не менее если всплывающая на экране виртуальная клавиатура заслоняет половину пользовательского интерфейса, то она практически бесполезна. Подобной ситуации необходимо избегать.
Решение
Нужно слушать уведомления, поступающие от клавиатуры, и перемещать компоненты пользовательского интерфейса вверх или вниз либо полностью перегруппировывать их так, чтобы пользователь мог видеть нужную ему часть графического интерфейса, даже если клавиатура и занимает половину экрана. Сами уведомления, посылаемые клавиатурой, подробнее рассматриваются в подразделе «Обсуждение» данного раздела.
Обсуждение
У устройств, работающих с операционной системой iOS, нет физической клавиатуры. Но у них есть виртуальная клавиатура, всплывающая на экране всякий раз, когда пользователь должен ввести текст в какое-нибудь текстовое поле (UITextField, см. раздел 1.19) или текстовый вид (UITextView, см. раздел 1.20). На iPad пользователь даже может делить клавиатуру на части и перемещать их вверх и вниз. Есть несколько пограничных случаев, о которых, вам, возможно, придется позаботиться при проектировании пользовательского интерфейса. Можете обратиться к помощи дизайнеров пользовательских интерфейсов (если в вашей компании есть такие специалисты) и рассказать им, что на iPad пользователь может делить клавиатуру на части. Перед тем как приступать к творческой работе, они должны об этом узнать. В этом разделе мы коснемся подобного пограничного случая.
Сначала рассмотрим, как выглядит клавиатура в iPhone. Виртуальная клавиатура может отображаться в книжной или альбомной ориентации. В книжной ориентации клавиатура iPhone выглядит так, как на рис. 15.1.
Рис. 15.1. Клавиатура на iPhone, книжная ориентация
А клавиатура в альбомной ориентации на iPhone будет выглядеть так, как на рис. 15.2.
Рис. 15.2. Клавиатура в iPhone, альбомная ориентация
Но на iPad клавиатура выглядит немного иначе. Самое очевидное отличие заключается в том, что эта клавиатура гораздо крупнее, чем на iPhone, поскольку сам экран у iPad шире. При альбомной ориентации клавиатура iPad значительно шире, но на ней содержится тот же набор клавиш, что и при книжной ориентации. Кроме того, пользователь может при желании «разбирать» клавиатуру iPad на части. Благодаря этому он лучше контролирует функционирование клавиатуры, зато появляются дополнительные проблемы у программистов, дизайнеров пользовательских интерфейсов и пользовательских взаимодействий.
iOS широковещательно распространяет различные уведомления, касающиеся отображения клавиатуры на экране. Вот список этих уведомлений и краткие характеристики каждого из них:
• UIKeyboardWillShowNotification — распространяется, когда клавиатура вот-вот появится на экране. Уведомление несет с собой словарь с пользовательской информацией, в котором содержатся различные данные о клавиатуре, анимации, которая будет применяться при выводе клавиатуры на экран, и другая информация;
• UIKeyboardDidShowNotification — распространяется, когда клавиатура уже появилась на экране;
• UIKeyboardWillHideNotification — распространяется, когда клавиатура вот-вот будет убрана с экрана. Уведомление несет с собой словарь с пользовательской информацией, в котором содержатся различные данные о клавиатуре, анимации, которая будет применяться при уходе клавиатуры с экрана, длительности анимации и т. д.;
• UIKeyboardDidHideNotification — распространяется после того, как клавиатура полностью скроется с экрана.
Уведомления UIKeyboardWillShowNotification и UIKeyboardWillHideNotification несут с собой словари с пользовательской информацией. В этих словарях содержатся валидные ключи и значения. Далее перечислены те из ключей, которые могут быть вам интересны:
• UIKeyboardAnimationCurveUserInfoKey — значение этого ключа характеризует тип анимационной кривой, которая будет использоваться при выводе клавиатуры на экран или ее скрытии. Этот ключ содержит значение типа NSNumber (инкапсулированное в объекте типа NSValue), которое, в свою очередь, содержит беззнаковое целое типа NSUInteger;
• UIKeyboardAnimationDurationUserInfoKey — значение данного ключа указывает в секундах длительность анимации, применяемой при отображении или скрытии клавиатуры. Если клавиатура вот-вот будет отображена, то это будет рамка, в которой появится клавиатура. Если клавиатура уже есть на экране, это будет контур, обрамляющий клавиатуру на экране перед тем, как она уйдет с экрана. Этот ключ содержит значение типа CGRect (инкапсулированное в объекте типа NSValue);
• UIKeyboardFrameBeginUserInfoKey — значение данного ключа указывает размеры рамки клавиатуры до того, как начнется анимация. Если клавиатура вот-вот отобразится, то перед появлением клавиатуры появится эта рамка. Если клавиатура в данный момент отображена и вот-вот уйдет с экрана, это будет рамка, фактически занимаемая клавиатурой на экране, прежде чем клавиатура уйдет с экрана в сопровождении соответствующей анимации. Этот ключ содержит значение типа CGRect (инкапсулированное в объект типа NSValue);
• UIKeyboardFrameEndUserInfoKey — значение данного ключа описывает контур клавиатуры, который оформится после того, как будет применена анимация. Если клавиатура вот-вот появится на экране, то она займет именно этот контур. Если клавиатура уходит с экрана, то именно этот контур от нее освободится. Этот ключ содержит значение типа CGRect (инкапсулированное в объекте типа NSValue).
Рассмотрим пример. Мы собираемся создать простое приложение с единственным видом. Это приложение будет работать только на iPhone. В нем будут отображаться вид с изображением и текстовое поле. Текстовое поле находится в нижней части экрана. Итак, когда пользователь дотрагивается до текстового поля, чтобы ввести в него некоторый текст, на экране всплывает виртуальная клавиатура, полностью заслоняющая текстовое поле. Наша задача — анимировать содержимое вида и перераспределить элементы так, чтобы все они оставались видимыми, даже если экран наполовину закрыт клавиатурой. В этом приложении мы будем использовать раскадровки. В контроллере вида заполним вид с изображением, поместив в него прокручивающийся вид, и поместим в этом прокручивающемся виде и вид с изображением, и текстовое поле (рис. 15.3).
Рис. 15.3. Простая раскадровка, содержащая вид с изображением и текстовое поле
Прокручивающийся вид в данном примере является родительским видом как для вида с изображением, так и для текстового поля. Он целиком заполняет пространство своего родительского вида.
Я уже прикрепил прокручивающийся вид, вид с изображением и текстовое поле, определенные в раскадровке, к файлу реализации контроллера вида, вот так:
#import «ViewController.h»
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
@property (weak, nonatomic) IBOutlet UITextField *textField;
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
…
Теперь, когда аутлеты прикреплены к свойствам в контроллере нашего вида, можно приступить к слушанию клавиатурных уведомлений:
— (void) viewWillAppear:(BOOL)paramAnimated{
[super viewWillAppear: paramAnimated];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver: self selector:@selector(handleKeyboardWillShow:)
name: UIKeyboardWillShowNotification object: nil];
[center addObserver: self selector:@selector(handleKeyboardWillHide:)
name: UIKeyboardWillHideNotification object: nil];
}
— (void)viewWillDisappear:(BOOL)paramAnimated{
[super viewWillDisappear: paramAnimated];
[[NSNotificationCenter defaultCenter] removeObserver: self];
}
Многие программисты допускают распространенную ошибку: продолжают слушать клавиатурные уведомления, когда контроллер вида не отображается на экране. Они начинают слушать уведомления в методе viewDidLoad, а удаляют элементы, действовавшие в качестве наблюдателей, только в методе dealloc. Такой подход проблематичен, так как, когда наш вид не отображается на экране, а клавиатура вновь открывается в каком-то другом виде, вы не должны корректировать положение каких-либо других компонентов контроллера вида. Учтите, что клавиатурные уведомления, как и любые другие, широковещательно передаются всем объектам-наблюдателям в контексте вашего приложения. Поэтому придется принимать дополнительные меры, чтобы программа реагировала не на все клавиатурные уведомления. Если клавиатурные уведомления поступают от вида, не отображаемого на экране в данный момент, они должны игнорироваться.
В предыдущем фрагменте кода мы начали слушать уведомления типа «клавиатура будет отображена» в методе экземпляра handleKeyboardWillShow: контроллера нашего вида. Уведомления типа «клавиатура будет скрыта» мы ожидаем в методе handleKeyboardWillHide:. Пока эти методы еще не написаны. Начнем с первого метода, handleKeyboardWillShow:. В этом методе нам требуется определить высоту клавиатуры, воспользовавшись ключом UIKeyboardFrameEndUserInfoKey из словаря с пользовательской информацией, сопровождающего уведомление. Это значение мы используем, чтобы переместить содержимое вида вверх — так, чтобы все необходимые элементы оказались над клавиатурой. Здесь приятно вспомнить, что мы поместили все нужное содержимое в прокручивающемся виде. Соответственно, потребуется всего лишь откорректировать краевые отступы прокручивающегося вида:
— (void) handleKeyboardWillShow:(NSNotification *)paramNotification{
NSDictionary *userInfo = paramNotification.userInfo;
/* Получаем длительность клавиатурной анимации — время, за которое
клавиатура успеет отобразиться на экране. При анимировании и перемещении
содержимого вида мы будем применять такое же значение длительности.
*/
NSValue *animationDurationObject =
userInfo[UIKeyboardAnimationDurationUserInfoKey];
NSValue *keyboardEndRectObject = userInfo[UIKeyboardFrameEndUserInfoKey];
double animationDuration = 0.0;
CGRect keyboardEndRect = CGRectMake(0.0f, 0.0f, 0.0f, 0.0f);
[animationDurationObject getValue:&animationDuration];
[keyboardEndRectObject getValue:&keyboardEndRect];
UIWindow *window = [UIApplication sharedApplication].keyWindow;
/* Переводим размеры контура клавиатуры из координатной системы окна
в координатную систему нашего вида. */
keyboardEndRect = [self.view convertRect: keyboardEndRect
fromView: window];
/* Определяем, в какой степени наш вид накрыт клавиатурой */
CGRect intersectionOfKeyboardRectAndWindowRect =
CGRectIntersection(self.view.frame, keyboardEndRect);
/* Прокручиваем прокручивающийся вид таким образом, чтобы содержимое
нашего вида отображалось полностью */
[UIView animateWithDuration: animationDuration animations: ^{
self.scrollView.contentInset =
UIEdgeInsetsMake(0.0f,
0.0f,
intersectionOfKeyboardRectAndWindowRect.size.height,
0.0f);
[self.scrollView scrollRectToVisible: self.textField.frame animated: NO];
}];
}
У нас получился довольно интересный и прямолинейный код. Единственная деталь, возможно требующая дополнительного разъяснения, — это функция CGRectIntersection. В ней мы получаем информацию о прямоугольном контуре клавиатуры (о верхней границе, левой границе, ширине и высоте). Это параметры клавиатуры в момент завершения анимации, когда она полностью отобразится на экране. Теперь, зная параметры клавиатуры, можем воспользоваться функцией CGRectIntersection и определить, какая часть нашего вида накрыта клавиатурой. Итак, берем контур клавиатуры, контур вида, а затем определяем, какая часть контура вида накрыта контуром клавиатуры. В результате получаем структуру типа CGRect, соответствующую той прямоугольной области вида, которая накрыта клавиатурой. Известно, что клавиатура появляется на нижней границе экрана и в ходе анимации выплывает вверх. Поэтому нас интересует вертикаль этой области. Итак, мы получаем высоту области пересечения контура клавиатуры и контура вида, а затем поднимаем на эту высоту содержимое вида. Длительность анимации перемещения задаем равной длительности анимации выдвижения клавиатуры. Таким образом, движения клавиатуры и поднимающихся экранных элементов синхронизируются.
Далее нужно написать метод handleKeyboardWillHide:. В нем мы будем скрывать клавиатуру — соответственно, она больше не будет закрывать наш вид. Итак, в этом методе всего лишь требуется сбросить размеры краевых отступов прокручивающегося вида к начальным значениям, перенести все элементы обратно вниз, чтобы вид выглядел точно так же, как было до появления клавиатуры:
— (void) handleKeyboardWillHide:(NSNotification *)paramSender{
NSDictionary *userInfo = [paramSender userInfo];
NSValue *animationDurationObject =
[userInfo valueForKey: UIKeyboardAnimationDurationUserInfoKey];
double animationDuration = 0.0;
[animationDurationObject getValue:&animationDuration];
[UIView animateWithDuration: animationDuration animations: ^{
self.scrollView.contentInset = UIEdgeInsetsZero;
}];
}
И последний важный момент. Поскольку наш контроллер вида является делегатом текстового поля, необходимо обеспечить уход клавиатуры с экрана, если пользователь нажимает клавишу Return (Ввод) после ввода той или иной информации в текстовое поле:
— (BOOL) textFieldShouldReturn:(UITextField *)paramTextField{
[paramTextField resignFirstResponder];
return YES;
}
См. также
Разделы 1.19 и 1.20.
15.4. Планирование локальных уведомлений
Постановка задачи
Вы разрабатываете приложение, оперирующее данными о времени, например программу-будильник или программу-календарь. Это приложение должно информировать пользователя о событии в определенный момент времени, даже если в данный момент это приложение работает в фоновом режиме или вообще не запущено.
Решение
Инстанцируйте объект типа UILocalNotification, сконфигурируйте его (далее будет рассказано о том, как это делается) и запланируйте с помощью метода экземпляра scheduleLocalNotification:, относящегося к классу UIApplication. Чтобы получить экземпляр объекта вашего приложения, воспользуйтесь методом класса sharedApplication, относящимся к классу UIApplication.
Обсуждение
Если в данный момент приложение работает в фоновом режиме или не работает вообще, система выдает пользователю так называемое локальное уведомление. Чтобы запланировать доставку локального уведомления, используется метод экземпляра scheduleLocalNotification:, относящийся к классу UIApplication. Если приложение работает в приоритетном режиме и в это время срабатывает запланированное локальное уведомление, пользователь не получает никакого оповещения об этом. Вместо этого iOS бесшумно дает вам знать о том, что было выдано уведомление, — это делается через делегат приложения. Пока не будем вдаваться в детали этого процесса, рассмотрим его чуть позже.
Можно приказать iOS доставить локальное уведомление пользователю когда-нибудь в будущем, когда ваше приложение даже не будет работать. Кроме того, такие уведомления могут быть периодическими, например запускаться каждую неделю в определенное время. При этом необходимо особенно внимательно указывать дату запуска (Fire Date) ваших уведомлений.
Метод экземпляра cancelAllLocalNotifications отменяет доставку всех стоящих в очереди локальных уведомлений, поступивших от вашего приложения.
Уведомление типа UILocalNotification имеет много свойств. Наиболее важными из них являются следующие:
• fireDate — это свойство типа NSDate, сообщающее iOS, когда должен быть запущен экземпляр локального уведомления. Данное свойство является обязательным;
• timeZone — это свойство типа NSTimeZone сообщает iOS, к какому часовому поясу относится конкретная дата запуска. Для получения актуального часового пояса используется метод экземпляра timeZone, относящийся к классу NSCalendar. Вы можете получить актуальный календарь, воспользовавшись методом класса currentCalendar, относящимся к вышеупомянутому классу;
• alertBody — это свойство относится к типу NSString и задает текст, который должен выводиться для пользователя при отображении вашего уведомления на экране;
• hasAction — логическое свойство. Сообщает iOS, собирается ли ваше приложение предпринимать какое-либо действие, когда происходит уведомление. Если установить его в YES, iOS отобразит для пользователя диалоговое окно, указанное в свойстве alertAction (описано далее). Если установить его в NO, iOS выдаст пользователю диалоговое окно с простым сообщением о том, что пришло уведомление;
• alertAction — если свойство hasAction установлено в YES, то значением этого свойства должна быть локализованная строка, описывающая действие, которое пользователь может совершить над вашим уведомлением в случаях, когда уведомление произошло, но в этот момент приложение не работает в приоритетном режиме. После этого iOS выведет уведомление в центре уведомлений или на экране блокировки. Если свойство hasAction имеет значение NO, то свойство alertAction должно иметь значение nil;
• applicationIconBadgeNumber — если при срабатывании данного уведомления обязательно должен измениться номер ярлыка вашего приложения, то в этом свойстве можно задать желаемый номер. Значением этого свойства всегда является целое число. Когда вы присваиваете новое значение этому свойству, оно, как правило, должно представлять собой актуальный номер ярлыка вашего приложения плюс 1. Чтобы узнать текущий номер ярлыка приложения, пользуйтесь свойством applicationIconBadgeNumber класса UIApplication;
• userInfo — это экземпляр словаря NSDictionary, прикрепляемый к вашему уведомлению и получаемый приложением при доставке этого уведомления. Обычно такие словари используются для сообщения дополнительной информации о локальном уведомлении.
Благодаря суммарному эффекту свойств hasAction и alertAction пользователь может жестом смахивания запустить ваше уведомление в центре уведомлений. После этого iOS откроет приложение. Именно так пользователь может воздействовать на локальные уведомления. Это очень удобно, особенно если вы разрабатываете приложение-календарь. В таком приложении вы можете выдавать пользователю локальное уведомление за несколько дней до наступления дня рождения друга этого пользователя. Затем вы предоставляете пользователю возможность выполнить какую-то операцию над этим уведомлением. Например, когда пользователь открывает ваше приложение, вы можете предложить на выбор несколько виртуальных подарков, которые можно рассылать друзьям ко дню рождения.
Предположим, что в Лондоне сейчас 13:00, лондонец работает с вашим приложением на своем устройстве. Очевидно, что он находится в гринвичском часовом поясе (GMT + 0). Вы хотите доставить пользователю определенное уведомление в 14:00, даже если в этот момент ваше приложение не будет работать. И вот наш пользователь садится на самолет в лондонском аэропорту Гэтвик и собирается лететь в Стокгольм, то есть в часовой пояс GMT + 1. Допустим, полет длится полчаса. Тогда пользователь окажется в Стокгольме в 13:30 по лондонскому времени. Но когда самолет приземлится, iOS обнаружит, что часовой пояс изменился, и время на пользовательском устройстве также изменится — теперь будет 14:30. Вслед за этим iOS обнаружит, что необходимо отобразить уведомление (и так уже с опозданием на 30 минут, поскольку изменился часовой пояс), и отобразит его.
Проблема заключается в том, что ваше уведомление должно было быть отображено в 14:00 по времени GMT + 0 или в 15:00 по времени GMT + 1, но не в 14:30 по GMT + 1. Чтобы избежать подобных ситуаций (а ведь они довольно часты при нынешнем темпе жизни), при указании даты и времени для вывода на экран локальных уведомлений нужно также сообщать часовой пояс.
Теперь все это протестируем на практике. Напишем простое приложение, доставляющее локальное уведомление через 8 секунд после того, как пользователь впервые открывает это приложение:
#import «AppDelegate.h»
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
UILocalNotification *notification = [[UILocalNotification alloc] init];
/* Настройки времени и часового пояса */
notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:8.0];
notification.timeZone = [[NSCalendar currentCalendar] timeZone];
notification.alertBody =
NSLocalizedString(@"A new item is downloaded.", nil);
/* Настройки действий */
notification.hasAction = YES;
notification.alertAction = NSLocalizedString(@"View", nil);
/* Настройки ярлыка */
notification.applicationIconBadgeNumber =
[UIApplication sharedApplication].applicationIconBadgeNumber + 1;
/* Дополнительная информация, пользовательский словарь */
notification.userInfo = @{@"Key 1": @"Value 1",
@"Key 2": @"Value 2"};
/* Назначаем уведомление */
[[UIApplication sharedApplication] scheduleLocalNotification: notification];
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Все это хорошо, но локальные уведомления практически бесполезны, пока мы не умеем на них реагировать и обрабатывать их при срабатывании. В разделе 15.5 подробнее рассказано об обработке таких уведомлений.
См. также
Раздел 15.0.
15.5. Слушание локальных уведомлений и реагирование на них
Постановка задачи
Вы научились планировать локальные уведомления (см. раздел 15.4). При поступлении этих уведомлений в приложение на них нужно правильно реагировать.
Решение
Реализуйте метод application: didReceiveLocalNotification: делегата вашего приложения и считайте ключ UIApplicationLaunchOptionsLocalNotificationKey, относящийся к словарю параметров запуска вашего приложения при вызове метода application: didFinishLaunchingWithOptions: в делегате приложения. В подразделе «Обсуждение» данного раздела подробнее объяснено, почему приходится обрабатывать локальное уведомление в двух местах, а не в одном.
Обсуждение
Когда происходит доставка локального уведомления и вам приходится его обрабатывать, приложение может находиться в одном из нескольких состояний. В зависимости от состояния обработка уведомления будет происходить по-разному. Вот ряд ситуаций, в которых iOS может доставить вашему приложению заранее запланированное локальное уведомление.
• В момент прихода локального уведомления приложение открыто и пользователь работает с ним. В таком случае при доставке уведомления вызывается метод application: didReceiveLocalNotification:.
• Локальное уведомление доставлено, но пользователь перевел приложение в фоновый режим. Как только пользователь дотрагивается до появившегося на экране уведомления, iOS может запустить приложение. В таком случае опять же вызывается метод application: didReceiveLocalNotification: делегата вашего приложения.
• В момент доставки локального уведомления приложение вообще неактивно. В данном случае вызывается метод application: didFinishLaunchingWithOptions: делегата приложения. Ключ UIApplicationLaunchOptionsLocalNotificationKey в словарном параметре didFinishLaunchingWithOptions этого метода содержит локальное уведомление, которое и привело к активизации приложения.
• Локальное уведомление поступает, когда пользовательское устройство заблокировано, независимо от состояния приложения: работает ли оно в приоритетном режиме, в фоновом режиме или вообще не работает. В таком случае приложение будет запущено одним из вышеупомянутых способов, зависящим от того, находилось ли ваше приложение в фоновом режиме, когда пользователь попытался открыть его через уведомление.
Разовьем код, рассмотренный в качестве примера в разделе 15.4. При запуске уведомления независимо от того, в каком состоянии в этот момент находится приложение, мы обработаем это уведомление (выведем для пользователя окно с предупреждением). Сначала используем код, изученный в разделе 15.4, в отдельном методе. Так мы сможем просто вызвать этот метод и назначить новое локальное уведомление. Вот почему поступаем именно так: в данном случае мы сможем посмотреть в центре уведомлений iOS, открылось ли приложение после того, как пользователь нажал появившееся на экране локальное уведомление. Если приложение открылось, то мы не будем запускать другое локальное уведомление. Однако если локальное уведомление не открыло наше приложение, то запланируем новое локальное уведомление. Далее приведен метод приложения, назначающий локальные уведомления, которые должны доставляться приложению через 8 секунд после вызова метода:
— (void) scheduleLocalNotification{
UILocalNotification *notification = [[UILocalNotification alloc] init];
/* Настройки времени и часового пояса */
notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:8.0];
notification.timeZone = [[NSCalendar currentCalendar] timeZone];
notification.alertBody =
NSLocalizedString(@"A new item is downloaded.", nil);
/* Настройки действий */
notification.hasAction = YES;
notification.alertAction = NSLocalizedString(@"View", nil);
/* Настройки ярлыка */
notification.applicationIconBadgeNumber =
[UIApplication sharedApplication].applicationIconBadgeNumber + 1;
/* Дополнительная информация, пользовательский словарь */
notification.userInfo = @{@"Key 1": @"Value 1",
@"Key 2": @"Value 2"};
/* Назначаем уведомление */
[[UIApplication sharedApplication] scheduleLocalNotification: notification];
}
Метод, который мы здесь написали, называется scheduleLocalNotification. Как понятно из его названия, он просто создает объект уведомления и запрашивает iOS назначить это уведомление. Не путайте наш собственный метод scheduleLocalNotification с методом iOS, который называется scheduleLocalNotification: и относится к классу UIApplication (как и у всех методов iOS, в конце названия этого метода стоит двоеточие). Этот метод можно считать удобным вспомогательным инструментом, выполняющим сложную задачу назначения локального уведомления. При этом сам он просто создает объект уведомления, а назначение этого уведомления делегирует iOS.
Теперь в методе application: didFinishLaunchingWithOptions мы проверим, открылось ли приложение именно по причине поступления имеющегося уведомления. Если это так, то будем работать с имеющимся локальным уведомлением. В противном случае назначим новое:
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
if (launchOptions[UIApplicationLaunchOptionsLocalNotificationKey]!= nil){
UILocalNotification *notification =
launchOptions[UIApplicationLaunchOptionsLocalNotificationKey];
[self application: application didReceiveLocalNotification: notification];
} else {
[self scheduleLocalNotification];
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Когда в предыдущем коде наше приложение запускалось в результате поступления локального уведомления, мы перенаправляли локальное уведомление в метод application: didReceiveLocalNotification:, где оперировали имеющимся уведомлением и отображали для пользователя предупреждение. Вот простая реализация вышеупомянутого метода:
— (void) application:(UIApplication *)application
didReceiveLocalNotification:(UILocalNotification *)notification{
NSString *key1Value = notification.userInfo[@"Key 1"];
NSString *key2Value = notification.userInfo[@"Key 2"];
if ([key1Value length] > 0 &&
[key2Value length] > 0){
UIAlertView *alert =
[[UIAlertView alloc] initWithTitle: nil
message:@"Handling the local notification"
delegate: nil
cancelButtonTitle:@"OK"
otherButtonTitles: nil];
[alert show];
}
}
Теперь испытайте его. Опробуйте разные комбинации. Откройте приложение и поработайте с ним в приоритетном режиме, потом переведите его в фоновый режим, можете даже вообще закрыть. Посмотрите, как приложение функционирует в разных условиях.
См. также
Разделы 15.0 и 15.4.
15.6. Обработка локальных системных уведомлений
Постановка задачи
Когда ваше приложение возвращается в приоритетный режим, вам требуется возможность получать уведомления о важных системных изменениях, например об изменении локализации (языковых и культурных настроек) пользовательского устройства.
Решение
Нужно просто слушать конкретное уведомление из числа тех, которые операционная система iOS посылает приложениям, переходящим в приоритетный режим. Далее перечислены некоторые из таких уведомлений:
• NSCurrentLocaleDidChangeNotification — доставляется приложениям, когда изменяется локализация устройства. Например, на странице Settings (Настройки) пользователь активизирует испанский язык вместо английского;
• NSUserDefaultsDidChangeNotification — запускается, когда пользователь изменяет настройки приложения на странице Settings (Настройки) устройства с iOS — при условии, что пользователь может изменить какую-либо настройку в вашем приложении;
• UIDeviceBatteryStateDidChangeNotification — запускается всякий раз, когда на устройстве с iOS изменяется состояние батареи. Например, если устройство подключается к компьютеру, когда приложение работает в приоритетном режиме, а потом отключается от компьютера, но приложение к этому моменту уже находится в фоновом режиме, то приложение получит такое уведомление (предполагается, что оно зарегистрировано на получение таких уведомлений). В подобном случае для считывания состояния можно узнать значение свойства batteryState экземпляра класса UIDevice;
• UIDeviceProximityStateDidChangeNotification — направляется приложению всякий раз, когда изменяется состояние датчика близости (Proximity Sensor). Последнее состояние можно узнать в свойстве proximityState экземпляра UIDevice.
Обсуждение
Пока ваше приложение работает в фоновом режиме, может произойти многое! Например, пользователь может вдруг изменить локализацию устройства с iOS на странице Settings (Настройки) и задать, к примеру, испанский язык вместо английского. Приложения могут регистрироваться для получения таких уведомлений. Эти уведомления будут объединяться, а потом вместе доставляться приложению, переходящему в приоритетный режим. Объясню, что я понимаю в данном случае под объединением). Предположим, что ваше приложение работает в приоритетном режиме и вы зарегистрировали его для получения уведомлений UIDeviceOrientationDidChangeNotification. Вот пользователь нажимает кнопку Home (Домой), и ваше приложение уходит в фоновый режим. Потом пользователь изменяет ориентацию устройства с книжной на альбомную правую, затем возвращает книжную ориентацию и, наконец, переводит в альбомную левую. И когда пользователь вернет ваше приложение в приоритетный режим, оно получит всего одно уведомление типа UIDeviceOrientationDidChangeNotification. Это и есть объединение. Все остальные изменения ориентации, которые происходили, пока ваше приложение не было открыто, игнорируются (действительно, они не имеют значения, раз программы не было на экране, когда они происходили), и система не будет сообщать информацию о них вашему приложению. Тем не менее система доставит вам как минимум одно уведомление по каждому аспекту, связанному с устройством, — в частности, по ориентации. Так вы сможете узнать самую актуальную информацию о том, в каком положении находится устройство.
Вот реализация простого контроллера вида, в котором эта техника используется для определения изменений ориентации:
#import «ViewController.h»
@implementation ViewController
— (void) orientationChanged:(NSNotification *)paramNotification{
NSLog(@"Orientation Changed");
}
— (void)viewDidAppear:(BOOL)paramAnimated{
[super viewDidAppear: paramAnimated];
/* Слушаем уведомление */
[[NSNotificationCenter defaultCenter]
addObserver: self
selector:@selector(orientationChanged:)
name: UIDeviceOrientationDidChangeNotification
object: nil];
}
— (void) viewDidDisappear:(BOOL)paramAnimated{
[super viewDidDisappear: paramAnimated];
/* Прекращаем слушать уведомление */
[[NSNotificationCenter defaultCenter]
removeObserver: self
name: UIDeviceOrientationDidChangeNotification
object: nil];
}
@end
Теперь запустите приложение на устройстве. После того как на экране отобразится контроллер вида, нажмите кнопку Home (Домой) для перевода приложения в фоновый режим. После этого попробуйте пару раз изменить ориентацию устройства, а потом перезапустите приложение. Просмотрите результаты и обратите внимание на то, что, когда приложение открывается, обычно направляется одно уведомление к методу orientationChanged:.
Теперь допустим, что в вашем приложении пользователю предоставляется пакет с настройками. Как только приложение возвращается в приоритетный режим, требуется получать уведомления о тех изменениях, которые пользователь внес в настройки программы (пока приложение было в фоновом режиме).
Решение
Зарегистрируйтесь для получения уведомлений NSUserDefaultsDidChangeNotification.
Обсуждение
В приложениях, написанных для iOS, файл пакета настроек может быть предоставлен пользователю для внесения собственных настроек. Эти настройки будут доступны пользователю в приложении (Settings) на устройстве. Чтобы лучше понять, как работает этот механизм, создадим пакет с настройками.
1. В Xcode выберите File — New File (Файл — Новый файл).
2. Убедитесь, что слева задана категория iOS.
3. Выберите подкатегорию Resources (Ресурсы).
4. В качестве типа файла укажите пакет настроек (Settings Bundle), а потом нажмите Next (Далее).
5. Назовите файл Settings.bundle.
6. Нажмите Save (Сохранить).
Итак, теперь у вас в Xcode есть файл под названием Settings.bundle. Оставьте этот файл как есть, не вносите в него никаких изменений. Нажмите кнопку Home (Домой) и перейдите в приложение Settings (Настройки). Если вы назовете свое приложение foo, то в окне настроек, показанном на рис. 15.4, также будет указано Foo. (Мое приложение я назвал Handling local System Notifications, это название вы видите на рисунке.)
Рис. 15.4. Пакет Settings.bundle отображается в приложении Settings (Настройки) в симуляторе iOS
Щелкните на имени приложения, чтобы просмотреть, какие настройки в приложении предоставляются пользователю. Нас интересует, когда пользователь вносит изменения в эти настройки, чтобы при необходимости мы могли соответствующим образом изменить внутреннее состояние приложения.
Далее начнем слушать в делегате нашего приложения уведомления NSUserDefaultsDidChangeNotification. Когда приложение завершится, мы, само собой, удалим делегат из цепочки адресатов уведомлений:
#import «AppDelegate.h»
@implementation AppDelegate
— (void) handleSettingsChanged:(NSNotification *)paramNotification{
NSLog(@"Settings changed");
NSLog(@"Notification Object = %@", paramNotification.object);
}
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
[[NSNotificationCenter defaultCenter]
addObserver: self
selector:@selector(settingsChanged:)
name: NSUserDefaultsDidChangeNotification
object: nil];
return YES;
}
— (void)applicationWillTerminate:(UIApplication *)application{
[[NSNotificationCenter defaultCenter] removeObserver: self];
}
@end
А теперь попробуйте изменить некоторые из этих настроек, пока приложение работает в фоновом режиме. Когда закончите, переведите приложение в приоритетный режим — и увидите, что программе доставлено уведомление NSUserDefaultsDidChangeNotification. Объект, прикрепленный к этому уведомлению, будет относиться к типу NSUserDefaults и содержать настройки вашего приложения user defaults.
15.7. Настройка приложения для получения пуш-уведомлений
Постановка задачи
Требуется сконфигурировать приложение таким образом, чтобы сервер мог по своей инициативе отправлять уведомления на различные устройства.
Решение
Выполните следующие шаги.
1. Настройте профиль инициализации для вашего приложения, активизировав в нем возможность получения пуш-уведомлений.
2. В приложении зарегистрируйте устройство для получения пуш-уведомлений для этого приложения.
3. Возьмите идентификатор пуш-уведомлений данного устройства для вашего приложения и отошлите этот идентификатор на сервер.
В этом разделе мы поговорим о приложении, применяемом для внесения настроек и регистрации вашего приложения для получения пуш-уведомлений. Серверную часть этой технологии затрагивать не будем, этим вопросам будет посвящен отдельный раздел.
Обсуждение
Пуш-уведомления похожи на локальные уведомления тем, что позволяют сообщать пользователю определенную информацию, даже если ваше приложение неактивно при поступлении уведомления. В то время как локальные уведомления назначаются самим приложением, пуш-уведомления конфигурируются на сервере и отсылаются с него в Apple, а Apple сама раздает эти уведомления на разные устройства, работающие по всему миру. Мы должны выполнять серверную часть работы, поэтому сами составляем пуш-уведомления и отправляем их на серверы APNS (Apple Push Notifications Services). Затем APNS пытается доставлять наши пуш-уведомления по защищенным каналам на устройства, которые зарегистрированы для получения таких уведомлений.
Чтобы приложение для iOS могли получать пуш-уведомления, у него должен быть валидный профиль инициализации, в котором активизирована возможность получения пуш-уведомлений. Чтобы правильно сконфигурировать профиль, выполните следующие шаги.
Предполагается, что вы уже настроили на портале разработчика ваши сертификаты для разработки и распространения приложения. Чтобы автоматически сконфигурировать сертификаты, можете воспользоваться новой настройкой Accounts (Учетные записи), появившейся в Xcode. Просто перейдите в раздел Xcode Preferences (Настройки), в нем откройте область Accounts (Учетные записи). Добавьте в список учетных записей ваш идентификатор Apple ID, а далее Xcode сконфигурирует сертификаты за вас.
1. Войдите в центр для разработки для iOS.
2. Перейдите в раздел Certificates, Identifiers & Profiles (Сертификаты, идентификаторы и профили) — он расположен справа.
3. В разделе Identifiers (Идентификаторы) создайте для себя новый идентификатор App ID. Он должен иметь валидный Explicit App ID (явный идентификатор приложения), например com.pixolity.ios.cookbook.PushNotificationApp. Обратите внимание: это имя построено по принципу обратной записи домена в стиле, который я выбрал для этого приложения. Выберите такой идентификатор приложения в стиле обратной записи домена, который подходит вам и вашей организации.
4. В разделе App Services (Сервисы приложения) на странице идентификатора приложения убедитесь, что установили флажок для пуш-уведомлений (Push Notifications) (рис. 15.5).
Рис. 15.5. Активизация пуш-уведомлений для App ID
5. Как только вас устроит конфигурация идентификатора приложения (рис. 15.6), отправьте ваш App ID в Apple.
Рис. 15.6. Создание идентификатора приложения с поддержкой пуш-уведомлений
6. После того как настроите все детали конфигурации идентификатора приложения, перейдите в раздел Provisioning Profiles (Профили инициализации) вашего iOS-портала.
7. Создайте профиль инициализации для разработки (Development). Позже вы можете создать и другие профили — Ad Hoc и App Store, так что об этом не волнуйтесь. Когда будете готовы отправить ваше приложение в App Store, можете просто вернуться к этому шагу и сгенерировать профили Ad Hoc и App Store.
8. Убедитесь, что новый профиль инициализации для разработки приложения связан с идентификатором приложения, который вы сгенерировали ранее. Это первый вопрос, который у вас попытается выяснить система при генерации профиля инициализации.
9. Когда профиль будет готов, скачайте его и перетащите в программу iTunes на своем компьютере, чтобы установить. При установке профиля не делайте двойных щелчков на нем. При двойном щелчке кнопкой мыши имя файла установленного профиля будет сброшено и заменено хеш-именем MD5, по которому файл очень сложно идентифицировать на диске. Если вы аккуратно перетащите профиль в iTunes, то iTunes установит профиль под его оригинальным именем.
10. В Xcode в настройках сборки вашего приложения просто выберите сборку в соответствии с тем профилем, который только что создали. Убедитесь, что при разработке (схема Development) вы используете именно этот профиль, а затем примените в схеме Release (Релиз) профиль App Store или Ad Hoc, созданием которых займетесь позже.
11. Перетащите ваш профиль инициализации в текстовый редактор для OS X — например, в TextEdit. Найдите в файле профиля ключ Entitlements. Весь этот раздел моего профиля инициализации выглядит так:
12. Создайте новый PLIST-файл для вашего проекта в Xcode, назовите этот файл Entitlements.plist. В Xcode щелкните на этом файле правой кнопкой мыши, выберите Open As (Открыть как) и далее Source Code (Исходный код). Содержимое вашего файла изначально будет выглядеть вот так:
13. Поместите разрешения вашего профиля инициализации прямо в файл Entitlements.plist, чтобы его содержимое выглядело вот так:
Значения, приведенные в рассматриваемых здесь листингах, относятся к профилям, которые создал я. В вашем профиле будут другие значения и, конечно же, будет другой идентификатор приложения (App ID). Поэтому внимательно выполните предыдущие шаги, чтобы правильно создать идентификатор вашего приложения и профиль. Потом возьмите разрешения из своего профиля и вставьте их в файл Entitlements.plist вашего проекта.
14. Далее перейдите в настройки сборки вашего проекта. В разделе Code Signing Entitlements (Разрешения для подписания кода) введите значение $(SRCROOT)/$(TARGET_NAME)/Entitlements.plist (если создали файл разрешений в целевом каталоге проекта) или $(SRCROOT)/Entitlements.plist (если создали файл разрешений в корневом каталоге, включающем в себя весь исходный код). Если не знаете, какой вариант выбрать, просто попробуйте оба этих значения, попытайтесь собрать проект с каждым из них. Если Xcode сообщит, что не может найти файл разрешений, поставьте второе значение, и оно сработает. Настройка сборки Code Signing Entitlements (Разрешения для подписания кода) требует указания относительного пути, идущего к файлу разрешений от корневого каталога исходного кода. Поэтому, если вы поместите файл разрешений в какой-то другой каталог, вам потребуется выстроить этот путь вручную, а потом записать полученный путь в это поле.
15. Соберите проект и убедитесь, что Xcode не выдает никаких ошибок. Если получите сообщение о какой-либо ошибке, то, вероятно, дело в том, что вы задали неверный профиль инициализации либо указали в настройках сборки неверный путь к файлу разрешений для подписания кода.
16. В делегате вашего приложения вызовите метод registerForRemoteNotificationTypes:, относящийся к классу UIApplication, а затем передайте этому методу значения UIRemoteNotificationTypeAlert, UIRemoteNotificationTypeBadge и UIRemoteNotificationTypeSound, как показано далее:
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:
UIRemoteNotificationTypeAlert |
UIRemoteNotificationTypeBadge |
UIRemoteNotificationTypeSound];
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Так вы зарегистрируете ваше приложение для получения пуш-уведомлений. Такие уведомления могут содержать информацию с предупреждениями, изменения номера ярлыка вашего приложения, а также звуки. Пока не будем вдаваться в эти подробности. Просто зарегистрируйте приложение для получения пуш-уведомлений, как показано ранее. Как только вы это сделаете, iOS отошлет запрос о регистрации к APNS. Перед тем как это делать, iOS попросит у пользователя разрешения зарегистрировать данное приложение для получения пуш-уведомлений. Пользовательский интерфейс, открывающийся в iOS при этом запросе, показан на рис. 15.7.
Рис. 15.7. iOS выдает пользователю запрос о разрешении на получение пуш-уведомлений
17. Теперь реализуйте метод application: didRegisterForRemoteNotificationsWithDeviceToken: делегата вашего приложения. Этот метод вызывается, когда iOS удается успешно зарегистрировать устройство в APNS и присвоить ему маркер. Этот маркер действует только для конкретного приложения, установленного именно на данном устройстве.
18. Далее реализуйте метод application: didFailToRegisterForRemoteNotificationsWithError: делегата вашего приложения. Этот метод вызывается, если iOS не удается зарегистрировать приложение для получения пуш-уведомлений. Это может произойти потому, что ваш профиль настроен неправильно или устройство не подключено к Интернету, а также по многим другим причинам. В параметре didFailToRegisterForRemoteNotificationsWithError этого метода вы получите ошибку типа NSError. Ее можно проанализировать и узнать, по какой причине возникла проблема.
Теперь вы знаете все необходимое, чтобы настроить ваше приложение на получение пуш-уведомлений.
См. также
Раздел 15.0.
15.8. Доставка пуш-уведомлений в приложение
Постановка задачи
Требуется отправлять пуш-уведомления на пользовательские устройства, которые зарегистрированы для получения таких уведомлений.
Решение
Убедитесь, что вы собрали маркеры-идентификаторы пуш-уведомлений этих приложений (см. раздел 15.7). Затем сгенерируйте SSL-сертификаты, которые будут использоваться вашими веб-сервисами для отправки пуш-уведомлений на устройства. Затем создайте простой веб-сервис для отправки пуш-уведомлений на зарегистрированные устройства.
Этот материал — продолжение раздела 15.7. Обязательно прочтите предыдущий раздел и полностью в нем разберитесь, прежде чем переходить к изучению данного раздела.
Обсуждение
Чтобы обмениваться информацией с серверами APNS, ваши веб-сервисы должны совершить акт квитирования (handshaking). Это обмен сигналами с сервером, при котором используется выданный Apple SSL-сертификат. Чтобы сгенерировать такой сертификат, выполните следующие шаги.
1. Войдите в центр разработки для iOS.
2. Перейдите в раздел Certificates, Identifiers & Profiles (Сертификаты, идентификаторы, профили), расположенный справа.
3. В разделе идентификаторов приложений (App ID) найдите идентификатор вашего приложения, для которого задано получение пуш-уведомлений, выберите этот идентификатор и нажмите кнопку Settings (Настройки), чтобы его сконфигурировать, как показано на рис. 15.8.
Рис. 15.8. Изменение настроек имеющегося идентификатора приложения
4. В разделе настроек, называемом Push Notifications (Пуш-уведомления), найдите подраздел Development SSL Certificate (SSL-сертификат для разработки) и нажмите кнопку Create Certificate (Создать сертификат) (рис. 15.9). Далее следуйте указаниям Apple по созданию сертификата. Пока мы создаем SSL-сертификат для разработки приложения, поскольку на данном этапе нас интересует исключительно разработка. Позже, когда будем готовы отправить наше приложение в App Store, просто повторите подобный процесс и создайте SSL-сертификаты для распространения (Distribution).
Рис. 15.9. Создание SSL-сертификата, используемого для операций с пуш-уведомлениями и разработки приложения
5. Как только сертификат будет готов (рис. 15.10), скачайте его на компьютер и дважды щелкните на нем кнопкой мыши, чтобы импортировать его в вашу связку ключей.
Рис. 15.10. SSL-сертификат для обмена информацией с APNS на этапе разработки приложения готов к скачиванию
6. Теперь откройте окно Keychain Access (Доступ к связке ключей) на OS X и перейдите к связке ключей Login (если эта связка ключей задана у вас по умолчанию). В разделе My Certificates (Мои сертификаты) найдите тот сертификат, который вы только что импортировали в связку ключей, и раскройте его, щелкнув на маленькой кнопке со стрелкой слева. Так вы узнаете ассоциированный с этим приложением закрытый ключ (рис. 15.11).
Рис. 15.11. Сертификат для работы с пуш-уведомлениями на этапе разработки и его закрытый ключ
7. Щелкните на сертификате правой кнопкой мыши и экспортируйте его как. cer-сертификат (а не как файл. p12). Назовите его PushCertificate.cer.
8. Щелкните правой кнопкой мыши на закрытом ключе и экспортируйте его как файл. p12 (а не как файл сертификата). Назовите его PushKey.p12. Здесь потребуется указать пароль к закрытому ключу. Обязательно используйте такой пароль, который впоследствии сможете вспомнить.
Отлично. Теперь, чтобы упростить дальнейший материал этого раздела, перейдем к использованию PHP. Применим этот язык для отправки на наше устройство простого пуш-уведомления. В разделе 15.7 мы уже настроили получение пуш-уведомлений в этом приложении. Поскольку настройка использования PHP на сервере Apache не относится к темам этой книги, мы воспользуемся упрощенным вариантом и применим MAMP. MAMP установит у вас на компьютере Apache и PHP, если вы еще не сделали этого. На сайте MAMP даются подробные инструкции о том, какие шаги потребуется выполнить. Когда вы установите MAMP, корневой каталог со всеми вашими PHP-файлами будет находиться по адресу /Applications/MAMP/htdocs/. Если при последующей установке этот каталог изменится, откройте MAMP, перейдите в раздел Preferences (Настройки), а потом — к Apache. Вы найдете там и корневой каталог Apache.
Чтобы наш PHP-сценарий мог обмениваться информацией с APNS, потребуется задать ему SSL-сертификат, который мы сгенерировали ранее, на портале разработки для iOS. Именно поэтому мы извлекли. cer-файл сертификата и файл. p12 закрытого ключа — теперь мы должны задать эти файлы нашему PHP-сценарию. Чтобы это сработало, используем в окне терминала openssl, а затем комбинируем сертификат и закрытый ключ. p12 в едином PEM-файле. В этой книге мы не можем подробно поговорить о PEM-файлах — это тема для отдельной книги. Однако вы можете подробнее познакомиться с этой темой в документе RFC 1421.
Для создания PEM-файла выполните следующие шаги (предполагается, что вы уже экспортировали файлы PushKey.p12 и PushCertificate.cer на ПК, как было описано ранее).
1. Откройте окно терминала (Terminal) в OS X. Введите в окне терминала следующую команду:
openssl x509 — in PushCertificate.cer — inform der — out PushCertificate.pem
2. Чтобы преобразовать файл. p12 в PEM-файл, введите в окне терминала следующую команду:
openssl pkcs12 — nocerts — in PushKey.p12 — out PushKey.pem
3. Система потребует ввести пароль, который вы задали для этого закрытого ключа, когда экспортировали его из раздела Keychain Access. После того как пароль для импорта будет проверен и подтвержден, OpenSSL запросит у вас фразу-пароль для результирующего PEM-файла. Этот пароль должен содержать не менее четырех символов. Задайте такой пароль и хорошо запомните его для последующего использования.
4. Теперь у вас на ПК должно быть два PEM-файла: PushCertificate.pem и PushKey.pem. Нужно сложить их в единый PEM-файл — этот формат распознается PHP. Для этого воспользуйтесь следующей командой:
cat PushCertificate.pem PushKey.pem > PushCertificateAndKey.pem
5. Теперь проверим, сможем ли мы подключиться к песочнице (речь идет о тестовой версии, только для целей разработки) с помощью сгенерированных нами PEM-файлов. В качестве песочницы используется защищенный APNS-сервер. Выполните в окне терминала следующую команду:
openssl s_client — connect gateway.sandbox.push.apple.com:2195 \
— cert PushCertificate.pem — key PushKey.pem
Если все будет нормально, то на данном этапе потребуется ввести фразу-пароль для вашего закрытого ключа. Помните ее? Хорошо, вводите. Если соединение будет успешно установлено, откроется OpenSSL, ожидающий от вас нескольких символов в качестве ввода. После получения этого ввода соединение будет закрыто. Введите любые случайные символы и нажмите Enter (Ввод). Соединение закрыто. Вам удалось успешно связаться с APNS-сервером, воспользовавшись вашим сертификатом и закрытым ключом.
Теперь напишем несложный PHP-сценарий для отправки простого пуш-уведомления на устройство. Но прежде чем мы двинемся дальше, необходимо получить маркер пуш-уведомлений, действующий на нашем устройстве. Мы должны получить его в таком формате, который понятен PHP. iOS инкапсулирует маркер пуш-уведомления в экземпляр NSData, но PHP неизвестно, что такое NSData. Нам нужно преобразовать этот маркер в строку, которую мы сможем использовать в нашем PHP-сценарии. Для этого считываем весь маркер байт за байтом и преобразуем каждый байт в шестнадцатеричное строковое представление:
— (void) application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
/* Каждый байт данных будет преобразован в свое шестнадцатеричное
значение, например 0x01 или 0xAB. При этом часть 0x отбрасывается.
Итак, для представления 1 байта нам потребуются два символа, поэтому
здесь указано * 2 */
NSMutableString *tokenAsString = [[NSMutableString alloc]
initWithCapacity: deviceToken.length * 2];
char *bytes = malloc(deviceToken.length);
[deviceToken getBytes: bytes];
for (NSUInteger byteCounter = 0;
byteCounter < deviceToken.length;
byteCounter++){
char byte = bytes[byteCounter];
[tokenAsString appendFormat:@"%02hhX", byte];
}
free(bytes);
NSLog(@"Token = %@", tokenAsString);
}
Запустите ваше приложение и убедитесь, что маркер устройства выводится на консоль, вот так:
Token = 05924634A8EB6B84437A1E8CE02E6BE6683DEC83FB38680A7DFD6A04C6CC586E
Отметьте себе этот маркер устройства, так как мы будем пользоваться им в PHP-сценарии:
/* При разработке мы пользуемся защищенной версией APNS. При подготовке
приложения для использования в реальных условиях измените это значение
на ssl://gateway.push.apple.com:2195 */
$apnsServer = 'ssl://gateway.sandbox.push.apple.com:2195';
/* Убедитесь, что это значение совпадает с паролем, который вы задали
для закрытого ключа при экспорте в.pem-файл, когда использовали openssl
в системе OS X */
$privateKeyPassword = '1234';
/* Если хотите, запишите здесь собственное сообщение */
$message = 'Welcome to iOS 7 Push Notifications';
/* Запишите здесь маркер вашего устройства */
$deviceToken =
'05924634A8EB6B84437A1E8CE02E6BE6683DEC83FB38680A7DFD6A04C6CC586E';
/* Замените эту информацию именем файла, указанного в файле вашего
сценария. В этом файле должны содержаться сгенерированные вами ранее
сертификат и закрытый ключ */
$pushCertAndKeyPemFile = 'PushCertificateAndKey.pem';
$stream = stream_context_create();
stream_context_set_option($stream,
'ssl',
'passphrase',
$privateKeyPassword);
stream_context_set_option($stream,
'ssl',
'local_cert',
$pushCertAndKeyPemFile);
$connectionTimeout = 20;
$connectionType = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT;
$connection = stream_socket_client($apnsServer,
$errorNumber,
$errorString,
$connectionTimeout,
$connectionType,
$stream);
if (!$connection){
echo «Failed to connect to the APNS server. Error no = $errorNumber
»;
exit;
} else {
echo «Successfully connected to the APNS. Processing…»;
}
$messageBody['aps'] = array('alert' => $message,
'sound' => 'default',
'badge' => 2,
);
$payload = json_encode($messageBody);
$notification = chr(0).
pack('n', 32).
pack('H*', $deviceToken).
pack('n', strlen($payload)).
$payload;
$wroteSuccessfully = fwrite($connection, $notification,
strlen($notification));
if (!$wroteSuccessfully){
echo «Could not send the message
»;
}
else {
echo «Successfully sent the message
»;
}
fclose($connection);
Если вы даже не программируете на PHP, внимательно просмотрите этот сценарий и почитайте комментарии к нему. Обязательно замените все значения в этом сценарии теми, что соответствуют вашему приложению. Например, используемый здесь маркер относится к моему устройству. Пользуйтесь маркером своего устройства, который выяснили ранее в этом разделе. У вас будут иные пароли, pem-файлы, скорее всего, будут находиться в других местах. Ради дополнительного упрощения этого раздела я поместил свой PHP-сценарий в тот же каталог, в котором ранее сохранил закрытый ключ и. pem-файл сертификата (PushCertificateAndKey.pem). Поэтому я могу обращаться к. pem-файлу просто по его имени.
Если вы правильно выполнили все шаги и инструкции, описанные в этом разделе, то ваш PHP-сценарий должен открываться в браузере. После этого на устройство начнут поступать уведомления. Сценарий посылает уведомление на APNS-сервер, который уже доставляет это уведомление на устройство. Когда пуш-уведомление попадает на устройство (предполагается, что на устройстве в этот момент отображается экран блокировки), вы увидите на экране примерно такую картинку, какая показана на рис. 15.12.
Рис. 15.12. Пуш-уведомление, отображенное на экране блокировки
См. также
Раздел 15.7.
15.9. Реагирование на пуш-уведомления
Постановка задачи
Проработав раздел 15.8, вы научились доставлять в ваше приложение пуш-уведомления, но не знаете, как реагировать на них в программе.
Решение
Реализуйте метод application: didReceiveRemoteNotification: делегата вашего приложения.
Обсуждение
Метод application: didReceiveRemoteNotification: делегата вашего приложения вызывается всякий раз, когда в систему iOS поступает пуш-уведомление и пользователь реагирует на это уведомление каким-то образом, инициируя открытие приложения. Этот метод срабатывает, когда приложение функционирует в приоритетном или в фоновом режиме, а не завершено. Например, пользователь может проигнорировать поступившее уведомление. Тогда и этот метод вызван не будет. Если пользователь нажимает на экране окно с пуш-уведомлением, в результате чего ваше приложение открывается, то iOS, открыв это приложение, переводит программу в приоритетный режим. После этого в делегате вашего приложения будет вызван вышеупомянутый метод.
Если приложение завершено и не работает даже в фоновом режиме, то iOS инкапсулирует пуш-уведомление, инициирующее запуск приложения, в параметрах запуска. Эти параметры будут переданы методу application: didFinishLaunchingWithOptions: делегата вашего приложения. Чтобы получить объект уведомления, просто запросите параметр didFinishLaunchingWithOptions этого метода (относящийся к типу NSDictionary) и поищите ключ UIApplicationLaunchOptionsRemoteNotificationKey. Значением этого ключа и будет то пуш-уведомление, которое запустило ваше приложение.
Параметр didReceiveRemoteNotification этого свойства несет в себе словарь типа NSDictionary. Этот словарь будет содержать в себе корневой объект под названием aps. Ниже этого объекта располагается словарь со следующими ключами, зависящими от того, как сервер создал пуш-уведомление (сервер может и не прислать все ключи сразу):
• badge — значением этого ключа является номер, который будет задан для ярлыка (значка) вашего приложения;
• alert — это содержащееся в пуш-уведомлении сообщение типа String. Сервер может прислать вам модифицированную версию значения этого ключа. Такое значение само по себе будет словарем, содержащим ключи body и show-view. Если вам будет прислана такая модифицированная версия предупреждения, то ключ body будет содержать именно тот текст, который находится в теле предупреждения. Ключ show-view будет содержать логическое значение, определяющее, должно ли предупреждение отображаться для пользователя. Кнопка Action (Действие) позволяет пользователю нажать на поступившее уведомление в центре уведомлений, чтобы открыть ваше приложение;
• sound — это строка, указывающая имя звукового файла, который должно воспроизводить ваше приложение;
• content-available — значением этого ключа является число. Если здесь стоит число 1, это означает, что на сервере появился новый контент, который приложение может скачать. Сервер может послать этот ключ приложению, затребовав таким образом, чтобы оно забрало с сервера список новых элементов. Ваше приложение может и не выполнять это требование. Это просто протокол, действующий между сервером и клиентом. Если в вашем приложении это целесообразно, следуйте данному протоколу.
См. также
Раздел 15.8.