Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deinit not called on a UIViewController, but Dealloc is

Tags:

It seems like the Swift equivalent of dealloc is deinit. However, when you attempt to define the method on a UIViewController, it doesn't behave as you would expect...

Setup

  1. Create a new Single View project using Xcode 7.0, in either Swift or Objective-C.
  2. Add a "dismiss" button on the view controller that was created with the storyboard (I'll refer to this view controller as VC2; its class is ViewController).
  3. Add a new view controller and set it to be the initial view controller (VC1, class is nil).
  4. Add a "present" button to VC1 with a "Present Modally" segue to VC2.
  5. In VC2's code, put a breakpoint in deinit (Swift) or dealloc (Objective-C).
  6. In VC2, make the "dismiss" button's action do the following:

    // Swift:
    presentingViewController?.dismissViewControllerAnimated(true, completion: nil)
    
    // Objective-C:
    [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
    
  7. Run the app and tap both buttons to first present VC2 and then dismiss it.

Notice how in Objective-C, the dealloc breakpoint is hit.

In Swift, on the other hand, the deinit breakpoint is never hit.

Why is deinit never called? Is this a bug or by design?

If this is by design, where should I put clean up code to free up resources when the view controller will no longer be needed? (It can't be in viewDidUnload since that method is deprecated. It can't be in viewDidDisappear because something else might be holding a reference to it and will eventually show it again.)


Note: If you attempt to define a dealloc method in Swift, you get the following error:

Method 'dealloc()' with Objective-C selector 'dealloc' conflicts with deinitializer with the same Objective-C selector.

If you have the Swift view controller inherit from an Objective-C controller, and you put a breakpoint in the Objective-C dealloc method, you will get the same buggy behavior defined above: the deinit will not be called, but the dealloc will be called.

If you attempt to use Allocations to view the number of instances of the class in memory, both versions show the same thing: The # Persistent is always 1, and the # Transient increases each time you show the second view controller.

Given the above setup, there should be no strong reference cycle holding on to the view controller.

like image 321
Senseful Avatar asked Oct 13 '15 20:10

Senseful


People also ask

Why is Deinit not called?

So the deinit is not called if there is no additional scope.

Which of the following method is called after UIViewController is initialized?

First UIViewController is alloc'ed by some other object, then init is immediately called (or some other init method, like initWithStyle). Only once the object is initialized would I expect it to call its own loadView function, after which the view, once loaded, calls the viewDidLoad delegate method.

How do you call a Deinit in Swift?

class Person{ let name:String; init(name:String){ self.name = name; println("\(name) is being initialized."); } deinit{ println("\(name) is being deInitialized."); } } var person:Person?; person = Person(name:"leo"); person = nil; When initialized, print is ok.

What is Dealloc?

Deallocates the memory occupied by the receiver.


2 Answers

TLDR:

Breakpoints will only work in deinit if there is an executable line of code ahead of them.

  • If you place a breakpoint on a line of executable code, then it will work.
  • The executable line of code must belong to the deinit method.

Thanks to Adam for pointing me in the right direction. I didn't do extensive tests, but it looks like breakpoints behave differently in deinit than everywhere else in your code.

I will show you several examples where I added a breakpoint on each line number. Those that will work (e.g. pause execution or perform their action such as logging a message) will be indicated via the ➤ symbol.

Normally breakpoints are hit liberally, even if a method does nothing:

➤ 1
➤ 2  func doNothing() {
➤ 3 
➤ 4  }
  5

However, in a blank deinit method, NO breakpoints will ever get hit:

  1
  2  deinit {
  3 
  4  }
  5

By adding more lines of code, we can see that it depends on if there is an executable line of code following the breakpoint:

➤ 1 
➤ 2  deinit {
➤ 3      //
➤ 4      doNothing()
➤ 5      //
➤ 6      foo = "abc"
  7      //
  8  }
  9

In particular, play close attention to lines 7 and 8, since this differs significantly from how doNothing() behaved!

If you got used to this behavior of how the breakpoint on line 4 worked in doNothing(), you may incorrectly deduce that your code is not executing if you only had a breakpoint on line 5 (or even 4) in this example:

➤ 1  
➤ 2  deinit {
➤ 3      number++
  4  //    incrementNumber()
  5  }
  6

Note: for breakpoints that pause execution on the same line, they are hit in the order that they were created. To test their order, I set a breakpoint to Log Message and Automatically continue after evaluating actions.

Note: In my testing there was also another potential pitfall that might get you: If you use print("test"), it will pop up the Debug Area to show you the message (the message appears in bold). However, if you add a breakpoint and tell it to Log Message, it will log it in regular text and not pop open the Debug Area. You have to manually open up the Debug Area to see the output.

Note: This was all tested in Xcode 7.1.1

like image 103
Senseful Avatar answered Oct 18 '22 05:10

Senseful


I haven't tried it yet but I did find this for you:

It seems the function won't be called unless some code is put inside the deinit (weird) must be part of swift's optimisation stage.

Try putting a print statement inside your deinit as suggested and report your findings

like image 32
Adam Campbell Avatar answered Oct 18 '22 07:10

Adam Campbell