Adding Swift Convenience Initializers

Do you ever find yourself wishing that Apple included an extra initializer to a UIKit class? Swift extensions make it easy to add new initializers to types without subclassing. My quick notes on adding a new convenience initializer to a UIKit class using a Swift extension.

Designated or Convenience?

First a recap on the two types of initializer:

A designated initializer is the primary initializer for a class. It must fully initialize all properties introduced by its class before calling a superclass initializer. A class can have more than one designated initializer.

A convenience initializer is a secondary initializer that must call a designated initializer of the same class. It is useful when you want to provide default values or other custom setup. A class does not require convenience initializers.

The Three rules

With that clear you need to remember three rules for designated and convenience initializers for class types:

  • A designated initializer must call a designated initializer from the immediate superclass.
  • A convenience initializer must call another initializer from the same class.
  • A convenience initializer must ultimately call a designated initializer.

What does this mean for us? In simple terms, do not call super from your convenience initializer. Call another initializer (convenience or designated) from the same class.

A Practical Example

Let’s give it a try with a practical example. Creating and configuring an image view from an image in the asset catalog ready for us to use with Auto Layout needs several lines of code:

// Create the image
let heartImage = UIImage(named: "Heart")

// Now create the image view using the image
let heartImageView = UIImageView(image: heartImage)

// Configure the view remembering to disable
// the autoresizing mask if we are using
// Auto Layout
heartImageView.translatesAutoresizingMaskIntoConstraints = false

// Sometime we must also override the default content mode
heartImageView.contentMode = .scaleAspectFit

// Finally add to the view hierarchy
view.addSubview(heartImageView)

This gets boring quickly and I always end up forgetting to disable the translation of the auto resizing mask.

Creating a Convenience Initializer

Let’s create a new UIImageView convenience initializer that performs the configuration for us. Our initializer will take the name of the image in the asset catalog and the content mode. Since we do not always want to set the content mode we will make it default to scale to fill. Here is the code:

extension UIImageView {
  convenience init?(named name: String, contentMode: UIViewContentMode = .scaleToFill) {
    guard let image = UIImage(named: name) else {
      return nil
    }

    self.init(image: image)
    self.contentMode = contentMode
    translatesAutoresizingMaskIntoConstraints = false
  }
}

Some points to note:

  • If the image name parameter is invalid our initializer will fail and should return nil. You define a failable initializer with a question mark after the init keyword (init?).
  • Once we have a valid UIImage we call the designated initializer of UIImageView to create the image view.
  • We can only set properties on the UIImageView object after we have called the designated initializer.

Example Usage

// Default contentMode
let heart = UIImageView(named: "Heart")

// Specifying a contentMode
let star = UIImageView(named: "Star", contentMode: .scaleAspectFit)

Note that the failable initializer means the return type is an optional (UIImageView?) so we would need to unwrap it before adding it to the superview.

Property Access Before Initializing Self

One final note on when you can access self to modify properties. If we try to set a property of UIImageView before we have called self.init(image: UIImage?) we get an error:

// Does not compile
self.contentMode = contentMode
self.init(image: image)

// Use of 'self' in property access 'contentMode'
// before self.init initializes self

If you remember that a designated initializer must fully initialize all properties of its class you can maybe see why this is not allowed. Even if the compiler allowed us to do it the designated initializer would overwrite our value with the default value.

Further Reading

The sections on initialization and extensions in the Swift Programming Language guide have lots more details: