60

How do I add NotificationCenter.default.addObserve in SwiftUI?

When I tried adding observer I get below error

Argument of '#selector' refers to instance method 'VPNDidChangeStatus' that is not exposed to Objective-C

But when I add @objc in front of func I get below error

@objc can only be used with members of classes, @objc protocols, and concrete extensions of classes

Here is my code

let NC = NotificationCenter.default

var body: some View {
     VStack() {

     }.onAppear {

           self.NC.addObserver(self, selector: #selector(self.VPNDidChangeStatus),
                              name: .NEVPNStatusDidChange, object: nil)

     }
} 

@objc func VPNDidChangeStatus(_ notification: Notification) {
    //    print("VPNDidChangeStatus", VPNManager.shared.status)
}
O-mkar
  • 5,430
  • 8
  • 37
  • 61
  • Take a tour on this it might helps -https://stackoverflow.com/questions/38980887/protocol-extension-on-an-objc-protocol – Abhishek Nov 12 '19 at 12:39

6 Answers6

87

The accepted answer may work but is not really how you're supposed to do this. In SwiftUI you don't need to add an observer in that way.

You add a publisher and it still can listen to NSNotification events triggered from non-SwiftUI parts of the app and without needing combine.

Here as an example, a list will update when it appears and when it receives a notification, from a completed network request on another view / controller or something similar etc.

If you need to then trigger an @objc func for some reason, you will need to create a Coordinator with UIViewControllerRepresentable

struct YourSwiftUIView: View {

    let pub = NotificationCenter.default
            .publisher(for: NSNotification.Name("YourNameHere"))


    var body: some View {
        List() {
            ForEach(userData.viewModels) { viewModel in
                SomeRow(viewModel: viewModel)
            }
        }
        .onAppear(perform: loadData)
        .onReceive(pub) { (output) in
            self.loadData()
        }
    }

    func loadData() {
        // do stuff
    }
}
MadeByDouglas
  • 2,509
  • 1
  • 18
  • 22
  • 5
    this works. you can then just post a notification like this -> self.nc.post(name: Notification.Name("RemoteContatcsReceived"), object: nil) With xCode 11.5 I did not have to use @objc yay! – Dave Kozikowski Jun 09 '20 at 21:19
  • If you move it into a state object then it won't screw up your previews because state objects aren't init when previewing. – malhal Nov 10 '20 at 20:18
  • 1
    `NotificationCenter.publisher(for:object:)` *does* use `Combine`. You can *call* an Objective C function from any code you want. Presumably you're referring to *defining* an objective C function. Even that has absolutely nothing to do with `UIViewControllerRepresentable` or coordinators. You just need to define the function in a class. – Peter Schorn Jun 13 '21 at 13:16
  • struct YourSwiftUIView: View { @State updateYourSwiftUIView: Bool = false ... } func loadData() { // do stuff updateYourSwiftUIView.toggle() } } – Neph Muw Aug 16 '21 at 13:18
46

I have one approach for NotificationCenter usage in SwiftUI.

For more information Apple Documentation

Notification extension

extension NSNotification {
    static let ImageClick = Notification.Name.init("ImageClick")
}

ContentView

struct ContentView: View {
    var body: some View {
        VStack {
            DetailView()
        }
        .onReceive(NotificationCenter.default.publisher(for: NSNotification.ImageClick))
        { obj in
           // Change key as per your "userInfo"
            if let userInfo = obj.userInfo, let info = userInfo["info"] {
              print(info)
           }
        }
    }
}

DetailView

struct DetailView: View {
    var body: some View {
        Image(systemName: "wifi")
            .frame(width: 30,height: 30, alignment: .center)
            .foregroundColor(.black)
            .onTapGesture {
                NotificationCenter.default.post(name: NSNotification.ImageClick, 
                                                object: nil, userInfo: ["info": "Test"])
        }
    }
}
Rohit Makwana
  • 4,337
  • 1
  • 21
  • 29
16

I use this extension so it's a bit nicer on the call site:

/// Extension

extension View {
    func onReceive(
        _ name: Notification.Name,
        center: NotificationCenter = .default,
        object: AnyObject? = nil,
        perform action: @escaping (Notification) -> Void
    ) -> some View {
        onReceive(
            center.publisher(for: name, object: object), 
            perform: action
        )
    }
}

/// Usage

struct MyView: View {
    var body: some View {
        Color.orange
            .onReceive(.myNotification) { _ in
                print(#function)
            }
    }
}

extension Notification.Name {
    static let myNotification = Notification.Name("myNotification")
}

tadija
  • 2,981
  • 26
  • 37
7

It is not SwiftUI-native approach, which is declarative & reactive. Instead you should use NSNotificationCenter.publisher(for:object:) from Combine.

See more details in Apple Documentation

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
6

This worked for me

   let NC = NotificationCenter.default



   self.NC.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: nil, 
                       using: self.VPNDidChangeStatus)


   func VPNDidChangeStatus(_ notification: Notification) {


    }
O-mkar
  • 5,430
  • 8
  • 37
  • 61
1

exchange this

self.NC.addObserver(self, selector: #selector(self.VPNDidChangeStatus),
                          name: .NEVPNStatusDidChange, object: nil) 

to

self.NC.addObserver(self, selector: #selector(VPNDidChangeStatus(_:)),
                          name: .NEVPNStatusDidChange, object: nil)
Chris
  • 7,579
  • 3
  • 18
  • 38