Swift Tip: Custom Types for Codable Conformance
Sometimes we want to add Codable
conformance to a struct when one of its member types isn't codable. Revisiting Swift Talk episodes #72 and #73, where we built an app for comparing running routes, we encountered just such a case.
Here's the struct we want to make codable:
struct Graph {
var edges: [CLLocationCoordinate2D: Destination]
struct Destination: Codable, Equatable {
var location: CLLocationCoordinate2D
var distance: CLLocationDistance
var trackNames: [Track.Name]
}
}
In this example, we're building a graph of running routes on which we can use Dijkstra's algorithm to find the shortest route between two points.
However, CLLocationCoordinate2D
is neither Codable
nor Hashable
. This means we can't use a Core Location coordinate as key in a dictionary, and we don't get auto-synthesized support for coding and decoding on this struct.
The easiest approach would be to conform CLLocationCoordinate2D
to Codable
ourselves:
extension CLLocationCoordinate2D: Codable {
public func encode(to encoder: Encoder) throws {
// ...
}
public init(from decoder: Decoder) throws {
// ...
}
}
We might regret this decision in the future: what if Apple makes CLLocationCoordinate2D
codable in a future update? There is a decent chance that they might use slightly different coding keys, and we would no longer be able to read the data persisted with our own Codable
conformance (for more detail, see this post on the Swift Evolution mailing list).
So we decided to use a different approach, creating our own Coordinate
struct:
struct Coordinate: Codable, Hashable {
let latitude, longitude: Double
}
Since this struct only contains two Double
properties, the compiler synthesizes the Codable
and Hashable
implementations for us. Using this custom type, we can now conform our Graph
struct to Codable
and Hashable
without any further work on our part:
struct Graph: Codable, Hashable {
var edges: [Coordinate: Destination]
struct Destination: Codable, Equatable, Hashable {
var location: Coordinate
var distance: CLLocationDistance
var trackNames: [Track.Name]
}
}
Once we're independent of the CLLocationCoordinate2D
type for encoding and decoding, a potential Codable
implementation by Apple won't break our code anymore. There's a downside to this approach though: we have to convert between the Coordinate
and CLLocationCoordinate2D
types at a few points in our code.
To do this, we create two initializers:
extension CLLocationCoordinate2D {
init(_ coordinate: Coordinate) {
self = .init(latitude: coordinate.latitude, longitude: coordinate.longitude)
}
}
extension Coordinate {
init(_ coordinate: CLLocationCoordinate2D) {
self = .init(latitude: coordinate.latitude, longitude: coordinate.longitude)
}
}
If you'd like to learn more about Codable
, our book Advanced Swift has a full chapter on encoding and decoding, and you can find useful examples in Codable Enums and Networking with Codable, both published earlier this year. ๐