Last active
October 28, 2019 22:35
-
-
Save GeekAndDad/c3b3fb030595b18fe153d526a815128e to your computer and use it in GitHub Desktop.
How to make an AnyEncodable protocol that allows you to pass any struct conforming to Encodable to a function.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// based on clever example from Nick Lockwood: https://gist.github.com/nicklockwood/833fabacbc4b2d11ae7c7d4752b8fd18 | |
// I only needed Encodable so trimmed it down to just that. | |
protocol AnyEncodableValue: Encodable {} | |
extension AnyEncodableValue { | |
func encode(to container: inout SingleValueEncodingContainer) throws { | |
try container.encode(self) | |
} | |
} | |
struct AnyEncodable: Encodable { | |
let value: AnyEncodableValue | |
init(_ value: AnyEncodableValue) { | |
self.value = value | |
} | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
try value.encode(to: &container) | |
} | |
} | |
// Test it out: | |
struct Bar: AnyEncodableValue { | |
var bar: Int | |
} | |
struct Baz: AnyEncodableValue { | |
var baz: String | |
} | |
let coder = JSONEncoder() | |
coder.outputFormatting = .prettyPrinted | |
let values: [AnyEncodableValue] = [ | |
Bar(bar: 5), | |
Baz(baz: "Hello"), | |
] | |
let typeErasedValues = values.map(AnyEncodable.init) | |
let data = try coder.encode(typeErasedValues) | |
print(String(data: data, encoding: .utf8)!) | |
// try as function argument: | |
func test(value: [AnyEncodable]) throws -> Data { | |
let coder = JSONEncoder() | |
coder.outputFormatting = .prettyPrinted | |
let data = try coder.encode(value) | |
return data | |
} | |
let dd = try test(value: typeErasedValues) | |
print(String(data: dd, encoding: .utf8)!) | |
// try single value | |
let typeErasedValue = AnyEncodable(values[0]) | |
func test(value: AnyEncodable) throws -> Data { | |
let coder = JSONEncoder() | |
coder.outputFormatting = .prettyPrinted | |
let data = try coder.encode(value) | |
return data | |
} | |
let d = try test(value: typeErasedValue) | |
print(String(data: d, encoding: .utf8)!) | |
// Single value without the pre-type erasing step required (nice!): | |
func test(value: AnyEncodableValue) throws -> Data { | |
let typeErasedValue = AnyEncodable(value) | |
let coder = JSONEncoder() | |
coder.outputFormatting = .prettyPrinted | |
let data = try coder.encode(typeErasedValue) | |
return data | |
} | |
let d3 = try test(value: values[0]) | |
print(String(data: d3, encoding: .utf8)!) |
Added test of normal Encodable types which I didn't do properly late last night:
// Single value without the pre-type erasing step required (nice!):
func testV(value: Encodable) throws -> Data {
let typeErasedValue = AnyEncodable(value)
let coder = JSONEncoder()
coder.outputFormatting = .prettyPrinted
coder.dateEncodingStrategy = .iso8601
let data = try coder.encode(typeErasedValue)
return data
}
print("\ntestV\n")
let s = "Hello!"
let now = Date()
let d3 = try testV(value: s)
print(String(data: d3, encoding: .utf8)!)
let d4 = try testV(value: now)
print(String(data: d4, encoding: .utf8)!)
output:
testV
"Hello!"
"2019-10-28T03:11:11Z"
Ah, this made me realize that this doesn't work for my case. I need keyed encoding which requires the generic which causes the problem. doh!
I need keyed encoding which requires the generic which causes the problem. doh!
I'm not sure what you mean by this. String and Date are encoded with single-value containers by default, but a more complex object will be encoded as a dictionary using keyed encoding. The fact we use a singleValueContainer
for the AnyEncodable
wrapper won't change that.
Oi! Thanks so much for the reply! Made me look again and realize my test code was flawed (grrr). Sure enough! As you said, it works great:
struct User: Encodable {
var name: String
var phone: String
var userID: String
var joinedDate: Date
}
let user = User(name: "Joe Bob", phone: "15551212", userID: "201910280001", joinedDate: Date())
let typeErasedValue = AnyEncodable(user)
func test(value: AnyEncodable) throws -> Data {
let coder = JSONEncoder()
coder.outputFormatting = .prettyPrinted
coder.dateEncodingStrategy = .iso8601
let data = try coder.encode(value)
return data
}
let d = try test(value: typeErasedValue)
print(String(data: d, encoding: .utf8)!)
// Try single value without the pre-type erasing step required:
func testV(value: Encodable) throws -> Data {
let typeErasedValue = AnyEncodable(value)
let coder = JSONEncoder()
coder.outputFormatting = .prettyPrinted
coder.dateEncodingStrategy = .iso8601
let data = try coder.encode(typeErasedValue)
return data
}
print("\ntestV\n")
let d2 = try testV(value: user)
print(String(data: d2, encoding: .utf8)!)
produces:
{
"phone" : "15551212",
"userID" : "201910280001",
"name" : "Joe Bob",
"joinedDate" : "2019-10-28T22:33:13Z"
}
testV
{
"phone" : "15551212",
"userID" : "201910280001",
"name" : "Joe Bob",
"joinedDate" : "2019-10-28T22:33:13Z"
}
Bingo! 💥
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
As long as it's not annotated with
@objc
it's fine for several different files to define the same private extension function, even within the same module. Such is the joy of Swift's proper access levels and name-spacing (as opposed to C-style privacy-by-obscurity).