Search code examples
iosswiftcore-datauikitcombine

Can't get Combine subscription to work every time I route to the main screen of the app


I have this problem: On my Home Screen I have a notifications bell (shows only local notifications that are stored in Core Data) that I can press on and route to the notifications screen. If I receive a new notification - a red badge appears on top of the bell, it indicates that I have unread notifications. I made a publisher that fetches unread notifications from core data and gets their count. If the count is greater than 1 - I show the red badge. Here is the code:

 public func fetchCountPublisher<T: PersistentModel>(_ type: T.Type,
                                                        predicate: NSPredicate?) -> AnyPublisher<Int, Error> {
       //request creating code...
        request.predicate = predicate
        
        let count = try? context.count(for: request)
        return Just(count)
            .compactMap { $0 }
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }

As you see, I return a Just publisher here, which only works once. However, this is not correct for my case. I am going to show you my full code and explain the problem:

 public func hasUnreadNotifications() -> AnyPublisher<Int, Error> {
        repository.fetchCountPublisher(
            LocalNotificationCD.self,
            predicate: \LocalNotificationCD.isUnread == true
        )
    }

Code from my notifications manager, it fetches the count of the unread notifications. I will use this method in my interactor:

//some DI stuff and other publishers
var hasUnreadNotificationsPublisher: AnyPublisher<Bool, Error> {
        localNotificationsManager.hasUnreadNotifications()
            .map { $0 > 1 ? true : false }
            .eraseToAnyPublisher()
    }

Using this .map operator I map the Int publisher to Bool. Next this publisher is used in my view model:

final class HomeViewModelState: ObservableObject {
    @Published var hasUnreadNotifications: Bool = false
}

protocol HomeViewModelProtocol {
    var state: HomeViewModelState { get }
}

final class HomeViewModel {
    // MARK: Public
    let state: HomeViewModelState
    
    //other properties, initializer and DI...
    init() {
        binding()
    }
    
    func binding() {
        interactor.hasUnreadNotificationsPublisher
            .sink(receiveCompletion: { _ in },
                  receiveValue: { [weak self] hasUnreadNotifications in
                self?.state.hasUnreadNotifications = hasUnreadNotifications
            })
            .store(in: &cancellables)
    }
}

And from the view model I get it in my view controller:

class HomeViewController: UIViewController {
    private var viewModel: HomeViewModelProtocol
    
    // DI
    //binding called in viewDidLoad
    
    private func binding() {
        viewModel.state.$hasUnreadNotifications
            .sink { [weak self] hasUnreadNotifications in
                self?.badgeView.isHidden = !hasUnreadNotifications // не обновляет юай
            }
            .store(in: &cancellables)
    }
}

The problem is that, when I first launch my app, core data fetches the count of the unread notifications and correctly indicates weather there are unread notifications, however, after reading them (they are marked as read when you scroll the notifications list) the unread indicator still remains there. It does not go anywhere until you fully close your app. The binding method is triggered only once since it's in the viewDidLoad method. I think that using Just in the Core Data layer is not really ok for my case because it only works once... What do I do to properly subscribe to the hasUnreadNotifications property and autoupdate the UI? I don't want to do the binding stuff in viewDidAppear.
Thanks in advance.


Solution

  • The question is when do you know that you have new notifications? What events should trigger the pipeline to emit a new alert count?

    What I suspect you want to do is monitor the NSMangedObjectContext and report new alerts when the context has them. Since I don't have your full application I would aim for something like:

    let alertPublisher = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)
        .tryCompactMap( { _ in
            // Run the predicate count the results
            // return the count, throw errors on errors etc.
        })
        .eraseToAnyPublisher()
    

    When the Notification Center sends a notification that the context was saved, convert that notification into a count of the number of alerts that are there and publish the count.

    Reactive pipelines transform one thing into another... in this case you want to transform some indication that the number of alerts may have changed into a concrete count of those alerts. You'll have to choose an originating event based on your application domain.