iWasRobbed iWasRobbed - 14 days ago 8
Swift Question

Swift array of UIView subclasses conforming to a protocol

What is the best way to mirror Obj-C’s

@property (nonatomic) NSArray <SomeProtocol>* items;
where the items are
UIView
subclasses?

In the example below, I'd like to store an array of
UIKit
components (e.g.
UILabel
,
UIButton
, etc) that all conform to a protocol, however this gives an error
Protocol can only be used as a generic constraint because it has Self or associated type requirements


What are some alternative ways of modeling this?

Example playground:

import UIKit

/// Protocol representing a form field model
protocol FieldRepresentable {}

/// Protocol representing a form UI control
protocol FormControllable {
associatedtype FieldRepresentable

init(model: FieldRepresentable)

var model: FieldRepresentable { get }
}

/// Example label model
class FormLabelElement: FieldRepresentable {}

/// Example label UI control
class FormLabel: UILabel, FormControllable {

required init(model: FormLabelElement) {
self.model = model

super.init(frame: CGRect.zero)
}

let model: FormLabelElement

required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}

/// Example form
class Form: UIView {

// Error here
var fields: [FormControllable]?

}

Answer

Nate Cook suggested

One simple-ish way to approach that would be to remove the associated type from FormControllable and make its init failable. Each conforming type would basically have to validate that it knows what to do with the specific FieldRepresentable type passed. I think you'd lose a little bit of type safety/expression but would gain the ability to have a non-uniform array

so the final version for that approach ended up being:

import UIKit

/// Protocol representing a form field model that is used to instantiate a UI control
protocol FieldRepresentable: class {}

/// Protocol representing a form UI control
protocol FormControllable: class {
    init?(model: FieldRepresentable)
}

/// Example label model
class FormLabelElement: FieldRepresentable {}

/// Example label UI control
class FormLabel: UILabel, FormControllable {

    required init?(model: FieldRepresentable) {
        guard let model = model as? FormLabelElement else { return nil }
        self.model = model

        super.init(frame: CGRect.zero)
    }

    let model: FormLabelElement

    required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}

/// Example form
class Form: UIView {
    var fields = [FormControllable]()
}

// Create a form
let form = Form()
let labelModel = FormLabelElement()
let labelField = FormLabel(model: labelModel)!
form.fields.append(labelField)
print(form.fields)

form.fields.forEach { (field) in
    if field is FormLabel {
        print("We have a label field")
    }
}

Alternatively, if your model protocol is generic enough, Soroush suggested

One idea is to make your field representable type into an enum, since theres a set number of types of form field elements

Then you just need a big switch that turns an enum into a form element

And you can take an array of field data cases and map them into an array of form elements

Comments