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
Related
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.
Let's say I have a view controller that I show using an adaptive popover segue when clicking on a button. Now in some cases, I might want to wrap the destination view controller in (for example) a navigation controller. So, I set myself as the delegate for the popoverPresentationController's delegate, and implement the presentationController:viewControllerForAdaptivePresentationStyle: method.
But I noticed something strange: in some cases, objects were not being deallocated. If, in the previously mentioned method, I wrap the presented viewcontroller in a navigation controller:
func presentationController(controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
return UINavigationController(rootViewController: controller.presentedViewController)
}
On dismiss the navigation controller gets deallocated, but the presented view controller remains allocated.
If, in contrast, I directly show a navigation controller via adaptive popover segue, then on dismiss both the navigation controller and the details controller it contains get deallocated correctly.
For demonstration purposes, please refer to this test project (Swift): https://github.com/djbe/AdaptivePopoverSegue-Test
What we get when dynamically wrapping in a navigation controller (tap the "Popover, nav automatically added" button):
--- Showing details ---
Loaded details view controller (0x7fab31632b70)
Loaded navigation controller (0x7fab32815600)
Deinit navigation controller (0x7fab32815600)
As you can see, the details view controller is never deallocated.
I checked the documentation for presentationController:viewControllerForAdaptivePresentationStyle: but there are no specific mentions of ownership, strong retains, etc...
I tried using Instruments with the Allocations tool, but there are so many retain/releases involved in this (simple) case that I couldn't directly find the problem.
Has anyone ever encountered this issue? Or do you have an idea on how to solve this?
Solution
As mentioned below by #TomSwift, there is a bug due to a circular reference between the controller and the segue. The only way to solve this, and still wrap the destination controller in a navigation controller, is by doing the wrapping in the init method of the segue (custom).
I've updated my sample code on Github to showcase how this would be achieved using the solution as mentioned by #Vasily, but still allow for dynamic wrapping behaviour using protocols, without resorting to hacky workarounds using NSUserDefaults.
Using XCode8 I noted that there is a circular reference between the DetailsViewController and the UIStoryboardSegue. I don't see a way to cleanly break this cycle as it's internal to UIKit. There's seemingly a secondary circular reference involving an NSDictionary ivar "_externalObjectsTableForLoading". You should report this to Apple!
A solution is to not reuse the DetailsViewController that was pre-loaded by the segue. If you manually instantiate it yourself you can bypass this problem. Here's a possible implementation (requires you set the restoration identifier in the storyboard!):
func presentationController(controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
if (wrapInNavigationController) {
let vc = controller.presentedViewController
if let restorationIdentifier = vc.restorationIdentifier {
return NavigationController(rootViewController: vc.storyboard!.instantiateViewControllerWithIdentifier(restorationIdentifier))
}
}
return controller.presentedViewController
}
Solution
You need to create custom UIStoryboardSegue class and override init function.
Sample:
class StoryboardSegue: UIStoryboardSegue {
override init(identifier: String?, source: UIViewController, destination: UIViewController) {
super.init(identifier: identifier, source: source, destination: NavigationController(rootViewController: destination))
}
}
Main.storyboard
result
I have a UIViewController that I have had in a storyboard for a while with no problems. As my application grew, and I was using that view controller in more and more places, I realized that I should probably make it more portable, rather than have so many segues to it from hither and yon across the board. I've done splits like this before, so I did what I figured was logical here. I selected that view controller, cut it, and pasted into an empty .xib file. After changing each call to performSegueWithIdentifier to an init(nibName:bundle:) and presentViewController, I get a crash, with an object found unexpectedly nil in viewDidLoad()...
I set the value of this object after each init(...) call, just before presenting the view controller. The nil object is called from viewDidLoad(). This is a problem. I just set this, and now it's gone?!
I overrode the init(...) method, and found that self in init(nibName:bundle:) doesn't have the same memory address as self in viewDidLoad(). Also strange.
I overrode the other init() methods, and found that, after I call to present my view, my object is being instantiated again via init(coder:)! The self in here happens to be the exact self where my property is found nil!
The only reason I see for init(coder:) to be called at all is that I am loading my view from a .xib, but I thought this was handled in init(nibNamed:bundle:)? According to the docs, I do indeed get a call to init(coder:) if I'm loading from a storyboard, and doesn't touch the former... It also says that the nib isn't loaded until the controller's view is queried. If I understand it correctly, my view shouldn't get queried until I present the view. As the crash happens only when I present it, the issue likely stems from that.
I'm stuck here. I still need to get this contextual information to the view controller before it's presented. I've even tried making a proxy class to do the instantiating and property setting before presentation, but I still can't shake this second instance! I get one from init(nibName:bundle:), and another from init(coder:). Neither gets presented, and the latter gives me a nil object error. Any help at all in understanding why this is, and how I might work around this bug (feature?) would be much appreciated. Thank you!
Update:
On a whim, I decided to paste the view controller back into the storyboard, separate from the main hierarchy, and try instantiating it by its identifier. It worked! Not entirely sure how, but by George it worked! Now my question is this: Why?? What is so terribly evil and taboo about .xibs that Xcode and iOS won't tell me? I'm not a little flummoxed by this behavior. I'll keep trying with the .xib, if only to keep Xcode from yelling at me about entrance points...
I don't know what dark magic Xcode is doing, but here's two helper methods I wrote to easily instantiate any Storyboard VC - you just need the Storyboard name and VC identifier (optionally, otherwise will initial VC). By splitting up my VCs into many different Storyboards, I avoid dealing with xibs while still keeping things simple. One loads it into a nav controller of your choice, the other just returns it by itself:
struct StoryboardHelper {
///instantiates a VC with (optional) identifier viewController from storyboardName, pushes it to hierarcy of navigationController, and runs setup block on it, animated specifies whether the push is animated
internal static func showStoryboard(storyboardName: String, viewController: String?, navigationController: UINavigationController, animated: Bool = true, setup: (UIViewController) -> () ){
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
let destinationVC = viewController != nil ? storyboard.instantiateViewControllerWithIdentifier(viewController!) : storyboard.instantiateInitialViewController()!
setup(destinationVC)
navigationController.pushViewController(destinationVC, animated: animated)
}
///instantiates and returns a VC with (optional) identifier viewController from storyboardName
internal static func instantiateViewControllerFromStoryboard(storyboardName: String, viewController: String?) -> UIViewController{
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
return viewController != nil ? storyboard.instantiateViewControllerWithIdentifier(viewController!) : storyboard.instantiateInitialViewController()!
}
}
I bounce in a quite weird issue. When I push a specific view controller for another one, the former get dismissed soon after being showed. When I push it fom the main View Controller, it stays put without any problems. I put breakpoints and the viewDidDisappear is in fact called just after the viewDidAppear.
By smell it look like the second view controller becomes nil in one way, but how is it possible if that is wired to the storyboard?
Has anyone got any idea about what could be the reason for the weird behavior?
The main view controller and the first view controller are both in Swift, the pushed controller is still in Objective-c.
This is how I open the second view controller:
func didSelectRow(indexPath: NSIndexPath, from owner: DestinationsViewController){
if let currentElement=DestinationsContentProvider.sharedContentProvider().stations[indexPath.row]{
print("a \(indexPath.row) elemento \(currentElement)")
let targetModel = currentElement.model
//NSLog(#"targetMetro:%# targetPaletta=%#", owner.targetMetro, owner.targetPaletta);
if ((targetModel != nil) && (targetModel!.myTraffic != nil)){
targetModel!.segueExecute()
}
}
segueExecute is called on the model that is not dismissed. I put a breakpoint on the dealloc and it is never reached.
The only peculiar issue is that in the model I perform the segue on the main controller instead of the actual controller by this piece of code:
mapController.performSegueWithIdentifier("ShowWaiting", sender:self)
Still the same behavior happens even if I manually push the controller by executing:
let mainStoryboard:UIStoryboard!
if (UIDevice.currentDevice().userInterfaceIdiom == .Pad){
mainStoryboard=UIStoryboard(name:"StoryboardiPad", bundle: nil)
} else {
mainStoryboard=UIStoryboard(name:"MainStoryboard_iPhone", bundle: nil)
}
let controller = mainStoryboard.instantiateViewControllerWithIdentifier("Situation") as! StationSituation
controller.model=targetModel;
InArrivoHDViewController.sharedDetailController().navigationController?.pushViewController(controller, animated: true)
without using the segue construct.
Just check whether second view controller used for pushing is a property or not. If secondVC instance is created within the method in which pushing is done, secondVC will become nil after execution of the method.
I fixed the issue by directly calling performSegue on the view controller rather than delegating it to the root controller. For some reason this delegation works if there is the same kind of view controller on the Navigation queue in which you are pushing the controller: I have this construct in another class and I just checked it actually work. Otherwise the effect is the weird one I experienced.
I think, but I may not be sure, that in Objective-c the situation was different.
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.