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]
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 theSetting
protocol as required by the generic where clausewhere ... , LocaleQuery.Result.Element: Setting
in the generic classLocaleViewController<LocaleQuery: Query>
. Note that protocol inheritance is not equivalent to conformance. Since you are creatingJSONLanguageSetting
instances from JSON in theLanguageSettingQuery
, replacewith
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 thehandle(with:)
method.Why you have to cast
The compiler is forcing you to cast because it cannot guarantee that the generic type
LocaleQuery
isArray<Setting>
. WhileArray<Setting>
does conform to theSequence
protocol whose elements are theSetting
type, the where clauses so defined are not specific enough to guarantee thatquery.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:This will crash the program as it will attempt to cast
MyWeirdType<JSONLanguageSetting>
asArray<Setting>
in the initializer ofLocaleViewController
which cannot be done. If you are expecting an array, try the following:Note
LocaleViewController
has become less generic as the where clause is more specific than it is currently.