Complying to the Codable
protocol is simple thanks to synthesized initializers and coding keys. Similarly making your class observable using the Combine framework is trivial with ObservableObject
. But attempting to merge these two protocols in a single implementation poses a few obstables. Let’s find out!
A simple class can be made Encodable
and Decodable
simultaneously by simply declaring it as Codable
.
class CodableLandmark: Codable {
var site: String = "Unknown site"
var visited: Bool = false
}
After this you can easily convert objects to JSON format and back.
import Foundation
class CodableLandmark: Codable {
…
// Encode this instance as JSON data
var asJsonData: Data? {
try? JSONEncoder().encode(self)
}
// Create an instance from JSON data
static func fromJsonData(_ json: Data) -> Self? {
guard let decoded = try? JSONDecoder().decode(Self.self, from: json) else {
return nil
}
return decoded
}
}
An identical class could alternatively serve as a model for some SwiftUI view simply by adhering to ObservableObject
. Annotating properties with the @Published
wrapper will ensure that notifications are generated when these values change.
import Combine
class ObservableLandmark: ObservableObject {
@Published var site: String = "Unknown site"
@Published var visited: Bool = false
}
Initially just adding the ObservableObject
to our codable class' protocol list produces no error. But when we try to mark the first variable as published we encounter error messages Type 'LandmarkModel' does not conform to protocol 'Decodable'
and Type 'LandmarkModel' does not conform to protocol 'Encodable'
error.
class LandmarkModel: Codable, ObservableObject {
@Published // Error
var site: String = "Unknown site"
@Published // Error
var visited: Bool = false
}
The solution is to simply implement some of the Codable
requirements manually rather than have them synthesized. Namely the initializer (for decoding) and the encode
function for encoding. This forces us to additionally declare our coding keys explicitly.
class LandmarkModel: Codable, ObservableObject {
…
// MARK: - Codable
enum CodingKeys: String, CodingKey {
case site
case visited
}
// MARK: - Codable
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
site = try values.decode(String.self, forKey: .site)
visited = try values.decode(Bool.self, forKey: .visited)
}
// MARK: - Encodable
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(site, forKey: .site)
try container.encode(visited, forKey: .visited)
}
}
This approach of tightly coupling the codable and observable models can be useful for simple projects as it avoids a lot of boilerplate code.
Check out the associated Working Example to see this technique in action.