Home > OS >  Should I use weak self when I getting new Notification in swift?
Should I use weak self when I getting new Notification in swift?

Time:02-01

I am posing a notification and also i am getting the notification and the app works, my app would be working with or without weak self so I am little confused about using or not using it, maybe some one could help me and show me if i need weak self and why I need it or why i do not need it here.

This code is for macOS Storyboard Cocoa project.

import Cocoa
import SwiftUI

var appName: String = "My App Name"

class ViewController: NSViewController {
    
    override func viewWillAppear() {
        
        NotificationCenter.default.addObserver(forName: myNotificationName, object: nil, queue: .main) { [weak self] newNotification in
            
            if let unwrappedNewNotification = newNotification.object as? String {
                self?.titleUpdater(value: unwrappedNewNotification)
            }
        }
        
        let controller = NSHostingController(rootView: ContentView())
        self.view = controller.view
        self.view.window?.title = appName
    }
    
    private func titleUpdater(value: String) {
        appName = value
        self.view.window?.title = value
    }
    
}





struct ContentView: View {
    var body: some View {
        VStack {
            Button("Change", action: {
                postMyNotification(value: appName   " updated!")
            })
        }
        .frame(width: 400.0, height: 300.0)
    }
}




let myNotificationName: Notification.Name = Notification.Name(rawValue: "myNotificationName")

func postMyNotification(value: String) {
    NotificationCenter.default.post(Notification(name: myNotificationName, object: value))
}

Update:

deinit {
    if let unwrappedObserver: NSObjectProtocol = observer {
        print("worked for deinit!")
        NotificationCenter.default.removeObserver(unwrappedObserver)
    }
}

CodePudding user response:

Yes, you should use weak self in this case, because NotificationCenter has to hold on to the closure strongly. If you don't use weak self, your ViewController can never be destroyed.

But you should also ensure that your observer is destroyed when the ViewController is destroyed. The addObserver(forName:object:queue:using:) method returns an object which you should save in an instance property and pass to NotificationCenter.default.removeObserver in deinit.

class ViewController: NSViewController {
    private var observer: NSObjectProtocol? = nil

    deinit {
        if let observer {
            NotificationCenter.default.removeObserver(observer)
        }
    }

    override func viewWillAppear() {
        observer = NotificationCenter.default.addObserver(forName: myNotificationName, object: nil, queue: .main) { [weak self] newNotification in
            
            if let unwrappedNewNotification = newNotification.object as? String {
                self?.titleUpdater(value: unwrappedNewNotification)
            }
        }

        let controller = NSHostingController(rootView: ContentView())
        self.view = controller.view
        self.view.window?.title = appName
    }

    private func titleUpdater(value: String) {
        appName = value
        self.view.window?.title = value
    }
}

But I would recommend using a different API so you don't have to write a deinit at all. Use Combine instead, like this:

import Combine
import SwiftUI

class ViewController: NSViewController {
    private var tickets: [AnyCancellable] = []

    override func viewWillAppear() {
        NotificationCenter.default.publisher(for: myNotificationName)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] in
                if let self, let newTitle = $0.object as? String {
                    self.titleUpdater(value: newTitle)
                }
            }
            .store(in: &tickets)

        let controller = NSHostingController(rootView: ContentView())
        self.view = controller.view
        self.view.window?.title = appName
    }

    private func titleUpdater(value: String) {
        appName = value
        self.view.window?.title = value
    }
}

This way, you don't have to write a deinit. When the ViewController is destroyed, it automatically destroys its instance properties, including the tickets array and its contents. When the ticket (AnyCancellable) returned by sink is destroyed, it cancels the subscription.

You might even be able to eliminate the use of self entirely. You didn't show the declaration of appName. If it's global, you can subscribe like this instead:

        NotificationCenter.default.publisher(for: myNotificationName)
            .receive(on: DispatchQueue.main)
            .sink { [view] in
                if let newTitle = $0.object as? String {
                    appName = newTitle
                    view.window?.title = newTitle
                }
            }
            .store(in: &tickets)

Now the closure captures view directly instead of capturing self, so there's no possibility of a retain cycle.

  • Related