NotificationCenter.default.addObserver keep getting called multiple times with Unwind Segue - ios

I am using an show segue and an unwind segue to navigate between two iOS viewControllers, VC1 and VC2. In the viewDidLoad() of VC2 I make VC2 an observer. Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: "buzzer updated"), object: nil, queue: OperationQueue.main) { _ in
print("set beeperViewImage")
}
}
Every time I use the unwind segue to go from VC2 back to VC1 the addObserver() gets called an additional time, e.g., on the fourth return segue addObserver is called 4 time; on the fifth segue, five times, etc. This behavior happens even when the app is sent to the background and recalled. It remembers how many segues happened in the previous session and picks up the count from there.
I have no problems with multiple calls in VC1, which is the initial VC.
I have tried to set VC2 to nil after unwind segueing.
Looking forward to any guidance.

This is undoubtedly a case where your view controllers are not getting released. Perhaps you have a strong reference cycle.
For example, consider this innocuous example:
extension Notification.Name {
static let buzzer = Notification.Name(rawValue: Bundle.main.bundleIdentifier! + ".buzzer")
}
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(forName: .buzzer, object: nil, queue: .main) { _ in
self.foo()
}
}
func foo() { ... }
}
If I then enter and leave this view controller three times and then click on the “debug memory graph” button, I will see the following:
I can see three instances of my second view controller in the panel on the left and if they were properly deallocated, they wouldn’t show up there. And when I click on any one of them in that panel, I can see a visual graph of what still has a strong reference to the view controller in question.
In this case, because I turned on the “Malloc Stack” feature under “Product” » “Scheme” » “Edit Scheme...” » “Run” » “Diagnostics” » “Logging”, I can see the stack trace in the right most panel, and can even click on the arrow button and be taken to the offending code:
In this particular example, the problem was that I (deliberately, for illustrative purposes) introduced a persistent strong reference where the Notification Center is maintaining a strong reference to self, imposed by the closure of the observer. This is easily fixed, by using the [weak self] pattern in that closure:
NotificationCenter.default.addObserver(forName: .buzzer, object: nil, queue: .main) { [weak self] _ in
self?.foo()
}
Now, I don’t know if this is the source of the strong reference cycle in your case because the code in your snippet doesn’t actually reference self. Perhaps you simplified your code snippet when you shared it with us. Maybe you have something completely else that is keeping reference to your view controllers.
But by using this “Debug Memory Graph” button, you can not only (a) confirm that there really are multiple instances of your relevant view controller in memory; but also (b) identify what established that strong reference. From there, you can diagnose what is the root cause of the problem. But the code in your question is insufficient to produce this problem, but I suspect a strong reference cycle somewhere.

Thank you all for your comments on my problem. Based on them, I started searching for what might be holding on to my VC2. Turns out that a call to read a bluetooth radio link in my VC2 viewWillAppear() was the culprit but I don't understand why:
self.radio?.peripheral?.readValue(for: (self.radio?.buzzerChar)!)
Everything works fine after removing the above line of code. Thanks again for pointing out in which direction to search.

Related

Is it bad practice to modify view after viewDidDisappear called?

I have UITabBarController and in one of the UIViewController there I scroll UICollectionView each 5 second using Timer. Here is short code how I do it:
override func viewDidLoad() {
super.viewDidLoad()
configureTimer()
}
private func configureTimer() {
slideTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(scrollCollectionView), userInfo: nil, repeats: true)
}
#objc func scrollCollectionView() {
collectionView.scrollToItem(at: someIndexPath, at: .centeredHorizontally, animated: true)
}
It perfectly works. But I think it has a big issue. Of course, I can open another screen from this UIViewController (for example, I can tap to another tab or push another UIViewController). It means, my UIViewController's view, containing UICollectionView, disappears. In another words, viewDidDissapear will be called. But my timer still exists and I am having strong reference to it, possibly, there is retain cycle. It keeps working and each 5 second scrollCollectionView method is called even my view dissapeared. I don't know how, but iOS somehow handles it. In other words, it can modify view even it is not visible. How is that possible and is it good practice? Of course, I can invalidate my timer in viewDidDissapear and start it in viewDidAppear. But I don't want to loose my timer value and don't want to start it from zero again. Or may be it is ok to invalidate my timer in deinit?
My question covers pretty common situation. For instance, if I make network request and open another UIViewController. After that request finished, I should modify UI, but now am on another screen. Is it ok to allow iOS to modify UI even it is not visible?
A couple of thoughts:
If the timer is updating the UI at some interval, you definitely should start it in viewDidAppear and stop it in viewDidDisappear. There’s no point in wasting resources updating a view that is not visible.
By doing this, you can solve your strong reference cycle, too.
In terms of “losing” your timer value and starting at zero, we generally would just save the time you’re counting from or to and calculate the necessary value when restarting the timer later.
We do this, anyway, because you really shouldn’t be using timers to increment values because you’re technically not assured that they’ll be called with the frequency you expect.
All of that said, I don’t know what timer “value” you’re worried about losing in this example.
But definitely don’t waste time updating a UI that is no longer visible. It’s not scalable and blurs the distinction between the model (what you’re counting to or from) from the UI (the update that happens every five seconds).

