Search code examples
swiftxcodeios14uicontrol

Build error: Must call a designated initializer of the superclass 'UIControl'


I want to use one initializer or the other depending on the iOS version. If it is not iOS 14, I want to set the action to use later. My call to the super constructor happens inside an if/else:

class Control: UIControl {

    var action: (() -> Void)?

    init(action: @escaping () -> Void) {
        self.action = action
        if #available(iOS 14.0, *) {
            let primaryAction = UIAction(handler: { _ in action() })
            super.init(frame: .zero, primaryAction: primaryAction)
        } else {
            super.init(frame: .zero)
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

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

The call is:

let controlThatDoesNotWork = Control(action: { print("this doesn not work") })

The build error is

error: must call a designated initializer of the superclass 'UIControl'
    super.init(frame: .zero, primaryAction: primaryAction)
    ^

Any idea how I need to call this convenience initializer so it builds?


Solution

  • Your init(action: @escaping () -> Void) is a designated initializer, and designated initializers are not allowed to call convenience initializers from the base class, they must call another designater initializer.

    This is enforced here:

    Rule 1
        A designated initializer must call a designated initializer from its immediate superclass.

    This the base initializer in discussion:

    /// Initializes the control and adds primaryAction for the UIControlEventPrimaryActionTriggered control event. Subclasses of UIControl may alter or add behaviors around the usage of primaryAction, see subclass documentation of this initializer for additional information.
    @available(iOS 14.0, *)
    public convenience init(frame: CGRect, primaryAction: UIAction?)
    

    So, you need to convert your initializer to a convenience one, and have to call with self instead of super the other intializers (again, due to the initializer rules):

    class Control: UIControl {
    
        var action: (() -> Void)?
    
        convenience init(action: @escaping () -> Void) {
            if #available(iOS 14.0, *) {
                let primaryAction = UIAction(handler: { _ in action() })
                self.init(frame: .zero, primaryAction: primaryAction)
            } else {
                self.init(frame: .zero)
            }
            self.action = action
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
        }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        } 
    }
    

    Alternatively, as adding a new subclass of UIControl will force all your custom classes to derive from that one, you could extend the UIControl with a new convenience initializer:

    extension UIControl {
        // associated object dance to allow "stored" properties
        // in extensions and protocols over NSObject subclasses
        private static var primaryActionKey: UInt8 = 0
        private var primaryAction: (() -> Void)? {
            get {
                objc_getAssociatedObject(self, &Self.primaryActionKey) as? () -> Void
            }
            set {
                objc_setAssociatedObject(self, &Self.primaryActionKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
        
        convenience init(action: @escaping () -> Void) {
            if #available(iOS 14.0, *) {
                let primaryAction = UIAction(handler: { _ in action() })
                self.init(frame: .zero, primaryAction: primaryAction)
            } else {
                self.init(frame: .zero)
                self.primaryAction = action
                addTarget(self, action: #selector(onPrimaryAction), for: .primaryActionTriggered)
            }
        }
        
        @objc private func onPrimaryAction() {
            primaryAction?()
        }
    }