Mobile Dan
Mobile Dan

Reputation: 7114

How to Hide Storyboard and Nib Specific Initializers in UI Subclasses

Question

In cases where I know init?(coder:) and other storyboard/nib specific initializers will not be called, can I avoid the requirement to implement or call them in UI subclasses?

 

Background

Many UIKit classes including UIViewController, UIView and subclasses of UIControl (UIButton, UITextField, etc) adopt the NSCoding protocol. The NSCoding init?(coder:) method is used to instantiate these classes from a storyboard or nib.

NSCoding protocol:

public protocol NSCoding {
   func encode(with aCoder: NSCoder)
   init?(coder aDecoder: NSCoder)
}

Classes that adopt a protocol with an initializer must implement that initializer as required. Required initializers must be implemented by all subclasses.

I often build iOS apps without storyboards or nibs. I implement UIViewController, UIView and UIControl subclasses completely in code. Yet, I must implement init?(coder:) in subclasses to appease the compiler if I want to provide my own init methods (which I often do). The following examples illustrate this.

The following does not compile

class CustomView: UIView {
    init() {
        super.init(frame: CGRect.zero)
    }
}

// Error:'required' initializer 'init(coder:)' must be provided by subclass of 'UIView'

The following does compile because I provided an implementation of init?(coder:). For code-only UI subclasses, I typically implement 'init(coder:)' by throwing a fatal error to assert that I don't expect it to be called.

class CustomView: UIView {
    init() {
        super.init(frame: CGRect.zero)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("NSCoding not supported")
    }
}

Subclasses of CustomView also need to implement 'init(coder:)' for reasons stated above.

class SubClassOfCustomView: CustomView {
    let someProperty: Int

    init(someProperty: Int) {
        self.someProperty = someProperty
        super.init()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("NSCoding not supported")
    }
}

Upvotes: 1

Views: 655

Answers (1)

Mobile Dan
Mobile Dan

Reputation: 7114

UI Subclasses & @available(*, unavailable)

*The code below was written and tested in Swift 3

The crux of this solution is creating base subclasses that your custom UI subclasses inherit from. In the examples below, these subclasses are named BaseViewController, BaseView and BaseButton. These subclasses include an initializer that defaults arguments of the superclass's designated initializer that are hidden from their subclasses.

init(coder:) must be implemented in all subclasses since it is a required initializer of the UI superclasses. You can get around this by placing the attribute @available(*, unavailable) above an implementation of that initializer.

The available attribute is used "to indicate the declaration’s lifecycle relative to certain platforms and operating system versions". Using this attribute with the following arguments: @available(*, unavailable) makes the block of code that follows unavailable on all versions of all platforms.  

UIViewController

class BaseViewController: UIViewController {

    // This initializer hides init(nibName:bundle:) from subclasses
    init() {
        super.init(nibName: nil, bundle: nil)
    }

    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        fatalError("NSCoding not supported")
    }
}

class CustomViewController: BaseViewController {
    let someProperty: Int

    init(someProperty: Int) {
        self.someProperty = someProperty
        super.init()
    }
}

let customViewController = CustomViewController(someProperty: 1)

UIView

class BaseView: UIView {

    // This initializer hides init(frame:) from subclasses
    init() {
        super.init(frame: CGRect.zero)
    }

    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        fatalError("NSCoding not supported")
    }
}

class CustomView: BaseView {
    let someProperty: Int

    init(someProperty: Int) {
        self.someProperty = someProperty
        super.init()
    }
}

let customView = CustomView(someProperty: 1)

UIButton - UIControl Subclass

This UIButton example illustrates how to subclass UIControl subclasses.

internal class BaseButton: UIButton {

    // This initializer hides init(frame:) from subclasses
    init() {
        super.init(frame: CGRect.zero)
    }

    @available(*, unavailable)
    required init?(coder aDecoder: NSCoder) {
        fatalError("NSCoding not supported")
    }
}

class CustomButton: BaseButton {
    let someProperty: Int

    init(someProperty: Int) {
        self.someProperty = someProperty
        super.init()
    }
}

let customButton = CustomButton(someProperty: 1)

 

Considerations

I don't advise regularly using @available(*, unavailable) to avoid implementing required initializers. It's beneficial in reducing redundant code that will not be called in this case (since you don't plan to use storyboard/nibs). The appearance of @available(*, unavailable) is reduced by using it in the base classes (and having custom subclasses inherit from the base classes) as opposed to in every custom subclass.

I'm aware that this works in Swift 2 and 3. Its possible that future versions of Swift will not allow this. However, I hope the Swift team comes up with a better way to avoid this redundant code in custom UI subclasses.

Out of curiosity, I tried initiating a BaseViewController subclass from a storyboard. I expected the app to crash with a selector not found error but it called the init?(coder) method even though it was hidden from all platforms. This may be because the available attribute does not hide the init?(coder) initializer from Objective C and that is where storyboard instantiation code runs.

UIKit often makes use use of classes and inheritance whereas the Swift community encourages structs and protocol oriented programming. I include the following headers above my base UI class declarations to discourage base UI classes from becoming a dumping ground for global settings and functionality.

/**
 *  This base UIViewController subclass removes the requirement to override init(coder:) and hides init(nibName:bundle:) from subclasses.
 *  It is not intended to create global functionality inherited by all UIViewControllers.
 *  Alternatively, functionality can be added to UIViewController's via composition and/or protocol oriented programming.
 */

/**
 *  This base UIView subclass removes the requirement to override init(coder:) and hides init(frame:) from subclasses.
 *  It is not intended to create global functionality inherited by all UIViews.
 *  Alternatively, functionality can be added to UIView's via composition and/or protocol oriented programming.
 */

Reference: I found the Initialization section of the Swift Language Guide helpful in understanding the rules for Initializers.

Upvotes: 4

Related Questions