How can I manage and free memory through ViewControllers - ios

I'm in front of a big issues, and the only reason I can't find a solution is because my lack of knowledge about swift and memory management in swift. So here is my concerns. I'm working in swift 4.0 and iOS 9.3
I'm actually making a picture gallery app with login/logout. Basic application.
I'm working in cleanSwift So I don't have those HUGE ViewControllers.
My application is separate in 3 VC : The login, the gallery and the settings (which contains the LogOut).
Here is my problem. When I log out, I want to create a new loginVC and clear all previous VC.
So I have my cleanMemory function which set all the UIImage to nil
func cleanMemory(request: Gallery.Request) { // Interactor
worker.cleanMemory(completionHandler: { (Value) in
self.interventions?.removeAll() // Interventions contains UIImages
self.interventionsSelected.removeAll() // InterventionsSelected contains UIImages
})
}
and then I delete the rests of the UIImage and the VC
func cleanMemory() {
interactor?.cleanMemory(request: Gallery.Request())
self.displayedInterventions.removeAll() // displayedInterventions contains UIImages
interactor = nil
router = nil
self.removeFromParentViewController()
self.navigationController?.popViewController(animated: true)
}
But when I create my new LoginVC.. my RAM didn't decrease.. And when I check the app memory, not a single VC was deleted.. And when I execute the loop Logout/Login 3 times, my app crash because I'm not well managing my RAM_
So where did I get wrong, and why ??
Thanks you for your answer.
EDIT: I was having 2 problems :
My completionHandler was keeping my VC alive
I was switching VC with .present, so that was keeping my VC in memory.
So you should change VC like that :
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let destinationVC = storyboard.instantiateViewController(withIdentifier: "LoginController")
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.window?.rootViewController = destinationVC

To remove viewController from memory you simply need to remove it from navigation stack. So when you call navigationController?.popViewController(animated: true) and back to previous view controller you already destroy that controller.
Then,
Here is my problem. When I log out, I want to create a new loginVC and clear all previous VC. So I have my cleanMemory function which set all the UIImage to nil
on logout it's good practice to stop all request but you don't need to do any changes to UI, because it takes some time and it doesn't need to "remove controller from memory". How to check if view controller completely removed from navigation stack? Simply write print statement in deinit func, compile code and go back from this view controller.
deinit {
print("ViewController deinit")
}
If this print works fine (you can see text in xcode console), you achieve the result - controller has been removed from navigation stack, but if there is no print result you probably forget to right manage your closures. For example
worker.cleanMemory(completionHandler: { (Value) in
...
})
this closure may hold your controller when your think that controller already deallocated and it means that your controller present somewhere in memory. To prevent these retain cycles you need to use [unowned self] of [weak self] (just google for this keywords, it's very easy to understand) like this:
// or you can use `[unowned self]`
worker.cleanMemory(completionHandler: { [weak self] (Value) in
guard let `self` = self else { return } // only for `weak` way
...
})
So, in this case there are nothing that can hold your controller alive after pop from navigation stack action or so.
These are simple rules that you should follow to write well managed code.

Related

Close a viewcontroller after a segue

What I want is to close a viewController after performing a segue so that the back button of the navigation controller on the new view doesn't go back to the view that I just closed, but it goes to the view that precedes it in the storyboard like it is the first time that it is loaded.
I already tried stuff like dismiss and so but it doesn't really work for me as it only closes the view in which the button that I pressed for performing the function is located :
#objc func goToList(){
self.dismiss(animated: true, completion: nil)
performSegue(withIdentifier: "goToList", sender: nil)
}
The navigation controller maintains a stack (array) of ViewControllers that have been opened (pushed). It also has the ability to pop these ViewControllers off the stack until it gets to a specific one.
For example, if you wished to return to a previous view controller of type MyInitialVC then you'd want to search through the stack until you found that type of VC, and then pop to it:
let targetVC = navigationController?.viewControllers.first(where: {$0 is MyInitialVC})
if let targetVC = targetVC {
navigationController?.popToViewController(targetVC, animated: true)
}
NB. written from memory without XCode, so you may need to correct minor typos
You can use unwind segue to get back to each viewController that you want.
Read more here:
Unwind Segues Step-by-Step (and 4 Reasons to Use Them)

Navigation Controller back button is not releasing memory of dismissed viewController

