Joris
Joris

Reputation: 6306

Swift error: cannot convert from 'A' to 'A?' when using generics

The problem is this:

I want to create a class using generics. That class has a delegate that contains functions that take values of the generic type as arguments. The delegate is declared using a protocol.

For some reason, I cannot get this to compile in Swift 5. This is the code:


protocol SelectItemViewControllerDelegate: AnyObject {
    func selectItemViewController<T>(_ vc: SelectItemViewController<T>, selectedItem: T) where T: CustomStringConvertible
}

class SelectItemViewController<T> where T: CustomStringConvertible {
    
    weak var delegate: SelectItemViewControllerDelegate?
}

class MyClass: SelectItemViewControllerDelegate {
    
    private var selectItemCompletionBlock: ((String?) -> Void)?

    func selectItemViewController<String>(_ vc: SelectItemViewController<String>, selectedItem: String) {
        selectItemCompletionBlock?(selectedItem)
    }
}


The error I get is:

error: cannot convert value of type 'String' to expected argument type 'String?'
        selectItemCompletionBlock?(selectedItem)
                                      ^
                                                   as! String

When I do this:

selectItemCompletionBlock?(selectedItem as? String)

I get

error: cannot convert value of type 'String?' to expected argument type 'Swift.String?'
        selectItemCompletionBlock?(selectedItem as? String)
                                                   ^
                                                              as! String

What is going on here?

Upvotes: 1

Views: 81

Answers (1)

Sweeper
Sweeper

Reputation: 273380

The String here:

func selectItemViewController<String>

declares a generic parameter called String. It does not refer to Swift.String, which selectItemCompletionBlock can accept.

Since SelectItemViewController is generic, you want its delegate to specify for which T it is implementing the delegate. In the case of MyClass, it is String. To do this, you need an associated type, not a generic method:

protocol SelectItemViewControllerDelegate {
    associatedtype ItemType: CustomStringConvertible
    
    func selectItemViewController(_ vc: SelectItemViewController<ItemType>, selectedItem: ItemType)
}

Now MyClass can implement selectItemViewController(_:selectedItem:) just for ItemType == String:

class MyClass: SelectItemViewControllerDelegate {
    
    private var selectItemCompletionBlock: ((String?) -> Void)?

    func selectItemViewController(_ vc: SelectItemViewController<String>, selectedItem: String) {
        selectItemCompletionBlock?(selectedItem)
    }
}

The problem now becomes, this is now invalid

weak var delegate: SelectItemViewControllerDelegate?

You can't use a protocol with associated types as the type of a property. To solve this, you need to create a type eraser:

struct AnySelectItemViewControllerDelegate<T: CustomStringConvertible>: SelectItemViewControllerDelegate {
    let selectedItemFunc: (SelectItemViewController<T>, T) -> Void
    
    init<DelegateType: SelectItemViewControllerDelegate>(_ del: DelegateType) where DelegateType.ItemType == T {
        // keep a weak reference to del here, so as not to cause a retain cycle
        selectedItemFunc = { [weak del] x, y in del?.selectItemViewController(x, selectedItem: y) }
    }
    
    func selectItemViewController(_ vc: SelectItemViewController<T>, selectedItem: T) {
        selectedItemFunc(vc, selectedItem)
    }
}

Then, you can declare the delegate as:

var delegate: AnySelectItemViewControllerDelegate<T>?

And to assign the delegate, create an AnySelectItemViewControllerDelegate. Suppose self is the delegate...

selectItemVC.delegate = AnySelectItemViewControllerDelegate(self)

That's a lot of work isn't it? If you only have few delegate methods, I suggest not using a delegate. Just declare some closure properties in SelectItemVC:

var onItemSelected: ((T) -> Void)?

Upvotes: 1

Related Questions