0

I have UIViewController subclass. I have a constant that I wanted to lazy load. To do this I used a function to set it's default value. I have the two init methods in here because I was previously using them to set the value. Then I found that I can not call a common method to set the value. It seems that only init methods can set the value of a class constant. When I created the function to set the default value, I found that I have an infinite loop that repeatedly calls my function for goal from the init(nibName, bundle) function. I'm new to swift and not sure what I'm doing wrong.

class ViewController: UIViewController {

  let goal: Goal =
  {
    return Goal()
  }()

  override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?)
  {
    super.init()
  }

  required init(coder decoder: NSCoder)
  {
    super.init()
  }
}
Brian
  • 3,571
  • 7
  • 44
  • 70

1 Answers1

1

When you override a methods you should call super implementation of this method.

required override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
  super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}

required init(coder aDecoder: NSCoder) {
  super.init(coder: aDecoder)
}

The constant let values can be set only in initializers. If you have more than 1 initializer think how you can chain them. It's rely useful to have 1 designated initialiser and many convenience. In this way you would set you variables only in 1 place. Example

class A {

  let age: Int
  let name: String
  let type: String

  init(age: Int, name: String, type: String) {
    self.age = age
    self.type = type
    self.name = name
  }

  convenience init() {
    self.init(age: 0, name: "", type: "")
  }
  convenience init(age: Int) {
    self.init(age: age, name: "", type: "")
  }
  convenience init(age: Int, name: String) {
    self.init(age: age, name: name, type: "")
  }

In swift you have to fully initialize class in init method. It means you have to set values for all your properties and call super init, if it exist. After that you can access self and call methods.

In the Cocoa Framework classes have more then 1 way to instantiate them and have many initialiser. Example UIViewController has
init(nibName: String?, bundle: NSBundle?) and init(coder :NSCoder) How we would initialize this class ?

  1. Set Variables value

This solution has 1 huge disadvantage - It's not DRY
You have the same code in 2 places. If you need to change it or fix it, you need to do the same in 2 places. Really bad architecture and code smell.

var name: String

  required init(coder aDecoder: NSCoder) {
    name = "Name"
    super.init(coder: aDecoder)    
  }

  override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
    name = "Name"
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  }

Can we do better? The solution looks obvious - Extract name initialisation in separate method. But we can't do that because of Swift class safety rule
Initialization Steps

  1. Set all you properties
  2. Call super.init(...)
  3. Now both your class and super class has been fully initialized. Only here you can access self and call methods on self

Code:

 required init(coder aDecoder: NSCoder) {
    setDefaultName() // Error. Can't access self yet
    super.init(coder: aDecoder)
  }

 func setDefaultName() {
    name = "Name"
  }
  1. Default Value

Set default value for variable.
This looks better because we initialize property only in one place.

 var name: String = ""

  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }

  override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  }

If you need more complicated logic to initialize your variable you can use closure for that.
I don't like that because now you have logic (a closure in the same as function) in you class variable declaration.
Can we do better? Yes we can!

 var name: String = {
    var name = "Name"
    // ....
    // Other complicated logic
    name + " Is Best"
    return name
  }()
  1. Factory Methods

It would be better to take that closure and move it outside class variable definition,
so we can do something like that var name = Factory.defaultName()

Swift support inner classes. You can create a class inside a class. This way you can group some functionality inside a class. This sound perfect to group initialisers methods into Factory class
Final Example:

var name: String = Factory.defaultName()

  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }

  override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?) {
    super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  }


  class Factory {
    class func defaultName () -> String {
      return "Name"
    }
  }
m.s.
  • 16,063
  • 7
  • 53
  • 88
Kostiantyn Koval
  • 8,407
  • 1
  • 45
  • 58
  • Thanks. I didn't notice that I wasn't calling the correct init in each case. When I am extending UIViewController, is there a way that I could have a method that does common initialization stuff for my subclass? I understand what you are saying about the convenience inits, but I don't think I can do that when subclassing someone else's implementation since Xcode and Storyboards will dictate which initializer will be called. – Brian Oct 20 '14 at 02:48
  • @Brian I've added more explanation to my solution. Please have a look on it – Kostiantyn Koval Oct 20 '14 at 06:21
  • Thanks for the detailed explanation. I miss the use of common init methods from Objective-C. It would be nice to have a method that we could denote that could set common functionality. – Brian Oct 20 '14 at 12:11