8

when I try to create a generic class which implement UICollectionViewDataSource in swift it say that my class does not conform to protocol (and sometime Xcode crash).

Does it mean that we can't create generic data provider for UICollectionView and that we have to duplicate code ?

Here is the generic code :

// Enum protocol
protocol OptionsEnumProtocol
{
    typealias T
    static var allValues:[T] {get set}
    var description: String {get}
    func iconName() -> String
}

// enum : list of first available options
enum Options: String, OptionsEnumProtocol
{
    typealias T = Options

    case Color = "Color"
    case Image = "Image"
    case Shadow = "Shadow"

    static var allValues:[Options] = [Color, Image, Shadow]

    var description: String {
        return self.rawValue
    }

    func iconName() -> String
    {
        var returnValue = ""

        switch(self)
        {
            case .Color: returnValue = "color_icon"
            case .Image: returnValue = "image_icon"
            case .Shadow: returnValue = "shadow_icon"
        }

        return returnValue
    }
}

// class to use as the uicollectionview datasource and delegate
class OptionsDataProvider<T>: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
{
    private let items = T.allValues

    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
    {
        return items.count
    }

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
    {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(OptionsCellReuseIdentifier, forIndexPath: indexPath) as! GenericIconLabelCell

        let item = self.items[indexPath.row]
        // Configure the cell
        cell.iconFileName = item.iconName()
        cell.labelView.text = item.description

        return cell
    }
}

But because it failed I have to use this non generic form instead :

enum Options: String
{
    case Color = "Color"
    case Image = "Image"
    case Shadow = "Shadow"

    static var allValues:[Options] = [Color, Image, Shadow]

    var description: String {
        return self.rawValue
    }

    func iconName() -> String
    {
        var returnValue = ""

        switch(self)
        {
            case .Color: returnValue = "color_icon"
            case .Image: returnValue = "image_icon"
            case .Shadow: returnValue = "shadow_icon"
        }

        return returnValue
    }
}

class OptionsDataProvider: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
{
    private let items = Options.allValues

    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
    {
        return items.count
    }

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
    {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(OptionsCellReuseIdentifier, forIndexPath: indexPath) as! GenericIconLabelCell

        let item = self.items[indexPath.row]
        // Configure the cell
        cell.iconFileName = item.iconName()
        cell.labelView.text = item.description

        return cell
    }
}

which obligate me to duplicate the class for each enum type I have.

Exact error :

enter image description here

Dragouf
  • 4,676
  • 4
  • 48
  • 55
  • Why a down vote ? May I have a reason ? – Dragouf Jul 03 '15 at 14:56
  • Can you please clarify what you want to achieve? I am asking because if you create an NSObject subclass which adopts all methods of UICollectionViewDelegate and UICollectionViewDataSource you will be able to use it as delegate and data source for your collection view. So, it is not clear what you want to achieve – Andriy Gordiychuk Jul 10 '15 at 15:11
  • Yes I already use it as delegate and datasource of my uicollectionview. For this it works. The problem is that I duplicate this "delegate/datasource" class to each of my data type. I mean, I use enums as data and I have to create a class per enum. But I would like to create only one "delegate/datasource" and specify the enum type as T parameter. I will update my question to let you understand. – Dragouf Jul 10 '15 at 15:21
  • I have updated my question to let you understand better what I want to achieve. – Dragouf Jul 10 '15 at 15:36

3 Answers3

5

You are right, it is not possible to write a generic class. However, I have found a workaround. It doesn't use enums and so maybe you don't find it very useful. However, it achieves what you want - you are getting a collection view data source which can be used with different classes providing necessary data. Here is the code:

protocol OptionsProviderProtocol
{
    func allValues() -> [OptionsItem]
}

class OptionsItem:NSObject {
    let itemDescription:String
    let iconName:String

    init(iconName:String,description:String) {
        self.itemDescription = description
        self.iconName = iconName
    }
}

