Push в ios приложениях
Коротко как работают push уведомления для ios(подробная статья тут)
Мобильное приложение iOs.
Для того, чтобы подключить push сообщения, необходимо добавить в AppDelegate
Для ios7:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { #if !TARGET_IPHONE_SIMULATOR [application registerForRemoteNotificationTypes: UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound]; #endif application.applicationIconBadgeNumber = 0;
Для ios8:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { #if !TARGET_IPHONE_SIMULATOR //-- Set Notification if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) { [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge) categories:nil]]; [[UIApplication sharedApplication] registerForRemoteNotifications]; } else { [[UIApplication sharedApplication] registerForRemoteNotificationTypes: (UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert)]; } #endif application.applicationIconBadgeNumber = 0;
Добавляем прием сообщений:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { application.applicationIconBadgeNumber = 0; // We can determine whether an application is launched as a result of the user tapping the action // button or whether the notification was delivered to the already-running application by examining // the application state. if (application.applicationState == UIApplicationStateActive) { // Nothing to do if applicationState is Inactive, the iOS already displayed an alert view. UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Внимание!" message:[NSString stringWithFormat:@"%@", [[userInfo objectForKey:@"aps"] objectForKey:@"alert"]] delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alertView show]; } }
Необходимо зарегистрировать устройство. Для этого в AppDelegate добавляем:
#pragma mark Remote notifications - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { // You can send here, for example, an asynchronous HTTP request to your web-server to store this deviceToken remotely. NSLog(@"Did register for remote notifications: %@", deviceToken); NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration]; NSURL *url = [NSURL URLWithString:@"http://your_site/reg_client_device"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0]; NSString *noteDataString = [NSString stringWithFormat:@"token=%@&os=%@&version=%@&language=%@", deviceToken, @"ios", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"], [[NSLocale preferredLanguages] objectAtIndex:0]]; [request setHTTPMethod:@"POST"]; request.HTTPBody = [noteDataString dataUsingEncoding:NSUTF8StringEncoding]; NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { //NSLog(@"Data: %@", data); //NSLog(@"Responce: %@", response); if(error) NSLog(@"Can't register device. Error: %@", error); }]; [postDataTask resume]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { NSLog(@"Fail to register for remote notifications: %@", error); }
Или с помощью AFNetworking
#pragma mark - Remote notifications - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { // You can send here, for example, an asynchronous HTTP request to your web-server to store this deviceToken remotely. NSLog(@"Did register for remote notifications: %@", deviceToken); NSString *urlAsString = [NSString stringWithFormat:@"%@%@", DOMAIN_NAME, @"api/devices"]; AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager]; NSDictionary *parameters = @{@"device_token": deviceToken, @"device_os": @"ios", @"device_lang": [[NSLocale preferredLanguages] objectAtIndex:0], }; [manager POST:urlAsString parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) { NSLog(@"Register device."); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { NSLog(@"Can't register device. Error: %@", error); }]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { NSLog(@"Fail to register for remote notifications: %@", error); }
Со стороны мобильного приложения больше ничего делать не надо. Переходим к серверу.
Сертификаты (для сервера и приложения).
1) Необходимо создать app Id с разрешенными push
2) настроить ssl сертификат. Для этого переходим в app id Edit-> Config SSl Cert
3) Чтобы сгенирировать для сертификата ключ, открываем сязку ключей Связка Ключей -> Ассистент Сертификации -> Запросить сертификат у бюро сертификации (выбираем сохранить на диске)
4) полученный файл загружаем в member center SSL Cert
5) скачиваем сгенерированный сертификат
6) Приватный ключ переводим в формат p12. Для этого заходим в Связку Ключей выбираем Вход и Ключи, на личном ключе правой кнопкой мыши -> экспортировать
7) Преобразовываем .cer в .pem сертификаты. В терминале:
openssl x509 -in aps_development.cer -inform der -out PushSECert.pem
8) Преобразуем ключ p12 в pem
openssl pkcs12 -nocerts -out PushSEKey.pem -in PushSEKey.p12
9) Объединяем сертификаты
cat PushSECert.pem PushSEKey.pem > ck.pem
10)Тестируем
telnet gateway.sandbox.push.apple.com 2195 openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert PushSECert.pem -key PushSEKey.pem
Размещаем сертификат на сервере.
Сервер.
Для начала нужно написать api для регистрации устройств.
Создаем таблицу (например, MySql):
CREATE TABLE `device` ( `device_id` int(11) NOT NULL AUTO_INCREMENT, `device_token` varchar(255) NOT NULL, `device_os` varchar(100) NOT NULL, `device_lang` varchar(100) NOT NULL, PRIMARY KEY (`device_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
У меня админская часть написана на Yii2, поэтому для написания api я использовала его.
С помощью gii я сгенерировала модель Device на основе созданной таблицы.
Создала контролер DeviceController на основе ActiveController (подробней почитать можно тут )
<?php namespace app\controllers\api; use Yii; use app\models\Device; use yii\rest\ActiveController; use yii\web\Response; /** * DeviceController implements the CRUD actions for Device model. */ class DeviceController extends ActiveController { public $modelClass = 'app\models\Device'; public $serializer = [ 'class' => 'yii\rest\Serializer', 'collectionEnvelope' => 'device', ]; public function behaviors() { $behaviors = parent::behaviors(); $behaviors['contentNegotiator']['formats']['application/json'] = Response::FORMAT_JSON; return $behaviors; } public function actions() { $actions = parent::actions(); // disable the "delete" and "create" actions unset($actions['delete'], $actions['index'], $actions['options']); // customize the data provider preparation with the "prepareDataProvider()" method return $actions; } public function afterAction($action, $result) { $result = parent::afterAction($action, $result); // your custom code here if($action->id == 'create' || $action->id == 'update') { $i = 0; foreach ($result as $singleResult) { if(isset($singleResult['message'])) { $singleResult['success'] = false; $singleResult['error_code'] = $this->getErrorCodeForActionId($action->id); $result[$i] = $singleResult; } else { $result = array($result); break; } $i++; } } $result = array('device' => $result); return $result; } function getErrorCodeForActionId($actionId) { switch ($actionId) { case 'update': return 1; case 'create': return 2; default: return 0; } } }
Так как в iOS deviceToken имеет такой вид:
<xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx>,
а для push нужен без пробелов и <>, пришлось немного отредактировать сгенерированную модель:
<?php namespace app\models; use Yii; /** * This is the model class for table "device". * * @property integer $device_id * @property string $device_token * @property string $device_os * @property string $device_lang */ class Device extends \yii\db\ActiveRecord { public static function tableName() { return 'device'; } public function rules() { return [ [['device_token', 'device_os', 'device_lang'], 'required'], [['device_token'], 'unique'], [['device_token'], 'string', 'max' => 255], [['device_os', 'device_lang'], 'string', 'max' => 100] ]; } public function attributeLabels() { return [ 'device_id' => 'Device ID', 'device_token' => 'Device Token', 'device_os' => 'Device Os', 'device_lang' => 'Device Lang', ]; } public function beforeValidate() { if($this->device_os == 'ios') $this->device_token = str_replace(array(' ', '<', '>'), '', $this->device_token); return parent::beforeValidate(); } }
Осталось написать отправку push сообщений на определенные устройства.
Для iOS я использую библиотеку ApnsPhp github.com
Для Google — класс GCMPushMessage github.com
Эти библиотеки я разместила в basic/vendor/push.
В файле basic/web/index.php необходимо добавить пути к этим библиотекам:
//before add yii require_once ( __DIR__ .'/../vendor/push/ApnsPHP/Autoload.php'); require_once ( __DIR__ .'/../vendor/push/GCMPushMessage.php'); require(__DIR__ . '/../vendor/autoload.php'); require(__DIR__ . '/../vendor/yiisoft/yii2/Yii.php');
Созданный ранее сертификат я добавила в basic/web/cert/ckDev.pem
В класс модели Device я добавила метод
public function sendOnePush($messageTitle='Push', $message='Push') { //ANDROID PUSH if($this->device_os == 'android') { $apiKey = "your_api_key"; $devicesAndroid = array(); array_push($devicesAndroid, $this->device_token); $gcpm = new \GCMPushMessage($apiKey); $gcpm->setDevices($devicesAndroid); $response = $gcpm->send($message, array('title' => $messageTitle)); $push_android = new Push(); $push_android->push_date = date("Y-m-d H:i:s"); $push_android->push_os = 'android'; $push_android->push_title = $messageTitle; $push_android->push_text = $message; $android_result = json_decode($response, true); $push_android->push_state = 'Успешно отправлено: '.$android_result['success'] .'. Не получилось отправить: '.$android_result['failure']; $push_android->save(); //die(var_dump($push_android->getErrors())); return true; } //IOS PUSH if($this->device_os == 'ios') { $push = new \ApnsPHP_Push( \ApnsPHP_Abstract::ENVIRONMENT_SANDBOX, 'cert/ckDev.pem' ); $push->setProviderCertificatePassphrase('your_cert_password'); // Set the Root Certificate Autority to verify the Apple remote peer //$push->setRootCertificationAuthority('cert/entrust_2048_ca.cer'); $push->connect(); $messageAPNs = new \ApnsPHP_Message(); $messageAPNs->addRecipient($this->device_token); // Set a custom identifier. To get back this identifier use the getCustomIdentifier() method // over a ApnsPHP_Message object retrieved with the getErrors() message. $messageAPNs->setCustomIdentifier("Message-Push-Update"); $messageAPNs->setText($message); $messageAPNs->setSound(); // Set the expiry value to 30 seconds $messageAPNs->setExpiry(30); $push->add($messageAPNs); $push->send(); $push->disconnect(); // Examine the error message container $aErrorQueue = $push->getErrors(); if (!empty($aErrorQueue)) { var_dump($aErrorQueue); } $push_ios = new Push(); $push_ios->push_date = date("Y-m-d H:i:s"); $push_ios->push_os = 'ios'; $push_ios->push_title = $messageTitle; $push_ios->push_text = $message; if (!empty($aErrorQueue)) { $push_ios->push_state = var_dump($aErrorQueue); } else $push_ios->push_state = 'Push был отправлен'; $push_ios->save(); //die(var_dump($push_ios->getErrors())); return true; } return false; }
Для примера создам кнопку, при нажатии на которую будет отправляться push на определенное устройство.
//in view <?= Html::a('SendTestPush', ['send-push', 'id' => $model->device_id], [ 'class' => 'btn btn-default', 'data' => [ 'confirm' => 'Are you sure you want to send push?', 'method' => 'post', ], ]) ?> //in controller public function actionSendPush($id) { $device = $this->findModel($id); $device->sendOnePush('Test push', 'Test push'); return $this->redirect(['index']); }