본문 바로가기

iOS/PROJECT

[burstcamp] Remote PushNotification 도입 2편 - 어디서나 푸시알림 받아서 디테일 화면 띄우기

이미지 클릭시 burstcamp github로 이동합니다.

 

https://luen.tistory.com/211

 

[burstcamp] 백엔드 개발자 없이.. Remote PushNotification 도입 1편 - Firebase Functions

푸시알림 왜 썼나? burstcamp는 캠퍼들의 블로그 글들을 모아서 보여주는 앱인데, 앱을 들어오지 않고도 글을 추천해서 보여줄 수 있도록 푸시알림을 도입하게 되었습니다. 어떤 푸시알림? 로컬에

luen.tistory.com

 

1편에서 푸시알림을 보내고

이제 앱에서 푸시알림을 받을 수 있게 되었습니다!

 

앱에서 처리해야 할 일은 이제 푸시알림을 탭했을 때 디테일화면을 띄워주는 일이 남았네요.

 

두가지의 경우를 처리해줘야 합니다.

 

- 백그라운드에서 알림을 받았을 때

 

- 포그라운드에서 알림을 받았을 때

 

 

 

백그라운드와 포그라운드에서 처리를 해주기 위해서

우선 UNUserNotificationCenterDelegate 를 채택해줘야합니다.

 

https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate

 

Apple Developer Documentation

 

developer.apple.com

UNUserNotificationCenterDelegate들어오는 알림과 받은 알림을 처리해주기 위해서 채택하는 Delegate입니다.

AppDelegate가 이 Delegate를 채택하면 willPresent 함수와 didReceive 함수를 사용할 수 있습니다.

// foreground 상태에서 푸시알림을 받았을 때 호출되는 함수
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (
        UNNotificationPresentationOptions
    ) -> Void
) 

// 푸시알림을 사용자가 탭했을 때 호출되는 함수
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
)

 

그렇다면,

 

포그라운드의 경우 앱이 모두 로드된 상태에서 willPresent 함수를 실행해서 푸시알림을 띄우고,
사용자가 이를 탭하면 didReceive가 호출되고,

 

백그라운드의 경우 푸시알림을 탭하면 didReceive 함수만 실행이 된 후에 앱이 시작된다는 것을 알 수 있습니다.

 

 

burstcamp의 코디네이터 구조

앱의 코디네이터 구조를 보면

앱을 실행중일 때 푸시알림을 받으면 무조건 TabCoordinator 아래의 화면이라는 것을 알 수 있습니다.

저희 앱은 로그인 전에는 탭바 화면을 볼 수 없기 때문입니다.

앱이 포그라운드 상태에서 푸시알림을 탭하면 TabCoordinator에서 디테일뷰를 바로 푸시하면 되겠다.

AppDelegate에서 TabCoordinator로 푸시알림의 정보 (feed 아이디)를 전달하기 위해서

NotificationCenter의 post를 사용하였습니다. (AppDelegate에서 SceneDelegate로의 바로 접근을 피하기 위해서)

이에 관한 견해는 아래 링크를 참조하였습니다.

https://jeonyeohun.tistory.com/314

 

 

문제는 백그라운드일 때 푸시를 받는 경우였습니다.

백그라운드일때 푸시알림을 탭하고, Notification Post를 보내면

앱이 준비되기 전에 이 과정이 일어나기 때문에 디테일화면 -> (앱 코디네이터 준비)홈화면 순서로 반대로 진행되었습니다.

디테일 -> 홈

 

 

이 현상을 해결하기 위해 위에서 foreground 상태일 때만 호출되는 willPresent 함수와 UserDefaults 를 이용하기로 했습니다.

 

 

그렇게 탄생한 푸시알림을 받는 과정입니다. 복잡해보이지만 복잡하지 않습니다..

 

 

AppDelegate

isForeground 키를 가지는 UserDefaults 값을 willPresent 함수에서 true로 세팅해줍니다.

(주의할 점은 Bool 타입의 UserDefaults 값은 값을 설정안하면 false로 반환한다는 점입니다.. nil 이 아닙니다 😭)

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (
        UNNotificationPresentationOptions
    ) -> Void
) {
    UserDefaultsManager.save(isForeground: true)
    completionHandler([.banner, .badge, .sound])
}

 

탭했을시 실행되는 didReceive 함수에서는 메세지 데이터에 담긴 feedUUIDUserDefaults 에 저장합니다.

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    guard let feedUUID = response.notification.request.content.userInfo[
        NotificationKey.feedUUID
    ] as? String
    else { return }

    UserDefaultsManager.save(notificationFeedUUID: feedUUID)
}

 

여기서 isForeground 값을 확인하여 포그라운드에서 받은 알림이라면 Notification을 보내게됩니다.

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    guard let feedUUID = response.notification.request.content.userInfo[
        NotificationKey.feedUUID
    ] as? String
    else { return }

    UserDefaultsManager.save(notificationFeedUUID: feedUUID)
    // foreground 확인!
    if UserDefaultsManager.isForeground() {
        NotificationCenter.default.post(name: .Push, object: nil)
    }
}

 

 

TabBarCoordinator

그리고 TabBarCoordinator에서 notification을 받아서 디테일뷰를 보여줄 수 있도록 처리합니다.

디테일뷰를 보여준 후에는 UserDefaults에 저장된 isForeground, feedUUID 값을 모두 삭제합니다!

foreground 처리는 여기서 끝나게 됩니다.

// MARK: - handle Push Notification

