Extension that makes wrapper that dispenses self typed as self

Viewed 113

Sorry for the weird title. Here's a toy sketch of my code:

extension UIControl {
    func makeHolder() -> ControlHolder {
        ControlHolder(control: self)
    }
}

struct ControlHolder {
    let control : UIControl
    init(control: UIControl) {
        self.control = control
    }
    func retrieve() -> UIControl {
        return self.control
    }
}

I admit this is a toy reduction, and I don't like when people do that, but it illustrates the syntactical issue perfectly, so let's go with it.

Okay, so we have an extension on UIControl that is a method that returns a wrapper object. Now, the problem is this:

let holder = UISwitch().makeHolder()
let output = holder.retrieve()

The result, output, is typed as UIControl, obviously. But that isn't what I want. I want it to be typed as UISwitch, because I started with a UISwitch. OK, so that sounds like a generic. The problem is, I can't figure out how to make that generic.

It's easy, I think, to make ControlHolder a generic:

struct ControlHolder<T:UIControl> {
    let control : T
    init(control: T) {
        self.control = control
    }
    func retrieve() -> T {
        return self.control
    }
}

I'm pretty sure I've got that part right. But then how do I write the extension declaration in such a way as to resolve that generic to the actual type of self, the UIControl on which makeHolder is called?

I tried introducing a generic to the extension, obeying the compiler until I got it to compile:

extension UIControl {
    func makeHolder<T>() -> ControlHolder<T> {
        ControlHolder<T>(control: self as! T)
    }
}

But that's pretty silly, and output is still typed as UIControl.

Obviously I can add another parameter passing the type explicitly into makeHolder and thus resolving it:

extension UIControl {
    func makeHolder<T>(ofType: T.Type) -> ControlHolder<T> {
        ControlHolder<T>(control: self as! T)
    }
}

Now when I call makeHolder I pass in the type:

let holder = UISwitch().makeHolder(ofType: UISwitch.self)
let output = holder.retrieve()

And now of course output is typed as UISwitch. But this is idiotic! I want the extension to just know that the type is UISwitch, because I'm calling makeHolder on a UISwitch.

I feel like I'm coming at this all wrong. Maybe someone can straighten me out? Or am I aiming for something that's just impossible?

1 Answers

The trick to this is to define a protocol, an extension of that protocol, and put the makeHolder method in that extension. That way, you can use Self as the generic type for the returned ControlHolder.

First define a new protocol (let's call it "HoldableControl") and require that conformers must be UIControls. It doesn't need any other requirements because we just care about adding the makeHolder function to an extension.

protocol HoldableControl: UIControl {}

Then, add an extension of HoldableControl and define makeHolder in it, returning ControlHolder<Self>. We are allowed to use Self here because it is allowed in protocol extensions, unlike in an extension to UIControl.

extension HoldableControl {
    func makeHolder() -> ControlHolder<Self> {
        ControlHolder(control: self)
    }
}

Then, we just need to have UIControl conform to this protocol:

extension UIControl: HoldableControl {}

And make your ControlHolder generic, as you've already done:

struct ControlHolder<T: UIControl> {
    let control: T
    
    init(control: T) {
        self.control = control
    }
    
    func retrieve() -> T {
        control
    }
}

And now it will work:

let holder = UISwitch().makeHolder() // type is ControlHolder<UISwitch>
let output = holder.retrieve()       // type is UISwitch
Related