Right view lifecycle when disconnecting

I have an app with a UITabBarController containing 5 items. In the last item (profile), the user can log out or delete his account and will be automatically redirected to the OnBoarding screen :
func signout(ofViewController sender: UIViewController, action: ENLoginScreenAction) {
let onBoardingVC = ENOnBoardingViewController()
onBoardingVC.withAction = action
onBoardingVC.modalPresentationStyle = .formSheet
ENUserInstance.userLogout()
sender.present(onBoardingVC, animated: true)
}
I don't understand very well the UIView lifecycle notion so it appears that after logout, none of my 5 UITabBarController children are deinited.
I am pretty sure I am missing something about it so is there a way to deinit those children or is it normal to not do it after a logout or something similar in term of application lifecycle?
You right they are still here. And it's fine to have them there, as long as user can't access them if they are not supposed to. Don't worry about the memory, it's very light (depending actually on what you have in it).
The os will deinit them if it considers that they are not useful anymore, which can be the case if they are not referenced anymore. Your tabbarcontroller might reference them, so that's maybe why they are not deinit

UIKeyCommands don't work when intermediary viewController contains two viewControllers

I believe this is a non-trivial problem related to UIKeyCommands, hierarchy of ViewControllers and/or responders.
In my iOS 9.2 app I have a class named NiceViewController that defines UIKeyCommand that results in printing something to the console.
Here's NiceViewController:
class NiceViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let command = UIKeyCommand(input: "1", modifierFlags:UIKeyModifierFlags(),
action: #selector(keyPressed), discoverabilityTitle: "nice")
addKeyCommand(command)
}
func keyPressed() {
print("works")
}
}
When I add that NiceViewController as the only child to my main view controller all works correctly - pressing button "1" on external keyboard (physical keyboard when used in simulator) works like a charm. However when I add a second view controller to my main view controller the UIKeyCommands defined in NiceViewController stop working.
I'd love to understand why does it happen and how to ensure that having multiple child view controllers to my main view controller doesn't stop those child view controllers from handling UIKeyCommands.
Here is my main view controller:
class MainViewController: UIViewController {
let niceViewController = NiceViewController()
let normalViewController = UIViewController()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(niceViewController.view)
self.addChildViewController(niceViewController)
self.view.addSubview(normalViewController.view)
// removing below line makes niceViewController accept key commands - why and how to fix it?
self.addChildViewController(normalViewController)
}
}
I do not believe this is a problem with UIKeyCommands
In iOS, only one View Controller at a time may manage key commands. So with your setup, you have a container view controller with a couple child view controllers. You should tell iOS that you would like NiceViewController to have control of key commands.
Defining First Responders
At a high level, in order to support key commands, you not only must create a UIKeyCommand and add it to the view controller, but you must also enable your view controller to become a first responder so that it is able to respond to the key commands.
First, in any view controller that you would like to use key commands for, you should let iOS know that that controller is able to become a first responder:
override func canBecomeFirstResponder() -> Bool {
// some conditional logic if you wish
return true
}
Next, you need to make sure the VC actually does become the first responder. If any VCs contain some sort of text fields that become responders (or something similar), that VC will probably become the first responder on its own, but you can always call becomeFirstResponder() on NiceViewController to make it become the first responder and, among other things, respond to key commands.
Please see the docs for UIKeyCommand:
The system always has the first opportunity to handle key commands. Key commands that map to known system events (such as cut, copy and paste) are automatically routed to the appropriate responder methods. For other key commands, UIKit looks for an object in the responder chain with a key command object that matches the pressed keys. If it finds such an object, it then walks the responder chain looking for the first object that implements the corresponding action method and calls the first one it finds.
Note: While someone is interacting with the other VC and it is the first responder, NiceViewController cannot be the first responder at the same time, so you might want some key commands on the other VC as well.
Why this isn't always necessary
When only one VC is presented, iOS appears to assume that it will be the first responder, but when you have a container VC, iOS seems to treat the container as the first responder unless there is a child that says it is able to become the first responder.
Following #Matthew explanation solution is adding becomeFirstResponder() request; in viewDidAppear instead of viewDidLoad resolve my similar problem.
Swift4
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
becomeFirstResponder()
print("becomeFirstResponder?: \(isFirstResponder)")
}
I found while experimenting with this that if you manually call becomesFirstResponder() on the child view controllers, it allows you to have multiple first responders and all key commands show up when hitting command.
I'm not sure why this works exactly as surely you're only supposed to have a single firstResponder at any one time.

Swift screenshot observer executing number of times viewController is viewed per session (not once as it should)

