Can I use a protocol to provide View init methods?

I frequently find myself overriding the two standard view init methods to call super, then call commonInit()

Is there a way to do this with a protocol so that I could just write

class CustomView: SomeKindOfNSViewSubclass, CommonInitView {
 func commonInit() {
  //do my initialisation here
}
}

I'm thinking something like the following

protocol CommmonInitView where Self:NSView {
    func commonInit()
}

extension CommmonInitView {
    init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        commonInit()
    }
    
    init?(coder decoder: NSCoder) {
        super.init(coder: decoder)
        commonInit()
    }
}

but that generates the error 'super' cannot be used outside of class members

thanks in advance

cc @Slava_Pestov should or does the Self: Object constraint in protocols extensions allow super calls?

It should only allow convenience initializer, so self.init, not super.init.

@Lantua - so does that mean it is impossible to replace initialisers with composition rather than inheritance

or more specifically - swift doesn't allow what I'm trying to do?

Yeah, it needs to override init, which makes no sense in protocol and extension.

What I would suggest would be to put the init code somewhere else, like ViewController.viewDidLoad

I know the rule about convenient init but I was wondering about the general super call outside the class scope on statically proved class instances. But I guess this is not possible because we can't write UIView().super to get the super class (UIResponder) but we can do .self pretty much everywhere.

For other functions it does allow super call (and will correctly invoke the super implementation), but init is a little special since it doesn't really follow the flow of normal function; for one, it won't allow you to leave until every class member is initialised.

In fact compiler won't even let you use super.init anywhere else but inside designated initialiser.

@Lantua - I'm just looking for a way to avoid some boilerplate.
I have this code all over the place

class FrameView: NSStackView {

    func commonInit() {
      //init here
    }
    
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        commonInit()
    }
    
    required init?(coder decoder: NSCoder) {
        super.init(coder: decoder)
        commonInit()
    }
    
}

as for it not making sense - when I look at it naively - this would just be a protocol providing a couple of methods.

I understand init is different to ordinary methods - but the reasons why that is necessarily so are way beyond me.

thanks for the input anyway. It's good to know that this is unsupported rather than just something I can't figure out.

How? This statement seems to be incorrect:

protocol Foo {}
extension Foo where Self : UIView {
  func foo() {
    // error: 'super' cannot be used outside of class members
    super.isFirstResponder
  }
}

Sorry, my mind switched back to Class Extension, not Protocol Extension.
Since Swift doesn't allow for protocol to require base class yet, I don't think super will work there.

A small side note, you don't really initialize anything in your commonInit method anymore, it's just a common setup, but everything else at that point of time has already been initialized.

1 Like

Well in Swift 4.2 or maybe in 4.1 this feature slipped by as protocol Foo where Self : MyClass and has been kept but also generalized in Swift 5 to protocol Foo : MyClass. That's why I'm wondering if we should be able to make super calls in an extension where it's proved by the compiler that we're inside a class.

1 Like

@latntua

isn't this requiring that the protocol is a class?

protocol Foo: class {}

extension Foo where Self : NSView {

  func foo() {
    // error: 'super' cannot be used outside of class members*
    super.isFirstResponder
  }

} 

@DevAndArtist re init - I guess I use the term more generically than may be technically correct...

2 Likes

: class constraint is deprecated but not emitting any warning yet, the new way to express it is : AnyObject. However in my example it's an unnecessary constraint because in the extension it's already proven that we're inside a class.


Anyway, I'm just trying to find out if it's just a non-lifted limitation or if there are other technical issues which prevent this from working. ;)

FWIW, most of the time you could safely add setup code in awakeFromNib. Especially since then, AND ONLY THEN, is it guaranteed that all the Interface Builder linkages are properly initialized.

There are two issues that keep this from working:

  • Before you call super.init, you must initialize all stored properties, but a protocol doesn't know what the conforming type's stored properties are going to be. So protocol extensions can only add convenience initializers.

  • Even if there were some way around that, protocols cannot provide @objc implementations, since the ObjC runtime finds methods differently than the Swift runtime.

So I'm afraid commonInit() is the best you can do without some significant (undesigned) changes to how Swift works.

1 Like

@Lantua - I create a lot of views manually, so that requires init(frame - which in turns requires the required initialiser init(coder

at that point - it seems like bad form not to have both paths running through a common setup function.

it's a shame I can't compose that boilerplate away - but I see the problems it would face.

What's the problem with awakeFromNib?
It's the common path that both inits will pass through, so you can remove init altogether.
If you're initializing variables, you could make them var name: Type! instead of let name: Type.

Programmatically created views as @ConfusedVorlon just mentioned don't get a call on awakeFromNib if they are not indeed created from a xib / storyboard. Furthermore I highly discourage you from writing Type! if it's not an outlet.

1 Like