Installation des Remote Push Notifications iOS avec Swift et envoi de notifications depuis un back-end PHP Symfony

Ce tutoriel vise à mettre en place les remote push notifications dans une application iOS 12 avec un serveur d'envoi de notifications sur Symfony.

Publié le 07/05/2019

1 - Configuration de l'application

On va créer une application iOS de type single view et la configurer comme suit :

Aller dans les propriétés du projet, puis dans l'onglet "Capabilities" et ajouter les fonctionnalités suivantes à l'aide du bouton "+" en haut à gauche dans Xcode :

  • Push Notifications
  • Background Modes et cocher la case "Remote Notifications"

On va avoir besoin d'un certificats pour signer les requêtes des notifications vers les serveurs de Notifications d'Apple.

1 .1 - Création d'une demande de certificat

Depuis l'Application "Trousseau d'accès" dans macOS, créer une demande de certificat à une autorité de certificat.

Menu > Trousseau d'accès > Assistant de certification > "Demander un certificat à une autorité de certificat"

Informations sur le certificat :

  • Adresse e-mail de l'utilisateur : email compte développeur
  • Nom commun : Laisser le nom par défaut
  • La requête est : Enregistrée sur le disque

Sauvegarder le fichier et reserver le pour la suite.

1.2 - Création d'un certificat de développement pour l'application

Depuis l'administration des clés et certificats sur developer.apple.com :

Dans le volet gauche, cliquer sur "Certificates, IDs & Profiles".

1.2.1 - Création d'un App ID

Dans le volet de gauche, cliquer sur "App IDs", puis ajouter un ID en cliquant sur "+"

  • Name : AppName
  • App Services : [cocher]

App ID Suffix :

  • [x] Explicit App ID
  • Bundle ID : com.domain.appname
  • [x] Push Notifications
1.2.2 - Configuration du service de notification pour l'App ID

