Right view lifecycle when disconnecting - ios

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

Related

Memory Allocations Profiler and Steadily Increasing Persistent Memory - Signs of Trouble?

I have an app I am developing and the stakeholder using it said that the app becomes slow and unusable/unresponsive after consistent usage all day. Killing it and starting over causes it to run fine.
I don't seem to have this trouble on my device, but I started looking at the memory usage in both simulator/phone in debugger, and observed my memory would steadily increase if I took the basic action of going between screen to screen. These are pretty involved screens, but if I just go forward to the 'add new item' screen, then back to the product listing screen, the memory jumps up 30mb. If I keep doing this same action, over and over and over, I can get it to 1.1gb of memory
I then took it a step further, hooked up my phone, and ran profiler (specifically memory leaks). I found one leak involving my usage of ads, so I just commented out all the code for a test and while the leaks are gone, the memory continues to go up steadily.
I then ran the allocations tool, and after a few min of going back and forth in the same manner, here is the output:
As you can see, it's 1.53GB and if I kept doing the same action I can get it to 2GB+. Oddly enough, my phone never seems to mind, and the screens are just slightly laggy at times otherwise not too bad. Certainly usable.
Before I start ripping out the floor boards, I wanted to confirm this is a likely sign of a problem. Any suggestions on where I can start looking? If persistent memory is the issue, what would be some typical gotchas or pitfalls? What is "anonymous vm?"
Thank you so much if you're reading this far, and appreciate any guidance!
UPDATE/EDIT
After some guidance here, I noticed, oddly enough, that on the "add product" page it causes the memory to jump ~10MB each time I visit it. After commenting out code, I narrowed it down to this section (and even the line of code) causing the jump. Removing this code causes it to remain stable and not increase.
//Render collection views
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath as IndexPath)
let member: MemberDto = groupMembers[indexPath.item]
let contactInitials = cell.viewWithTag(1) as! UILabel
let contactAvatar = cell.viewWithTag(2) as! UIImageView
contactAvatar.image = UIImage(named: "anonymous")
contactInitials.text = member.displayName
contactAvatar.layer.cornerRadius = contactAvatar.frame.size.width / 2
contactAvatar.clipsToBounds = true
contactAvatar.contentMode = UIViewContentMode.scaleAspectFill
contactAvatar.layer.borderWidth = 5.0
if (member.profileImage.trimmingCharacters(in: CharacterSet.whitespaces) != "") {
UserService.getProfilePicture(userId: member.userId) {
response in
contactAvatar.image = response.value
}
}
So, the offending line of code is here:
contactAvatar.image = response.value
Adding it in, and going back and forth to this tableviewcontroller causes the memory to go up and up and up all the way to 2gb. Removing that one line of code (where I set the image) keeps it stable at ~40-70mb, or it goes up but very very slowly (dozens of repeats only got it to 80mb)
I realized I was not caching this image
I decided to try caching this with my framework, and that immediately resolved the issue. I suppose the line of code was pulling the image into memory or something like that? It doesn't seem like the networking call is the actual issue, since I left that in (and even went so far to make additional calls to my API) and that doesn't seem to do much by way of memory increase.
Just a few pieces of info:
From the main screen, you tap on a + symbol in the navigation menu bar to come to this screen.
I am using a regular segue on my storyboard, associated with the navigationbutton, to take the user here
Placing deinit on this vc does not seem to ever hit, even with print/code in there and breakpoints
Making API calls from within my uitableviewcontroller doesn't seem to cause the image to load UNLESS I combine that with SETTING the image. If I make a network call, but don't set the image, it doesn't increase.
What mistake did I make? I feel like caching the image is a bandaid - I recall reading that you're not supposed to make calls to images within a UITableViewController but what is the alternative, to pull all user images from the collection in advance and cache them before the tableview loads?
EDIT 2
As #matt suggested, this was just a bandaid. The true problem still lingered as I knew deinit() was not being called. After pulling out major chunks of code, I found this
lblMessage.addTapGestureRecognizer {
self.txtMessage.becomeFirstResponder()
}
which maps to an extension class:
public func addTapGestureRecognizer(action: (() -> Void)?) {
self.isUserInteractionEnabled = true
self.tapGestureRecognizerAction = action
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
self.addGestureRecognizer(tapGestureRecognizer)
}
public func addLongPressGestureRecognizer(action: (() -> Void)?) {
self.isUserInteractionEnabled = true
self.longPressGestureRecognizerAction = action
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture))
self.addGestureRecognizer(longPressGestureRecognizer)
}
// Every time the user taps on the View, this function gets called,
// which triggers the closure we stored
#objc fileprivate func handleTapGesture(sender: UITapGestureRecognizer) {
if let action = self.tapGestureRecognizerAction {
action?()
} else {
print("no action")
}
}
So somewhere in here the problem must lie. I'm taking this to a new thread:
Deinit not calling - Cannot find why something is retaining (code provided)
Thanks! Hope this helps someone.
Yes it's a problem, and yes you need to fix it. The two usual causes of this sort of thing are:
You've got a retain cycle such that at least some of your view controllers are never able to go out of existence.
You've designed the storyboard (or manual segue) sequence incorrectly, so that (for example) you present from view controller A to view controller B, and then in order to get "back" you present from controller B to view controller A. Thus you are not actually going "back"; instead, you are piling up a second view controller A on top of the first one, and so on, forever.
Either way, you can rapidly test that that sort of thing is going on just by implementing deinit to print(self) in all your view controllers. Then play with the app. If you don't see the printout in the log every time you go "back", you've got a serious memory problem, because the view controller is not being released when it should be, and you need to fix it.

