웰코발
웰코's iOS
웰코발
전체 방문자
오늘
어제
  • 분류 전체보기 (63)
    • Swift (26)
    • rxSwift (13)
    • SwiftUI (3)
    • iOS (12)
    • 기타 (1)
    • 개발관련 용어정리 (6)
    • 면접준비 (0)
    • 공공데이터 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • UI
  • 대기오염통계 현황
  • swiftUI
  • delay
  • uitableview
  • Scroll
  • Coordinator
  • 측정소정보
  • 디자인
  • SWIFT
  • ReactorKit
  • Observable
  • WKWebView
  • content_available
  • 주제구독
  • alamofire
  • collectionview
  • rxswift
  • cell
  • ios

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
웰코발

웰코's iOS

Swift

[Swift] FCM 주제 구독 및 백그라운드 처리

2024. 7. 1. 21:31

오랜만에 글을 남기고 싶은 부분이 있어서 글을 남긴다..

 

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번부터 따라하자

https://dokit.tistory.com/49

 

[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
    'Swift' 카테고리의 다른 글
    • [Swift] 의존성 주입, DIContainer(IOC Container)만들기
    • [Swift] WKWebView와 Javascript 사이 통신을 만들어보자
    • [Swift] 좌우 무한 collectionView 를 만들어 보자
    • [Swift] 상단 탭바 페이지 뷰컨트롤러 만들기 (Upper Tab Page View)
    웰코발
    웰코발
    나의 개발 일지

    티스토리툴바