David James
David James

Reputation: 2466

Limit supported methods to type of generic parameter in Swift 4

Given a struct of some generic type, I want to limit which methods may be called to only those which are subtypes of the generic type.

struct ViewBuilder<T:UIView> {

    let parent:UIView

    func createView() -> UIView {
        let view = UIView()
        parent.addSubview(view)
        return view
    }
}

extension ViewBuilder where T:UIButton {

    func createButton() -> UIButton {
        let button = UIButton()
        parent.addSubview(button)
        return button
    }
}

In this code I specify the builder should only create UIControls:

let builder = ViewBuilder<UIControl>(parent:UIView())
let view = builder.createView() // ok
let button = builder.createButton() // 🛑 'UIControl' is not a subtype of 'UIButton'

However, as you can see, I get the opposite behavior; a UIView is not a UIControl; a UIButton is a UIControl. Any way to do this in Swift? (I'm using Swift 4.) I've tried several things using generics and protocols but no luck.

(I understand why the above code works the way it does, but I'm looking for alternative the overall problem if one exists. If anyone can propose a good solution perhaps I can improve the question to fit.)

Upvotes: 1

Views: 730

Answers (1)

Rob Napier
Rob Napier

Reputation: 299345

It is not possible to remove a method from a subtype (T where T:... is a subtype of T). That's a fundamental feature of types. A subtype must be able to do everything a type can do. Otherwise, if a function took a generic ViewBuilder<T>, how would it know whether createView were allowed on it or not? Extensions are extensions. They may not even be visible to consumers of the type.

In principle, this is what you're really asking for:

struct ViewBuilder<T: UIView> {

    let parent: UIView

    func create<U:T>() -> U {
        let view = U()
        parent.addSubview(view)
        return view
    }
}

This provides a create method for any type that is a subtype of T, which must itself be a subtype of UIView. Unfortunately, this is not currently legal Swift (SR-5213). The problem is that a generic type parameter that is constrained by a class is not itself considered a class for the purpose of constraining additional type parameters.

Given this limitation, in most cases I would probably use composition like this:

struct ViewBuilder {
    let parent: UIView

    func create<View: UIView>(_ type: View.Type) -> View {
        let view = View()
        parent.addSubview(view)
        return view
    }
}

struct ControlBuilder {
    private let builder: ViewBuilder
    var parent: UIView { return builder.parent }

    init(parent: UIView) {
        builder = BaseViewBuilder(parent: parent)
    }

    func create<Control: UIControl>(_ type: Control.Type) -> Control {
        return builder.create(type)
    }
}

let builder = ControlBuilder(parent:UIView())
let button = builder.create(UIButton.self)

Here a ControlBuilder HASA ViewBuilder rather than ISA ViewBuilder. This has some limitations if you want to accept "something that can create a kind of view" since it's pretty much impossible (as far as I can tell) to create a protocol that covers both of these because of SR-5213. But given your examples, this looks like it would match your use case.

(I'm kind of suspicious of this entire use case, though. I'm unclear how "a view that can only contain controls" is useful. It feels like what you really want are extensions on UIView. "Builder" feel like an attempt to import Java patterns into Swift that may not fit.)

Upvotes: 1

Related Questions