I have a problem to put structs with a generic type in one array. I know that Swift converts the meta type of an Array into a concrete type and that this is the conflict. I tried to find a different solution but I think I need your help.
Here I define the structs and protocols:
protocol ItemProtocol {
var id: String { get }
}
struct Section<T: ItemProtocol> {
var items: [T]
var renderer: Renderer<T>
}
struct Renderer<T> {
var title: (T) -> String
}
Here two example structs that implement the ItemProtocol
:
struct Book: ItemProtocol {
var id: String
var title: String
}
struct Car: ItemProtocol {
var id: String
var brand: String
}
This is how i setup the sections:
let book1 = Book(id: "1", title: "Foo")
let book2 = Book(id: "2", title: "Bar")
let books = [book1, book2]
let bookSection = Section<Book>(items: books, renderer: Renderer<Book> { (book) -> String in
return "Book title: \(book.title)"
})
let car1 = Car(id: "1", brand: "Foo")
let car2 = Car(id: "2", brand: "Bar")
let cars = [car1, car2]
let carSection = Section<Car>(items: cars, renderer: Renderer<Car> { (car) -> String in
return "Car brand: \(car.brand)"
})
Now i want to put the sections together. Here is what i tried. But each of these 3 lines give me an error:
let sections: [Section<ItemProtocol>] = [bookSection, carSection]
let sections2: [Section] = [bookSection, carSection]
let sections3: [Section<AnyObject: ItemProtocol>] = [bookSection, carSection]
sections.forEach({ section in
section.items.forEach({ item in
let renderedTitle = section.renderer.title(item)
print("\(renderedTitle)")
})
})
For the declaration of the sections
array i get this error:
Using 'ItemProtocol' as a concrete type conforming to protocol 'ItemProtocol' is not supported
For the declaration of the sections2
array this error:
Cannot convert value of type 'Section' to expected element type 'Section'
And sections3
throws this:
Expected '>' to complete generic argument list
The problem is there is no common ground (excepting Any
) between different Section
types (with different generic arguments). One possible solution would be to unify all Section
types into one protocol, and use that protocol to build the array:
protocol SectionProtocol {
var genericItems: [ItemProtocol] { get }
var renderedTitles: [String] { get }
}
extension Section: SectionProtocol {
var genericItems: [ItemProtocol] { return items }
var renderedTitles: [String] {
return items.map { renderer.title($0) }
}
}
let sections: [SectionProtocol] = [bookSection, carSection]
sections.forEach { section in
section.renderedTitles.forEach { renderedTitle in
print("\(renderedTitle)")
}
}
So instead of iterating through the elements, you iterate through the rendered titles, which each section should be able to construct.
Now this addresses the basic use case from your question, however depending on you use the section in your app it might not be enough and you'll have to recourse to type erasers, as other answerers mentioned.
The struct Section
is generic so you cannot use it as a type. One solution could be to use a type erasure:
Create any ItemProtocol
wrapper:
protocol ItemProtocol {
var id: String { get }
}
struct AnyItem : ItemProtocol {
private let item: ItemProtocol
init(_ item: ItemProtocol) {
self.item = item
}
// MARK: ItemProtocol
var id: String { return item.id }
}
And a type erased Section, Any section:
protocol SectionProtocol {
associatedtype T
var items: [T] { get }
var renderer: Renderer<T> { get }
}
struct Section<Item: ItemProtocol>: SectionProtocol {
typealias T = Item
var items: [Item]
var renderer: Renderer<Item>
var asAny: AnySection {
return AnySection(self)
}
}
struct AnySection : SectionProtocol {
typealias T = AnyItem
private let _items: () -> [T]
private let _renderer: () -> Renderer<T>
var items: [T] { return _items() }
var renderer: Renderer<T> { return _renderer() }
init<Section : SectionProtocol>(_ section: Section) {
self._items = { section.items as! [AnySection.T] }
self._renderer = { section.renderer as! Renderer<AnySection.T>}
}
}
Now you can have a collection of AnySection
s:
let sections: [AnySection] = [bookSection.asAny, carSection.asAny]
To implement it with protocols you will need to use type erasure or casting to Any
, and it's pretty complex or doesn't type safe. You can take another route and implement it with algebraic enums, for example like this:
protocol ItemProtocol: CustomStringConvertible {
var id: String { get }
}
enum ItemType {
case book(title: String)
case car(brand: String)
}
struct Item: ItemProtocol {
let id: String
let type: ItemType
var description: String {
switch self.type {
case .car(let brand):
return "Car brand: \(brand)"
case .book(let title):
return "Book title: \(title)"
}
}
}
let book1 = Item(id: "1", type: .book(title: "Title1"))
let book2 = Item(id: "2", type: .book(title: "Title2"))
let car1 = Item(id: "1", type: .car(brand: "Brand1"))
let car2 = Item(id: "2", type: .car(brand: "Brand2"))
struct Section {
let items: [Item]
}
let section1 = Section(items: [book1, book2])
let section2 = Section(items: [car1, car2])
let sections = [section1, section2]