오랜만에 글을 남기고 싶은 부분이 있어서 글을 남긴다..
FCM 의 종류로는 3가지로 단일기기, 기기그룹, 주제구독 이 있는데
글쓴이는 주제구독을 사용했다.
특징을 잘 나타낸 블로그가 있는데 안드로이드 기준 글이라
내용만 살펴보면 될 것 같다. (감사합니다~)
https://donghun.dev/Firebase-Cloud-Messaging
[Firebase] FCM에 대해서 알아보자. 🔔
Note : 이 글은 지극히 주관적인 생각을 토대로 작성된 글입니다. 혹시나 잘못된 부분이 있다면 메일 또는 코멘트를 통해 알려주시면 감사하겠습니다. 😄 제 메일은 About 탭에서 확인하실 수 있습
donghun.dev
서버에서 보내는 메세지 payload의 규칙은 다음 글을 보며 파악해보자.
https://sweetcoding.tistory.com/44
Tip of Firebase cloud Message payload
Android 와 iOS 는 Notification의 payload(Push 전송 데이터)에 따라 어떤식으로 동작하는지 간단하게 적어 보았습니다. 1. iOS와 Android 에서 기본 형식으로 Notification 을 노출 시키기 위해서는 { "notification":
sweetcoding.tistory.com
{
"content_available": true, // 백그라운드를 위한 플래그 값, 꼭 넣자!
"data" : {
"message" : {
"title" : "tes1t111",
"content": "content",
"imageUrl" : "url"
}
}
}
이러한 payload 메시지가 와야 정상적으로 백그라운드에서도 메시지를 수신할 수 있다.
혹여나 xcode에서 백그라운드를 위한 설정을 모르겠으면 다음 블로그의 3번부터 따라하자
[iOS] FCM으로 Push Notification (푸시 알림) 구현하기
FCM(Firebase Cloud Messaging)같은 경우는 공식문서와 설정 튜토리얼이 정말 잘 되어있어서 공식 문서를 따라가시는 것도 좋을 것 같습니다! ⚠️ 원격 푸시를 구현하기 위해선 개발자 계정 멤버십이
dokit.tistory.com
다음으로는 가장 중요한 AppDelegate에 어떤 코드를 작성할지 봐보자.
import FirebaseCore
import FirebaseMessaging
import UserNotifications
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
...
// FCM을 위한 Firebase 등록 (추후 설정)
FirebaseApp.configure()
Messaging.messaging().delegate = self
// 앱 실행 시 사용자에게 알림 허용 권한을 받음
UNUserNotificationCenter.current().delegate = self
application.registerForRemoteNotifications()
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Messaging.messaging().apnsToken = deviceToken
}
/// background, foreground 상태에서 일반 push가 오면 반응
/// Signing&Capabilities 에서 Remote notifications 체크 해주어야함 (background)
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Log.debug("application didReceiveRemoteNotification fetchCompletionHandler 실행")
Log.debug("userInfo: \(userInfo)")
if let title = userInfo["title"] as? String,
let body = userInfo["message"] as? String,
let channel = userInfo["channel"] as? String {
// channel의 값을 판단하여 배너알림을 노출시킬지 판단 (true면 상단 배너알림 노출)
if checkOnOffSettingWithChannel(channel: channel) {
// 배너 알림 인스턴스 만들고 노출 시키기
makeMutableNotificationContent(title: title, body: body, userInfo: userInfo)
}
}
completionHandler(UIBackgroundFetchResult.newData)
}
...
}
extension AppDelegate: UNUserNotificationCenterDelegate {
/// 알림를 노출시키기 전에 completionHandler([옵션])의 옵션에 따라 알림을 띄울지 말지 결정하게 됨.
/// background 상태 에서는 해당 함수 내부의 로그가 찍히지 않음. 하지만 background에서 해당 함수 자체는 동작함. (중요) (로그만 안찍힐뿐 동작함)
/// foreground에서는 로그가 찍힘. 동시에 동작도 함
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
Log.debug("application userNotificationCenter willPresent 실행")
let userInfo = notification.request.content.userInfo
// Print full message.
Log.debug("willPresent")
Log.debug(userInfo)
completionHandler([.list, .banner, .badge, .sound])
}
}
extension AppDelegate: MessagingDelegate {
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
Log.network("Firebase registration token: \(String(describing: fcmToken))")
}
}
makeMutableNotificationContent 함수는 조건에 따라 상단의 배너 알림을 띄울 것인지 판단하여 알림 표시를 하도록 하는 함수이다.
다음의 코드블록을 참고하자. (주석 참고)
extension AppDelegate {
func makeMutableNotificationContent(title: String, body: String, userInfo: [AnyHashable : Any]) {
Log.debug("makeMutableNotificationContent func")
/// APN에서 받은 데이터를 토대로 배너, 뱃지 알림을 나타내기 위해 알림 인스턴스 생성 후 트리거 작성 함 -> 알림이 노출되기 전에 func userNotificationCenter(willPresent:)에서 반응하게 됨. -> 함수 내부에서 띄울지 말지 판단
/// UNUserNotificationCenter.current().delegate = self 와
/// func userNotificationCenter(_ center: , willPresent, withCompletionHandler) 를 지정 해줘야 알림이 옴.
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
// content.badge = 1
content.userInfo = userInfo
// UserDefault에 저장된 앱 알림 개수를 나타내자. 만약 저장이 되어있지 않으면 UserDefault에 1개를 저장시켜놓는다.
if let appPushTopic = DataManager.getInstance().getValue(key: LocalDataKeys.FCM.Topic.appPush) as? Int {
Log.debug("appPushTopic:\(appPushTopic)")
content.badge = NSNumber(value: (appPushTopic + 1))
DataManager.getInstance().putValue(key: LocalDataKeys.FCM.Topic.appPush, data: appPushTopic + 1)
} else {
content.badge = 1
DataManager.getInstance().putValue(key: LocalDataKeys.FCM.Topic.appPush, data: 1)
}
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) // Schedule the notification.
UNUserNotificationCenter.current().add(request) { (error : Error?) in
if let theError = error {
Log.error("UNMutableNotificationContent 알림 추가 실패: \(theError)")
}
}
}
func checkOnOffSettingWithChannel(channel: String) -> Bool {
switch channel {
....
// 앱 비지니스 로직 내 알림 끄고 켜기 기능이 있을 경우
// FCM에서 받은 채널값이 코드A이고 UserDefault에 코드A에 대한 알림설정이 꺼져있다면 false 켜져있다면 true (기본값은 true)
case FCMSubscribeManager.Code.A.rawValue:
if let aCodeOnOff = DataManager.getInstance().getValue(key: LocalDataKeys.FCM.OnOff.A) as? Bool {
if aCodeOnOff {
return true
} else {
return false
}
} else {
// 값이 없으면 초기 앱 단계이므로 true (앱 처음 받은 이후는 default설정이 On 상태)
return true
}
....
default:
return false
}
}
}
import Foundation
import FirebaseCore
import FirebaseMessaging
/*
내부 저장 데이터를 다루는 class
*/
class FCMSubscribeManager {
static let shared = FCMSubscribeManager()
enum Code: String {
…
case A
var msg: String {
switch self {
…
case .A:
return "A에 관한 메세지 출력"
…
}
}
}
private init() {
}
/// AppPush 주제구독 진행 (만일 이미 주제구독 중이면 덮어쓰기 기능 O)
func subscribeAppPush(uid: String) {
if let prevAppPushTopic = DataManager.getInstance().getValue(key: LocalDataKeys.FCM.Topic.appPush) as? String {
Log.custom(category: "FCM SDK", "APPPUSH 구독이 이미 등록되어 있음 - subscribeAppPush")
// 1-1. 이전 APPPUSH 구독 취소
Log.debug("1-1. 이전 APPPUSH 구독 취소 진행")
Messaging.messaging().unsubscribe(fromTopic: prevAppPushTopic) { error in
if let error = error {
Log.custom(category: "FCM SDK", "APPPUSH[prev] 구독 취소 실패: \(error.localizedDescription)")
} else {
Log.custom(category: "FCM SDK", "APPPUSH[prev] 구독 취소 성공 [Topic: \(prevAppPushTopic)]")
DataManager.getInstance().removeValue(key: LocalDataKeys.FCM.Topic.appPush)
// 1-2. 새로운 APPPUSH 구독
Log.debug("1-2. 새로운 APPPUSH 구독 진행")
Messaging.messaging().subscribe(toTopic: "APPPUSH%\(uid)") { error in
if let error = error {
Log.custom(category: "FCM SDK", "APPPUSH[curr] 구독 실패: \(error.localizedDescription)")
} else {
Log.custom(category: "FCM SDK", "APPPUSH[curr] 구독 성공 [TOPIC: \("APPPUSH%\(uid)")]")
DataManager.getInstance().putValue(key: LocalDataKeys.FCM.Topic.appPush, data: "APPPUSH%\(uid)")
}
}
}
}
} else {
Log.custom(category: "FCM SDK", "APPPUSH 구독이 아무것도 등록되어 있지 않음 - subscribeAppPush")
// 2-1. 새로운 APPPUSH 구독
Log.debug("2-1. 새로운 APPPUSH 구독 진행")
Messaging.messaging().subscribe(toTopic: "APPPUSH%\(uid)") { error in
if let error = error {
Log.custom(category: "FCM SDK", "APPPUSH[curr] 구독 실패: \(error.localizedDescription)")
} else {
Log.custom(category: "FCM SDK", "APPPUSH[curr] 구독 성공 [TOPIC: \("APPPUSH%\(uid)")]")
DataManager.getInstance().putValue(key: LocalDataKeys.FCM.Topic.appPush, data: "APPPUSH%\(uid)")
}
}
}
}
func unsubscribeAppPush() {
if let appPushTopic = DataManager.getInstance().getValue(key: LocalDataKeys.FCM.Topic.appPush) as? String {
Log.custom(category: "FCM SDK", "APPPUSH 구독이 이미 등록되어 있음 - unsubscribeAppPush")
Log.debug("1. 이전 APPPUSH 구독 취소 진행")
Messaging.messaging().unsubscribe(fromTopic: appPushTopic) { error in
if let error = error {
Log.custom(category: "FCM SDK", "APPPUSH 구독 취소 실패: \(error.localizedDescription)")
} else {
Log.custom(category: "FCM SDK", "APPPUSH 구독 취소 성공 [Topic: \(appPushTopic)]")
DataManager.getInstance().removeValue(key: LocalDataKeys.FCM.Topic.appPush)
}
}
} else {
Log.custom(category: "FCM SDK", "APPPUSH 구독이 아무것도 등록되어 있지 않음 - unsubscribeAppPush")
}
}
func removeAllOnOffLocalSetting() {
…
DataManager.getInstance().removeValue(key: LocalDataKeys.FCM.OnOff.A)
…
}
}
여기서 DataManager는 그냥 UserDefault에서 흔히 사용하는 Get Set delete (CRUD) 함수가 들어있는 매니저 클래스(싱글톤) 이다.
입맛에 맞춰 바꾸면된다.
'Swift' 카테고리의 다른 글
[Swift] 의존성 주입, DIContainer(IOC Container)만들기 (0) | 2024.02.15 |
---|---|
[Swift] WKWebView와 Javascript 사이 통신을 만들어보자 (0) | 2024.01.22 |
[Swift] 좌우 무한 collectionView 를 만들어 보자 (2) | 2024.01.05 |
[Swift] 상단 탭바 페이지 뷰컨트롤러 만들기 (Upper Tab Page View) (0) | 2024.01.02 |
[Swift] UICollectionView 내부 내용에 따른 Cell 동적 높이 설정법 (0) | 2023.11.10 |