ARC not working properly when using NSNotificationCenter - ios

Why doesn't deist get called on an object that has used NSNotificationCenter, I have included below a simple version of my code. Where I create an object that observes for a notification and when the notification is fired, it removes the observer's subscription. I also remove the subscription if the object is freed up. However, when running profiling for the app, you can see that after viewDidAppear finishes there is a persistent allocation for the test object that is now nil and should have been freed up. Why is this the case?
import UIKit
class ViewController: UIViewController {
var t: test?
override func viewWillAppear(animated: Bool) {
t = test()
fire()
t = nil
}
func fire() {
NSNotificationCenter.defaultCenter().postNotificationName("Hello",
object: nil)
}
}
class test {
var e: NSObjectProtocol?
init() {
e = NSNotificationCenter.defaultCenter().addObserverForName(
"Hello", object: nil, queue: NSOperationQueue.mainQueue(),
usingBlock: sayHello)
}
deinit {
if let e = e { NSNotificationCenter.defaultCenter().removeObserver(e) }
}
func sayHello(notification: NSNotification) {
if let e = e { NSNotificationCenter.defaultCenter().removeObserver(e) }
}
}
I would appreciate an answer even in Objective-C, since it will probably answer this question as well.
Thank you very much

Passing in a function of self as a closure parameter will create a retain cycle.
What you're doing is effectivity short hand for:
init() {
e = NSNotificationCenter.defaultCenter().addObserverForName("Hello", object: nil, queue: NSOperationQueue.mainQueue() { notification in
self.sayHello(notification)
}
}
As you can see self is being captured here. To get around this you should defined self as unowned in a capture list:
init() {
e = NSNotificationCenter.defaultCenter().addObserverForName("Hello", object: nil, queue: NSOperationQueue.mainQueue() { [unowned self] notification in
self.sayHello(notification)
}
}
This will prevent the retain cycle.
As you're removing the observer in sayHello you should also set e to nil in there too after removing the observer.
See this question for more info about retain cycles, capturing, etc. when using this method on NSNotificationCenter.

you don't add self as observer but another block.
BUT
Then you remove yourself (though never added) but forget the block
--- so:
self.observer = center.addObserverForName(didEnterBackground, object: nil, queue: nil) {
...
}
then later
center.removeObserver(self.observer)

Related

Swift - Timer not able to access updated instance property

Im my react native project's native module I need to send some data periodically from Objective-C to Swift so I am using NSNotificationCenter. I receive the data successfully in my Swift class, inside the function attached to the observer, and I store it in a property.
If I access this property from any instance method call I can see that the value has updated.
However if I access the same property in the selector function attached to the Timer it appears as if the value has not been updated and I cannot figure out why? It seems as if the timer selector function does not have access to anything except the initial value of the property - I have also tried passing the property as part of userInfo to the Timer but the issue is the same.
[[NSNotificationCenter defaultCenter] postNotificationName:#"stateDidUpdate" object:nil userInfo:state];
class StateController {
var state: Dictionary<String, Any> = Dictionary()
var timer: Timer = Timer()
func subscribeToNotifications() {
NotificationCenter.default.addObserver(
self, selector: #selector(receivedStateUpdate),
name: NSNotification.Name.init(rawValue: "stateDidUpdate"), object: nil)
}
#objc func receivedStateUpdate(notification: NSNotification) {
if let state = notification.userInfo {
self.state = (state as? Dictionary<String, Any>)!
print("\(self.state)") // I can see that self.state has been updated here
}
}
func runTimer() {
self.timer = Timer(timeInterval: 0.1, target: self, selector: #selector(accessState(timer:)), userInfo: nil, repeats: true)
self.timer.fire()
RunLoop.current.add(self.timer, forMode: RunLoop.Mode.default)
RunLoop.current.run(until: Date(timeIntervalSinceNow: 2))
}
#objc func accessState(timer: Timer) {
print("\(self.state)") // state is an empty Dictionary when accessed here
}
func printState() {
"\(self.state)" // value printed is the updated value received from the notification
}
}
I figured out that multiple instances of my Swift class being created was causing the issue. I assumed that React Native would create a singleton when calling a native module but it appears multiple instances are created as I could see how many times the init method was called. Switching to a Singleton pattern resolved the issue for me following this excellent video and this excellent gist on how to create a singleton in a react native project
class StateController {
static let shared = StateController()
private override init() {
}
}

How to removeObserver in Swift 5 using addObserver closure method

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!)
}
}

