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()!
}
}
Related
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
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 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.)
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.
My segue is returning with an error (unwrapping an optional value) from this line > self.navigationController?....
In my code...
let nextScreen = self.storyboard!.instantiateViewControllerWithIdentifier("onboarding-what") as? ViewController
self.navigationController?.pushViewController(nextScreen!, animated: true)
This above code is implemented in my onboarding-start view controller
This is my storyboard set up...
the last view controller has the id of "onboarding-what".
I've read the documentation for the navigation controller but can't seem to find my problem. Does anyone have a solution?
You can use performSegueWithIdentifier rather than pushViewController if you connected them in storyboard
However did you fill "onboarding-what" in the storyboardId field?
pls check this image
Also "onboarding-what" view controller should be instance of ViewController
(There's not default class called "ViewController" only "UIViewController" or "NSViewController" for osX.)
Obviously nextScreen is nil. It is most likely that "onboarding-what" identifier is incorrect