공식 문서에서는 등록 토큰 이라고 하는 FCM Token은 앱 시작 시 생성된다
등록 토큰을 수신하기 위한 delegate을 설정한다
// 공식 문서에 있는 코드
Messaging.messaging().delegate = self
최초 앱 시작 시, 토큰이 업데이트되거나 무효화될 때 신규 또는 기존 토큰을 가져온다
어떠한 경우든, 유효한 토큰이 있는 didReceiveRegistrationToken
메서드를 호출한다
이 때, 서버에 해당 토큰을 전달하거나, NotificationCenter
를 이용해 앱 전체에 알린다
// 공식 문서에 있는 코드
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
print("Firebase registration token: \(String(describing: fcmToken))")
let dataDict: [String: String] = ["token": fcmToken ?? ""]
NotificationCenter.default.post(
name: Notification.Name("FCMToken"),
object: nil,
userInfo: dataDict
)
// TODO: If necessary send token to application server.
// Note: This callback is fired at each app startup and whenever a new token is generated.
}
.token(completion: )
메서드를 통해 원하는 시점에 직접 토큰을 가져올 수도 있다
// 공식 문서에 있는 코드
Messaging.messaging().token { token, error in
if let error = error {
print("Error fetching FCM registration token: \(error)")
} else if let token = token {
print("FCM registration token: \(token)")
self.fcmRegTokenMessage.text = "Remote FCM registration token: \(token)"
}
}
AppDelegate - didFinishLaunchingWithOptions
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// GoogleSerice-info.plist에 File I/O 하는 기능
FirebaseApp.configure()
// 알림 delegate 설정
UNUserNotificationCenter.current().delegate = self
// 알림 허용 확인
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .sound, .badge, .providesAppNotificationSettings]) { didAllow, error in
print("Notification Authorization : \(didAllow)")
}
// 원격 알림에 앱 등록
application.registerForRemoteNotifications()
// Messaging delegate 설정
Messaging.messaging().delegate = self
return true
}
AppDelegate - didRegisterForRemoteNotificationsWithDeviceToken
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// deviceToken (APNs 토큰)을 가져와서 Messaging의 apnsToken 설정
Messaging.messaging().apnsToken = deviceToken
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print(error)
}
application.registerForRemoteNotifications()
이 성공적으로 실행되면 위 메서드를 통해 deviceToken을 받는다.didFailToRegisterForRemoteNotificationsWithError
메서드가 실행된다.AppDelegate - didReceiveRegistrationToken
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
let firebaseToken = fcmToken ?? "No Token"
KeychainStorage.shared.fcmToken = firebaseToken
}
Messaging.messaging().token { token, error in
if let error = error {
print("Error fetching FCM registration token: \(error)")
} else if let token = token {
print("FCM registration token: \(token)")
}
}
/v1/users/join
/v1/users/login
/v1/users/deviceToken
이번 프로젝트에서는 채팅 알림을 받도록 구현이 되어있다.
push notification 수신 시 구현 내용
채팅에 해당하는 채팅방에 들어가면 푸시 알림이 오지 않는다 | 푸시 알림을 클릭했을 때, 해당 채팅방으로 바로 화면 전환 |
// AppDelegate
// 포그라운드에서 알림 받기
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
guard let userInfo = notification.request.content.userInfo as? [String: Any] else { return }
// 1. 채널 채팅인 경우
if let channelChatInfo: PushChannelChattingDTO = self.decodingData(userInfo: userInfo) {
// userInfo 디코딩해서 푸시 알림 내용 확인
// 현재 보고 있는 채팅방이 아닌 경우만 푸시 알림
// (UserDefaults 이용해서 현재 보고 있는 채팅방 여부 확인)
if !self.checkCurrentChannel(chatInfo: channelChatInfo) {
completionHandler([.list, .badge, .sound, .banner])
}
}
// 2. 디엠 채팅인 경우 - 생략
}
푸시 알림 클릭 시, NotificationCenter를 통해 SceneDelegate에게 알림 내용을 보낸다.
// 푸시 알림을 클릭
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
guard let userInfo = response.notification.request.content.userInfo as? [String: Any] else { return }
// 1. 채널 채팅인 경우
if let channelChatInfo: PushChannelChattingDTO = self.decodingData(userInfo: userInfo) {
// userInfo 디코딩해서 푸시 알림 내용 확인
if let channelId = Int(channelChatInfo.channel_id),
let workspaceId = Int(channelChatInfo.workspace_id) {
let userInfo: [String: Any] = [
"channelId": channelId,
"workspaceId": workspaceId
]
// Notification Post -> SceneDelegate에 observer
NotificationCenter.default.post(
name: Notification.Name("channelChattingPushNotification"),
object: nil,
userInfo: userInfo
)
}
}
// 2. 디엠 채팅인 경우 - 생략
completionHandler()
}
NotificationCenter를 통해 푸시 알림 클릭에 대한 노티를 받으면,
AppCoordinator 메서드 실행
(모든 화면 초기화 후 해당되는 채팅방으로 화면 전환)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var appCoordinator: AppCoordinator?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
/* ... */
setNotification() // 노티 등록
}
}
extension SceneDelegate {
private func setNotification() {
// 1. 채널 채팅
NotificationCenter.default.addObserver(
self,
selector: #selector(channelChatPushClicked),
name: Notification.Name("channelChattingPushNotification"),
object: nil
)
}
// 1. 채널 채팅
@objc
private func channelChatPushClicked(_ notification: Notification) {
if let userInfo = notification.userInfo,
let channelId = userInfo["channelId"] as? Int ,
let workspaceId = userInfo["workspaceId"] as? Int {
appCoordinator?.showDirectChannelChattingView(
workSpaceId: workspaceId,
channelId: channelId,
channelName: nil
)
}
}
showDirectChannelChattingView
: 현재 쌓여있는 뷰를 모두 초기화시키고, 곧바로 채팅 화면으로 전환한다.
class AppCoordinator: AppCoordinatorProtocol {
/* ... */
func showDirectChannelChattingView(
workSpaceId: Int,
channelId: Int,
channelName: String?
) {
/*
1. AppCoordinator child removeAll
2. AppCoordinator showTabbarFlow(workspaceId: Int)
3. TabbarCoordinator prepareTabBarController(selectedItem = 0)
// channel
4 - 1. HomeDefaultCoordinator showChannelChatting
//dm
4 - 2. HomeDefaultCoordinator showDMChatting
*/
// 1. child coordinator removeAll
childCoordinators.removeAll()
navigationController.viewControllers.removeAll()
// 2. show tabBar flow
let tabBarCoordinator = TabBarCoordinator(navigationController)
tabBarCoordinator.finishDelegate = self
tabBarCoordinator.workSpaceId = workSpaceId
childCoordinators.append(tabBarCoordinator)
tabBarCoordinator.start()
// (-> homeDefaultCoordinator start)
// 3. HomeDefaultCoordinator show ChannelChatting
for i in 0...3 {
if let homeDefaultCoordinator = tabBarCoordinator.childCoordinators[i] as? HomeDefaultSceneCoordinatorProtocol {
homeDefaultCoordinator.showChannelChattingView(
workSpaceId: workSpaceId,
channelId: channelId,
channelName: channelName
)
}
}
}
Apple Developer - Registering your app with APNs
Firebase - Cloud Messaging
개발자 소들이 - APNs :: Push Notification 동작 방식
FCM을 도입할 때 고려할 것들
Jihyun Kim - IOS에서 이미지가 있는 푸시알림 구현하기
웅쓰 - iOS 앱 Push 알림 이해하기