UIActivityViewController showing blank options and can't be interacted

I was implementing new feature with share button on my app. Notice the activityViewController appeared blank. at first i thought the item i gave to share might be null, but when i tried to share a simple string, it still shows up like this, i revisited my old working code, and they are all acting like this. Even something as simple as this:
let activityViewController = UIActivityViewController(activityItems: ["test"], applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = self.view
DispatchQueue.main.async {
self.present(activityViewController, animated: true, completion: nil)
}
this is what i got:
Anybody have any idea what is causing this and how to fix this?
EDIT: Tested Using Actual Device, Causing problems, in my released APP, the feedback is that its not showing blank, it is showing with options, just not inter-actable, can't be clicked and can't dismiss it, the app will just "hang" and then the user will have to kill the app to reuse it. After testing, i'm still not sure what is causing the blank, i've disabled all my UIViewController extension,(many of them is not automated and need functions to call anyway so i don't think they are the problem), and when i put a debugPrint in the completion block of the present function, it doesn't even get called so the activity view controller is not finished initializing?
Okay, finally found the culprit, somebody tipped me of that i should look into extension of UIViewController to check if i'm overriding something, well i didn't but i did override UILabel and UIButton's awakeFromNib and setNeedsDisplay because my app have multiple language support and In-App change language support and i wanted to "automate" UILabel and UIButton to change language font(because some font is better looking) so that i can avoid attaching listener to viewcontrollers to change language font when they do in-app language changes.
override open func awakeFromNib() {
super.awakeFromNib()
if let prefLang = UserDefaults.languageCode{
self.font = switchFontForLang(lang: prefLang)
}
}
override open func setNeedsDisplay() {
super.setNeedsDisplay()
if let prefLang = UserDefaults.languageCode{
self.font = switchFontForLang(lang: prefLang)
}
}
Particularly the setNeedsDisplay() is causing problem, i've put a debugmessage in them both and i found out its being called endlessly, my guess is because the "in-app" language font changing is trying to change language to the setting but the UIActivityViewController is somehow trying to change the font back or at the very least calling setNeedsDisplay() when it detects something is not right, which it will call into the overrided method and then it will detect it back again thus creating a loop of endlessly calling setNeedsDisplay().

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

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.

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.

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