Things take time

[SWIFT] NotificationService Extension사용하기, Remote Notification(Silent Push)대신 사용하기, 백그라운드 푸시, 푸시 데이터 저장하기 본문

iOS (기능)

[SWIFT] NotificationService Extension사용하기, Remote Notification(Silent Push)대신 사용하기, 백그라운드 푸시, 푸시 데이터 저장하기

겸손할 겸 2022. 6. 8. 13:24

[개요]

기본적으로 사용자가 푸시를 받았을 때, 포어그라운드 상태라면 AppDelegate내 함수에서 처리할 수 있다.

 

만약 서버쪽에서 사용자에게 푸시를 보냈을때, 특정 값을 저장 했다가 앱이 실행될 때 해당 값을 검사하여 원하는 작업을 수행하길 원한다고 하자. 포어그라운드 상태라면 위의 didReceive함수(ios 10미만의 경우, application의 didReceiveRemoteNotification일 것이고 이상인 경우 userNotification의 willPresent, didReceive)에 UserDefaults등을 활용하여 MainViewController나 특정 뷰에서 처리하면 된다.

 

그렇다면, 사용자가 앱을 끈 상태(inactive)거나 백그라운드(background)상태일 때는 어떻게 할 것인가?

 

기본적으로 iOS는 백그라운드 작업을 지원하지않는다.

다만, 여기에 추가적으로 애플이 허용한 앱 혹은 설정에 따라 백그라운드 오디오, fetch, remote notification 등을 지원하는데 여기서 고려해볼 만한 것은 Silent Push, 무음 푸시라하여 Background Mode 내 Remote Notification기능을 활성화하는 것이다.

 

[사일런트 푸시]

이 기능의 경우, 사용자는 푸시를 볼 수 없다. 그러므로 사용자 입장에서는 푸시에 대해 신경 쓸 필요가 없이 특정한 기능을 사용할 수 있다.

그러나 이 기능의 경우 단점이 많은데, 대표적인 예가 푸시발송을 100% 보장하지 않으며, 잦은 푸시를 사용할 경우도 권장하지 않는 등의 단점이 있다.

https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app

 

Apple Developer Documentation

 

developer.apple.com

사용자는 서버가 푸시를 발송했을 때 알지 못한다. 그러나 앱은 알 수 있다.

AppDelegate내 함수 중 applicatiom(didReceiveRemoteNotification)함수에서 알 수 있는데, 이 함수는 iOS10 미만의 푸시 접수 함수이기도 하지만,  사일런트 푸시 수신시에도 호출된다.

사일런트 푸시의 경우, 아래의 조건과 서버 측의 apns payload 양식이다.

Xcode 내 설정

{
    "aps" : {
        "content-available" : 1
    },
    "acme1" : "bar",
    "acme2" : 42
}

출력(applicatiom(didReceiveRemoteNotification))

didReceiveRemoteNotification userInfo: [AnyHashable("acme2"): 42, AnyHashable("acme1"): bar, AnyHashable("aps"): {
    "content-available" = 1;
}]

** 참고 : 알림 전송 시 payload 양식(payload guide)

https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html

 

Local and Remote Notification Programming Guide: Creating the Remote Notification Payload

Local and Remote Notification Programming Guide

developer.apple.com

사일런트 푸시가 안좋은것은 아니다. 용도에 맞춰서 써야하는게 가장 기본이므로, 시간당 몇 개를 못보낸 다는 이유로 나는 사일런트 푸시 대신 다른 방법을 택했다. 

 

[NotificationService Extension]

이제 새로운 타겟인 익스텐션을 추가해야한다. iOS에서는 다양한 익스텐션이 존재하는데, 예를 들어 설명하자면 웹페이지에서 공유하기를 클릭 했을 때, 카카오톡을 선택하면 카카오톡이 열리면서 친구목록/대화목록 화면이 나오는 것을 알 수 있다. 근데 이 화면은 일반적으로 카카오톡을 켰을때 나오는 화면이랑 다른 화면이다. 즉, 공유하기를 통해 왔을 때 사용자에게 새로운 화면을 중간에 넣어서, 그 화면내에서 독립적인 처리를하는 것인데, 이는 Share Extension이라하며 익스텐션 중 한 종류다.

 

