Fry
Fry

Reputation: 6275

Swift custom encode for encoding Int time like pretty string

I have a custom object stored in a Struct:

struct Event {
   let type: String
   let value: String
   let time: Int
}

where time is a UnixTime in millis. In the App I have a scrollable text field when I print this event in json format

{
   "type" : "rain",
   "value" : "0.5",
   "time" : 1681663944
}

but for time parameter I would like to convert the UnixTime in a human readable form (i.e. 16/04/2023), so I need to customize haw this parameter is encoded. The problem is that all my objects are conforms to this protocol, so I have the toJsonString() method for free, based on the power of Codable.

protocol BaseObject: Codable {
    
}

extension BaseObject {
    func toJsonString() -> String {
        let jsonEncoder = JSONEncoder()
        do {
            let jsonData = try jsonEncoder.encode(self)
            let json = String(data: jsonData, encoding: String.Encoding.utf8)
            return json ?? ""
        } catch {
            return ""
        }
    }
}

So, I don't have a toJsonString() method for each object separately. Naïve solution should be write a custom toJsonString() for each object, but I have a lot of struct and if I found a smart way to intercept time attributes I can override the encode representation of all attributes in the App with this type.

Ideas ?

Thanks.

EDIT

For now I found this solution:

func toJsonString() -> String {
        let jsonEncoder = JSONEncoder()
        do {
            let jsonData = try jsonEncoder.encode(self)
            var modData = Data()
            
            if var dict = jsonData.toDictionary() {
                
                dict.keys.filter{ $0.contains("time") }.forEach { key in
                    if let t = dict[key] as? Int {
                        dict[key] = t.toStringDate()
                    }
                }
                
                modData = dict.toData()
            }
            
            let json = String(data: modData, encoding: String.Encoding.utf8)
            return json ?? ""
        } catch {
            return ""
        }
    }

Upvotes: 1

Views: 549

Answers (1)

Alexander
Alexander

Reputation: 63399

You can use a property wrapper to hook in and give custom encoding behaviour for a specific property:

import Foundation

@propertyWrapper
struct PrettyEncodedDate {
   static let formatter = {
      let df = DateFormatter()
      // Customize the date formatter however you'd like
      df.dateStyle = .full
      return df
   }()
   
   let wrappedValue: Date
}

extension PrettyEncodedDate: Decodable {
   struct InvalidDateError: Error {
      let dateString: String
   }
   
   init(from decoder: Decoder) throws {
      let container = try decoder.singleValueContainer()
      let dateString = try container.decode(String.self)
      guard let date = Self.formatter.date(from: dateString) else {
         throw InvalidDateError(dateString: dateString)
      }
      self.init(wrappedValue: date)
   }
}

extension PrettyEncodedDate: Encodable {
   func encode(to encoder: Encoder) throws {
      var container = encoder.singleValueContainer()
      let formattedDate = Self.formatter.string(from: self.wrappedValue)
      try container.encode(formattedDate)
   }
}

Here's an example usage:

struct Event: Codable {
   let type: String
   let value: String
   @PrettyEncodedDate var time: Date
}


let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let event = Event(type: "some type", value: "some value", time: Date.now)
let prettyJSON = try encoder.encode(event)
print(String(data: prettyJSON, encoding: .utf8)!)

Which prints:

{
  "type" : "some type",
  "value" : "some value",
  "time" : "Sunday, April 16, 2023"
}

This relies on time to be a Date (which it probably should be). If it must stay an Int (though I'd caution against that), you can tweak the property wrapper to change the current behaviour (String -> Date, Date -> String) to add an extra processing step (String -> Date -> Int, Int -> Date -> String).

Upvotes: 1

Related Questions