What causes this initializer inheritance issue with UIBarButtonItem?

4k views Asked by At

I'm trying to create a simple Swift subclass of UIBarButtonItem:

class LabelBarButtonItem: UIBarButtonItem {
  let label: UILabel

  init(text: String) {
    self.label = UILabel(frame: CGRectZero)
    self.label.tintColor = UIColor.grayColor()
    super.init(customView: self.label)
    self.label.text = text
  }

  required init(coder aDecoder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
  }
}

but when I try to instantiate this:

let countButton = LabelBarButtonItem(text: "0 items")

the code compiles correctly but fails at runtime with:

fatal error: use of unimplemented initializer 'init()' for class 'TestProject.LabelBarButtonItem'

I can't understand why this happens in this case, or what principle causes this problem in general. The init(text) initializer should delegate directly to init(customView) in the superclass. Why is init() called at all? It shouldn't be involved.

Can anyone explain what's happening? Similar problems have popped up when I've tried to subclass other UIKit classes

2

There are 2 answers

12
matt On BEST ANSWER

The problem is that init(customView:) calls init().

And since you have a required initializer, you no longer inherit init(). Thus, you have to implement it, even if just to call super.init().

You then face the (mild) annoyance of having to initialize all your properties in init() as well as in init(text:). However, in your particular case, there was no need to do that in an initializer in the first place. I'd write your class like this:

class LabelBarButtonItem: UIBarButtonItem {
    let label = UILabel(frame: CGRectZero)
    init(text: String) {
        super.init(customView: self.label)
        self.label.text = text
        self.label.tintColor = UIColor.grayColor()
    }
    private override init() {
        super.init()
    }
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

In your comment, you ask:

But now if I override init(), clients of my class can create an invalid instance of LabelBarButtonItem by just calling LabelBarButtonItem()

Correct. That's why I have marked the init() override as private. Note that this works only if this class is off in a file by itself.

Another possibility is to mark all your initializers (including init(coder:)) as convenience. Now you inherit init() and the whole problem goes away. However, this introduces another set of issues and is left as an exercise for the reader.

Just to clarify, I regard this entire situation as a bug in Swift. It didn't behave like this, IIRC, until quite a recent revision (Xcode 6.1?). You are being hemmed in by the fact that super.init(customView:) calls your init(). You could argue that that's wrong; you have a good use case for a bug report. Also the crash at runtime seems like wrong behavior; if this was going to happen, why didn't the compiler stop us? Perhaps someone else can justify what Swift does in this situation. I'm just trying to explain it.

EDIT This answer is from 2014 and was intended to work around a bug that existed at that time. In modern iOS versions the bug is gone, and you can either say this:

class LabelBarButtonItem: UIBarButtonItem {
    init(text: String) {
        let label = UILabel(frame: .zero)
        label.text = text
        label.sizeToFit()
        super.init()
        self.customView = label
    }
    required init(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

or you can say this:

class LabelBarButtonItem: UIBarButtonItem {
    convenience init(text: String) {
        let label = UILabel(frame: .zero)
        label.text = text
        label.sizeToFit()
        self.init(customView: label)
    }
}

Neither of those was possible at the moment in time when the question was originally asked.

2
Nahuel Roldan On

I had to subclass UIBarButtonItem for a project and following the answer of Matt was more than enough, with one little difference. I had to add convenience to the initializer for the code to work. In my case, I don't have a property in my subclass.

Here's the code working for Swift 3, in case it helps anyone.

class PlayerBarButtonItem: UIBarButtonItem {

convenience init(assetName: String, target: Any, selector: Selector) {

    let barButton = UIButton(frame: CGRect(x: 0, y: 0, width: 34, height: 34))
    let barButtonImage = UIImage(named: assetName)?.withRenderingMode(.alwaysTemplate)
    barButton.setImage(barButtonImage, for: .normal)
    barButton.tintColor = UIColor.white
    barButton.addTarget(target, action: selector, for: .touchUpInside)

    self.init(customView: barButton)
}

private override init() {
    super.init()
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}