loading...
Swift You and I

Codable observable objects

diegolavalle profile image Diego Lavalle Originally published at swiftui.diegolavalle.com ・2 min read

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!

Codable observable objects

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.

FEATURED EXAMPLE: Real-Time JSON - Observe the codable object


Originally published at Swift You and I.

Posted on by:

diegolavalle profile

Diego Lavalle

@diegolavalle

Mad computer scientist. Builder of apps. Apple Frameworks, Web Standards, Swift, Swift UI, Advanced Javascript, React, React Native, Node.

Swift You and I

Swift You and I is a publication centered around SwiftUI, Combine and related Apple frameworks. Official app (https://swiftui.diegolavalle.com/app) with interactive examples available on the App Store.

Discussion

markdown guide