I am using this function to detect a screenshot in Swift:
let mainQueue = OperationQueue.main
NotificationCenter.default.addObserver(forName: UIApplication.userDidTakeScreenshotNotification,
object: nil,
queue: mainQueue) { notification in
print("[!]detected screenshot")
}
It is located in the viewDidLoad() and each time I access the viewController it adds another screenshot observer. So if I were to access the view controller twice in the same session, it would execute two times when I take a screenshot. If I were to visit the view controller this function is running four times, it the screenshot observer would execute four times. How do I keep this from being redeclared between view controller sessions? Thank you for the help.
The problem is that it seems every example of screenshot observer we run at in the internet use the main queue. This is kind of misleading, since it means that the observer is added in a more general context, instead of the view controller context, which is what you want.
The way to do it would be to add the following to the viewDidLoad (or to the viewWillAppear, or whichever fits better in your navigation flow):
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(didTakeScreenshot), name: UIApplicationUserDidTakeScreenshotNotification, object: nil)
(You should replace didTakeScreenshot with your desired method's name)
And then, on deinit (or viewDidDisappear...), you should remove the observer:
NSNotificationCenter.defaultCenter().removeObserver(self)
(for removing all observers)
or
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIApplicationUserDidTakeScreenshotNotification, object: nil)
(for removing that specific observer only)
I know this was asked a long time ago, but here's the answer, just in case somebody comes across the same problem. The most simple answer is to override the method viewDidAppear and subscribe to your notification there, and override viewWillDisappear and unsubscribe to the notification there. That way if you go to the view controller, you subscribe, and if you leave, you unsubscribe. In my opinion, it is not good to subscribe in viewDidLoad. The reason is that viewDidLoad is not called every time the view appears. Swift only calls the method if the view controller has not been loaded before, so, if you unsubscribe when it disappears, but do not subscribe when it appears, you will not be subscribed because viewDidLoad will not be called.

presenting a modal in viewdidappear with swift

I'm new to swift and ios programming in general. I'm trying to display a modal view when my app first loads which it does. The problem I'm running into is that my modal keeps appearing over and over and over. Not sure where I'm going wrong.
BONUS QUESTION: Ultimately I'd like this to only happen the first time the user opens the app.
class ViewController: UIViewController {
var introModalDidDisplay = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
showIntroModal()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func showIntroModal() {
if (!introModalDidDisplay) {
println(introModalDidDisplay)
introModalDidDisplay = true
let intro = self.storyboard?.instantiateViewControllerWithIdentifier("introModal") as IntroModalViewController
intro.modalPresentationStyle = UIModalPresentationStyle.FormSheet
self.presentViewController(intro, animated: true, completion: nil)
}
}
}
Found it. My "intro" class was extending ViewController rather than UIViewController...apparently that's bad. Thanks for the help! Sorry for the wild goose chase.
When you close the modal view you show your ViewController view again, firing viewDidAppear once more and entering an infinite loop of showing your modal view, since the first view is always "appearing"
I'd suggest doing this in viewDidLoad, as the view is supposed to load only once. Try and experiment with these events and see when they are fired.
As for firing only once I'd suggest setting a flag in localStorage (plist) indicating whether it's the first time the user opens the app or not. For example set a flag in the first view's viewDidLoad and if that flag is false show your modal view and set the flag to true.
Here's a question about writing in plists in Swift: Save Data to .plist File in Swift
A couple of observations:
Are you saying that you're seeing this appear again and again while you're using the app? That would suggest that you have multiple instances of this view controller instantiated. For example, you might be doing a segue back to this view controller (which will create new instance) rather than unwinding/popping/dismissing back to it (which will return to the previous instance).
I'd suggest you have a breakpoint or logging statement in viewDidLoad and confirm that you see this once and only once. If you're seeing it multiple times, that means that you have some circular reference amongst your storyboard scenes (and, BTW, you are abandoning memory, a type of leak).
To handle this only presenting itself once between uses of the app, you need to save this introModalDidDisplay in some form of persistent storage. Often NSUserDefaults is used for this. For example, define introModalDidDisplay to look up the status in the standard user defaults:
var introModalDidDisplay = NSUserDefaults.standardUserDefaults().boolForKey("introModalDidDisplay")
Then your showIntroModal can update this setting in the user defaults:
func showIntroModal() {
if !introModalDidDisplay {
introModalDidDisplay = true
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "introModalDidDisplay")
NSUserDefaults.standardUserDefaults().synchronize()
let intro = self.storyboard?.instantiateViewControllerWithIdentifier("introModal") as IntroModalViewController
intro.modalPresentationStyle = UIModalPresentationStyle.FormSheet
self.presentViewController(intro, animated: true, completion: nil)
}
}
Clearly, you can use whatever persistent storage technique you want (plist, archive, user defaults, Core Data, SQLite), but the idea is the same: Retrieve the status from persistent storage and once the intro screen has been presented, update that persistent storage accordingly.
By the way, by looking this up in persistent storage, we also fix the symptom of the problem I discussed in point #1. But you really want to fix the root cause of that first point, too, because otherwise you'll be leaking memory (if, of course, you're really instantiating multiple copies of the ViewController class).
By the way, looking ahead to the future, rather than storing just a boolean, I might suggest storing a version number identifier, too. That way, when you release version 2.0 of the app, you'll be able to decide whether the v1.0 users might see the updated intro screen again (or perhaps a custom one that focuses on what's new).

Resources