Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convenience initialization of UINavigationController subclass makes subclass constant init twice

I have subclasses of UINavigationController and UITableViewController.

To initialize subclasses I decided to use some convenience init methods that call some designated initializer of the superclass. Also, each subclass has some let constant:

let someValue: SomeClass = SomeClass()

Each class is successfully initialized by calling its newly created convenience init method.

The problem is that the let constant is initialized TWICE in UINavigationController subclass.

import UIKit
import PlaygroundSupport

final class Navigation: UINavigationController {
    convenience init(anyObject: Any) {
        self.init(rootViewController: UIViewController())
    }
    let service = Constant("Constant Initialization -> Navigation")
}

final class Table: UITableViewController {
    convenience init(anyObject: Any) {
        self.init(style: .plain)
    }
    let service = Constant("Constant Initialization -> Table")
}

class Constant: NSObject {
    init(_ string: String) {
        super.init()
        debugPrint(string)
    }
}

Navigation(anyObject: NSNull())
Table(anyObject: NSNull())

Are we allowed to use convenience init like above? Why?

Why is the convenience init behavior is different in these two cases?

Checked with: Version 8.2 beta (8C30a), Version 8.2 (8C38), Version 8.2.1 (8C1002)

P.S. Playground log of the code above:

"Constant Initialization -> Navigation"
"Constant Initialization -> Navigation"
"Constant Initialization -> Table"
like image 460
iWheelBuy Avatar asked Dec 21 '16 12:12

iWheelBuy


1 Answers

So this seems to be weirdly "expected behavior" with Swift. The reason it's happening is due to the constant initialized property service. Namely, this post summarizes the issue your seeing pretty well: blog post

Essentially, the Obj-C underlying super classes are leaking memory and your service property is initialized twice because of this pattern of Obj-C initialization:

- (id)init {
  self = [super init];
  if (self) {
    self = [[self.class alloc] initWithNibName:nil bundle:nil];
  }
  return self;
}

A simple solution to avoid this is the following pattern:

import UIKit

final class NavigationController: UINavigationController {
    var service: ObjectTest?

    convenience init() {
        self.init(rootViewController: UIViewController())
        self.service = ObjectTest("init nav")
    }
}
class ObjectTest: NSObject{
    init(_ string: String) {
        super.init()
        print(string)
    }
}

All that's changing from your implementation is initializing the service only after your class itself is initialized.

Another solution, though, is to use the designated initializer for the superclass your initializing. Meaning that you don't use a convenience initializer which would employ the above Obj-C initialization pattern.

like image 146
BHendricks Avatar answered Oct 17 '22 03:10

BHendricks