首页 文章

在Swift中采用FIRGeoPoint到Codable协议

提问于
浏览
3

我有一个Firebase Firestore文档,其中包含String,Number和GeoPoint值 . 这是由print()函数打印的示例控制台输出 .

[
  "name": "Test", 
  "location": <FIRGeoPoint: (37.165300, 27.590800)>, 
  "aNumber": 123123
]

现在我想为这个文档创建一个结构,符合 Codable 协议 .

struct TestStruct: Codable {

  let name: String
  let aNumber: Double
  let location: GeoPoint

  struct CodingKeys: CodingKey {
    case name, location, aNumber
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    name = try container.decode(String.self, forKey: CodingKeys.name)
    aNumber = try container.decode(Double.self, forKey: CodingKeys.aNumber)
    location = try container.decode(GeoPoint.self, forKey: CodingKeys.location)
  }
}

// encode is not implemented yet.

此代码将显示错误,因为GeoPoint不符合Codable协议 .

所以我试着让GeoPoint符合Codable:

extension GeoPoint: Codable {

  enum CodingKeys: CodingKey {
    case latitute, longitude
  }

  public required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let latitude = try container.decode(Double.self, forKey: CodingKeys.latitute)
    let longitude = try container.decode(Double.self, forKey: CodingKeys.latitute)

    super.init(latitude: latitude, longitude: longitude)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(latitude, forKey: CodingKeys.latitute)
    try container.encode(longitude, forKey: CodingKeys.longitude)
  }
}

现在,IDE对我很生气!

初始值设定项 init(from:) 应为 required ,但扩展名不能包含所需的初始值设定项 . 此外,扩展名不能指定初始化程序,因此初始化程序也应该是 convenience . 一个愚蠢的死胡同 .

为了绕过它,我将GeoPoint子类化:

class ANGeoPoint: GeoPoint, Codable {

  enum CodingKeys: CodingKey {
    case latitute, longitude
  }

  public required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let latitude = try container.decode(Double.self, forKey: CodingKeys.latitute)
    let longitude = try container.decode(Double.self, forKey: CodingKeys.latitute)

    super.init(latitude: latitude, longitude: longitude)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(latitude, forKey: CodingKeys.latitute)
    try container.encode(longitude, forKey: CodingKeys.longitude)
  }
}

并改变了

location = try container.decode(GeoPoint.self, forKey: CodingKeys.location)

location = try container.decode(ANGeoPoint.self, forKey: CodingKeys.location)

现在代码清除了IDE警告 . 我们来试试吧:

Firestore.firestore()
    .collection("testCollection")
    .document("qweasdzxc")
    .getDocument { (snap, error) in
        if let data = snap?.data() {
            let jsonData = JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
            let myStruct = try? JSONDecoder().decode(TestStruct.self, from: jsonData)
        } 
}

当我们运行我们的测试代码时,它会破坏!这是我全新的婴儿运行时错误:

由于未捕获的异常'NSInvalidArgumentException'终止应用程序,原因:'JSON写入中的无效类型(FIRGeoPoint)'

让我们回顾一下示例数据的控制台输出:

... "location": <FIRGeoPoint: (37.165300, 27.590800)> ...

即使我尝试将 location 解码为ANGeoPoint,同时将数据解码为TestStruct, the location data coming from Firestore is still GeoPoint . 并且JSONDecode无法解码非Codable对象 .

更多,因为你记得Xcode不让我创建一个Codable GeoPoint .

现在我被卡住了!有什么建议?谢谢 .

编辑:我在Firebase iOS SDK中找到了这个:https://github.com/firebase/firebase-ios-sdk/commit/13e366738463739f0c21d4cedab4bafbfdb57c6f

但即使我使用的是最新版本,我的代码也没有 . 所以我手动添加:

protocol CodableGeoPoint: Codable {
  var latitude: Double { get }
  var longitude: Double { get }

  init(latitude: Double, longitude: Double)
}

extension CodableGeoPoint {
  public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let latitude = try container.decode(Double.self, forKey: CodingKeys.makeKey(name: "latitude"))
    let longitude = try container.decode(Double.self, forKey: CodingKeys.makeKey(name: "longitude"))

    self.init(latitude: latitude, longitude: longitude)
  }

  public func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(latitude, forKey: CodingKeys.makeKey(name: "latitude"))
    try container.encode(longitude, forKey: CodingKeys.makeKey(name: "longitude"))
  }
}

extension GeoPoint: CodableGeoPoint {}

现在 GeoPoint 是Codable . 但我仍然无法用JSONDecoder解码它 .

1 回答

  • 0

    以下是使GeoPoint可编码的方法 .

    我在Firebase iOS SDK中找到了这个:https://github.com/firebase/firebase-ios-sdk/commit/13e366738463739f0c21d4cedab4bafbfdb57c6f

    但即使我使用的是最新版本,我的代码也没有这个,我不知道为什么 . 所以我手动添加:

    protocol CodableGeoPoint: Codable {
      var latitude: Double { get }
      var longitude: Double { get }
    
      init(latitude: Double, longitude: Double)
    }
    
    enum CodableGeoPointCodingKeys: CodingKey {
      case latitude, longitude
    }
    
    extension CodableGeoPoint {
      public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodableGeoPointCodingKeys.self)
        let latitude = try container.decode(Double.self, forKey: .latitude)
        let longitude = try container.decode(Double.self, forKey: .longitude)
    
        self.init(latitude: latitude, longitude: longitude)
      }
    
      public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodableGeoPointCodingKeys.self)
        try container.encode(latitude, forKey: .latitude)
        try container.encode(longitude, forKey: .longitude)
      }
    }
    
    extension GeoPoint: CodableGeoPoint {}
    

    由于GeoPoint保存在[String:Any]中?作为一个对象,JSONSerilalization只能保存字符串和数字,你不能将数据序列化为json .

    首先,您必须使用JSONSerialization create和jsonObject(aka Dictionary)对GeoPoint进行编码,并使用此jsonObject替换GeoPoint,然后您就可以将数据解码为Struct . 这也适用于Timestamp对象 .

    这是我所说的代码表示:

    Firestore.firestore()
        .collection("testCollection")
        .document("qweasdzxc")
        .getDocument { (snap, error) in
            if var data = snap?.data() {
             // check every key's value if it is a GeoPoint. If it is, convert it into Dictionary. You have to do these for inner values too.
              for key in data.keys {
                  if let val = data[key] as? GeoPoint {
                      let locData = try JSONEncoder().encode(val)
                      data[key] = try JSONSerialization.jsonObject(with: locData, options: .allowFragments)
                  }
               }
               let jsonData = JSONSerialization.data(withJSONObject: data, options: .prettyPrinted)
               let myStruct = try? JSONDecoder().decode(TestStruct.self, from: jsonData)
            } 
    }
    

    另一种解决方案是将GeoPoint保存在变量中并从数据中删除它 . 完成序列化和解码后,您可以手动将struct的GeoPoint数据设置为您拥有的数据 .

    经过2天的头痛,这是我最好的解决方案 . 希望有人能找到更好的并在这里分享 .

相关问题