Remote notification

  • Device에서 APNs에 푸쉬 토큰을 요청하고 APNS는 푸쉬 토큰을 디바이스에 발급해줌, 이후 다비아시는 Provider에 푸쉬 토큰을 전달해줌
  • 푸쉬 토큰을 전달 받은 Provider는 추후 디바이스에 푸쉬를 전송할 대 APNs에 요청을 하고 APNs가 디바이스에 푸쉬를 전송함


Remote Notification 구현하기

  • 1. 실제 iOS기기 준비
    • 시뮬레이터로 테스트 불가능했지만 Xcode 11.4부터 시뮬레이터에서 푸쉬를 수신할 수 있는 기능을 지원하기 시작했음
  • 2. 애플 개발자 사이트 설정하기
    •  App Identifier 등록하기
    •  Bundle Identifier 및 Description 추가하기
    •  Capabilities -> Push Notification On
  • 3. Apple push 알림 인증 키 발급받기
    • 유료 애플 개발자 프로그램을 등록해야 푸시 키를 발급받을 수 있음
    • 푸시 인증 키는 여러 앱에서 동시에 사용이 가능함
    • p8 인증키를 발급 받음
  • 4. Xcode의 App > Capabilities(기능)에서 푸시 알림 사용 설정
    • Debug/Release Mode 모두 설정해주어야 함
    • Push Notification
    • Background Modes -> Remote Notification On
  • 5. Firebase Cloud System 연동
    • Firebases는 푸쉬를 Cloud Messaging Service라고 지칭하고 있음
    • 푸시 인증키 및 개발자 팀 키 등록
    • 프로젝트 설정 -> 클라우드 메시징 탭 - > APN 인증 키 업로드
  • 6. 코드 구현
    •  원격 알림 권한을 등록해야 함
    •  iOS 시스템 정책으로 원격 알림 시스템에 앱 등록하는 과정이 필요함
    • 파이어베이스도 푸쉬를 전송할 때 APNs 서버에 요청하게 됨으로 실질적 발송 및 관리자는 애플 시스템의 영향을 받음
    • UNUserNotificationCenterDelegate
    • 메시지 대리자 설정
      • 파이어베이스가 푸시 메시지를 대신 전송할 수 있도록 대리자를 설정하는 과정이 필요
      •  MessagingDelegate
@main
class AppDelegate: UIResponder, UIApplicationDelegate  {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        UIViewController.swizzleMethod()
        FirebaseApp.configure()
        
        //알림 시스템에 앱을 등록
        if #available(iOS 10.0, *) {
          // For iOS 10 display notification (sent via APNS)
          UNUserNotificationCenter.current().delegate = self

          let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
          UNUserNotificationCenter.current().requestAuthorization(
            options: authOptions,
            completionHandler: { _, _ in }
          )
        } else {
          let settings: UIUserNotificationSettings =
            UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
          application.registerUserNotificationSettings(settings)
        }

        application.registerForRemoteNotifications()
        
        //메시지 대리자 설정
        Messaging.messaging().delegate = self
        
        //현재 등록된 토큰 가져오기
        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)")
          }
        }
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Messaging.messaging().apnsToken = deviceToken
    }
    
    //포그라운드 알람 수신: 로컬/푸쉬 동일
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        
        //.banner, .list: ios 14+
        guard let viewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController?.topViewController else { return }
        if viewController is SettingViewController {
            
        } else {
            completionHandler([.badge, .sound,  .banner, .list])
        }
    }
    
    //푸시 클릭: 호두과자 장바구니 담는 화면
    //유저가 푸시를 클릭했을 때에만 수신 확인 가능
    
    //특정 푸시를 클릭하면 특정 상세화면으로 화면 전환 ->
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        print("사용자가 푸쉬를 클릭했습니다.")
        print(response.notification.request.content.body)
        print(response.notification.request.content.userInfo)
        
        let userInfo = response.notification.request.content.userInfo
        
        if userInfo[AnyHashable("sesac2")] as? String == "project2" {
            print("Sesac PROJECT")
        } else {
            print("EMPTY")
        }
        guard let viewController = (UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate)?.window?.rootViewController?.topViewController else { return }
        
        print(viewController)
        
        if viewController is ViewController {
            viewController.navigationController?.pushViewController(SettingViewController(), animated: true)
        } else if viewController is ProfileViewController {
            viewController.dismiss(animated: true)
        }
    }
}

extension AppDelegate: MessagingDelegate {
    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.
    }
}
  •  7. 테스트 메시지 전송
    •  콘솔에서 토큰 확인 후 파이어베이스에서 테스트 후 전송

 추가 사항

  • Foreground
    • 출시 앱 프로젝트를 할 때도 겪었던 이슈 백그라운에서는 기본적으로 동작하지만 포그라운드 수신은 동작하지 않음
      • 별도 처리를 해주어야 로컬/푸시 알림이 포그라운드 상태에서도 수신을 처리할 수 있음
   func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
       completionHandler([.badge, .sound,  .banner, .list])
   }
  • completionHandler([.badge, .sound, .alert]는 iOS14부터 Deprecated 됨
  • 따라서 최소 버전에 따른 분기 처리가 필요함

  • Receive Response
    • iOS에서는 푸시가 수신되었는지 여부를 확인할 수 없음
    • 대신 사용자가 특정 푸시를 클릭했을 때에만 수신 확인이 가능
    • 특정 푸시를 클릭했을 때 별도의 로직 구현을 하지 않으면 앱을 Active 상태로 올려주는 정도로 작동

  • Interruption Lebel
    • iOS15부터 알림 수준을 다르게 설정할 수 있음
    • Ciritical은 재난 문자 알림과 같은 레벨 -> 따라서 거의 사용허지 않음
    • TimeSensitive는 중요한 일정이나 보안 알림 등에서 사용
      • 사용자가 방해금지 모드를 사용하고 있더라도 알림이 전달됨
      • TimeSensitive 레벨의 푸시를 여러 번 받게 되면 사용자에게 단계를 내릴지 alert을 시스템에서 띄워줌
      • 아이폰 설정에서 각 앱에 대한 긴급한 알림 수신 여부를 거부하면 TimeSensitive 상태의 푸시도 Acitive 상태로 전달됨
    • Active는 일반적인 알림임
    • Passive는 주로 광고성 알림에 해당 화면을 켜거나 소리르 재생하지 않은 채 알림만 전달됨

+ Recent posts