Multiple_simultaneous_push_notifications

こんにちは。株式会社EveryDaySoft代表の永田です。

今回は題名のPush通知について手順を公開します。

実施する内容は、アプリをインストールすると、AWS上でPush通知に必要なサブスクリプションを作成し、同時に複数端末にPush通知が送信されます。
https://twitter.com/dbank0208/status/1307078514592616448?s=20
https://twitter.com/i/status/1307081526253957121
アプリのインストールした際にデバイストークンを作成するコード
 2. Print device token to use for PNs payloadsコメント部分でtokenを生成しています。tokenを生成するメソッドは、Apple標準のfunctionです。

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {


    func registerForPushNotifications() {
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {
            (granted, error) in
            print("Permission granted: \(granted)")
            // 1. Check if permission granted
            guard granted else { return }
            // 2. Attempt registration for remote notifications on the main thread
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
    }
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        registerForPushNotifications()
        return true
    }


    // MARK: UISceneSession Lifecycle
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }


    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // 1. Convert device token to string
        let tokenParts = deviceToken.map { data -> String in
            return String(format: "%02.2hhx", data)
        }
        let token = tokenParts.joined()
        // 2. Print device token to use for PNs payloads
        print(token)
        print("Device Token: \(token)")
    }


    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        // 1. Print out error if PNs registration not successful
        print("Failed to register for remote notifications with error: \(error)")
    }

}
APIのコード push通知 AWS Lambda APIGateWay
import boto3
import boto3
import json
import datetime


def lambda_handler(event, context):
    PLATFORM   = 'default'
    TargetArn = 'TargetArn'

    dict = {'aps':{"alert":"Hallo World","badge":1,"sound":"default"}}
    apns_string = json.dumps(dict,ensure_ascii=False)
    message = {'default':'default message','APNS_SANDBOX':apns_string}
    messageJSON = json.dumps(message,ensure_ascii=False)

    client = boto3.client('sns')

    request = {
        'TargetArn': TargetArn,
        'Message': messageJSON,
        'MessageStructure': 'json'
    }
    response = client.publish(**request)
APIのコード サブスクリプション作成 AWS Lambda APIGateWay
import boto3
import json
import datetime

def lambda_handler(event, context):
  client = boto3.client('sns')
  
  BUCKET_NAME = 'S3のBUCKET_NAME'
  s3 = boto3.client('s3')
  PRIVATE = 'PRIVATE.txt'
  res = s3.get_object(Bucket=BUCKET_NAME, Key=PRIVATE)
  body = res['Body'].read() # b'テキストの中身'
  bodystr = body.decode('utf-8')

  CERTIFICATE = 'CERTIFICATE.txt'
  res2 = s3.get_object(Bucket=BUCKET_NAME, Key=CERTIFICATE)
  body2 = res2['Body'].read() # b'テキストの中身'
  bodystr2 = body2.decode('utf-8')

  response = client.create_platform_application(
    Name='goto',
    Platform='APNS_SANDBOX',
    Attributes={
      'PlatformCredential': bodystr,
      'PlatformPrincipal' : bodystr2       
      }
  )

  response2 = client.create_platform_endpoint(
    PlatformApplicationArn=response['PlatformApplicationArn'],
    Token=event['push'],
    CustomUserData='abc'
  )

  response3 = client.subscribe(
    TopicArn='AmazonSNSのTopicArn',
    Protocol='application',
    Endpoint=response2['EndpointArn'],
    ReturnSubscriptionArn=True|False
  )
  return response3
  
  
S3に証明書をアップロード方法
Macのユーティリティ->キーチェーン->証明書を書き出し->ターミナルコマンド,ファイル作成->S3にアップロード
AppConnectでdevelop契約が前提です。AppConnect内で、iOSはAPNSの設定をします。
qiitaの記事を参照させていただきます。
https://qiita.com/b_a_a_d_o/items/e3bf9cd52b6cd9252088
ターミナルコマンド(macでP12ファイルと同じ階層で実施)
openssl pkcs12 -in P12ファイル名 -nocerts -nodes -out 秘密鍵ファイル名 
openssl pkcs12 -in P12ファイル名 -clcerts -nokeys -out 証明書ファイル名
S3にアップロードは簡単なので、割愛します。
この作業手順の他にもDeleteするAPIも作成する必要があります。アプリがアンインストールされた場合にDeleteAPIを使用する形になります。アプリをアンインストールされた場合の処理はcronなどで定期実行し、レスポンス結果により判定する設計にすると自動で対応できます。
APIのコード DeleteAPI作成 AWS Lambda APIGateWay
import boto3
import json
import datetime

def lambda_handler(event, context):
  client = boto3.client('sns')
  
  BUCKET_NAME = 's3のBUCKET_NAME'
  s3 = boto3.client('s3')
  PRIVATE = 'PRIVATE.txt'
  res = s3.get_object(Bucket=BUCKET_NAME, Key=PRIVATE)
  body = res['Body'].read() # b'テキストの中身'
  bodystr = body.decode('utf-8')
  # print(bodystr)

  CERTIFICATE = 'CERTIFICATE.txt'
  res2 = s3.get_object(Bucket=BUCKET_NAME, Key=CERTIFICATE)
  body2 = res2['Body'].read() # b'テキストの中身'
  bodystr2 = body2.decode('utf-8')

  response = client.create_platform_application(
    Name='goto',
    Platform='APNS_SANDBOX',
    Attributes={
      'PlatformCredential': bodystr,
      'PlatformPrincipal' : bodystr2       
      }
  )

  response2 = client.create_platform_endpoint(
    PlatformApplicationArn=response['PlatformApplicationArn'],
    Token=event['deleate'],
    CustomUserData='abc'
  )
  
  response3 = client.subscribe(
   TopicArn='Amazon snsのTopicArn',
   Protocol='application',
   Endpoint=response2['EndpointArn'],
   ReturnSubscriptionArn=True|False
  )
  
  response4 = client.unsubscribe(
    SubscriptionArn=response3['SubscriptionArn']
  )
 
  return response4
DataBaseの基本CRUDを意識して、アーキテクチャを作る事により、管理データが常に正しく運用できると思います。API側とiOS側に精通する事により、アプリ側ですべき内容、API側ですべき内容が分かります。少し先の未来ではMacアプリ、Android、Webも対応していきたいと考えています。

以上、貴重なお時間お読み下さいまして、誠にありがとうございます。