Обработка Push уведомлений на клиенте при их получении. И немного кода

от автора

Привет, Хабр!
При разработке приложения мы столкнулись с проблемой правильной обработки Push (т.н. пушей) уведомлений на стороне клиента. Перед разбором логики и тонкостей настройки пуш-уведомлений предлагаю определиться, в каких случаях возникает необходимость в изменении пушей? 

Типичные кейсы для применения изменяемых пушей:

● Аналитика получения пуш-нотификации;

● Подгрузка картинки в пуш;

● Кастомное изменение счетчика;

● Локализации текстов;

● Другие изменения тела пуша/тайтлов.

Теперь, разобравшись, для чего могут понадобиться навыки обработки пуш-уведомлений, мы можем перейти к более к детальному разбору данной технологии. И начнем с механизма Push notification extension. 

Ниже расскажу про метод его создания и подключения, сертификаты и возможности для пушей, приведу подробные примеры кода с пояснениями. Также поделюсь некоторыми тонкостями. Например, про логирование информации о получении пуша с применением опции keychain sharing, загрузку картинки в пуш-уведомление и изменение счетчика пушей.

Push Notification Extension

В iOS для обработки пуш-уведомлений существует отдельный механизм под названием Push Notificaions Extenstion. Сам по себе extension является отдельным процессом, который упаковывается в ту же самую ipa, что и основное приложение, и устанавливается на устройство вместе с приложением. Жизненный цикл Push Notification Extension не зависит от жизненного цикла самого приложения. А в случае получения пуш-нотификации от приложения, сама операционная система вызывает его, передавая данные пуш-нотификации, и позволяет эту пуш-нотификацию изменять. Она вызывается, только если в пуше есть поле mutable-content: 1

Создание экстеншена потребует написание нативного кода, и может одинаково использоваться как в React Native, так и в нативных приложениях, поэтому большая часть листингов будет именно на objC/swift.

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

Подключение и создание notification extension

В нашем xcode проекте создаем File -> New -> Notification Service Extension. По аналогии с обычным приложением указываем product name (для конечного пользователя оно нигде не будет отображаться), язык, команду, в которую его создаем (должна быть одинаковой с командой, чьими сертификатами подписывается основное приложение), и самое главное, на что следует обратить внимание — поле Embedded in Application — тут всегда указываем родительское приложение.

Сертификаты и Capabilities приложения – следующие шаги для настройки

Сертификаты

Экстеншен имеет отдельный bundle identifier, то есть для него создается свой application id в developer.apple.com. Поэтому необходимо иметь отдельные провижн профайлы для dev, adhoc, release. Для экстеншн также создается отдельный info.plist, и entitlements-файлы.

Capabilities

При переходе на этот шаг надо создать Appgroup, а затем  в основном приложении и экстеншене включить для них общую группу. Это поможет передавать данные между ними. Если вы используете безопасное хранилище keychain, например для хранения логина/пароля, и вам необходимо при получении пуш-уведомлений делать запросы с авторизацией (POST запрос для логирования информации о пуше, или загрузку картинки), то необходимо включить опцию keychain sharing — это поможет получить доступ к кейчейну из обоих мест.

Теперь можно непосредственно приступить к написанию кода:

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {- (void)serviceExtensionTimeWillExpire {

Сам экстеншн состоит из двух функций. Первая используется непосредственно для модификации тела пуша.

Создаем копию содержимого пуша  (self.bestAttemptContent = [request.content mutableCopy];), изменяем ее как необходимо, и после изменений вызываем self.contentHandler с этим аргументом, что вызовет отображение пуш нотификации.

Приведу ниже примеры использования:

Простые — модификация тайта, счетчика и аналогичных полей.

self.bestAttemptContent.badge = @2; self.bestAttemptContent.title = @"My modified title"; self.contentHandler(self.bestAttemptContent);

Аналогично этому примеру можно изменять и остальные поля.

Для чего это нужно?

Таким образом можно подставлять нужные локализационные тексты, для этого в теле пуша у Apple предусмотрено поле text-loc. Оно используется для хранения ключа локализации. Затем получать по ключу нужный текст в зависимости от локали девайса и выставлять его в тайтл/сабтайтл.

Еще один пример использования — логирование информации о получении пуша (например, для целей аналитики).

Предположим, что логирование — это персонализированный запрос, который надо сделать от определенного юзера/токена. Токен храниться в shared keychain (его создание я описал выше в статье), а считать токен можно как в примере листинга ниже. 

- (NSString *)updateToken {     NSString *authenticationPrompt = @"Authenticate to retrieve secret";      NSDictionary *query = @{       (__bridge NSString *)kSecClass: (__bridge id)(kSecClassGenericPassword),       (__bridge NSString *)kSecAttrService: service,       (__bridge NSString *)kSecReturnAttributes: (__bridge id)kCFBooleanTrue,       (__bridge NSString *)kSecReturnData: (__bridge id)kCFBooleanTrue,       (__bridge NSString *)kSecMatchLimit: (__bridge NSString *)kSecMatchLimitOne,       (__bridge NSString *)kSecUseOperationPrompt: authenticationPrompt     };      // Look up service in the keychain     NSDictionary *found = nil;     CFTypeRef foundTypeRef = NULL;     OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef) query, (CFTypeRef*)&foundTypeRef);      if (osStatus == noErr) {         found = (__bridge NSDictionary*)(foundTypeRef);          if (found) {             NSString* token = [[NSString alloc] initWithData:[found objectForKey:(__bridge id)(kSecValueData)] encoding:NSUTF8StringEncoding];   return token;         }     Return null;     } }

Дальше можно использовать как библиотеки для создания API запросов, так и написать запрос с помощью стандартного URLRrequest:

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"https://example/logs/push-notification-received"]]];     NSDictionary *body = @{@"notification-payload":payload};     NSData *data = [NSJSONSerialization dataWithJSONObject:body options:kNilOptions error:nil];      [request setHTTPMethod:@"POST"];     [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];     [request setValue:[NSString stringWithFormat:@"Token token=\"%@\"",token] forHTTPHeaderField:@"Authorization"];     [request setHTTPBody:data];      [[[NSURLSession sharedSession] dataTaskWithRequest:request] resume];

