I have a generic struct allowing different types to be used. I do not want to constrain the whole struct to only Decodable items.
What is the best way to fix the following error, where I try to only execute some code if T conforms to Decodable:
Instance method '...' requires that 'T' conform to 'Decodable'
struct Something<T> {
...
func item<T>(from data: Data) -> T? where T: Decodable {
try? JSONDecoder().decode(T.self, from: data)
}
func getter() -> T {
let value = ...
if let value = value as? T { return value } // something simple like string
if let data = value as? Data, T.self is Decodable { // something complex
return item(from: data) ?? defaultValue // error is thrown here
}
return defaultValue
}
}
As you can see I'm checking the conformance with the if-clause, but that isn't enough to access the constrained method? :/
It makes no sense to me that T only needs to conform to Decodable in some part but not others. I would rewrite the struct as
struct Something<T: Decodable> {
func item(from data: Data) -> T? {
try? JSONDecoder().decode(T.self, from: data)
}
func getter() -> T {
let value = ...
if let data = value as? Data
return item(from: data) ?? defaultvalue
}
return defaultvalue
}
}
First of all you should constrain T to Decodable when you define the struct. Second, you must not define T as a generic parameter of the functions internally as it will not be treated as the same T the struct conforms to by the compiler. Instead it will be treated as a new and different generic type constraint (that you just happened to give the same name). Doing it like this is enough:
struct Something<T: Decodable> {
var defaultValue: T
var data: Data
func item(from data: Data) -> T? {
try? JSONDecoder().decode(T.self, from: data)
}
func getter() -> T {
item(from: data) ?? defaultValue
}
}
You can use an extension to define the more constrained method:
struct Something<T> {
var defaultValue: T
func getter() -> T {
return defaultValue
}
}
extension Something where T: Decodable {
func getter() -> T {
// I'm assuming here that you have a property data: Data
try? JSONDecoder().decode(T.self, from: data) ?? defaultValue
}
}
It's not entirely clear how you're going to use this type in a meaningful way. The way your code is constructed, value is Any type. Is this what you meant? (I'm guessing, not)
Somewhere, you'd need to make a concrete version of Something - i.e. it would be Something<Int> or Something<String> or Something<SomeDecodableType> - at that point T will be that concrete type, and as you can see, there's nothing common between the various versions of T, except Any.
So figure out what parts of Something truly are common.
Related
I've been looking for the perfect UserDefaults wrapper that
seamlessly encodes and decodes Data objects from storage
works with #Published
My starting point was this StackOverflow answer, but it uses object:forKey which doesn't work with custom objects (encoding URLs is always non-trivial for me).
My idea is to be able to use it like so:
struct Server: Identifiable, Codable, Equatable, Hashable { /* vars */ }
class ServerPickerViewModel: ObservableObject {
#Published(wrappedValue: Server.defaultServers.first!,
type: Server.self,
key: "currentServer")
var currentServer: Server?
}
To achieve this, I modified the code from #VictorKushnerov's answer:
import Combine
private var cancellables = [String: AnyCancellable]()
extension Published {
init<T: Encodable & Decodable>(wrappedValue defaultValue: T, type: T.Type, key: String) {
let decoder = JSONDecoder()
var value: T
if
let data = UserDefaults.standard.data(forKey: key),
let decodedVal = try? decoder.decode(T.self, from: data) {
value = decodedVal
} else {
value = defaultValue
}
self.init(initialValue: value) // <-- Error
cancellables[key] = projectedValue.sink { val in
let encoder = JSONEncoder()
let encodedVal = encoder.encode(val) // <-- Error
UserDefaults.standard.set(encodedVal, forKey: key)
}
}
}
There are currently two errors I can't get through, which are the following:
Cannot convert value of type 'T' to expected argument type 'Value' it still relying on the underlying #Published's Value generic type, I wish I could override that with my type T.
Instance method 'encode' requires that 'Published<Value>.Publisher.Output' (aka 'Value') conform to 'Encodable'
You can fix your compilation errors by using where Value : Codable to restrict your extension. Then, you can get rid of your T generic altogether (& you don't have to use the type argument either):
extension Published where Value : Codable {
init(wrappedValue defaultValue: Value, key: String) {
let decoder = JSONDecoder()
var value: Value
if
let data = UserDefaults.standard.data(forKey: key),
let decodedVal = try? decoder.decode(Value.self, from: data) {
value = decodedVal
} else {
value = defaultValue
}
self.init(initialValue: value)
cancellables[key] = projectedValue.sink { val in
let encoder = JSONEncoder()
do {
let encodedVal = try encoder.encode(val)
UserDefaults.standard.set(encodedVal, forKey: key)
} catch {
print(error)
assertionFailure(error.localizedDescription)
}
}
}
}
This being said, I'd probably take the path instead of creating a custom property wrapper instead that wraps #AppStorage instead of extending #Published
Is there any way to prevent a caller from passing an Array or Dictionary to this function, either with a constraint or with a code check?
internal class Decoder {
func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
let decoder = try _Decoder(data: data)
return try T(from: decoder)
}
}
Not really. An overload with a special runtime error is the best you've got.
extension Decoder {
struct Error: Swift.Error { }
func decode<T: Decodable & Sequence>(_: T.Type, from _: Data) throws -> T {
throw Error()
}
}
You might need more than one overload if you can't find a protocol that will cut it.
You can constraint T like this:
func decode<T: Decodable>(_ type: T.Type,
from data: Data) throws -> T where T: AnyObject {
let decoder = try _Decoder(data: data)
return try T(from: decoder)
}
This way Xcode won't let you pass arrays to that function. But probably you'll want to pass a custom protocol or class as generic constraint, because with AnyObject you won't be able to pass structs.
My suggestion is that you create (if you didn't yet) a custom Decodable protocol and pass it as T constraint:
protocol ModelProtocol: Codable {}
class BaseModel: ModelProtocol {
}
struct Model: ModelProtocol {
}
func decode<T: ModelProtocol>(_ type: T.Type, from data: Data) throws {
let decoder = try _Decoder(data: data)
return try T(from: decoder)
}
That way you would get what you need
let baz = try? decode(BaseModel.self, from: data)
let bar = try? decode(Model.self, from: data)
let foo = try? decode([Model].self, from: data)
//Instance method 'decode(_:from:)' requires that '[Model]' conform to 'ModelProtocol'
I'm looking for something like:
func extractRawValue(fromPossibleRawRepresentable value: Any) -> Any? {
return (value as? RawRepresentable)?.rawValue
}
I don't mind if extracted RawValue needs to be static type...
func extractRawValue<T: RawRepresentable, U>(fromPossibleRawRepresentable value: Any, expecting: U.Type) -> U? where T.RawValue == U {
return (value as? T)?.rawValue
}
Context: I want to collect rather raw than actual values in mirror.
let d = Mirror(reflecting: self).children.reduce(into: [String: String](), {
guard let label = $1.label else {
return
}
$0[label] = extractRawValue(fromPossibleRawRepresentable: $1.value)
}
The problem is that RawRepresentable has an associatedtype, so you can't assign Any to it. You also can't use it as a generic type because then you have to use the concrete type itself in the function signature, which defeats the purpose.
You can circumvent these problems fairly easily using:
protocol RawString {
var rawValue: String { get }
}
This will allow you to extract the values using:
func extractRawValue(value: Any) -> String? {
return (value as? RawString)?.rawValue
}
For any type from which you want to extract the rawValue as a String, just add to it conformance to RawString, e.g.
enum Foo: String, RawString {}
// or
struct StringContainer: RawString {
var rawValue: String
}
The downsides to this approach is that you need to explicitly mark each type to conform to RawString, but unfortunately I can't see any other way.
I'm trying to get data by encode model which conforms to Encodable protocol. But it's failed to invoke func encode like code below:
// MARK: - Demo2
class TestClass2: NSObject, Encodable {
var x = 1
var y = 2
}
var dataSource2: Encodable?
dataSource2 = TestClass2()
// error: `Cannot invoke 'encode' with an argument list of type '(Encodable)'`
let _ = try JSONEncoder().encode(dataSource2!)
//func encode<T>(_ value: T) throws -> Data where T : Encodable
But in another demo, it works well, why?
// MARK: - Demo1
protocol TestProtocol {
func test()
}
class TestClass1: NSObject, TestProtocol {
func test() {
print("1")
}
var x = 1
var y = 2
}
var dataSource1: TestProtocol?
dataSource1 = TestClass1()
func logItem(_ value: TestProtocol) {
value.test()
}
logItem(dataSource1!)
Solution 1.
Try this code, which extend encodable
extension Encodable {
func toJSONData() -> Data? { try? JSONEncoder().encode(self) }
}
Solution 2.
To avoid polluting Apple-provided protocols with extensions
protocol MyEncodable: Encodable {
func toJSONData() -> Data?
}
extension MyEncodable {
func toJSONData() -> Data?{ try? JSONEncoder().encode(self) }
}
Use
var dataSource2: Encodable?
dataSource2 = TestClass2()
let data = dataSource2?.toJSONData()
You can't pass a protocol but you can use generics to require a class that conforms to one:
func printJSON<T: Encodable>(_ data: T) {
if let json = try? JSONEncoder().encode(data) {
if let str = String(data: json, encoding: .utf8) {
print(str)
}
}
}
// Now this should work
var dataSource2 = TestClass2()
printJSON(dataSource2!)
There are a number of approaches to solving this problem.
#SPatel solution of extending Encodable is one possibility. However, I personally try to avoid polluting Apple-provided protocols with extensions.
If I am reading between the lines, it appears what you are wanting is to pass any construct that conforms to Encodable to a function/method in some other struct/class.
Let's take an example of what I think you are trying to achieve:
struct Transform {
static func toJson(encodable: Encodable) throws -> Data {
return try JSONEncoder().encode(encodable)
}
}
However, Xcode will complain:
Protocol type 'Encodable' cannot conform to 'Encodable' because only concrete types can conform to protocols
A Swift-ier solution is to use a constrained generic on the function:
struct Transform {
static func toJson<EncodableType: Encodable>(encodable: EncodableType) throws -> Data {
return try JSONEncoder().encode(encodable)
}
}
Now the compiler can infer the type that conforms to Encodable, and we can call the function as intended:
let dataSource = TestClass2()
let jsonData = try? Transform.toJson(encodable: dataSource)
Your 2 examples are different.
JSONEncoder().encode() expects a concrete class which conforms to the procotol Encodable. The reference dataSource2 holds a protocol and not a concrete class.
logItem on the other hands, only takes a protocol as input, and NO concrete class which conforms to the protocol. This is the difference between your examples and why your second case is working and the first case does not.
With your current setup, it will not work. You need to pass in a concrete class to the JSONEncoder.
As long as TestClass2 is Encodable you can use following code. encode should know what to encode. It refers to class properties to do that. Encodable itself doesn't contain any properties.
JSONEncoder().encode(dataSource2 as! TestClass2)
Could it be you're just looking for this ?
func blah<T>(_ thing: T) where T: Codable {
do {
let data = try JSONEncoder().encode(thing)
.. do something with data
} catch {
print("severe problem in blah, couldn't encode")
}
}
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 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.