damianesteban damianesteban - 3 months ago 34
iOS Question

Swift Type Erasure with Generic Enum and Generic Protocol

I have had to use type erasure in Swift a few times however it always involved a generic protocol. In this case, it involves both a generic enum and and generic protocol and I'm stumped.

Here is my generic enum and generic protocol with the necessary extension:

enum UIState<T> {
case Loading
case Success([T])
case Failure(ErrorType)
}

protocol ModelsDelegate: class {
associatedtype Model
var state: UIState<[Model]> { get set }
}

extension ModelsDelegate {

func getNewState(state: UIState<[Model]>) -> UIState<[Model]> {
return state
}

func setNewState(models: UIState<[Model]>) {
state = models
}
}


And here is my type erased generic class:

class AnyModelsDelegate<T>: ModelsDelegate {
var state: UIState<[T]> {

get { return _getNewState(UIState<[T]>) } // Error #1
set { _setNewState(newValue) }
}

private let _getNewState: ((UIState<[T]>) -> UIState<[T]>)
private let _setNewState: (UIState<[T]> -> Void)

required init<U: ModelsDelegate where U.Model == T>(_ models: U) {
_getNewState = models.getNewState
_setNewState = models.setNewState
}
}


I'm getting the following errors (they are marked in the code sample):

Error #1:

Cannot convert value of type '(UIState<[T]>).Type' (aka 'UIState<Array<T>>.Type') to expected argument type 'UIState<[_]>' (aka 'UIState<Array<_>>')


I have been working on this for awhile and there have been quite a few variations on this code that "almost worked". The error always has to do with the getter.

Answer

The problem that causes this error, as @dan has pointed out is that on this line you're trying to pass a type as an argument, instead of an instance of that type:

get { return _getNewState(UIState<[T]>) }

However, I would also question your use of an argument to this function, surely a getting function should have no argument at all? In this case you'll simply want your _getNewState function to have the signature () -> UIState<[T]>, and call it like so:

get { return _getNewState() }

Also, if your getNewState and setNewState(_:) functions in your protocol extension only exist in order to forward the getting and setting of your state property to the type-erasure – you can simplify your code by getting rid of them entirely and use closure expressions in the type-erasure's init instead:

_getNewState = { models.state }
_setNewState = { models.state = $0 }

(These work by capturing a reference to the models argument, for more info see Closures: Capturing Values)

Finally, I suspect that you mean to refer to UIState<T> rather than UIState<[T]> throughout your code, as T in this case refers to an element in the array that your .Success case has as an associated value (unless you want a 2D array here).

All in all, with the above proposed changes, you'll want your code to look something like this:

enum UIState<T> {
    case Loading
    case Success([T])
    case Failure(ErrorType)
}

protocol ModelsDelegate: class {
    associatedtype Model
    var state: UIState<Model> { get set }
}

class AnyModelsDelegate<T>: ModelsDelegate {
    var state: UIState<T> {
        get { return _getNewState() }
        set { _setNewState(newValue) }
    }

    private let _getNewState: () -> UIState<T>
    private let _setNewState: (UIState<T>) -> Void

    required init<U: ModelsDelegate where U.Model == T>(_ models: U) {
        _getNewState = { models.state }
        _setNewState = { models.state = $0 }
    }
}
Comments