NotificationService Extension이란 기본적으로 사용자에게 푸시 노티를 띄우기 전에 껴드는 것이다. Share의 경우에도 중간에 껴드는 것처럼 같은 맥락으로 이해하면 된다. 푸시를 이쁘게(?) 보내는 앱들을 보면, 노티 안에 그림도 들어가고 클릭 했을 때 이벤트도 있고 하는 경우가 이 익스텐션을 사용한 경우인데, 나의 경우에는 노티를 꾸미기 위해 사용하는 것이 아니라, inactive/background 상태일 때 푸시를 받고, 그 때 푸시 데이터를 저장하여 AppDelegate에서 뷰로 전달하기 위해 사용하는 용도이므로, UI에 대한 설명은 생략한다.

위의 절차를 통해 생성했을 경우, 아래처럼 새로운 디렉토리와 Target도 추가된다.

NotificationService.swift라는 파일이 생성이 되는데, 이 안에는 didReceive함수와 serviceExtensionTimeWillFire함수가 있다.

didReceive의 경우가 실제 원하는 기능을 구현할때 사용한다. 후자의 경우, 익스텐션이 시스템에의해 사라질때 호출된다는데, 딱히 검증하지 않았다. AppGroupsName은 개발사이트 내 AppGroups에서 생성한 이름이다. 모르겠으면 아래 참고.

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        if let bestAttemptContent = bestAttemptContent {
            // Modify the notification content here...
            bestAttemptContent.title = "G-Y-E-O-M"
            bestAttemptContent.body = "B-O-D-Y"
            
            
            if let userDefault = UserDefaults(suiteName: "AppGroupsName"){
                let randomNum = Int.random(in: 0...100)
                userDefault.set(randomNum, forKey: "test")
                userDefault.synchronize()
            }
            contentHandler(bestAttemptContent)
        }
    }

함수 부분을 보자, UNMutableNotificationContent 타입의 변수인 bestAttemptContent의 title, body 속성을 변경하면 푸시 노티를 아래와 같이 변경할 수 있다. 이미지 관련 부분도 속성 중 하나일 것이다.

어쨌든, 중요한 것은 그쪽이 아니고 아래의 userDefault이다. 즉, appGroups로 지정한 UserDefaults의 경우, 실제 저장 장소가 다른 본 앱과 익스텐션 사이의 데이터를 공유할 수 있다. 여기에서 푸시를 받았을 때 노티를 띄우기 전 저장을하고, 본앱의 AppDelegate + ViewControllers에서 가져다 쓰면 된다.

 

위의 사일런트 푸시의 경우 "content-available" : 1가 들어갔다면, 노티피케이션서비스 익스텐션의 경우 "mutable-content": 1 가 들어가야한다. 예시로 보낸 페이로드의 경우 다음과 같다.

{  
   "aps":{  
      "sound":"default",
      "mutable-content": 1,
      "alert":{  
         "body":"body message"
      }
   }
}

** AppGroups 등록 방법

AppGroups의 경우 Xcode내에서도 다음과 같이 각각의 Target에서 AppGroups를 +Capability를 눌러서 세팅해야한다.

그런데 AppGroups가 보이지 않는다면 

 

1. https://developer.apple.com

 

Apple Developer

There’s never been a better time to develop for Apple platforms.

developer.apple.com

2. account

3. 로그인

4. Identifiers

5. + 버튼

생성 후 Xcode에서 AppGroups reload하면 끝이다.

 

어쨌든, 익스텐션의 didReceive에서 저장이 되었다면 아래와 같이 appDelegate에서 확인할 수 있다.(예)

    func applicationDidBecomeActive(_ application: UIApplication) {
        print("applicationDidBecomeActive")
        if let groupUserDefaults = UserDefaults(suiteName: "AppGroupsName"){
            print("applicationDidBecomeActive : => \(groupUserDefaults.value(forKey: "test"))")
        }
    }

이렇게 하면 아래와 같이 출력되는 걸 확인할 수 있다.

applicationDidBecomeActive : => Optional(90)

 

** didReceive에서 print출력 방법

일단, 익스텐션의 경우 자체 print로 넣는다고 출력되지 않는다. 현재 실행한 타겟은 본앱이기 때문인데, 나는 검수를 꼭 해보고 싶다면 아래와 같이 한다. (NotificationExtension이 본앱, NotificationService가 익스텐션(옆에 E버튼))

디버그 실행을 익스텐션인 NotificationService로 시작하고 앱을 선택하는 것에서는 NotificationExtension로 한다. 그리고 d푸시를 발송하고, didReceive에 print문이 정상 출력되는지 확인한다.