이번 글에서는 Remote Notification(Server Push/FCM)으로 전송한 푸시를 시스템이 디바이스에 푸시를 노출시키기 전에 미리 수신을 받아 Payload 재구성 및 IOS 앱 뱃지 카운트 작업 등을 수행하는 방법에 대해서 알아보도록 하겠다.
Flutter Firebase_messaging 라이브러리를 통해서 수신을 받을 수도 있지만 이슈가 많아 네이티브(swift)에서 수신을 받아 Platform Channel을 통해서 스트림으로 flutter에 전달하는 방법을 사용하도록 하겠다.
플랫폼 통신 - 1 > Method Channel - IOS
플랫폼 통신 - 2 > Event Channel - IOS
Flutter <-> Native Platform 간의 통신 방식인 Platform Channel에 대해서 설명한 글은 위 링크에서 확인할 수 있다.
IOS는 swift 코드로 개발을 진행하였다.
앱의 생명주기(Life Cycle)에 따른 활용 방법에 대해서 따로 나눠서 코드를 작성하였다.
먼저 Swift 코드에 대해서는 잘 알지 못하기에 디테일한 설명은 아래 코드를 참고해서 검색해 보면 자세히 설명해주는 글을 참고하기 바란다.
먼저 AppDelegate 객체 아래 백 그라운드와 앱 종료 상태에서 사용할 Event Channel을 등록해 준다.
여기서 Event Channel을 따로 등록한 이유는 푸시가 수신되는 시점만 알고 싶고 Push payload를 재구성 할 생각은 없어서 따로 수신하였다.
NotificationStreamHandler() 객체는 아래에서 작성하는 내용이다.
var notiWithTerminate : FlutterEventChannel?
var notiWithBackground : FlutterEventChannel?
let notiStreamHandler = NotificationStreamHandler()
아래와 같이 Event Channel에 호출할 name과 Flutter Controller를 등록하여 Streamhandler에 넘겨주면 된다.
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
notiWithTerminate = FlutterEventChannel(name: "notification/terminate", binaryMessenger: controller.binaryMessenger)
notiWithBackground = FlutterEventChannel(name: "notification/background", binaryMessenger: controller.binaryMessenger)
notiWithTerminate?.setStreamHandler(notiStreamHandler)
notiWithBackground?.setStreamHandler(notiStreamHandler)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
여기 부분이 실제로 Notification이 수신 받는 코드이다.
앱 종료 상태와 백그라운드 상태일 때를 따로 분기하여 해당하는 Event Channel에 스트림을 등록해주면 기본 세팅은 끝이난다.
이렇게만 작성한다고 작동을 하는 것은 아니고 아래에서 FCM 애플 payload 세팅도 추가해 주어야 한다.
또 하나 중요한 부분이 해당 함수는 UIBackgroundFetchResult를 리턴해야 하는데 else 문 또는 Foreground 상태에 대한 적절한 값이 작성 안되어 있어서 원하는 대로 리턴 필드를 추가해 주어야 한다.
여기서 리턴 값이 없는 상태로 포어그라운드 상태에서 푸시 노출을 호출하면 IOS가 30초 정도 시스템을 block처리해서 앱이 멈추는 이슈가 있다.
포어그라운드 상태의 푸시 노출은 다른 글에서 설명하도록 하겠다.
override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let state = UIApplication.shared.applicationState
if state == .inactive {
// 앱 종료 상태
notiWithTerminate?.setStreamHandler(notiStreamHandler)
notiStreamHandler.handleNotification("State TERMINATE")
} else if state == .background{
// 앱 백그라운드
notiWithBackground?.setStreamHandler(notiStreamHandler)
notiStreamHandler.handleNotification("State BACKGROUND")
}
}
앱 실행 중 환경에서는 userNotificationCenter로 수신 받아 Flutter에 위에서 구현한 것처럼 Event Channel로 넘겨주면 되는데, 앱 실행 중 상태에서는 flutter로 처리해도 크게 이슈가 없어 그냥 flutter에서 처리하고 있다.
아래 코드는 참고만 하고 flutter에서 처리 해주는게 편하다.
override func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
NSLog("Foreground")
// completionHandler([[.alert, .sound]])
}
아래 코드에 대해서는 여기서 따로 설명하지는 않고 위에 링크해둔 Event Channel 글을 참고하기 바란다.
class NotificationStreamHandler : NSObject, FlutterStreamHandler {
var eventSink: FlutterEventSink?
var queued = [String]()
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
queued.forEach({ events($0) })
queued.removeAll()
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
func handleNotification(_ notification: String) -> Bool {
guard let eventSink = eventSink else {
queued.append(notification)
return false
}
eventSink(notification)
return true
}
}
위에서 작성한 swift 코드를 flutter에서 수신 받을 수 있는 리스너를 세팅할 것이다.
리스너 안에 들어가는 함수에 대해서는 자유롭게 작성하면 된다.
swift 코드에서 등록해준 Platform Channel을 flutter에서도 같은 name으로 선언을 해준다.
final EventChannel? _terminate = EventChannel("notification/terminate");
final EventChannel ? _background = EventChannel("notification/background");
메모리 효율성을 위해 앱 종료 상태에서 해당 코드는 한 번만 실행되고 스트림을 차단해 주어도 되고, 앱을 실행했을 때 스트림 등록을 하지 않는 방식으로 구현하면 된다.
_terminate.receiveBroadcastStream().listen((event) async{
// 앱 종료 상태에서 수행할 코드 작성
});
// 스트림 구독 취소 기능 구현이 필요
앱이 실행 중 백그라운드 상태로 생명 주기가 변경 되었을 때 푸시를 수신받는 스트림으로 해당 스트림의 구독을 취소하게 되면 더 이상 스트림은 작동하지 않는다.
_background.receiveBroadcastStream().listen((event) async{
// 백그라운드 상태에서 수행할 코드 작성
});
또 하나 중요한 부분이 있는데, 푸시를 보낼 때 작성하는 payload안에 필수로 넣어주어야 하는 값들이 있다.
해당 값이 없게되면 IOS가 디바이스에 수신을 보내지 않게 된다.
해당 값이 제대로 설정되어 Notification 전송을 요청하고 있는지 확인을 반드시 해야한다.
content-available : 1
hearders : {
"apns-push-type" : "background",
"apns-priority" : 5
}
다음 번에는 Firebase_messaging 라이브러리에서 위와 같은 기능을 수행할 수 있는 방법에 대해서 알아보도록 할 예정이고, 푸시를 수신 받아서 어떤 작업을 수행해야 하는지에 대한 자세한 설명에 대해서 글을 작성할 예정이다.