merge와 debounce와 filter를 이용한 3단 콤비네이션
Observable 객체 하나를 A, B 두 객체에서 Subscribe 하고 있을 때, B 가 Subscribe를 받고 있다면 A는 Subscribe를 받지 말아야 할 때가 있다.
언제 이런 경우가 생길까? 앱 스펙이 다음과 같다면 그럴 때 가 온다.
1. 앱이 Active 되어있으니 푸시 알람이 오면 상단에 Notification Bar를 띄워준다.
2. 현재 DetailViewController가 화면에 보이고 있고 보고 있는 Post에 대한 푸시 알림이라면, 1을 실행하지 않는다.
2번을 실행하기 위해서 1번을 실행할 때, 최상위 ViewController가 DetailViewController 인지, 그리고 어떤 Post를 보고 있는지를 알아와야 한다. 그러려면 keywindow의 rootViewController로부터 최상의 ViewController를 가져오는 코드도 짜야하고 그 ViewController가 어떤 Class 인지 검사하는 코드도 넣어야 한다.
최상위 ViewController를 가져오는 코드는 한두 시간 삽질로는 버그가 많을 것 같고, 어디서 주워들은 건 있어가지고 Class를 검사하는 것도 뭔가 찜찜하다. 그래서 좋은 방법이 아닌 것 같다. DetailViewController를 모른 채 2번을 실행하고 싶다.
푸시 알림을 Subject로 받고 있다고 가정해보자.
enum NotificationType {
case Post(ID: Int)
case Void
}
class NotificationCenter {
static let pushNotification = PublishSubject<NotificationType>()
}
didRegisterForRemoteNotificationsWithDeviceToken를 통해 푸시 알림이 들어오면, 다음과 같이 Subject 에 Emit을 한다.
NotificationCenter.pushNotification.onNext(.Post(ID:27))
NotificationBar를 띄워주는 코드는 AppDelegate 에 있다고 가정해보자.
extension AppDelegate {
func setupPushNotification() {
NotificationCenter.pushNotification.subscribeNext { notificationType in
switch notificationType {
case .Post(let ID):
NotificationBar.show(ID: ID)
default:
break
}
}.addDisposableTo(disposeBag)
}
}
그리고 DetailViewController에서 푸시 알림을 받으면 자신이 해야 할 일을 한다.
extension DetailViewController {
func setupPushNotification() {
NotificationCenter.pushNotification.filter { notificationType in
switch notificationType {
case .Post(let ID) where ID == self.ID :
return true
default:
return false
}
}.subscribeNext{ notificationType in
// 푸시 알림 시 뷰컨트롤러에서 할 일 하기
// ex: 글 리로딩, 새 댓글 불러오기
}.addDisposableTo(disposeBag)
}
}
AppDelegate는 앱이 실행되고 있는 동안 없어지지 않지만, DetailViewController는 있을 수 도 있고 없을 수 도 있다. 있다고 해도 보고 있는 Post가 푸시 알림과 관계가 없을 수 도 있다. 그러므로 DetailViewController의 Subscribe가 실행될 때 만 AppDelegate의 Subscribe를 제한해야 한다.
원하는 상황을 RxMarble로 그려보면 다음과 같다.
그런데 아무리 찾아봐도 ReactiveX에 이런 모양이 나오는 함수가 없다. 그러니 직접 구현해보자.
1. DetailViewController가 PushNotification 이벤트를 먼저 받는다.
2. DetailViewController는 "내가 PushNotification을 받아서 처리했습니다!" 하고 외친다.
3. AppDelegate는 푸시 메시지를 받았을 때 잠시 기다리며 DetailViewController의 외침에 귀 기울여본다.
4. 외침이 있으면 가만히 있고, 외침이 없으면 Subscribe를 실행한다.
외침을 처리하는 PublishSubject를 NotificationCenter에 추가해보자.
enum NotificationType {
case Post(ID: Int)
case Void
}
class NotificationCenter {
static let pushNotification = PublishSubject<NotificationType>()
static let alreadyObservedNotification = PublishSubject<NotificationType>()
}
DetailViewController가 푸시 알림을 처리할 때 외침을 외치도록 코드를 한 줄만 추가해보자.
extension DetailViewController {
func setupPushNotification() {
NotificationCenter.pushNotification.filter { notificationType in
switch notificationType {
case .Post(let ID) where ID == self.ID :
return true
default:
return false
}
}.subscribeNext{ notificationType in
NotificationCenter.alreadyObservedNotification.onNext(NotificationType.Void)
// 푸시 알림 시 뷰컨트롤러에서 할 일 하기
// ex: 글 리로딩, 새 댓글 불러오기
}.addDisposableTo(disposeBag)
}
}
DetailViewController는 되었다. 그럼 AppDelegate는 이 외침을 어떻게 확인할까?
일단 PushNotification과 외침을 합친다.
let mergedObservable = Observable.of(NotificationCenter.pushNotification , NotificationCenter.alreadyObservedNotification).merge()
외침을 0.1초간 기다린다.
let debouncedObservable = mergedObservable.debounce(0.1, scheduler: MainScheduler.instance)
외침이 있었는지 확인한다.
let filteredObservable = debouncedObservable.filter { notificationType in
switch notificationType {
case .Post:
return true
default:
return false
}
}
filter 까지 거치고 나온 filteredObservable은 DetailViewController의 외침이 없는 경우에만 실행되는 깨끗한 녀석이 나온다. 이제 이 Observable을 Subscribe 하면 된다.
filteredObservable.subscribeNext { notificationType in
switch notificationType {
case .Post:
NotificationBar.show(ID: ID)
default:
break
}.addDisposableTo(disposeBag)
}
이러면 DetailViewController로 보고 있는 Post에 관한 푸시 알림은 더 이상 NotificationBar가 뜨지 않는다.
문제 해결!
다시 그림으로 표현해보면 이렇다.