4

When I call a backend service (login, value check…) I use a Notification publisher on the concerned Views to manage the update asynchronously. I want to unsubscribe to the notifications when the view disappear, or « pause » the publisher. I went first with the simple « assign » option from the WWDC19 Combine and related SwiftUI talks, then I looked at this great post and the onReceive modifier. However the view keeps updating with the published value even when the view is not visible.

My questions are:

  1. Can I « pause » this publisher when the view is not visible ?
  2. Should I really be concerned by this, does it affect resources (the backend update could trigger a big refresh on list and images display) or should I just let SwiftUI manage under the hood ?

The sample code: Option 1: onReceive

struct ContentView: View {

    @State var info:String = "???"
    let provider = DataProvider() // Local for demo purpose, use another pattern

    let publisher = NotificationCenter.default.publisher(for: DataProvider.updated)
    .map { notification in
        return notification.userInfo?["data"] as! String
    }
    .receive(on: RunLoop.main)

    var body: some View {
        TabView {
            VStack {
                Text("Info: \(info)")
                Button(action: {
                    self.provider.startNotifications()
                }) {
                    Text("Start notifications")
                }
            }
           .onReceive(publisher) { (payload) in
                    self.info = payload
            }
            .tabItem {
                Image(systemName: "1.circle")
                Text("Notifications")
            }
            VStack {
                Text("AnotherView")
            }
            .tabItem {
                Image(systemName: "2.circle")
                Text("Nothing")
            }
        }
    }
}

Option 2: onAppear / onDisappear

struct ContentView: View {

    @State var info:String = "???"
    let provider = DataProvider() // Local for demo purpose, use another pattern

    @State var cancel: AnyCancellable? = nil

    var body: some View {
        TabView {
            VStack {
                Text("Info: \(info)")
                Button(action: {
                    self.provider.startNotifications()
                }) {
                    Text("Start notifications")
                }
            }
            .onAppear(perform: subscribeToNotifications)
            .onDisappear(perform: unsubscribeToNotifications)
            .tabItem {
                Image(systemName: "1.circle")
                Text("Notifications")
            }
            VStack {
                Text("AnotherView")
            }
            .tabItem {
                Image(systemName: "2.circle")
                Text("Nothing")
            }
        }
    }

    private func subscribeToNotifications() {
       // publisher to emit events when the default NotificationCenter broadcasts the notification
        let publisher = NotificationCenter.default.publisher(for: DataProvider.updated)
            .map { notification in
                return notification.userInfo?["data"] as! String
            }
            .receive(on: RunLoop.main)

        // keep reference to Cancellable, and assign String value to property
        cancel = publisher.assign(to: \.info, on: self)
    }

    private func unsubscribeToNotifications() {
       guard cancel != nil else {
            return
        }
        cancel?.cancel()
    }
}

For this test, I use a dummy service:

class DataProvider {   
    static let updated = Notification.Name("Updated")
    var payload = "nothing"    
    private var running = true

    func fetchSomeData() {
        payload = Date().description
        print("DEBUG new payload : \(payload)")
        let dictionary = ["data":payload] // key 'data' provides payload
        NotificationCenter.default.post(name: DataProvider.updated, object: self, userInfo: dictionary)
    }

    func startNotifications() {
        running = true
        runNotification()
    }

    private func runNotification() {
        if self.running {
            self.fetchSomeData()
            let soon = DispatchTime.now().advanced(by: DispatchTimeInterval.seconds(3))
            DispatchQueue.main.asyncAfter(deadline: soon) {
                self.runNotification()
            }
        } else {
            print("DEBUG runNotification will no longer run")
        }
    }

    func stopNotifications() {
        running = false
    }   
}
lazi74
  • 622
  • 1
  • 7
  • 23

1 Answers1

3

It seems that there are two publishers name let publisher in your program. Please remove one set of them. Also self.info = payload and publisher.assign(to: \.info, on: self)} are duplicating.

               }
        .onAppear(perform: subscribeToNotifications)
        .onDisappear(perform: unsubscribeToNotifications)
        .onReceive(publisher) { (payload) in
              //  self.info = payload
            print(payload)
        }
        .tabItem {

In the following:

                  @State var cancel: AnyCancellable? = nil

            private func subscribeToNotifications() {
               // publisher to emit events when the default NotificationCenter broadcasts the notification
        //        let publisher = NotificationCenter.default.publisher(for: DataProvider.updated)
        //            .map { notification in
        //                return notification.userInfo?["data"] as! String
        //            }
        //           .receive(on: RunLoop.main)

                // keep reference to Cancellable, and assign String value to property
                if cancel == nil{
                    cancel = publisher.assign(to: \.info, on: self)}

            }

            private func unsubscribeToNotifications() {
               guard cancel != nil else {
                    return
                }
                cancel?.cancel()
            }

Now you can see, cancel?.cancel() does work and the info label no longer update after you come back from tab2. ~~~Publisher pause Here because subscription has been cancelled.~~~

Publisher is not paused as there is another subscriber in the view , so the print(payload) still works.

E.Coms
  • 11,065
  • 2
  • 23
  • 35
  • I was not using both options togethers, but your comment made me realize that my post was not clear: I clarified the two options. – lazi74 Nov 05 '19 at 11:04
  • So your question is if there is no subscriber, will the publisher `pause`? AS if you cancel the subscriber, you can not resume the subscription. But the publisher (if you save it as a instance variable) still exists and you can subscribe it again – E.Coms Nov 05 '19 at 15:22
  • Ah thanks for pointing the obvious: I can use the Cancellable subscriber with option 1 too ! – lazi74 Nov 05 '19 at 17:30