Следующая типичная задача — загрузка картинки в пуш нотификацию. 

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

Пример загрузки картинки:

NSString *urlString = [request.content userInfo][@"image"]; if (!urlString) {         self.contentHandler(self.bestAttemptContent);         return;     }      NSURL *fileUrl = [NSURL URLWithString:[appApiUrl stringByAppendingString:urlString]];      NSURLSessionDownloadTask *task = [[NSURLSession sharedSession] downloadTaskWithURL:fileUrl completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {          if (response && location) {             NSString *fileType = [self determineType:response.MIMEType];             NSString *fileName = [location.lastPathComponent stringByAppendingString:fileType];              NSString *tmpDirectory = NSTemporaryDirectory();             NSString *tmpFile = [[@"file://" stringByAppendingString:tmpDirectory] stringByAppendingString:fileName];             NSURL *tempUrl = [NSURL URLWithString:tmpFile];              if (tempUrl) {                 [[NSFileManager defaultManager] moveItemAtURL:location toURL:tempUrl error:&error];                  if (error == nil) {                     UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:attachmentIdentifier URL:tempUrl options:NULL error:NULL];                      if (attachment) {                         self.bestAttemptContent.attachments = @[attachment];                     }                 }             }         }          self.contentHandler(self.bestAttemptContent);     }];      [task resume];  - (NSString *)determineType:(NSString *)contentType {     if ([contentType isEqualToString:@"image/jpeg"]) {         return @".jpg";     }     if ([contentType isEqualToString:@"image/gif"]) {         return @".gif";     }     if ([contentType isEqualToString:@"image/png"]) {         return @".png";     }      return @".tmp"; }

Также, мы можем указать на кастомный экран запуска приложения, если приложение запускается из этого пуша. Для этого есть поле launchImageName.

Иногда возникает необходимость получить доступ к данным, которые сохраняются самим приложением. Для этого можно использовать UserDefaults с кастомным suite, если приложения объединены в одну группу (App group).

Запись в UserDefaults:

NSDictionary *userInfo = [request.content userInfo];     NSNumber *customData = userInfo[@customData"];      NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.ios.example"];  [sharedDefaults setObject: customData forKey:@”customDataKey”]; [sharedDefaults synchronize];

Считывание из UserDefaults:

NSObject* objectForKey = [sharedDefaults objectForKey:@”customDataKey”];

Существуют различные вспомогательные функции для хранения соответствующих типов данных:

- (void)setInteger:(NSInteger)value forKey:(NSString *)defaultName; - (void)setFloat:(float)value forKey:(NSString *)defaultName; - (void)setDouble:(double)value forKey:(NSString *)defaultName; - (void)setBool:(BOOL)value forKey:(NSString *)defaultName; - (void)setURL:(nullable NSURL *)url forKey:(NSString *)defaultName API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));

И на считывание:

- (NSInteger)integerForKey:(NSString *)defaultName; - (float)floatForKey:(NSString *)defaultName; - (double)doubleForKey:(NSString *)defaultName; - (nullable NSData *)dataForKey:(NSString *)defaultName; - (nullable NSString *)stringForKey:(NSString *)defaultName; - (nullable NSArray *)arrayForKey:(NSString *)defaultName; - (nullable id)objectForKey:(NSString *)defaultName;

Обратите внимание, что для считывания добавилась возможность получить строку, массив, данные (date) по ключу. Это просто обертка над objectForKey, с дополнительным приведением к указанному типу. Сохраняем эти значения (массив, строку и.т.д.) через обычный setObject.

В рамках js окружения, в react-native приложениях работу с UserDefaults упростить npm пакеты может react-native-default-preference. Тут важно обратить внимание на то, что из коробки он работает только вместе со скоупом UserDefaults standartDefaults, который не расшаривается между приложением и экстеншеном. Но в них есть возможность указания используемого suite. Поэтому, если данная функция для нас необходима, то мы можем делиться данными между приложением и расширением.

import UserDefaults from 'react-native-default-preference'; UserDefaults.setName(“group.com.ios.example”); const customData = await UserDefaults.get(@”customDataKey”)

Для чего необходимо счетчик пушей? Например, ваше приложение получает пуши от двух независимых серверов, у каждого из которых свой счетчик badge, а приложение должно их суммировать.

Следующая важная функция:

- (void)serviceExtensionTimeWillExpire {

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


ссылка на оригинал статьи https://habr.com/ru/company/sdventures/blog/660997/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *