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.
Related
I have two Views:
UITableViewController (View A)
UIViewController (View B)
I was wondering, if it's possible to load and setup the table from View B and then segue to View A, when the loading is done. I need this, since the Table View loads Data from Core Data and that takes some time; I would then show a Loading Animation or something. I have a function called loadData() in View A, which fetches all Elements from Core Data and then calls tableView.reloadData().
Does anyone know, how I could implement this? Or should I somehow show the loading View directly from View A with a SubView or something?
Remember to not think about the specifics but instead, think generally:
You want to move from one VC to another and you have some data that needs to be fetched asynchronically. Let's assume you can't know how long it will take.
My suggestion is to contain all data fetching related to a VC inside that VC itself (or services/facades related to it). So basically you should present the UITableViewController and then have it fetch the data while showing skeleton-cells/spinner/etc.
You want to have separation of concerns which means you don't want your ViewController to handle data related to another view controller.
Think about the following use-case: if you have code to fetch data in the previous VC, before presenting the TVC, what happens when you need to re-fetch the data or refresh something? You will have to duplicate the code in both the VC and the TVC.
That's why it's suggested to keep data fetching inside the view controller that needs it.
If, for some reason, you still want to have your answer for this specific question:
You can have the initial VC create the TVC, but not present it yet, call its methods to fetch the data, and have it send a callback (closure/delegate/etc) when it's done fetching. When the fetching is done, present the TVC.
Here is a quick example:
class MyTableVC: UITableViewController {
private var myData: [Int] = []
public func fetchData(completion: () -> Void) {
//Fetch data asyncly
myData = [1, 2 ,3]
completion()
}
}
class MyVC: ViewController {
private func loadTableVC() {
let tableVC = MyTableVC()
tableVC.fetchData { [weak self] in
self?.present(tableVC, animated: true, completion: nil)
}
}
}
Again, I wouldn't use this due to having tight coupling between the 2 view controllers, but it's always up to you to decide how to design your code.
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.
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
I've recently started using coordinators (Example: MVVM with Coordinators and RxSwift) to improve my current MVVM architecture. It's a nice solution to remove navigation related code from the UIViewController.
But I'm having trouble with 1 specific scenario. The issue arrises when a UIViewController is popped by the default back button or edge-swipe gesture.
Quick example using a list-detail interface:
A list UIViewController is shown by a ListCoordinator inside a UINavigationController. When an item is tapped, the ListCoordinator creates a DetailCoordinator, registers it as a child coordinator and starts it. The DetailCoordinator pushes the detail UIViewController onto the UINavigationController, just like every MVVM-C blog post illustrates.
What every MVVM-C blog post fails to illustrate is what happens when the detail UIViewController is popped by the default back button or edge-swipe gesture.
The DetailCoordinator should be responsible for popping the detail UIViewController, but a) it doesn't know the back button was tapped and b) the pop happens automatically. Also, the ListCoordinator wasn't able to remove the DetailCoordinator from its child coordinators.
One solution would be to use custom back buttons, which signal the tap and pass it on to the DetailCoordinator. Another one is probably using UINavigationControllerDelegate.
How have others solved this issue? I'm sure I'm not the first one.
I use Action for communication between coordinators and also between coordinator and a view controller.
AuthCoordinator
final class AuthCoordinator: Coordinator {
func startLogin(viewModel: LoginViewModel) {
let loginCoordinator = LoginCoordinator(navigationController: navigationController)
loginCoordinator.start(viewModel: viewModel)
viewModel.coordinator = loginCoordinator
// This is where a child coordinator removed
loginCoordinator.stopAction = CocoaAction { [unowned self] _ in
if let index = self.childCoordinators.index(where: { type(of: $0) == LoginCoordinator.self }) {
self.childCoordinators.remove(at: index)
}
return .empty()
}
}
}
LoginCoordinator
final class LoginCoordinator: Coordinator {
var stopAction: CocoaAction?
func start(viewModel: LoginViewModel) {
let loginViewController = UIStoryboard.auth.instantiate(LoginViewController.self)
loginViewController.setViewModel(viewModel: viewModel)
navigationController?.pushViewController(loginViewController, animated: true)
loginViewController.popAction = CocoaAction { [unowned self] _ in
self.stopAction?.execute(Void())
return .empty()
}
}
}
LoginViewController
class LoginViewController: UIViewController {
var popAction: CocoaAction?
override func didMove(toParentViewController parent: UIViewController?) {
super.didMove(toParentViewController: parent)
if parent == nil { // parent is `nil` when the vc is popped
popAction?.execute(Void())
}
}
}
So the LoginViewController executes the action when it's popped. It's coordinator LoginCoordinator is aware that the view is popped. It triggers another action from its parent coordinator AuthCoordinator. The parent coordinator AuthCoordinator removes its child LoginCoordinator from the childControllers array/set.
BTW, why do you need to keep the child coordinators in the array and then thinking about how to remove them. I tried another approach, the child coordinator retained by a view model, once the view model deallocated, the coordinator deallocates too. Worked for me.
But I'm personally don't like so many connections and thinking about a simpler approach using a single coordinator object for everything.
I wonder if you have already solved your architecture problem and if you'd like to share your solution. I asked something related to your problem here and Daniel T. suggested to subscribe to navigationController.rx.willShow: you get back events whenever a ViewController is popped OR pushed onto the view controller stack, so you need to check yourself what kind of event it is (a pop or a push). I think the viewModel / viewController shouldn't know anything of the next story to present, so I think the viewModel could emit an event ("show detail of table cell #n") and a coordinator should push or pop the right scene ("detail of cell #n"). This kind of architecture is too advanced for me to write, so I end up with a lot of circular references / memory leaks.
Unless I am missing something, you can solve this by using this piece of code in your coordinate method. I am specifically using didShow instead of willShow (which was suggested in another answer) for the possibility of edge swipe gestures.
if let topViewController = navigationController?.topViewController {
navigationController?.rx
.didShow
.filter { $0.viewController == topViewController }
.first()
.subscribe(onSuccess: { [weak self] _ in
// remove child coordinator
})
.disposed(by: disposeBag)
}
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.