// class stores first available options
class Options: NSObject, OptionsProviderProtocol
{

    let color = OptionsItem(iconName: "color_icon", description: "Color")
    let image = OptionsItem(iconName: "image_icon", description: "Image")
    let shadow = OptionsItem(iconName: "shadow_icon", description: "Shadow")

    func allValues() -> [OptionsItem] {
        return [color, image, shadow]
    }
}

// class to use as the uicollectionview datasource and delegate
class OptionsDataProvider: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
{
    private var items:[OptionsItem] = []

    convenience init(optionsProvider:OptionsProviderProtocol) {
        self.items = optionsProvider.allValues()
    }

    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
    {
        return items.count
    }

    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
    {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(OptionsCellReuseIdentifier, forIndexPath: indexPath) as! GenericIconLabelCell

        let item = self.items[indexPath.row]
        // Configure the cell
        cell.iconFileName = item.iconName()
        cell.labelView.text = item.description

        return cell
    }
}

If you have any questions please let me know.

Andriy Gordiychuk
  • 6,163
  • 1
  • 24
  • 59
  • Thank you Andriy, I think it's an acceptable workaround even if it's less convenient than enum. I just have to add kind of id to recognize OptionsItem type when I use it in condition. I note it as accepted answer but for the bounty I wait to see if someone solve it with enums. – Dragouf Jul 12 '15 at 07:13
  • Anyway I wonder if it will work with generic code and enum later in future version of swift because for me it look likes a bug. – Dragouf Jul 12 '15 at 07:14
  • @Dragouf I think it is more like an 'environment restriction' and not a bug that you can't operate with generics in the same way as with usual objc objects. Error message could be more specific though :) But anyway, what you want is actually possible (see [my answer](http://stackoverflow.com/a/31399259/3214178)). – Nevs12 Jul 14 '15 at 09:17
  • @Andriy, I change accepted answer to Nevs12 answer since he use generics in his solution. – Dragouf Jul 14 '15 at 23:07
2

When you inherit from a protocol you must implement all required methods. Swift 2 will change this somewhat. Perhaps you really want to inherit from a class.

zaph
  • 111,848
  • 21
  • 189
  • 228
  • I already did, that's not the problem. I didn't write all the methods to simplify. When I remove T it compile without any problem. – Dragouf Jul 03 '15 at 14:42
1

I had the similar problem/question when I was trying to inherit Generic class from NSOperation class. xCode didn't give me a compile error because there were no protocols involved, instead my override func main() was simply never called :)

Anyway... If you follow workaround that mr. Topal Sergey advised, you can achieve exactly what you want relatively easily.

class ViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView?
    private var defaultDataProvider = OptionsDataProvider<Options>()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        collectionView?.delegate = defaultDataProvider
        collectionView?.dataSource = defaultDataProvider
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}


// Enum protocol
protocol OptionsEnumProtocol {
    static var allValues: [OptionsEnumProtocol] {get set}
    var description: String {get}
    func iconName() -> String
}

// enum : list of first available options
enum Options: String, OptionsEnumProtocol {
    case Color = "Color"
    case Image = "Image"
    case Shadow = "Shadow"

    static var allValues: [OptionsEnumProtocol] = [Color, Image, Shadow]

    var description: String {
        return self.rawValue
    }

    func iconName() -> String
    {
        var returnValue = ""

        switch(self)
        {
        case .Color: returnValue = "color_icon"
        case .Image: returnValue = "image_icon"
        case .Shadow: returnValue = "shadow_icon"
        }

        return returnValue
    }
}

class OptionsDataProviderWrapper: NSObject, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    // MARK: protocols' funcs

    final func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return wrapperCollectionView(collectionView, numberOfItemsInSection: section)
    }

    final func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        return wrapperCollectionView(collectionView, cellForItemAtIndexPath: indexPath)
    }

    // MARK: for override

    func wrapperCollectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 0
    }

    func wrapperCollectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        return UICollectionViewCell()
    }
}

