Recently I have been reviewing my code and noticed in one of my view controllers, deinit() is not called.
After commenting out this line, deinit calls successfully:
NotificationCenter.default.addObserver(forName: .UIKeyboardWillShow, object: nil, queue: nil)
{ notification in self.keyboardWillShow(notification: notification) }
I'm aware of the fact you need to remove the observer, but if I replace
self.keyboardWillShow(notification: notification)
with
print("hello world")
deinit() calls successfully.
The code inside of my local function "keyboardWillShow" is call commented out, however the function signature is
func keyboardWillShow(notification: Notification)
Any recommendations on how I can improve this code to not retain references and properly hit deinit()/
Thank you!!
Your closure is capturing a strong reference to self, which prevents your object from being released; you have a retain cycle. When you don't reference self in the closure you avoid this cycle, which is why your view controller is released when you have just the print statement.
You can use [weak self] in the closure to prevent this; you then need to unwrap self in the closure before you use it.
NotificationCenter.default.addObserver(forName: .UIKeyboardWillShow, object: nil, queue: nil)
{ [weak self] notification in
self?.keyboardWillShow(notification: notification)
}
Related
This is my first post.
I'm Japanese iOS engineer (just became this month).
I have a trouble with removeObserver method of NotificationCenter in Swift 5.
I added observer to ViewController (VC) by using closure type addObserver.
I want to remove this Observer when VC's deinitialization has called.
I wrote NotificationCenter.default.removeObserver(self) in VC's deinit method. But, this seemed not to work for me.
What's the problem???
Additionally, if my code has memory leak problem, let me know how to fix it.
Here's piece of my code.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] notification in
guard let self = self else { return }
self.loadWeather(notification.object)
}
}
deinit {
print(#function)
print("ViewController died")
NotificationCenter.default.removeObserver(self)
}
}
Closure-based addObserver
If you use the closure-based variant of addObserver, the token (not self) is
the observer. The notification center has no knowledge at all about
self in this situation. The documentation is not super clear on
this.
from Twitter
Meaning it's meaningless to do: NotificationCenter.default.removeObserver(self)
The docs recommend two ways:
Normal way
Subscribe as you normally do:
let center = NSNotificationCenter.defaultCenter()
let mainQueue = NSOperationQueue.mainQueue()
self.localeChangeObserver = center.addObserverForName(NSCurrentLocaleDidChangeNotification, object: nil, queue: mainQueue) { (note) in
print("The user's locale changed to: \(NSLocale.currentLocale().localeIdentifier)")
}
Remove the observer at some point of code.
NotificationCenter.default.removeObserver(self.localeChangeObserver) e.g. through a function or in deinit
Note: You'd still need to use [weak self] to avoid retain cycles.
To avoid a retain cycle, use a weak reference to self inside the block when self contains the observer as a strong reference.
Single subscribe
Remove the observer immediately after the first time it gets a callback
let center = NSNotificationCenter.defaultCenter()
let mainQueue = NSOperationQueue.mainQueue()
var token: NSObjectProtocol?
token = center.addObserverForName("OneTimeNotification", object: nil, queue: mainQueue) { (note) in
print("Received the notification!")
center.removeObserver(token!)
}
Selector-based addObserver
If you use the selector-based add, self (there is no token) the observer. Having that said, you should avoid doing:
NotificationCenter.default.removeObserver(self)
because your code may not be the only code adding observers that involve the object. When removing an observer, remove it with the most specific detail possible. For example, if you used a name and object to register the observer, use removeObserver(_:name:object:) with the name and object.
It’s only safe to call removeObserver(something) in the deinit method, other than that don’t use it use removeObserver(_:name:object:) instead.
Calling removeObserver(self) is incorrect outside deinit, because you’d be removing all observations set for the object. Calling it inside deinit isn’t incorrect, but meaningless, because the object is to be deallocated immediately.
Set your observer object to current view controller.
From apple doc.s, object is
The object whose notifications the observer wants to receive; that is,
only notifications sent by this sender are delivered to the observer.
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification,
object: self,
queue: nil) { [weak self] notification in
guard let self = self else { return }
self.loadWeather(notification.object)
}
Removing observer from NotificationCenter
deinit {
NotificationCenter.default.removeObserver(self)
}
ANOTHER WAY
You can also make copy of Notification Observer object and remove it from NotificationCenter in deinit.
let notificationCenter = NotificationCenter.default
var loadWeatherObserver: NSObjectProtocol?
override func viewDidLoad() {
super.viewDidLoad()
loadWeatherObserver = notificationCenter.addObserver(forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: nil) { [weak self] notification in
guard let self = self else { return }
self.loadWeather(notification.object)
}
}
deinit {
if (loadWeatherObserver != nil) {
notificationCenter.removeObserver(loadWeatherObserver!)
}
}
I want to quit "DispatchQueue.main.asyncAfter" when deinit is called.
subView.swift
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.doSomething()
}
func doSomething(){
if self.subView.flgA == false { //error because subView's deinit is already called
//...
}
}
When ViewController do _ = self.navigationController?.popViewController(animated: true) and deinit is called, deinit in ViewController and subView is called at first and after a few minutes doSomething() is called.
I want to stop doSomething() when popViewController is executed.
How can I do it?
You could schedule a Timer with this code block
let timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { [weak self] timer in
self?.doSomething()
}
hold on to the timer and cancel it before popViewController like this:
timer.invalidate()
Notice the [weak self] and the self?.domeSomething() which I put there to avoid the hard reference to the viewcontroller and read Laffen's more detailed answer on this.
In Swift we have something called ARC(Automatic Reference Counting).
ARC will make sure any object with at least one strong reference to it, will not be removed from memory.
In your case you're creating a strong reference to self in the closure of the async task created by DispatchQueue.main.asyncAfter.
You need to tell the compiler that this reference is either weak or unowned(See attached link for more info.), this will enable the instance of self to be deinitialised even though you have a reference to self from the closure.
The weak keyword can be used in cases where you want the closure to run and make actions that doesn't require a reference to self. This is helpful to use if you don't know if self is alive or not.
The unowned keyword can be used in cases where you don't need the closure to run without a reference to self. This must be used in cases where you know self self is still alive.
Getting a weak or unowned reference of self in the closure can be achieved like in the example below:
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
[weak self] in
self?.doSomething()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
[unowned self] in
self.doSomething()
}
It's worth mentioning that if you're not using a deadline, but calling .async { ... } directly does not result in capturing self and therefore can be used safely without defining weak/unowned self.
I have a UIView similar to the one you can see below:
class ViewTaskViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
subscribeToNotifications()
}
func subscribeToNotifications() {
let notification = NotificationCenter.default
notification.addObserver(forName: Notification.Name(rawValue: "TimerUpdated"), object: nil, queue: nil, using: handleUpdateTimer)
print("Subscribed to NotificationCenter in ViewTaskViewController")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("TUFU TUFU TUFU")
NotificationCenter.default.removeObserver(self)
}
deinit {
print("DENINT")
}
#objc func handleUpdateTimer(notification: Notification) {
if let userInfo = notification.userInfo, let timeInSeconds = userInfo["timeInSeconds"] as? Int {
withUnsafePointer(to: &self.view) {
print("We got timeeeeee \(timeInSeconds) \($0)")
}
//do something here....
}
}
}
The issue I am having is that I am unable to remove the observers from this particular UIView when the user hits the back button and returns to another viewController.
ViewWillDisppear is called but deinit is not called. The strange thing is that if we remove subscribeToNotifications() from viewDidLoad() then the deinit is called.
The other issue is related to a memory leak. As you can see in the screenshot below, when the view does subscribe to notifications and the user leaves/re-enters the view, the memory usage increase.
Now compare that to when the subscribeToNotifications() is commented out, there is no increase in memory usage and only one instance of the viewController.
The conclusion is that there seems to be a correlation between the notification subscription creation of a new instance of the UIView hence the deinit is not being called.
I'd like to find out if there is a way we can deinitialize the view and unsubscribe from the notification.
Please let me know if you need further information. :)
I've found the removeObserver() only works if you use this version of addObserver()
notification.addObserver(self, selector:#selector(self.handleUpdateTimer), name: Notification.Name(rawValue: "TimerUpdated"), object: nil)
I'm guessing with the original version you aren't actually indicating who the observer is.
As #Spads said you can use
NotificationCenter.default.addObserver(self, selector: #selector(subscribeToNotifications), name: NSNotification.Name(rawValue: "TimerUpdate"), object: nil)
or the one you already have.
you can remove your notification by it's name or it's reference
NotificationCenter.default.removeObserver(self, name: "TimerUpdate", object: nil)
if you declared your notification at the top of your class then you can directly pass the reference of your notification to be removed in your case notification
NotificationCenter.default.removeObserver(notification)
You should store your newly added observer in a opaque object (NSObjectProtocol) and then call NotificationCenter.default.removeObserver(self.nameOfObserver)
Within extension UIViewController I have a method:
func setupUserAndCartButtons() {
let cartBarButtonItem = UIBarButtonItem(image: DBCart.sharedCart().icon, style: .Plain, target: self, action: Selector("cartButtonTapped:"))
NSNotificationCenter.defaultCenter().addObserverForName(DBOrdersChangedNotficationName, object: nil, queue: nil, usingBlock: { [weak self] notification in
print("---->\(self!.classForCoder)")
cartBarButtonItem.image = DBCart.sharedCart().icon
})
}
I use it to change the image for my UIBarButtonItem.
This is how I push notification:
NSNotificationCenter.defaultCenter().postNotificationName(DBOrdersChangedNotficationName, object: nil)
For every controller, within deinit, I need to remove observer:
NSNotificationCenter.defaultCenter().removeObserver(self, name: DBOrdersChangedNotficationName, object: nil)
But it doesn't work:
When I push notification and deinit is never called (what means that I never pop controller from the stack) it looks ok, no crash, but once I pop at least one view controller (deinit is called then) and push notification, there is a crash:
fatal error: unexpectedly found nil while unwrapping an Optional value
You are using addObserverForName:object:queue:usingBlock: to setup your observer. In order to later remove this observer you will need to use the return value of that method as the "observer" in any calls to the removeObserver:... variants.
Since you are setting up the observer in an extension method of UIViewController, and assuming you are calling this from concrete UIViewController subclasses, I'd suggest updating this method to return the observer
func setupUserAndCartButtons() -> NSObjectProtocol {
let cartBarButtonItem = UIBarButtonItem(image: DBCart.sharedCart().icon, style: .Plain, target: self, action: Selector("cartButtonTapped:"))
let observer = NSNotificationCenter.defaultCenter().addObserverForName(DBOrdersChangedNotficationName, object: nil, queue: nil, usingBlock: { [weak self] notification in
print("---->\(self!.classForCoder)")
cartBarButtonItem.image = DBCart.sharedCart().icon
})
return observer
}
You will have to setup a property in your view controllers where you make use of this method in order to keep a reference to the observer:
class DBSomeViewController: UIViewController {
var ordersChangedObserver: NSObjectProtocol?
...
}
Then assign the return value of setupUserAndCartButtons to this property:
ordersChangedObserver = setupUserAndCartButtons()
And finally remove the observer in the deinit:
deinit {
NSNotificationCenter.defaultCenter().removeObserver(ordersChangedObserver)
}
Credit
I can't take full credit for this answer as Dániel Nagy pretty much got it in his comment on the question.
I am building an iOS app using the new language Swift. Now it is an HTML5 app, that displays HTML content using the UIWebView. The app has local notifications, and what i want to do is trigger a specific javascript method in the UIWebView when the app enters foreground by clicking (touching) the local notification.
I have had a look at this question, but it does not seem to solve my problem. I have also come across this question which tells me about using UIApplicationState, which is good as that would help me know the the app enters foreground from a notification. But when the app resumes and how do i invoke a method in the viewController of the view that gets displayed when the app resumes?
What i would like to do is get an instance of my ViewController and set a property in it to true. Something as follows
class FirstViewController: UIViewController,UIWebViewDelegate {
var execute:Bool = false;
#IBOutlet var tasksView: UIWebView!
}
And in my AppDelegate i have the method
func applicationWillEnterForeground(application: UIApplication!) {
let viewController = self.window!.rootViewController;
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var setViewController = mainStoryboard.instantiateViewControllerWithIdentifier("FirstView") as FirstViewController
setViewController.execute = true;
}
so what i would like to do is when the app enters foreground again, i want to look at the execute variable and run the method as follows,
if execute{
tasksView.stringByEvaluatingJavaScriptFromString("document.getElementById('sample').click()");
}
Where should i put the code for the logic to trigger the javascript from the webview? would it be on viewDidLoad method, or one of the webView delegate methods? i have tried to put that code in the viewDidLoad method but the value of the boolean execute is set to its initial value and not the value set in the delegate when the app enters foreground.
If I want a view controller to be notified when the app is brought back to the foreground, I might just register for the UIApplication.willEnterForegroundNotification notification (bypassing the app delegate method entirely):
class ViewController: UIViewController {
private var observer: NSObjectProtocol?
override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [unowned self] notification in
// do whatever you want when the app is brought back to the foreground
}
}
deinit {
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
}
}
}
Note, in the completion closure, I include [unowned self] to avoid strong reference cycle that prevents the view controller from being deallocated if you happen to reference self inside the block (which you presumably will need to do if you're going to be updating a class variable or do practically anything interesting).
Also note that I remove the observer even though a casual reading of the removeObserver documentation might lead one to conclude is unnecessary:
If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method.
But, when using this block-based rendition, you really do need to remove the notification center observer. As the documentation for addObserver(forName:object:queue:using:) says:
To unregister observations, you pass the object returned by this method to removeObserver(_:). You must invoke removeObserver(_:) or removeObserver(_:name:object:) before any object specified by addObserver(forName:object:queue:using:) is deallocated.
I like to use the Publisher initializer of NotificationCenter. Using that you can subscribe to any NSNotification using Combine.
import UIKit
import Combine
class MyFunkyViewController: UIViewController {
/// The cancel bag containing all the subscriptions.
private var cancelBag: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
addSubscribers()
}
/// Adds all the subscribers.
private func addSubscribers() {
NotificationCenter
.Publisher(center: .default,
name: UIApplication.willEnterForegroundNotification)
.sink { [weak self] _ in
self?.doSomething()
}
.store(in: &cancelBag)
}
/// Called when entering foreground.
private func doSomething() {
print("Hello foreground!")
}
}
Add Below Code in ViewController
override func viewDidLoad() {
super.viewDidLoad()
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector:#selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func appMovedToForeground() {
print("App moved to foreground!")
}
In Swift 3, it replaces and generates the following.
override func viewDidLoad() {
super.viewDidLoad()
foregroundNotification = NotificationCenter.default.addObserver(forName:
NSNotification.Name.UIApplicationWillEnterForeground, object: nil, queue: OperationQueue.main) {
[unowned self] notification in
// do whatever you want when the app is brought back to the foreground
}