简体   繁体   中英

How to define Firestore reference type in a Swift struct

Issue description

There are the following domain structs, that represents Firebase documents. For this case, the payment document is not under (and does not belong) account document. It does not make sense to make ScheduledPayment as a sub collection of an Account.

import Foundation
import Firebase
import FirebaseFirestore
import FirebaseFirestoreSwift

struct Account: Codable, Identifiable {

  @DocumentID
  var id: String?
}

struct ScheduledPayment: Codable, Identifiable {

  @DocumentID
  var id: String?

  var account: Account
  var trigger: Timestamp
}

The account field needs to be of "reference" Firebase type. It cannot be a simple string. Here is the picture showing how it is defined in Firestore UI. 帐户字段定义,作为引用类型

I was able to do this in NodeJS (and it creates reference type as expected):

await db
  .collection("nextScheduledPayment")
  .add({
    account: db.doc(`accounts/${accountRef.id}`),
    trigger: Timestamp.now()
  }
);

Question

How to define a reference type in Swift struct? I tried these two approaches, but both of them throw Terminating app due to uncaught exception 'FIRInvalidArgumentException', reason: 'Unsupported type: __SwiftValue' terminating with uncaught exception of type NSException exception.

Here are the failed attempts:

1. using DocumentReference

This one is the closest implementation to what works in NodeJS (JavaScript), but it throws an exception mentioned above.

import Foundation
import Firebase
import FirebaseFirestore
import FirebaseFirestoreSwift

struct ScheduledPayment: Codable, Identifiable {

  @DocumentID
  var id: String?

  var account: DocumentReference
  var trigger: Timestamp
}

let ref = db.document("accounts/\(id)")
let payment = ScheduledPayment(account: ref, trigger: trigger)

let _ = try db
  .collection("nextScheduledPayment")
  .addDocument(from: payment)

2. using Account type

This might make sense, if there would be some @Reference annotation or something like that, but I was not able to find it in the documentation nor in the code of Firestore library.

import Foundation
import Firebase
import FirebaseFirestore
import FirebaseFirestoreSwift

struct ScheduledPayment: Codable, Identifiable {

  @DocumentID
  var id: String?

  var account: Account
  var trigger: Timestamp
}

let account = Account()
let payment = ScheduledPayment(account: account, trigger: trigger)

let _ = try db
  .collection("scheduledPayment")
  .addDocument(from: payment)

DocumentReference has a property named path which is a String representation of the document reference. Therefore, you can use this property in your Codable structure. For convenience, you can include a computed property in the structure that returns a DocumentReference using this path. The computed property won't interfere with the Codable protocol.

struct Payment: Codable {
    let docRefPath: String
    
    var docRef: DocumentReference {
        return Firestore.firestore().document(docRefPath)
    }
}

Usage is straightforward.

// set
Firestore.firestore().document("someCollection/someDocument").getDocument { (snapshot, error) in
    if let doc = snapshot,
       let docRef = doc.get("docRef") as? DocumentReference {
        let payment = Payment(docRefPath: docRef.path)
    }
}

// get
payment.docRef.getDocument { (snapshot, error) in
    // ...
}

The solution to persist a document reference is the following (the option #1 that was mentioned in the question):

import Foundation
import Firebase
import FirebaseFirestore
import FirebaseFirestoreSwift

struct ScheduledPayment: Codable, Identifiable {

  @DocumentID
  var id: String?

  var account: DocumentReference
  var trigger: Timestamp
}

let ref = db.document("accounts/\(id)")
let payment = ScheduledPayment(account: ref, trigger: trigger)

let _ = try db
  .collection("nextScheduledPayment")
  .addDocument(from: payment)

My error was caused by including an object in the whereField filter:

let account: Account = // the way you get the object
db.collection("scheduledPayment")
        .whereField("account", isEqualTo: account)

The correct way to filter is the following:

let account: Account = // the way you get the object
db.collection("scheduledPayment")
        .whereField("account", isEqualTo: db.document("accounts/\(id)"))

...hope it helps to someone...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM