Use Generic Protocol to filter array of subviews

Hey everyone, how're things going?

In our app, we have a canvas. The canvas could contain Stickers, Images, Texts, etc. We have a protocol CanvasItem that implement the common properties between these items:

public protocol CanvasItem: AnyObject {
var scale: CGFloat { get set }
var rotation: CGFloat { get set }
var alpha: CGFloat { get set }
var zIndex: Int { get }
}

Then each class-model (Stickers, Text etc) conform to CanvasItem, adding class-specific properties:

public class StickerItem: CanvasItem {
public var stickerName: String
}

public class ShapeItem: CanvasItem {
public var shapeColor: UIColor
}

To show those, we first created a base generic (I think) UIView class that can be inited only with CanvasItem:

class ViewClass<T: CanvasItem>: UIView {
let canvasItem: T
init (t: T) {
    self.canvasItem = t
    super .init(frame: .zero)
  }
}

and then for each of the models, we create specific UIView<CanvasItem> class:

class CanvasShapeView: ViewClass<ShapeItem> {
}

class CanvasStickerView: ViewClass<StickerItem> {
}

Then I'm trying to do the following:

let superview = UIView()
let shapeView = CanvasShapeView<ShapeItem>(t: <ShapeItem>)
let stickerView = CanvasStickerView<StickerItem>(t: <StickerItem>)
superview.addSubview(shapeView)
superview.addSubview(stickerView)

for canvasItemView in superview.subviews.compactMap({$0 as ? ViewClass<CanvasItem>}) {
print(canvasItemView.canvasItem) // **access only the common properties**
}

I'm trying to access only to the CanvasItem common properties but it doesn't let me, throwing a compile error:

Protocol type 'CanvasItem' cannot conform to 'CanvasItem' because only concrete types can conform to protocols

Any suggestions? We stuck on this for a few good days now ah.
Any help would be highly appreciated.

You are running into the "protocols cannot conform to protocols" error. In your protocol definition, the vars you define don't actually exist. They are requirements on concrete types that derive from the protocol. So, when you try and access those common properties, they don't actually exist, and you get the error that you got. You need to define concrete types that actually instantiate those common properties. You can instantiate them in each Item class, or you can define a base class that instantiates and initializes those properties, and you ...Item classes inherit from the base class.

Protocols are not base classes.

Hey Jonathan, thank you for replying. Isn't ViewClass considered a BaseClass?

ViewClass is a derived class from UIView. The base class I'm referring to is the base class for all of your ...Items that have to conform to your CanvasItem protocol. In fact, your ...Items do not conform to the CanvasItem protocol since they do not instantiate instances of your common properties like scale, rotation, etc.. How are they going to be initialized when you create an instance of StickerItem?

Again, CanvasItem is a protocol, not a class, and it does not have instantiations of your properties.

canvasItem is a property of you ViewClass, it's not related structurally at all to the CanvasItem type hierarchy.

An example:

public protocol CanvasItemProtocol 
{
    var scale : CGFloat { get set }
}

class CanvasItem : CanvasItemProtocol
{
    public var scale : CGFloat = 1.0
}

public class StickerItem: CanvasItem
{
   public var stickerName : String = ""
}

public class ViewItem<T: CanvasItemProtocol> : UIView
{
    let canvasItem: T
    init(_ item: T)
    {
        canvasItem = item
    }
}

Abbreviated to illustrate concept

Hey @jonprescott , thank you for your help! I've updated the question with implementations of the BaseClass for each view. I'm trying your solution in Playground, will update you in a sec.

@jonprescott It complies, but compactMap returns 0, as it's not detecting them as CanvasItem:

let shapeItem = ShapeItem()
let stickerItem = StickerItem()
let shapeView = ViewItem<ShapeItem>(shapeItem)
let stickerView = ViewItem<StickerItem>(stickerItem)
let superview = UIView()
superview.addSubview(shapeView)
superview.addSubview(stickerView)

for canvasItemView In superview.subviews.compactMap({$0 as ? ViewItem<CanvasItem>}) {
print(canvasItemView.canvasItem.scale) 
}

Am I doing it wrong for some reason?

Try this:

let stickerItem = StickerItem(); stickerItem.stickerName = "This is a sticker"
let stickerView = ViewItem<StickerItem>(stickerItem)

Same for shape (note that I changed your view class name from "ViewClass" to "ViewItem", sorry about that). Then, add the item views to your super UIClass.

Forget what I just sent. I think you did that.

@jonprescott We are at a good direction as it's at-least compiling. Now the only question is how to get them from an array, what is this evil condition we are missing?

in your compactMap call, I think you need to access each member as a ViewItem<CanvasItemProtocol>, which is the type. Note you need to add the protocol name, not the base class name.

Also, I think in the UIView class, there is an array property called views or subviews that contains the subviews attached to the view. I know there is for NSView in AppKit.

It does work, but prints (obviously) only the ShapeItem/StickerItem. It's possible to get them all of them so we won't need to cast N number of times for each class?

Yes, we are accessing it within the compactMap: superview.subviews.compactMap, but unfortunately it's not helping me, as I need to access the common properties..

Did you change the cast to ViewItem<CanvasItemProtocol>?

Yes :/ It returns:
error: protocol type 'CanvasItemProtocol' cannot conform to 'CanvasItemProtocol' because only concrete types can conform to protocols.
Here is a gist with the full playground code:

for item in superview.subviews
{
    print(item.canvasItem.scale)
}

superview.subviews is [UIView] so it's giving an error:
Value of type 'UIView' has no member 'canvasItem';
But I'm sure you know that haha. Did you mean to cast it?

Yes I did.

Terms of Service

Privacy Policy

Cookie Policy