Newbie question here.
Imagine a very basic storyboard with 2 vc (A and B).
A is embedded in a navController. A has a collectionView showing a grid of images. B is displaying the clicked grid item in big. So simple list->detail.
Doing all with IB, I ctrl-dragged from collectionView cell item to B and selected 'show (e.g Push)' segue.
Now when I run the app and click multiple times on image in grid and then on '< Back' button, I explore the memory graph.
I can see 10 'B' view controllers if I did the navigation 10 times.
That causes a lot of memory to be used and it grows every time.
I found a few posts speaking about unwind, and pop to root vc, but all are dealing with programmatic navigation. Here's just the case of simple storyboard done all with IB.
Expected: A->B->A. Memory: A
Reality: A->B->A. Memory: A, B
How can I avoid retaining the memory for those vc that are dismissed?
in A I have:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "dreamDetail" {
let newViewController = segue.destination as! DreamDetailViewController
newViewController.dream = allDreams?[(collectionView.indexPathsForSelectedItems?.first?.item)!]
newViewController.dreams = allDreams
}
}
in B I have:
weak var dream: DreamRealm?{
didSet {
}
}
var dreams: [DreamRealm]?{
didSet {
}
}
DreamRealm is just a Realm model.
class DreamRealm: Object {
#objc dynamic var filename: String? = nil
#objc dynamic var path = ""
To avoid retaining the memory for your VC that are dismissed, you need to check if there is any retain cycle.
One step to help you to check if your VCs are correctly deinitialised, you can implement a method called deinit. It's a method called when your VC is deinit and no longer in the memory. You can print a message to see if it's the case or not.
If it's not the case, you probably have a strong reference somewhere in your code. You need to avoid it by weakening your reference with weak keyword or unowned or just delete it if you don't need it.
All IB connections with UI elements(in view controller) must be weak!
Try to release strong objects in dealloc method;
Sometimes there are some delay before garbage collector deallocate objects, do "show" and "back", wait ~10 seconds and see if more memory are released.
As an addition to #Arrabidas92's answer:
watch out for double nested blocks
I built a retain cycle with the following code:
navigationItem.reactive.rightBarButtonItems <~ user.producer.map{ $0
.map{ [weak self] user in
guard let self = self else { return [] }
I thought, that the [weak self] of the inner map would suffice. It does not. The outer map already captures self (to pass it to the inner?). My retain cycle went away after writing user.producer.map{ [weak self] $0.

Delete unused ViewController from memory

I have 3 ViewController.
The first ViewController is checking if the user is logged in.
If yes performSegue to the mainVC and if no performSegue to loginVC.
When I am in loginVC, I log in and performSegue to mainVC.
What I want now is, I want to have all ViewControllers which are unused being "deleted", to save memory.
How is that going to work?
I found here in StackOverflow this piece of code:
class ManualSegue: UIStoryboardSegue {
override func perform() {
sourceViewController.presentViewController(destinationViewController, animated: true) {
self.sourceViewController.navigationController?.popToRootViewControllerAnimated(false)
UIApplication.sharedApplication().delegate?.window??.rootViewController = self.destinationViewController
}
}
}
Is that going to do what I want? It seems like yes because this method is popping the ViewController.
I am using "Show Detail" - segues only, except when using this method I created a custom Segue Segue.
Deletion should be handled by Apple, you (theoretically) shouldn't have to worry about it, so long as you don't create any retain cycles. As a rule, just don't have any strong references to self in blocks. Funny enough, the code you have above, that should dismiss the ViewController (and therefore delete it) also has a retain cycle. Adding [weak self] and strongSelf casts as needed should help:
override func perform() {
sourceViewController.presentViewController(destinationViewController, animated: true) { [weak self] in
guard let strongSelf = self else { return }
strongSelf.sourceViewController.navigationController?.popToRootViewControllerAnimated(false)
UIApplication.sharedApplication().delegate?.window??.rootViewController = strongSelf.destinationViewController
}
}
Memory question
Yes, that is how it works. You do not need to take care of freeing view controllers.
The system will keep track of references to view controller objects. When you do not have references to these anymore then the memory is deallocated. You can read about this more in swift language documentation:
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html
What the code is doing
presentViewController method is showing a view controller modally. The completion closure is performed after presenting the new view controller finishes. Inside closer 2 things happen
popToRootViewControllerRemoves all view controllers inside the sourceViewController object.
rootViewController of the window is set to new value.
This practically changes the root view controller to another one. This seems like a valid action after successful login.
I do not know if step 1 is necessary. That navigation view controller is going to go away anyway so why to pop view controllers inside it?
More about view controllers
You might be also interested in view controller life cycle. UIKit developer documentation contains in-depth details about view controllers:
https://developer.apple.com/documentation/uikit/uiviewcontroller

Progress bar crashes when updated from swift file that is not it's viewController

I have a file in Swift that holds all my queries. And when saving a record with saveOperation.perRecordProgressBlock this file call ChatView view controller and updates the progressBarUpdate function.
So far I can get the print within progressBarUpdate to print the progress just fine. But when I get to update progressBarMessage.setProgress(value!, animated: true) the application just crash with the following error: fatal error: unexpectedly found nil while unwrapping an Optional value
If I try to run progressBarMessage.setProgress(value!, animated: true) through viewDidLoad it updates the progress bar fine, no error. Which means the outlet is working just fine.
Other thing to consider, is that my print(".... perRecordProgressBlock - CHAT VIEW\(value)") works just fine. If gets the updates from Queris.swift. It is just the progressBarUpdate that is causing issues.
# my Queries.swift file option 1
saveOperation.perRecordProgressBlock = { (recordID, progress) -> Void in
print("... perRecordProgressBlock \(Float(progress))")
var chatView = ChatView()
chatView.progressBarUpdate(Float(progress))
}
# my Queries.swift file option 2
saveOperation.perRecordProgressBlock = { (recordID, progress) -> Void in
print("... perRecordProgressBlock \(Float(progress))")
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let chatViewController = storyboard.instantiateViewControllerWithIdentifier("ChatViewVC") as! ChatView
chatViewController.progressBarUpdate(Float(progress))
}
# ChatView view controller
func progressBarUpdate(value: Float)
{
print(".... perRecordProgressBlock - CHAT VIEW\(value)")
if (value as? Float) != nil
{
progressBarMessage.setProgress(value, animated: true)
}
}
The way you are instantiating the viewController is not the right way and hence the crash/nil val. viewController loads its view hierarchy only when something sends it a view message. The system will do this by its own when to put the view hierarchy on the screen. And it happens after calls like prepareForSegue:sender: and viewWillAppear: , loadView(), self.view.
So here your outlets are still nil since it is not loaded yet.
Just try to force your viewController to call self.view and then access the functions from that viewController.
var chatView = ChatView()
I'm going to go out on a limb here and say you are using storyboards/xibs. If so, the above would not be the correct way to instantiate a new view controller. Here's some information on the difference (the question refers to Objective-C but the concept is the same in Swift)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let chatViewController = storyboard.instantiateViewControllerWithIdentifier("identifier-you-set-in-storyboard") as! ChatView
Where identifier-you-set-in-storyboard is set in the interface builder (the linked question is old but illustrates the concept, the field label might have changed in newer versions)
If by some off chance you are creating you are setting up your views in code (as opposed to storyboards), you'd need to call chatView.loadView() before chatView.progressBarUpdate.... (Or just try to access the view property and it should call loadView for you.)

Pass data between three viewController, all in navigationController, popToRootView

The issue I'm having is this.
I have a navigation controller with 3 viewController. In the 1st controller, I have the user select an image. This image is passed to 2nd and 3rd controller via prepareForSegue.
At the 3rd controller, I have a button that takes the user back to the 1st view controller. I explored 2 ways in doing this:
1) use performSegue, but I don't like this because it just push the 1st controller to my navigation stack. So I have this weird "Back" button at the 1st Viewcontroller now, which is not what I want. I want the app to take user directly to 1st viewcontroller without the back button.
2) I tried Poptorootviewcontroller. This solves the issue of the "back" button. But, when I pop back to the 1st viewcontroller, the user's selected image is still on screen. I want to clear this image when the user goes from the 3rd viewcontroller back to the 1st viewcontroller.
So with approach 2), how do I make sure all memory is refreshed and the image becomes nil in the 1st viewcontroller? Since I'm not using performSegue, 3rd viewcontroller does not have access to the 1st Viewcontroller.
For refresh, you'd have to clear it in viewWillAppear but I find this rather dangerous. Best you can do there is to create a new copy of the view controller everytime and Swift will take care of the rest. I don't know if you are using the storyboard but I would recommend using the class UIStoryboard and the function instiantiateViewControllerWithIdentifier("something") as! YourCustomVC
As long as you stay in the navigation stack, you'll not lose any of the current configurations of previous View Controllers.
As for passing data back to the first controller. You can either just throw it in the global scope which is the easiest way but might be difficult to know when it was updated or if the data is fresh. But you can always just:
var something: String = ""
class someView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
something = "foo"
}
}
Something will be availabe everywhere then.
You could make a protocol and pass the delegate along the 3 view controllers. So when you are starting it you could do:
func someAction() {
let v = SomeViewController()
v.delegate = self
self.navigationController?.pushViewController(v, animated: true)
}
And then with each following view:
func someOtherAction() {
let v = SomeOtherViewController()
v.delegate = self.delegate
self.navigationController?.pushViewController(v, animated: true)
}
Although personally I find it hard to keep track of this.
Lastly you could use the NSNotificationCenter to pass an object along with all the data and catch it in a function on your first controller.
To do this you first register your VC for the action in viewDidLoad() or something:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "someAction:", name: "someNotification", object: nil)
Then when you are done in the 3rd view make some object or a collection of string and send it back as follows:
NSNotificationCenter.defaultCenter().postNotificationName("someNotification", object: CustomObject())
And then lastly you'll catch it in the function "someAction"
func someAction(note: NSNotification) {
if let object = note.object as? CustomObject {
//Do something with it
}
}
Hope this helps!
Use an unwind segue which provides the functionality to unwind from the 3rd to the 1st (root) view controller.
The unwind segue is tied to an action in the root view controller. Within this action, you simply nil the image:
#IBAction func unwindToRootViewController(sender: UIStoryboardSegue)
{
let sourceViewController = sender.sourceViewController
// Pull any data from the view controller which initiated the unwind segue.
// Nil the selected image
myImageView.image = nil
}
As you can see in the action, segues also let you pass data back from the source view controller. This is a much simpler approach than needing to resort to using delegates, notifications, or global variables.
It also helps keep things encapsulated, as the third view controller should never need to know specifics about a parent view controller, or try to nil any image that belongs to another view controller.
In general, you pass details to a controller, which then acts on it itself, instead of trying to manipulate another controller's internals.

Resources