extension TabBarCoordinator: ContainFeedDetailCoordinator {
    private func addForegroundPushNotificationObserver() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(moveToDetail),
            name: .Push,
            object: nil
        )
    }

    @objc func moveToDetail() {
        guard let feedUUID = UserDefaultsManager.notificationFeedUUID() else { return }
        UserDefaultsManager.removeIsForeground()
        UserDefaultsManager.removeNotificationFeedUUID()
        let feedDetailViewController = prepareFeedDetailViewController(feedUUID: feedUUID)
        sinkFeedViewController(feedDetailViewController)
        self.navigationController.pushViewController(feedDetailViewController, animated: true)
    }
}

 

백그라운드에서 알림을 받으면 앱이 준비가 될 때까지 기다려야합니다.

저는 UserDefaults에 isForeground 값을 저장해두고,

TabBarCoordinator가 실행될 때 이를 확인하도록 했습니다.

 

isForeground 의 값이 false 이고, feedUUID 값이 있다면

백그라운드에서 푸시알림을 받아서 탭했다는 뜻이게 됩니다.

HomeCoordinator가 준비될 때 이를 체크하고,

UserDefaults에 저장된 feedUUID 값을 삭제합니다.

// MARK: - handle push notification

extension TabBarCoordinator {
    private func checkNotificationFeed() {
        if !UserDefaultsManager.isForeground(),
           let feedUUID = UserDefaultsManager.notificationFeedUUID() {
            UserDefaultsManager.removeNotificationFeedUUID()
            moveToFeedDetail(feedUUID: feedUUID)
        }
    }
}

 

 

예외상황

구현을하고 예외 상황이 있었는데요.

포그라운드 상태에서 알림을 받으면 willPresent 함수가 무조건 실행되는 이유 때문이었습니다.

 

사용자가 알림을 받고 탭하지 않으면?!

 

willPresent에서 설정한 isForeground 값이 true인 상태로 남게되는 것이었습니다.

 

그럼 다음번에 백그라운드에서 알림을 탭하고 실행해도 isForeground 값이 true인 상태로 남게되었습니다.

 

그래서 백그라운드 상태로 앱이 진입하면 isForeground 정보를 false로 설정해주었습니다.

extension AppDelegate {
    func applicationDidEnterBackground(_ application: UIApplication) {
        UserDefaultsManager.removeIsForeground()
    }
}

 

기능을 구현하고 나서 코디네이터로 화면 전환의 역할을 분리하길 잘했다는 생각이 들었습니다.

알림을 받고 화면을 띄워주는 역할도 코디네이터가 담당하면

뷰컨트롤러는 이 과정이 어떻게 일어나는지 아예~ 몰라도 되는 점이 역할 분리가 잘 되었다는 느낌을 받았습니다.

분리가 되어있지 않았다면 역시 뷰컨트롤러에서 다 처리를 했을텐데.. 

그렇다면 또 코드가 길어지고 만능 뷰컨이 되었을 것..ㅎ

 

 

또 다른 예외상황

앞서 개발했을 때 백그라운드 상태가 완전히 앱이 종료된 상태라고 (무의식적으로..) 생각하고 개발했었습니다만..

테스트를 하면서 발견한 예외상황..!

앱이 완전히 종료되지 않고 앱을 나간 상태에서 알림을 받으면 백그라운드 상태에서 받은 알림이지만

클릭해서 들어가면 화면까지 모두 띄워진 상태이기 떄문에

Coordinator에 만들어놓은 checkNotificationFeed() 함수를 체크하지 않아서 화면이동이 되지 않는 오류가 있었습니다.ㅠ

func userNotificationCenter(...) -> Void {
    UserDefaultsManager.save(isForeground: true)
    completionHandler([.banner, .badge, .sound])
}

 

제가 만든 로직은 앱이 Suspended 상태에 있을 경우와 Foreground 상태에서만 제대로 동작하고

실제 Background 상태에서 받은 경우에서는 잘 동작하고 있지 않았던 것입니다.

Suspended 상태를 Background라고 착각하고 있던 것,,

 

라이프 사이클을 생각해서 구현방법을 다시 고려해봐야겠습니다..

 

월드컵 결승을 보면서 해결~ (연장전 너무 재밌다 멀리서보면 희극 적는데 3:3)

 

간단하게

알림 클릭시 => UserDefaults에 feedUUID 저장

 

- Suspended 알림받기 + Suspended 실행 => (app 코디네이터 start 후) UserDefaults feedUUID 확인, UserDefaults 삭제

- Suspended 알림받기 + foreground 실행 => (app 코디네이터 start 후) UserDefaults feedUUID 확인, UserDefaults 삭제

- background 알림받기 + background 실행 => (app 코디네이터 살아있음) NotificationCenter, UserDefaults 삭제

- background 알림받기 + foreground 실행 => (app 코디네이터 살아있음) NotificationCenter, UserDefaults 삭제

 

앞서 생각한 UserDefaults와 notificationcenter를 이용해서 해결했지만 확인하는 타이밍이 조금 바뀌었을 뿐 로직은 비슷하다.

로직 그림 변경은 

 

 

 

1편

https://luen.tistory.com/211

 

[burstcamp] 백엔드 개발자 없이.. Remote PushNotification 도입 1편 - Firebase Functions

푸시알림 왜 썼나? burstcamp는 캠퍼들의 블로그 글들을 모아서 보여주는 앱인데, 앱을 들어오지 않고도 글을 추천해서 보여줄 수 있도록 푸시알림을 도입하게 되었습니다. 어떤 푸시알림? 로컬에

luen.tistory.com