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()
Related
I'm running into this error on a viewController and not sure why it's happening. The controller is currently set up like this:
class ContainerViewController: UIViewController {
init(sideMenu: UIViewController, center: UIViewController) {
menuViewController = sideMenu
centerViewController = center
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)// This is where the error happens
}
}
Any clue why this may be happening?
The error is happening in the second initializer because, well, the property isn't initialized. All of your properties that aren't optional and don't have default values have to be initialized separately in each initializer, because only that particular initializer actually runs (unless it explicitly calls a different one).
If you're using the coder initializer, you'll need to assign it a value in there or make it an optional. If you aren't actually implementing that initializer, leave it with the fatalError default, since if that ever runs it means something went horribly wrong anyway.
Even after reading Swift's documentation about ARC, I'm still having trouble understanding why a property was set to nil even when it's instantiated within a function. I have the following sample code:
import UIKit
class ItemListViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView()
}
}
And here is the sample code for the test run:
import XCTest
#testable import PassionProject
class ItemListViewControllerTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func tests_TableViewIsNotNil_AfterViewDidLoad(){
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "ItemListViewController")
let sut = viewController as! ItemListViewController
_ = sut.view
XCTAssertNotNil(sut.tableView)
}
func tests_loadingView_SetsUITableViewDataSource(){
}
}
Inside the function that is being tested, we've created the correct instances and I understand that calling the view property fires the viewDidLoad() method. I'm not sure why the test states that the tableView property is nil when running the test. I'd like to understand ARC from this perspective and realize what is happening under the hood when the test states that the tableView property is nil. We've clearly instantiated the UITableView object inside viewDidLoad(). I understand that this test passes when removing "weak" from the tableView property but I don't understand why. Any explanation would be greatly appreciated to understand this better.
I'm still having trouble understanding why a property was set to nil even when it's instantiated within a function
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView()
}
Then you haven't understood what weak means. It means: this reference has no ability to keep this object alive. Thus, if nothing else keeps this object alive (i.e. retains it), the object will die no matter how it is instantiated.
Your code creates the table view: UITableView(). It assigns the table view to a variable: tableView = UITableView(). But that variable is weak. It has no power to keep this table view alive. And you didn't do anything else that would keep this table view alive. Therefore the table view instantly dies, vanishing in a puff of smoke, and leaving tableView pointing at nothing — and so tableView is reset to nil.
The usual thing, however, is to talk like this:
super.viewDidLoad()
let tv = UITableView(frame:someRect)
self.view.addSubview(tv)
tableView = tv
That makes all the difference in the world. The tableView variable is still weak, and still can't keep the table view alive. But we added the table view as a subview to self.view, and now its superview keeps it alive.
And that is the reason why outlets are often weak. It's because they refer to views that have superviews that already keep them alive, so there is no need for a second reference (the outlet property) to keep them alive as well.
I'm trying to make my App to automatically go back to the main menu after 2 minutes of inactivity. So far I have both parts working, but not together..
The App starts a counter if there's no touch input:
see user4806509's anwer on Detecting when an app is active or inactive through touches in Swift
And from my main viewcontroller I can control the segue I need with code:
func goToMenu()
{
performSegueWithIdentifier("backToMenu", sender: self)
}
I've implemented the code from Rob's answer on How to call performSegueWithIdentifier from xib?
So I've created the following class and protocol:
protocol CustomViewDelegate: class {
func goToMenu()
}
class CustomView: UIView
{
weak var delegate: CustomViewDelegate?
func go() {
delegate?.goToMenu()
}
}
The Function go() gets (successfully) called when the timer runs out. But the delegate?.goToMenu() doesn't work. If I change it to: delegate!.goToMenu(), the App crashes with:
fatal error: unexpectedly found nil while unwrapping an Optional value
My main viewcontroller is a CustomViewDelegate and the viewDidLoad contains:
let myCustomView = NSBundle.mainBundle().loadNibNamed("customView", owner: self, options: nil)[0] as! CustomView
myCustomView.delegate = self
I have the correct xib file, and that part is working.
I can't seem to find the solution to this seemingly easy problem, does anyone have a fix? Or better yet, a more elegant solution to my problem?
Thank you!
edit: SOLUTION:
I've removed all my old code and implemented the NSNotification method:
In the UIApplication:
let CallForUnwindSegue = "nl.timfi.unwind"
func delayedAction()
{
NSNotificationCenter.defaultCenter().postNotificationName(CallForUnwindSegue, object: nil)
}
In the main ViewController:
let CallForUnwindSegue = "nl.timfi.unwind"
func goToMenu(notification: NSNotification)
{
performSegueWithIdentifier("backToMenu", sender: self)
}
deinit
{
NSNotificationCenter.defaultCenter().removeObserver(self)
}
In the viewDidLoad: NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(PeriodViewController.goToMenu), name:CallForUnwindSegue , object: nil)
I believe that your issue is that because your delegate is declared as a weak var, your delegate is getting disposed while you wait for the timer to complete, thus the reference to the optional is lost (and returns nil when you force unwrap it).
try removing the weak keyword from your CustomView class.
Another solution would be to use the notification center to notify your view to call for the segue.
Something along the lines of what I proposed here
I hope this helps.
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
In AppDelagate I call the following in a method
func example() {
ViewController().test()
}
and In my ViewController the method is
func test() {
testView.userInteractionEnabled = true
buttonTapped(UIButton())
restartTimer()
}
but it crashes whenever I call the method, due to a nil error with testView. testView is a view in ViewController and not in AppDelegate, but I don't know how to make it so the method executes like how it would if i called it in the ViewController.
An IBOutlet is nil until the view controller calls viewDidLoad. viewDidLoad did not occur since you tried to instantiate the class directly via the call ViewController().
Thus, testView is nil, as expected. Your AppDelegate should not be responsible for the ViewController logic anyhow.
As you may know by now, your crash is happening due the fact you're accessing a IBOutlet before it get the chance to be initialized. So I'm guessing the testView is declared sort of like this:
#IBOutlet weak var testView: UIView!
Solution A
You can turn it optional instead to avoid this crash:
#IBOutlet weak var testView: UIView?
And change the syntax in your VC to:
testView?.userInteractionEnabled = true
Solution B
If you really need to turn the User Interaction off at this point, you can force the view to load:
let myVc = ViewController()
_ = myVc.view // this will force the myVc.view to be loaded
myVc.test()
This way your IBOutlets will have the chance to initialize before you run the test() method.