EDIT: Here is whole code example for Xcode 6.4
I have simple iOS application without storyboards. I set rootViewController for UIWindow in AppDelegate.swift like this:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let tabBarController = TabBarController()
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.rootViewController = tabBarController
window?.makeKeyAndVisible()
return true
}
TabBarController class implementation is as follows:
class TabBarController: UITabBarController {
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
// Next line is called after 'viewDidLoad' method
println("init(nibName: bundle:)")
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
println("viewDidLoad")
}
}
When I run application the console output looks like this:
viewDidLoad
init(nibName: bundle:)
It means that lines after line super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) are called after viewDidLoad method! This occurs only for classes that inherits from UITabBarController. If you try this same example with UIViewController descendant, everything is ok and viewDidLoad is called after init method is executed.
You are not guaranteed to have viewDidLoad to be called only after the init method is done. viewDidLoad gets called when a view-controller needs to load its view hierarchy.
Internally, TabBarController's init method (by calling super.init) is doing something which is causing the view to load.
This applies to all view-controllers. For example: if you create a UIViewController subclass and do anything with its view property on init, like adding a subview, or even just setting the backgroundColor property of the view - you will notice the same behavior.
From: http://www.andrewmonshizadeh.com/2015/02/23/uitabbarcontroller-is-different/
This should come as no surprise, but apparently UITabBarController has a different behavior than most view controllers. The life cycle may overall be the “same” between it and other view controllers, but the order it executes is not.
That is, when you create a subclass of UITabBarController and provide your own custom initializer, you will notice that the viewDidLoad method is called in an unexpected way. That is, as soon as you call [super init] (or other initializer on UITabBarController), it will call loadView during that initialization which will then lead to your viewDidLoad being called. This is likely not what you would expect because most (all?) other UIViewController subclasses do not instantiate their view during the initialization process. As such, if you provided a custom initializer and expected to do some setup before the view is loaded, and then once the view is loaded add your contained view controllers, this will break your logic.
The solution is to actually move your setup code out of the standard viewDidLoad method and into a special setup method that is called at the end of your custom initializer. This strikes me as a “code smell” that Apple should have never let through. Likely though, this is because the UITabBarController needs to add a UITabBar to the UIViewController’s view which requires that the view exist. Which is what fires loadView.
It seems that init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) for a UITabBarController calls viewDidLoad for some reason. If you set a breakpoint on your print("viewDidLoad") line you will see that the call is made as part of the initialisation sequence.
If you change your view controller to subclass UIViewController you will see that viewDidLoad is not called as part of the initialisation sequence, but rather as a result of calling makeKeyAndVisible
I don't know why Apple coded it this way, but I suspect it is to give the tab bar controller an opportunity to set things up before the content view controllers are loaded.
Regardless, it is just something you are going to have to deal with if you want to subclass UITabBarController
Related
I am programmatically creating a split view controller using the following code when a table view cell is touched:
let rootViewController: UIViewController = RootTableViewController()
let navVC: UINavigationController = UINavigationController(rootViewController: rootViewController)
let detailViewController: UIViewController = DetailTableViewController()
let splitVC: UISplitViewController = UISplitViewController()
splitVC.viewControllers = [navVC, detailViewController]
self.present(splitVC, animated: true, completion: nil)
but when I tap the tableViewCell I get the error: 'fatal error: unexpectedly found nil while unwrapping an Optional value' which appears to be linked to a UITextField (and all UI Elements) on the RootTableViewController. The first failure is in the viewDidLoad of RootViewController after executing the above code when a value is passed to a ui element
Where exactly does this happen? Wheres the error originated? I had a similar issue where I tried to access IBOutlets before they were created by the system which caused my app to crash.
Specifically I had a UI update function which was called after setting a property of the ViewController.
I got around this by checking for nil in the update function and since it was called before viewDidLoad was called the first time, I called the update function in viewDidLoad manually to make sure that when it shows for the first time, everything is correctly updated.
UPDATE
I think I have an idea of whats going on. You created a Storyboard, setup your UI and chose your ViewControllers as the classes of the ViewControllers in the Storyboard.
If you now want to use the ViewControllers, you have to instantiate them via the Storyboard rather than manually initializing them (something like this):
let storyboard = UIStoryboard(name: "MyStoryboardName", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "someViewController")
You also could use Segues instead of instantiating something at all. Build your complete UI using the Storyboard and then use a Segue to present the SplitViewController.
The last method I can think of is, that if you want to instantiate the ViewControllers manually and still make use of the Storyboard or a nib, you have to do some custom initialization in in the init functions of your ViewControllers (this code is from a custom view I have in a separate .xib):
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initializeSubviews()
}
override init(frame: CGRect) {
super.init(frame: frame)
initializeSubviews()
}
func initializeSubviews() {
UINib(nibName: "DatePickerKeyboard", bundle: nil).instantiate(withOwner: self, options: nil)
addSubview(view)
view.frame = self.bounds
}
I am working on a UISplitViewController subclass that has a UINavigationController as master viewcontroller (the reason for this is that I need the navigation bar). The UINavigationController has a FormViewController subclass as rootViewController. In the form I am using a PushRow to show a list of selectable options. The problem is that when I tap the PushRow in a regular-width environment the SelectorViewController is pushed in the master navigation stack. I would like to see the SelectorViewController showed as a UISplitViewController detail instead.
I created a DummyNavigationController that looks like this:
class DummyNavigationController: UINavigationController {
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func targetViewController(forAction action: Selector, sender: Any?) -> UIViewController? {
if let parent = parent as? UISplitViewController {
return parent.isCollapsed ? self : parent
}
return self
}
}
Using this as navigation controller for the FormViewController I am able to see the SelectorViewController on the detail but I don't like this approach.
Is there anything else that I can do?
I know this is not a very specific answer but I lack the reputation to comment.
You can embed only one side of the SplitViewController in a UINavigationController instead of the whole SplitViewController, this way when you segue to the SelectorViewController it will only show it on one side of the SplitViewController.
I have a weak instance variable holding onto a view controller in the UINavigationController.viewControllers stack.
My variable is automatically getting turned to nil, but the view controller hasn't been deallocated (since UINavigationController owns it).
Why is my weak reference getting zeroed?
class NavController: SuperNavigationController
{
weak var weakViewController: UIViewController?
required override init() {
let rootViewController: UIViewController
if (/* whatever */) {
rootViewController = ViewController1(/*whatever*/)
weakViewController = rootViewController
} else {
/* whatever */
}
/*** `weakViewController` is not `nil` at this point ***/
/***
*** This superclass function just does:
*** super.init(navBarClass:toolbarClass:)
*** viewControllers = [rootViewController]
***/
super.init(rootViewController: rootViewController)
}
// Without this, I get an "unimplemented initializer" exception
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
...
}
But as soon as I get to viewDidLoad, weakViewController is nil, even though self.viewControllers.first is still the exact same object I had when initializing.
Is there something weird about the way UINavigationController owns its viewControllers?
EDIT:
I managed to identify and fix the cause at a shallow level (see my answer below), but I'd still like to know why this happens. I'll happily accept and upvote an answer which can explain what's going on!
A weak reference says that if nothing else is pointing at this, I don't need it. So if the owner is the only one who has a ref and that is weak, arc is free to deallocate it.
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html
Calling super.init() was causing the weak subclass instance variables I had set to be zeroed out.
I fixed this by waiting to set weakViewController until after the call to super.init()
I'm trying to add a navigation controller to my UIViewController subclass programmatically (I'm not using storyboards) and I wanted to find the best place to init it and configure such.
I have tried viewDidLoad (the views weren't initialized by init) and a convenience init (just to make sure) method but no luck.
Here's how I'm creating it:
override init!(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
let nav = UINavigationController(rootViewController: self)
}
What would be the most appropriate method to place this logic in? I would like to keep this logic contained within this VC.
I am able to accomplish this by creating the nav. with a root vc from the presenting VC, but this leaks this logic and I'd rather not do that.
I'm not sure this is what you're looking for, but you could put some logic within a method in the controller you're presenting to determine whether it should be presented with or without a navigation controller; doing it this way means the presenting view controller doesn't need to know whether the controller it's presenting is embedded in a navigation controller or not. The presenting controller, would call this method after instantiating your controller. The presented controller could look something like this,
class NextViewController: UIViewController {
var wantsNavigationcontroller = true
func viewControllerWithOrWithoutNavigationController() -> UIViewController {
if wantsNavigationcontroller {
let nav = UINavigationController(rootViewController: self)
return nav
}else{
return self
}
}
}
The presenting controller would do this,
#IBAction func PresentNextcontroller(sender: UIButton) {
var nextVC = NextViewController()
self.presentViewController(nextVC.viewControllerWithOrWithoutNavigationController(), animated: true, completion: nil)
}
I wanted to make a custom container view controller and added some members to the subclass of UIViewController. When I try to init it from the app delegate by using following code:
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window?.rootViewController = CustomContainerViewController()
self.window?.makeKeyAndVisible()
all the members in CustomContainerViewController were initialized twice.
Here is CustomContainerViewController's code:
class CustomContainerViewController: UIViewController {
let tabBar = CustomTabBar()
override init() {
super.init()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
super.init(nibName: nibNameOrNil?, bundle: nibBundleOrNil?)
}
}
Here is CustomTabBar's code:
class CustomTabBar: UIView {
override init(){
println("init")
super.init()
}
override init(frame: CGRect) {
println("initWithFrame:")
super.init(frame: frame)
}
required init(coder aDecoder: NSCoder) {
println("initWithCoder:")
super.init(coder: aDecoder)
}
}
Whenever you init the CustomContainerViewController from the app delegate by using the code previously mentioned, is always prints "init", "initWithFrame" twice.
Incorrect designated initializer used.
UIViewController has only one designated initializer init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?).
As its comment says
The designated initializer. If you subclass UIViewController, you must call the super implementation of this method, even if you aren't using a NIB. (As a convenience, the default init method will do this for you, and specify nil for both of this methods arguments.) In the specified NIB, the File's Owner proxy should have its class set to your view controller subclass, with the view outlet connected to the main view. If you invoke this method with a nil nib name, then this class' -loadView method will attempt to load a NIB whose name is the same as your view controller's class. If no such NIB in fact exists then you must either call -setView: before -view is invoked, or override the -loadView method to set up your views programatically.
So whenever you override the init() method of UIViewController, once you call super, UIViewController's implementation would call init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) on behalf of you. So all the members in your UIViewController's subclass were initialized twice.
To solve this problem, use following code in the app delegate
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window?.rootViewController = CustomContainerViewController(nibName: nil, bundle: nil)
self.window?.makeKeyAndVisible()
And never call the init() method of UIViewController or override this method in subclasses.