Как стать автором
Обновить
0
Social Discovery Group
Global tech-company in social discovery

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

Время на прочтение 8 мин
Количество просмотров 4.7K

Привет, Хабр!

При разработке приложения мы столкнулись с проблемой правильной обработки 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 {

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

Теги:
Хабы:
+3
Комментарии 1
Комментарии Комментарии 1

Публикации

Информация

Сайт
socialdiscoverygroup.com
Дата регистрации
Численность
501–1 000 человек

Истории