Things take time

[SWIFT] APNS, 푸시를 보내보자. - php 본문

iOS (기능)

[SWIFT] APNS, 푸시를 보내보자. - php

겸손할 겸 2017. 6. 30. 14:12

[푸시]


안드로이드의 경우, 현재 FCM이란 방식으로 파이어베이스를 이용한 푸시를 서비스 할 수 있다. 물론 이 파이어베이스에서는 FCM을 이용해 iOS에서도 푸시를 이용할 수 있게 하는데.. 나같은 경우 안드로이드는 FCM, 아이폰은 APNS를 이용해보려 한다.


[예제]


예제가 여러 곳에서 설명되어있는데.. 그 중에 실제 테스트 후 잘 된 곳을 바탕으로 다시 정리하려한다.


기본적으로 키체인을 통해 발급되는 인증서(.CSR)는 있다고 가정한다. 개발자라면 당연히 있을테니.. 

이제 막 시작하는 사람이라면 키체인 접근->인증서지원-> 인증기관에서 인증서 요청을 통해 발급받는다.


1) 인증서 받기 : developer.apple.com에서 상단 Account로 이동한 뒤 아래로 인증서 관리 페이지로 이동한다.



여기서 볼 것은 Identifiers의 App IDs다. 이 곳으로 이동하여 + 버튼을 누르고.. 이름과 패키지명을 입력한 뒤, 인증서를 발급받는다. 그리고 발급 받을 때 하단에서 Push Notifications는 꼭! 체크 후 넘어간다.


등록을 마치고 다시 App IDs에서 만든 것을 클릭해보면 하단의 Edit이 있으니 그 버튼을 클릭하여 CSR인증서를 등록한다. 개발용, 배포용 두 가지가 있으며.. 앱스토어에 올린다면 당연 배포용까지 해야한다. 지금은 테스트용으로 할 것이므로 개발용으로만.






이 파일을 다운받아서 실행한다. 그러면 키체인에 자동 등록된다.


2) pem파일(서버용) 만들기


이 파일의 용도는 서버에 업로드해서, 서버(php)에서 해당 인증서 pem파일을 갖고 디바이스 토큰 값들과 메시지를 조합해 해당 기기에 푸시를 발송할때 사용하는 서버용 인증서다.

일단 키체인 접근 앱을 실행해서 인증서 -> 우클릭 -> 내보내기와 키 -> 우클릭 내보내기를 통해 두 개의 .p12파일을 만든다. (각각 클릭해서 따로 내보내기)


** 만약 왼쪽의 저 하단 화살표가 없고, 인증서만 있을 경우

=> 맥 기기를 옮겼을 때 발생한다. CSR인증서가 옮기기 전의 기기에 있는 것을 복사해오면 나타나는 현상이므로, 새로 CSR을 발급받아 푸시 인증서를 받고, 등록해야 하단과 같은 이미지가 나타난다.




위는 인증서를 내보내기 한 것이고, 밑의 열쇠 모양 키도 내보내기로 보낸다. 비밀번호도 같이 입력한다.

내 예제에서는 인증서를 cert, 키 를 key란 이름으로 내보내기 했다.


그러면 데스크탑에는 cert.p12, key.p12가 생성되었을 것이다.

이제 터미널 앱을 실행하여 cd 명령어로 데스크탑으로 이동한다. (물론 저장 위치를 다른 곳에 했다면 그 곳으로 이동한다)


그리고 하단의 명령어들을 입력한다. (당연히 한 줄씩)

openssl pkcs12 -clcerts -nokeys -out cert.pem -in cert.p12
openssl pkcs12 -nocerts -out key.pem -in key.p12
openssl rsa -in key.pem -out key.unencrypted.pem
cat cert.pem key.unencrypted.pem > apns.pem

이렇게해서 나온 apns.pem이 최종적으로 서버에서 사용하는 pem파일이다.

그리고 서버에 업로드 시킨다. php에서는 해당 경로를 지목해 그 안의 pem파일을 불러올 것이다.


3) 프로비저닝 프로필 등록하기


개발자페이지에서 Identifiers에서 푸시용 인증서를 발급받았다. 이제는 프로비저닝 파일이라는 것을 다운받을 것이다. Identifiers의 아래 아래에 있는 Provisioning Profiles의 All클릭 후, 개발용으로 하나 생성한다. (앱을 출시할 것이라면 당연히 배포용도)



