【问题标题】:Adopting FIRGeoPoint to Codable protocol in Swift在 Swift 中采用 FIRGeoPoint 到 Codable 协议
【发布时间】:2018-09-27 11:25:48
【问题描述】:

我有一个 Firebase Firestore 文档,其中包含字符串、数字和 GeoPoint 值。这是 print() 函数打印的示例控制台输出。

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

现在我想为这个文档创建一个结构,符合Codableprotocol。

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:) 应该是必需的,但扩展不能有必需的初始化器。另外,扩展不能有指定的初始化器,所以初始化器也应该方便。一个愚蠢的死胡同。

为了绕过它,我将 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,来自 Firestore 的位置数据仍然是 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 是可编码的。但是我仍然无法使用 JSONDecoder 对其进行解码。

【问题讨论】:

  • 如果问题是您尝试使用JSONDecoder 来解码无效的JSON(例如"location": &lt;FIRGeoPoint: (37.165300, 27.590800)&gt;),我认为您需要在JSONDecode 之前预先解析数据,并转换@ 987654338@ 变成 "location": {"lat":32.2, "lon":28.9}
  • 我想过,但是如果我应该预先解析数据,编码或解码数据的含义是什么。我可以手动将所有内容解析到结构中。
  • 数据是如何以这种格式进入 Firestore 的?你能改变它的存储格式吗?
  • 正如我所指定的,当您使用print(_:) 函数将数据打印到控制台时,它是数据的表示。这是一个[String: Any]? 字典。下面是 DocumentSnapshot 类的参考:https://firebase.google.com/docs/reference/swift/firebasefirestore/api/reference/Classes/DocumentSnapshot

标签: ios swift firebase google-cloud-firestore codable


【解决方案1】:

这是使 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] 中吗?作为对象,JSONSerialization 只能保存字符串和数字,不能将数据序列化为 json。

首先,您必须对 GeoPoint 进行编码,然后使用 JSONSerialization create 和 jsonObject(也称为 Dictionary),并用此 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 天后的最佳解决方案。希望有人能找到更好的并在这里分享。

【讨论】:

  • 老兄,这对我帮助很大。特别感谢??
【解决方案2】:

Google 确实发布了 FirebaseFirestoreSwift pod,它为包括 GeoPoint 在内的所有类型实现了 Codable 协议。

【讨论】:

  • 如果我没记错的话,是CodableFirebase的一个fork
  • 没有。它来自谷歌。
  • 它当然来自谷歌,我说的是“可编码”部分。
猜你喜欢
  • 1970-01-01
  • 2018-01-25
  • 1970-01-01
  • 2016-07-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多