UIApplicationSignificantTimeChange notification is not triggered

I want to use the UIApplicationSignificantTimeChange to check when the day has changed and I encapsulated in my own class so I can easy use it in more view controllers:
public final class DayChangedObserver {
private var token: NSObjectProtocol!
public init?(handler: #escaping () -> ()) {
token = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIApplicationSignificantTimeChange, object: self, queue: nil) { _ in
handler()
}
}
deinit {
NotificationCenter.default.removeObserver(token)
}
}
And I call this code from my view controller:
override func viewDidLoad() {
super.viewDidLoad()
_ = DayChangedObserver() {
print("Day has changed")
}
}
I am testing this on my iPhone and I manually change the time. But it seems that it doesn't work using my class.
Is it something wrong with my implementation ? Because it was working when I was using this event in the past (without my own class implementation).
EDIT1:
I seems that deinit is called immediately after, so I am using an instance variable to keep a strong reference and now it's not deinit anymore, but still doesn't work.
object: self change to object: nil,try it.

How to remove a notification observer set in a closure in Swift?

I have this code:
alert.addTextFieldWithConfigurationHandler { field in
field.placeholder = "password"
field.secureTextEntry = true
NSNotificationCenter.defaultCenter().addObserverForName(UITextFieldTextDidChangeNotification,
object: field, queue: NSOperationQueue.mainQueue()) { n in
delete.enabled = field.text?.characters.count >= 2 }
}
I'm wondering how I should go about removing the observer from "field" when the containing view is dismissed? Since 'field' only exists inside the closure, I'm not sure how to get a reference to it that I can pass to removeObserver later.
Or do I even need to remove the observer? Will it just remove itself when field is eventually deallocated?
Update
I realized that in this specific case, I can retrieve the reference to the field and remove the observer by doing:
if let alert = alert, textFields = alert.textFields where textFields.count > 0 {
NSNotificationCenter.defaultCenter().removeObserver(textFields[0])
}
But I'm going to leave the question up for a little because I'm curious about the general case where I do
{
let x = Object()
NSNotificationCenter.defaultCenter().addObserverForName("thing",
object: x, queue: NSOperationQueue.mainQueue())
}()
Update to the update
Per #rmaddy, my update was wrong. I was confusing the observer and the observing object.
Per #leo-dabus I should be doing
weakSelf?.observer = NSNotificationCenter.defaultCenter().addObserverForName(UITextFieldTextDidChangeNotification,
object: field, queue: NSOperationQueue.mainQueue()) { n in
delete.enabled = field.text?.characters.count >= 2 }
And then later in the action that dismisses the alert:
if let observer = weakSelf?.deleteAlertObserver {
NSNotificationCenter.defaultCenter().removeObserver(observer)
}
It looks like the consensus answer here is that we should create a variable in the outer scope to hold the reference to the observer that's created in the inner scope. Then when the observer is no longer needed, we can remove it. So, something along the lines of:
import UIKit
class Pointless
{
var observer: NSObjectProtocol?
init () {
class SomeObject {}
observer = NSNotificationCenter.defaultCenter().addObserverForName("something",
object: SomeObject(), queue: NSOperationQueue.mainQueue()) { n in
print(n)
}
}
deinit
{
if let observer = observer{
NSNotificationCenter.defaultCenter().removeObserver(observer)
}
}
}

Triggering a specific action when the app enters foreground from a local notification in iOS? (using swift)

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
}

Resources