Dans la liste des App IDs, sélectionner l'App ID et cliquer sur "Configure", puis dans "Development SSL Certificate" ou "Production SSL Certificate" cliquer sur "Create Certificate".

  • Uploader le fichier .csr "CertificateSigningRequest.certSigningRequest" créé avec le trousseau d'accès.
  • Télécharger le certificat et l'ajouter au trousseau d'accès en double cliquant sur le fichier.
  • Sélectionner la clé privée du certificat dans le trousseau et l'exporter au format .p12 en effectuant un clic droit dessus et en choisissant "Exporter ...".
  • Choisir une passphrase.
  • Si l'application dispose déjà d'un profil de provisionnement pour l'AppStore, le régénérer.

    Convertir le certificat et clé PKCS#12 en PEM avec les étapes suivantes :

    Environnement de développement

    Étape 1 : Créer le certificat .pem à partir du certificat .p12

    
    openssl x509 -in aps_development.cer -inform der -out PushChatCert.pem
    
    

    Étape 2: Créer la clé .pem from depuis la clé .p12

    
    openssl pkcs12 -nocerts -out apns-dev-key.pem -in apns-dev-key.p12
    
    

    Étape 3 : Facultatif (Si vous souhaiter supprimer la passphrase présente à l'étape 2)

    
    openssl rsa -in apns-dev-key.pem -out apns-dev-key-noenc.pem
    
    

    Étape 4 : Maintenant nous devons fusionner la clé et le certificat dans un fichier .pem

    
    cat PushChatCert.pem apns-dev-key-noenc.pem > apns-dev.pem (Si 3e étape effectuée)
    cat PushChatCert.pem apns-dev-key.pem > apns-dev.pem (Si 3e étape non effectuée)
    
    

    Étape 5 : Vérification de la validité du certificat et de la connectivité vers les serveurs APNS

    
    openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert PushChatCert.pem -key apns-dev-key.pem (Si 3e étape effectuée)
    openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert PushChatCert.pem -key apns-dev-key-noenc.pem (Si 3e étape non effectuée)
    
    
    Environnement de production

    Renouveler les mêmes étapes avec le certificat de production.

    2 - Implémentation du Bundle Symfony d'envoie de notification Push

    2.1 - Installation du bundle MobileNotifBundle

    GitHub : https://github.com/Linkvalue-Interne/MobileNotifBundle

    
    php composer.phar require linkvalue/mobile-notif-bundle
    
    

    2.2 - Configuration du bundle

    - Enregistrement du bundle dans l'AppKernel :

    
    // app/AppKernel.php
    
    public function registerBundles()
    {
    	$bundles = array(
    		// ...
    		new LinkValue\MobileNotifBundle\LinkValueMobileNotifBundle()
    	);
    }
    
    

    - Ajout du certificat dans le projet Symfony :

    Ajouter le fichier apns-push-cert.pem à la racine du projet.

    Ajout des paramètres dans config.yml

    
    # Push Notification LinkValueMobileNotifBundle
    #
    #    Development server: tls://gateway.sandbox.push.apple.com:2195
    #    Production server: tls://gateway.push.apple.com:2195
    link_value_mobile_notif:
    	clients:
    		apns:
    			my_apns_application:
    		      	params:
    		          		ssl_pem_path: "%kernel.root_dir%/../apns-push-cert-dev.pem"
    		           		ssl_passphrase: "PassPhrase"
    					endpoint: "tls://gateway.sandbox.push.apple.com:2195"
    
    

    3 - Implémentation des remotes notifications dans l'AppDelegate de l'application iOS

    Dans le fichier AppDelegate.swift :

    - Ajouter la directive : import UserNotifications

    - Implémenter les méthodes suivantes :

    
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    	let tokenString = deviceToken.reduce("", {$0 + String(format: "%02X",    $1)})
    	print("deviceToken: \(tokenString)")
    }
        
    //Called if unable to register for APNS.
    private func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) {
    	print(error)
    }
    // FIXED
    internal func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) {
    	print(error)
    }
    
    

    Puis dans la méthode func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool), Implémenter la demande de device token :

    
    let center = UNUserNotificationCenter.current()
    center.requestAuthorization(options:[.badge, .alert, .sound]) { (granted, error) in
    	// Enable or disable features based on authorization.
    }
    application.registerForRemoteNotifications()
    
    //FIXED
    center.requestAuthorization(options: [.badge, .alert, .sound]) { (granted, error) in
                DispatchQueue.main.async {
                    application.registerForRemoteNotifications()
                }
                
            }
    
    

    On va choisir le ViewController qui est le point d'entrée de notre application et lui implémenter la demande de permission pour les notifications :

    Ajouter la directive : import UserNotifications

    Implémenter la méthode :

    
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    	completionHandler([.alert, .badge, .sound])
    }
    
    

    4 - Récupération du device token et test de l'envoi de message

    Lors de l'exécution de l'application, la console va afficher le device token que l'on va récupérer pour tester l'envoie de notifications avec Symfony :

    
    php bin/console link_value_mobile_notif:apns:push "device_token" "Hello World!"
    
    

    5 - Création d'une entité Device dans le modèle Symfony

    On crée une entité qui modélise la device et va y stocker le deviceToken. On va ensuite ajouter une relation vers l'entité utilisateur : un utilisateur possède au moins une ou plusieurs devices.

    
    id;
        }
    
        /**
         * @return string
         */
        public function getName()
        {
            return $this->name;
        }
    
        /**
         * @param string $name
         */
        public function setName($name)
        {
            $this->name = $name;
        }
    
        /**
         * @return string
         */
        public function getDeviceToken()
        {
            return $this->deviceToken;
        }
    
        /**
         * @param string $deviceToken
         */
        public function setDeviceToken($deviceToken)
        {
            $this->deviceToken = $deviceToken;
        }
    
        /**
         * @return User
         */
        public function getUser()
        {
            return $this->user;
        }
    
        /**
         * @param User $user
         */
        public function setUser($user)
        {
            $this->user = $user;
        }
    }
    
    
    Et la relation coté User :
    
    
    devices = new ArrayCollection();
        }
    
    
            /**
         * @param $device
         * @return $this
         */
        public function addDevice($device)
        {
            $this->devices[] = $device;
            $device->setUser($this);
    
            return $this;
        }
    
        /**
         * @param $device
         * @return $this
         */
        public function removeDevice($device)
        {
            if ($this->devices->contains($device)) {
                $this->devices->remove($device);
            }
    
            return $this;
        }
    
        /**
         * @param $devices
         */
        public function setDevices($devices)
        {
            $this->devices->clear();
            foreach ($devices as $device) {
                $this->addDevice($device);
            }
        }
    
        /**
         * @return mixed
         */
        public function getDevices()
        {
            return $this->devices;
        }
    
        // ...
    }
    
    

    6 - Création d'un service Symfony pour l'envoie des notifications

    Création du service :

    
    
    namespace Greenmine\CommonBundle\Service;
    
    use Doctrine\ORM\EntityManager;
    use Greenmine\CommonBundle\Entity\User;
    use LinkValue\MobileNotif\Model\ApnsMessage;
    use Symfony\Component\DependencyInjection\Container;
    
    class APNService
    {
        private $entityManager;
    
        private $container;
    
        /**
         * APNService constructor.
         * @param Container $container
         */
        public function __construct(EntityManager $entityManager, Container $container)
        {
            $this->entityManager = $entityManager;
            $this->container = $container;
        }
    
        /**
         * Push notifications
         * @param $payload
         * @throws \Exception
         */
        public function pushNotification(string $title, string $body)
        {
            $message = new ApnsMessage();
            $message->setAlertTitle($title);
            $message->setAlertBody($body);
            // bingbong.aiff is the new default sound
            $message->setSound('bingbong.aiff');
            $message->setBadge(1);
    
            $user = $this->entityManager->getRepository('GreenmineCommonBundle:User')->find(1);
    
            $tokens = $this->getTokensFromUser($user);
    
            $message->setTokens($tokens);
    
            $this->container->get('link_value_mobile_notif.clients.apns.my_apns_application')->push($message);
        }
    
        /**
         * Community Push notifications
         * @param $payload
         * @throws \Exception
         */
        public function communityPushNotification(string $title, string $body, array $users)
        {
            $message = new ApnsMessage();
            $message->setAlertTitle($title);
            $message->setAlertBody($body);
            // bingbong.aiff is the new default sound
            $message->setSound('bingbong.aiff');
            $message->setBadge(1);
    
            $tokens = $this->getTokenFromCommunity();
    
            $message->setTokens($tokens);
    
            $this->container->get('link_value_mobile_notif.clients.apns.my_apns_application')->push($message);
        }
    
        /**
         * @param User $user
         * @return array
         */
        private function getTokensFromUser(User $user)
        {
            $tokenArr = [];
            $devices = $user->getDevices();
            foreach ($devices as $device) {
                $tokenArr[] = $device->getDeviceToken();
            }
    
            return $tokenArr;
        }
    
        /**
         * @return array
         */
        private function getTokenFromCommunity()
        {
            $users = $this->entityManager->getRepository('GreenmineCommonBundle:User')->findAll();
    
            $tokenArr = [];
            foreach ($users as $user) {
                foreach ($user->getDevices() as $device) {
                    $tokenArr[] = $device->getDeviceToken();
                }
            }
    
            return $tokenArr;
        }
    }
    
    
    Déclaration du service : Dans le fichier de déclaration des services du Bundle concerné, déclarer comme suit :
    
    greenmine_common.apn_service:
            class: Greenmine\CommonBundle\Service\APNService
            arguments: ['@doctrine.orm.entity_manager', '@service_container']
    
    

    7 - Génération d'un notification depuis un controller Symfony

    Nous allons maintenant pourvoir générer une notification depuis un controller Symfony :

    
    get('greenmine_common.apn_service')->pushNotification('Title', 'Hello World');
    
            return $this->render('@TestTest/Test/index.html.twig');
        }
    }
    
    

    8 - TestFlight et environnement de production

    Lors de la distribution d'une app via TestFlight ou l'App Store, configurer les propriétés du Bundle Symfony LinkValueMobileNotifBundle dans app/config/config.yml :

    
    link_value_mobile_notif:
        clients:
            apns:
                my_apns_application:
                    params:
                        ssl_pem_path: "%kernel.root_dir%/../apns-push-cert-prod.pem"
                        ssl_passphrase: "PassPhrase"
                        endpoint: "tls://gateway.push.apple.com:2195"