class OptionsDataProvider<T: OptionsEnumProtocol>: OptionsDataProviderWrapper {
    private let items = T.allValues

    override func wrapperCollectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return items.count
    }

    override func wrapperCollectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("reuseId", forIndexPath: indexPath) as! GenericIconLabelCell

        let item = self.items[indexPath.row]
        cell.labelView?.text = item.description

        return cell
    }
}

class GenericIconLabelCell: UICollectionViewCell {
    @IBOutlet weak var labelView: UILabel?
}

The key here is to create OptionsDataProviderWrapper that is not a generic and implements all your protocols. The only thing that it does - it redirects calls to another functions like func wrapperCollectionView...

Now you can inherit your Generic class from this OptionsDataProviderWrapper and override that wrapper functions.

Note: you have to override exactly wrapper functions because native func collectionView... functions will not be called in your generic subclass similarly to my issue with NSOperation. That's why I marked native functions with final.

Community
  • 1
  • 1
Nevs12
  • 599
  • 6
  • 13
  • I have a problem with your answer, it say me that T.T is not convertible to [T] when I call T.allValues. I wait to solve this before to give you bounty – Dragouf Jul 14 '15 at 23:19
  • @Dragouf Yeah. I saw this issue when I was trying the code myself. But it seems that the problem is not in the solution itself, it is in the way how generics are used. The cause of the problem should be here: `typealias T` in `OptionsEnumProtocol`. I'm not really sure for what it's needed, but I changed protocol and enum just a bit and it started to work well. I've updated my answer to present fully working code. – Nevs12 Jul 15 '15 at 06:07
  • typealias T is to return the type of the enum. Here allValues does not return a list of Options enum but OptionsEnumProtocol. So I can't work then with my enum anymore. Actually in my code when we tapped on a cell of the collection view it call a block with item from let item = self.items[indexPath.row] as parameter. And I need to test the enum type. – Dragouf Jul 15 '15 at 06:18
  • @Dragouf I see, but then compiler doesn't really know what the heck this `T` is. :) It can be literally anything. You know that `Options` enum is your `T` in this particular case, but compiler doesn't know it - it knows that `T` is 'something'. Also I'm not sure that you can do switch-case on some arbitrary enum, because compiler needs to know case-set beforehand. So you need to check what enum your `item` is, downcast it and then switch-case. Better option would be to get all needed things through protocol, make a complete abstraction from underlying enums. – Nevs12 Jul 15 '15 at 06:59
  • Nevs12 it's the reason why generic exist. It's to avoid casting. T exact type should be specify when we instantiate the class with notation. And T should become Options for this instance – Dragouf Jul 15 '15 at 07:31
  • @Dragouf For this specific instance - `T` will be `Options` ofc. But in compile time in your case `items = T.allValues` means that `items` is `[T]`. When you do `let item = items[indexPath.row]` it knows that `item` is `T`. And in compile time `T` is just 'something' or, better say, 'anything'. So when you write Generics you need to abstract a bit from possible instances and think as a compiler :) I would very recommend to read [this article](http://blog.indragie.com/post/99740598364/type-safe-table-views-with-swift) to have maybe a better overview how Generic datasources can be built ;) – Nevs12 Jul 15 '15 at 08:18
  • Nevs12, I know about generic. I think it's you misunderstood the principle. Anyway your solution seems smart but unfortunately I can't accept it since it still doesn't work:s – Dragouf Jul 15 '15 at 12:22
  • What you seems not understand about generic is that T is defined when you instanciate an object (and so on at compile time). And to be able to use some methods on T when you write generic you have to apply constraints with where keyword. Generic are there to avoid duplication and/or cast. – Dragouf Jul 15 '15 at 12:26
  • @Dragouf No problem man :) Seems like we just talking about different things. Anyway... was glad to try to help ;) – Nevs12 Jul 15 '15 at 12:43