Using as a concrete type conforming to protocol is not supported

839 Views Asked by At

I want to create a generic view controller for a settings page. Right now the settings come from a JSON, but the implementation might be switched out later, that is why I want to have protocols. For example the LanguageSetting protocol is empty, but by using it I can still preserve type-safety for the future, without having to settle for a specific implementation (e.g. JSON decoding).

// Protocols

protocol Query {
    associatedtype Result
    func handleResult(with data: Data) -> Result
}

protocol Setting {
    var name: String { get }
    var icon: URL? { get }
}

protocol LanguageSetting: Setting {
}

protocol CountrySetting: Setting {
}

// Implementations

struct LanguageSettingQuery: Query {
    func handleResult(with data: Data) -> [LanguageSetting] {
        return try! JSONDecoder().decode([JSONLanguageSetting].self, from: data)
    }
}

struct CountrySettingQuery: Query {
    func handleResult(with data: Data) -> [CountrySetting] {
        return try! JSONDecoder().decode([JSONCountrySetting].self, from: data)
    }
}

struct JSONLanguageSetting: LanguageSetting, Decodable {
    var name: String
    var icon: URL?
}

struct JSONCountrySetting: CountrySetting, Decodable {
    var name: String
    var icon: URL?
}

// A generic settings view controller
class LocaleViewController<LocaleQuery: Query>: UIViewController where
LocaleQuery.Result: Sequence, LocaleQuery.Result.Element: Setting {
    private var settingItems = [Setting]()

    init(query: LocaleQuery) {
        settingItems = query.handleResult(with: Data()) as! [Setting]
        super.init(nibName: nil, bundle: nil)
    }

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

let localeVC = LocaleViewController(query: LanguageSettingQuery())

Above is a very simple implementation I created in Playgrounds. The problem is that the last line:

let localeVC = LocaleViewController(query: LanguageSettingQuery())

throws the error:

Using 'LanguageSetting' as a concrete type conforming to protocol 'Setting' is not supported

Any ideas on how could I work around this?

On a side note:

Why is downcasting necessary here? Are not the generic type constraints sufficient for ensuring this?

settingItems = query.handleResult(with: Data()) as! [Setting]
1

There are 1 best solutions below

0
On

The error

Protocols define types; however, they differ from the other three types that can be defined (classes, structs, and enums) in that they cannot conform to other protocols or require that a protocol implement some API. The error you are receiving here is pointing this out: the LanguageSetting protocol type does not (and cannot) conform to the Setting protocol as required by the generic where clause where ... , LocaleQuery.Result.Element: Setting in the generic class LocaleViewController<LocaleQuery: Query>. Note that protocol inheritance is not equivalent to conformance. Since you are creating JSONLanguageSetting instances from JSON in the LanguageSettingQuery, replace

func handleResult(with data: Data) -> [LanguageSetting] {
    return try! JSONDecoder().decode([JSONLanguageSetting].self, from: data)
}

with

func handleResult(with data: Data) -> [JSONLanguageSetting] {
    return try! JSONDecoder().decode([JSONLanguageSetting].self, from: data)
}

and the error will be resolved. The compiler infers that the associated Result type to be [LanguageSetting] in the former, and [JSONLanguageSetting] in the latter, both based on the return type of the handle(with:) method.

Why you have to cast

The compiler is forcing you to cast because it cannot guarantee that the generic type LocaleQuery is Array<Setting>. While Array<Setting> does conform to the Sequence protocol whose elements are the Setting type, the where clauses so defined are not specific enough to guarantee that query.handleData(with: Data()) returns an array. It merely knows that it returns "some type conforming to Query and Sequence whose Element type is the Setting protocol type". It is very possible that this is custom type. Consider the following:

struct CustomIterator<T>: IteratorProtocol where T: Setting {
    typealias Element = T
    func next() -> T? { nil }
}

class MyWeirdType<S>: Sequence where S: Setting {
    typealias Iterator = CustomIterator<S>
    typealias Element = S
    func makeIterator() -> CustomIterator<S> { CustomIterator<S>() }
}

class WeirdQuery<Q>: Query where Q: Setting {
    typealias Result = MyWeirdType<Q>
    func handleResult(with data: Data) -> MyWeirdType<Q> { MyWeirdType<Q>() }
}

let localeVC = LocaleViewController(query: WeirdQuery<JSONLanguageSetting>())

This will crash the program as it will attempt to cast MyWeirdType<JSONLanguageSetting> as Array<Setting> in the initializer of LocaleViewController which cannot be done. If you are expecting an array, try the following:

class LocaleViewController<LocaleQuery: Query>: UIViewController where
LocaleQuery.Result == Array<Setting> {
    private var settingItems = [Setting]()

    init(query: LocaleQuery) {

        settingItems = query.handleResult(with: Data()) // Knows that the return is [Setting]

        super.init(nibName: nil, bundle: nil)
    }

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

Note LocaleViewController has become less generic as the where clause is more specific than it is currently.