Kenny Winker
Kenny Winker

Reputation: 12087

Swift: property that is a UIView subclass implementing a protocol

Here's the scenario. I have a number of different views I want to show, depending on the model object I'm showing the user. So I've set up a protocol whereby any view that implements it can be presented.

class MyItem { /* some model properties */ }

protocol ItemView: class {
    // some protocol methods (e.g. updateWithItem(), etc)
    func setupItem(item: MyItem)
}

class SpecificItemView: UIView, ItemView { /* there will be multiple classes like this */
    func setupItem(item: MyItem) {
        // do item setup
    }
}

class AnotherItemView: UIView, ItemView {
    func setupItem(item: MyItem) {
        // do DIFFERENT item setup
    }
}

Then when I go to use them in a view controller, I've been given one of my ItemView classes:

class MyViewController: UIViewController {

    var itemView: ItemView? // could be a SpecificItemView or AnotherItemView

    override func viewDidLoad() {
        itemView?.setupItem(MyItem())
        itemView?.removeFromSuperview() /* this line won't compile */
    }
}

Everything works except that last line, where I try to call up to a UIView method (removeFromSuperview). No surprise there, since ItemView has no relation to UIView.

In Objective C I would solve this issue by specifying my itemView ivar like so:

@property (nonatomic, strong) UIView<ItemView> *itemView;

But I can't seem to find a similar syntax for Swift. I suspect there is no way to use this pattern in Swift. How do I achieve my overall goal of interchangeable UIView classes in a Swift-friendly manner?

One hacky solution I've found so far, is to add any UIView methods I call (e.g. removeFromSuperview) to my ItemView protocol.

Another suggestion I got, not on SO, from Maurice Kelly, was to make a UIView subclass, implementing the ItemView protocol, that both SpecificItemView and AnotherItemView can descend from. You can see it implemented in this gist. While that solves the issue of encapsulating class AND protocol in a single type (e.g. var itemView: ItemViewParentClass) it basically makes the protocol pointless, since you're now implementing all the protocol's methods in your parent class and overriding them in your subclasses. The single biggest drawback of this solutions is that you have to cast instances of subclasses (SpecificItemView) to the new hypthetical parent class (ItemViewParentClass) when you refer to them in your View Controller.

Upvotes: 3

Views: 3946

Answers (4)

kieraj
kieraj

Reputation: 51

As of Swift 5, this is possible by constraining your protocol to only be applied to UIViews. To do this, just do the following:

protocol ItemView: UIView {
  func setupItem(item: MyItem)
}

By adding this constraint to your protocol, any object that the swift compiler recognizes as an ItemView will also have all UIView methods available on it.

Similarly, if a non-UIView subclass attempts to implement the ItemView protocol, your code will not compile.

Upvotes: 5

m_katsifarakis
m_katsifarakis

Reputation: 1907

Perhaps the following (in Swift 4) would do the trick?

class MyViewController: UIViewController {
    typealias T = UIView & ItemView

    var itemView: T?
}

Upvotes: 0

David Berry
David Berry

Reputation: 41226

As you're discovering, there's no way to specify both a class and a protocol for an object.

The easiest way to fix the problem is to add removeFromSuperview (and any other UIView methods you need to insure are present) to your protocol.

Upvotes: 1

Aggressor
Aggressor

Reputation: 13551

ItemView does not extend UIView, but your SpecificItemView does. Naturally, no removeFromSuperView exists on your ItemView.

In your ViewController you declare an ItemView type object, rather than a SpecificItemView

Change your MyViewController code to:

var itemView:SpecificItemView?

Upvotes: -1

Related Questions