그리고 App IDs에서 등록한 패키지 명, 개발자, 사용 기기등을 선택하고 생성한 프로비저닝 파일을 다운받는다.



4) 테스트용 프로젝트 만들기


XCode로 개발자사이트에 등록한 패키지명대로 프로젝트를 만든다.



프로젝트의 Capabilities의 Push Notifications를 On으로!

프로비저닝 관련 충돌나면.. 다운받은 프로비저닝 파일 더블 클릭하고 General에서 Signing에서 Automatically manage signing해제 했다가 체크해본다.


프로젝트의 General 하단의 Linked Framework..부분의 + 버튼으로 2개(PushKit, UserNotifications)를 추가한다.






5) 이제 네이티브쪽에서의 코드 세팅이 필요하다.


참고용

https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/index.html#//apple_ref/doc/uid/TP40008194-CH3-SW1


개발자 홈페이지에서 apns를 검색하고 들어가면 확인할 수 있다. 기본 영어만 할 수 있으면 확인할 수 있다.

권한 요청, 실시간 권한 요청에 따른 콜, 포어그라운드상태에서 푸시 처리 등.. 


이런 코드들은 AppDelegate.swift에서 처리한다. 이 스위프트 파일은 어플의 처음 시작, 어플이 꺼졌을 때, 백그라운드 상태일때 등을 관리한다.

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        
        
        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options: [.badge, .alert, .sound]) {
            (granted, error) in
            // Enable or disable features based on authorization.
            if(granted){
                print("사용자가 푸시를 허용했습니다")                
                DispatchQueue.main.async {
                    application.registerForRemoteNotifications()
                }
            } else {
                print("사용자가 푸시를 거절했습니다")
            }
        }
        return true
    }
    
    
    // 토큰 정상 등록(registerForRemoteNotifications()을 호출한 결과가 성공일 때)
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let deviceTokenString = deviceToken.reduce("", {$0 + String(format: "%02X", $1)})

        print("등록된 토큰은 \(deviceTokenString) 입니다.")
    }
    
    // 토큰 등록 실패 (registerForRemoteNotifications()을 호출한 결과가 실패)
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("에러 발생 : \(error)")
    }
    
    
    /* 
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
     // 이 함수를 사용하려면 Capailities의 Background Mode ON하고 Remote Message체크해야 함
        print("메시지 수신1 : \(userInfo)")
    }*/
    
    func application(_ application: UIApplication, didReceiveRemoteNotification data: [AnyHashable : Any]) {
        print("메시지 수신2 : \(data)")
        UIApplication.shared.applicationIconBadgeNumber += 1
    }
    
    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
        
        // 뱃지를 사용한다면 액티브 될 때마다 0으로 초기화 해줘야함
        // UIApplication.shared.applicationIconBadgeNumber = 0
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }


}

6) 테스트


http://apns-gcm.bryantan.info 에서 pem파일과 푸시 토큰 값을 입력하면 테스트하면 된다.

 

 

6-1) php

<?php $deviceToken = '토큰 값 입력할 것'; // 디바이스토큰ID $message = 'Message received from eye'; // 전송할 메시지 // 개발용 $apnsHost = 'gateway.sandbox.push.apple.com'; $apnsCert = 'apns-dev.pem'; if (file_exists($apnsCert)){ echo "exist"; } else { echo "not exist"; } // 실서비스용 //$apnsHost = 'gateway.push.apple.com'; //$apnsCert = 'apns-production.pem'; $apnsPort = 2195; $payload = array('aps' => array('alert' => $message, 'badge' => 0, 'sound' => 'default')); $payload = json_encode($payload); $streamContext = stream_context_create(); stream_context_set_option($streamContext, 'ssl', 'local_cert', $apnsCert); $apns = stream_socket_client('ssl://'.$apnsHost.':'.$apnsPort, $error, $errorString, 2, STREAM_CLIENT_CONNECT, $streamContext); //$apns = stream_socket_client('ssl://gateway.push.apple.com:2195', $error, $errorString, 2, STREAM_CLIENT_CONNECT, $streamContext); if($apns) { $apnsMessage = chr(0).chr(0).chr(32).pack('H*', str_replace(' ', '', $deviceToken)).chr(0).chr(strlen($payload)).$payload; fwrite($apns, $apnsMessage); fclose($apns); } ?>

php의 소스 예제다. 구글링을 통해 알아낸 곳이었는데, 댓글을 달려하니 회원 가입을 하라고 해서 못남겼다.

감사합니다 (__)