Firestore - Subcollections Swift - swift

So I'm trying to learn some Firestore basic functionality and have watched "Kilo Locos" videos on YouTube explaining CRUD operations. I want to take his method of code and create subcollections from it. Basically, how can I add a collection and make the 'User' collection a sub collection from this new collection. Any help is greatly appreciated, many thanks!!
Here is a link to download the project:
https://kiloloco.com/courses/youtube/lectures/3944217
FireStore Service
import Foundation
import Firebase
import FirebaseFirestore
class FIRFirestoreService {
private init() {}
static let shared = FIRFirestoreService()
func configure() {
FirebaseApp.configure()
}
private func reference(to collectionReference: FIRCollectionReference) -> CollectionReference {
return Firestore.firestore().collection(collectionReference.rawValue)
}
func create<T: Encodable>(for encodableObject: T, in collectionReference: FIRCollectionReference) {
do {
let json = try encodableObject.toJson(excluding: ["id"])
reference(to: collectionReference).addDocument(data: json)
} catch {
print(error)
}
}
func read<T: Decodable>(from collectionReference: FIRCollectionReference, returning objectType: T.Type, completion: #escaping ([T]) -> Void) {
reference(to: collectionReference).addSnapshotListener { (snapshot, _) in
guard let snapshot = snapshot else { return }
do {
var objects = [T]()
for document in snapshot.documents {
let object = try document.decode(as: objectType.self)
objects.append(object)
}
completion(objects)
} catch {
print(error)
}
}
}
func update<T: Encodable & Identifiable>(for encodableObject: T, in collectionReference: FIRCollectionReference) {
do {
let json = try encodableObject.toJson(excluding: ["id"])
guard let id = encodableObject.id else { throw MyError.encodingError }
reference(to: collectionReference).document(id).setData(json)
} catch {
print(error)
}
}
func delete<T: Identifiable>(_ identifiableObject: T, in collectionReference: FIRCollectionReference) {
do {
guard let id = identifiableObject.id else { throw MyError.encodingError }
reference(to: collectionReference).document(id).delete()
} catch {
print(error)
}
}
}
FIRCollectionReference
import Foundation
enum FIRCollectionReference: String {
case users
}
User
import Foundation
protocol Identifiable {
var id: String? { get set }
}
struct User: Codable, Identifiable {
var id: String? = nil
let name: String
let details: String
init(name: String, details: String) {
self.name = name
self.details = details
}
}
Encodable Extensions
import Foundation
enum MyError: Error {
case encodingError
}
extension Encodable {
func toJson(excluding keys: [String] = [String]()) throws -> [String: Any] {
let objectData = try JSONEncoder().encode(self)
let jsonObject = try JSONSerialization.jsonObject(with: objectData, options: [])
guard var json = jsonObject as? [String: Any] else { throw MyError.encodingError }
for key in keys {
json[key] = nil
}
return json
}
}
Snapshot Extensions
import Foundation
import FirebaseFirestore
extension DocumentSnapshot {
func decode<T: Decodable>(as objectType: T.Type, includingId: Bool = true) throws -> T {
var documentJson = data()
if includingId {
documentJson!["id"] = documentID
}
let documentData = try JSONSerialization.data(withJSONObject: documentJson!, options: [])
let decodedObject = try JSONDecoder().decode(objectType, from: documentData)
return decodedObject
}
}

The Firestore structure cannot have collection as children of other collections.
The answer to your question (How can I add a collection and make the 'User' collection a sub collection from this new collection?) is you cannot. Instead you must put a document between those two collections.
Read this for more information.
It says: Notice the alternating pattern of collections and documents. Your collections and documents must always follow this pattern. You cannot reference a collection in a collection or a document in a document.

Related

Unable to access certain Firestore methods but exact code has no problem in separate project

Originally wrote my Firebase/Firestore code in a separate project and am now beginning to manually integrate that code into the main tree. The exact code snippet throws no errors in separate project but does in the main:
import Foundation
import Firebase
import FirebaseFirestore
struct Title: Codable, Equatable {
var id: Int
var type: String
var title: String
var overview: String?
var imagePath: String?
}
class Titles: ObservableObject {
#Published var content: [Title]
#Published var title: Title = Title(id: -999, type: "init", title: "...")
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
init() {
self.content = []
}
deinit {
unsubscribe()
}
func unsubscribe() {
if listenerRegistration != nil {
listenerRegistration?.remove()
listenerRegistration = nil
}
}
func subscribe(_ uid: String) {
if listenerRegistration == nil {
listenerRegistration = db.collection("Lib").document(uid).collection("userLib").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.content = documents.compactMap { queryDocumentSnapshot in
// throwing: Argument passed to call that takes no arguments
// & Cannot convert value of type '()' to closure result type 'Title?'
try? queryDocumentSnapshot.data(as: Title.self)
}
}
}
}
func writeLibrary(_ uid: String) {
let titleDoc = db.collection("Lib").document(uid).collection("userLib").document(UUID().uuidString)
do {
// throwing: No exact matches in call to instance method 'setData
try titleDoc.setData(from: title.self)
print("writeLibrary() created document")
} catch {
print("Error writing from writeLirbary() \(error)")
}
}
}
As noted with the comments in above, the errors are thrown at the try? data(as: ) and try setData(from: ) methods.
Not sure if there is an issue with how Swift Package Manager imported dependancies or if I am missing an import. For example, under the imported package in Firebase master/Firestore/Swift/Source/Codable/ the DocumentSnapsot+ReadDecodable.swift is there but for some reason isn't imported.
Any community perspectives on this would be helpful.

How to download/map date from Firestore into a Swift Collection View

I am able to download the data & see it in the Xcode debug console when I print ("(products)" after completion(true) but when I try to use the products variable in the View Controller & print it's contents there I get an empty array []. How do I use the data after in a collection view after it is downloaded?
Model
import Foundation
import UIKit
import FirebaseFirestoreSwift
public struct StoreProducts: Codable {
#DocumentID var id: String?
var orderNumber: Int?
var country: String?
var description: String?
var price: Int?
var duration: String?
enum CodingKeys: String, CodingKey {
case orderNumber
case country
case description
case price
case duration
}
}
Model Class
import Foundation
import FirebaseFirestore
class StoreViewModel: ObservableObject {
public static let shared = StoreViewModel()
private let productsCollection: String = "products/country/subscription"
#Published var products : [StoreProducts]?
private var db = Firestore.firestore()
public func fetchProductData(completion: #escaping (Bool) -> Void) {
db.collection(canadianProductsCollection).getDocuments() { [self] (querySnapshot, err) in
//Handle Error:
if let err = err {
print("Error getting documents: \(err)")
completion(false)
} else {
//No Documents Found:
guard let documents = querySnapshot?.documents else {
print("no documents")
completion(false)
return
}
//Documents Found:
let products = documents.compactMap { document -> StoreProducts? in
return try! document.data(as: StoreProducts.self)
}
completion(true)
print ("\(products)")
}
}
}
}
View Controller
import Firebase
import FirebaseDatabase
class HomeViewController: UIViewController {
#ObservedObject private var storeViewModel = StoreViewModel()
override func viewWillAppear(_ animated: Bool) {
StoreViewModel.shared.fetchProductData(completion: { success in
if success {
print("Data loaded successfully")
print (storeViewModel.products)
} else {
//some break routine
}
})
}
}
You are storing the results from the database to a local variable and it is not passed on to your storeViewModel.
products = documents.compactMap { document -> StoreProducts? in
return try! document.data(as: StoreProducts.self)
}
I think removing the "let" might solve the problem.

Swift Realm Results Convert to Model

Is there any simple way to map a Realm request to a Swift Model (struct) when it is just a single row?
When it is an array of data I can do something like this and work with the array. This is not working on a single row.
func toArray<T>(ofType: T.Type) -> [T] {
return compactMap { $0 as? T }
}
But what is best to do when just a single row of data?
my databases are big so doing it manually is just a pain and ugly.
It would also be nice when the Swift Model is not 100% the same as the Realm Model. Say one has 30 elements and the other only 20. Just match up the required data.
Thank you.
On my apps I m using this class to do all actions. I hope that's a solution for your situation. There is main actions for realm.
Usage
class Test: Object {
var name: String?
}
class ExampleViewController: UIViewController {
private let realm = CoreRealm()
override func viewDidLoad() {
super.viewDidLoad()
let data = realm.getArray(selectedType: Test.self)
}
import RealmSwift
class CoreRealm {
// Usage Example:
// let testObject = RealmExampleModel(value: ["age":1 , "name":"Name"])
// let testSubObject = TestObject(value: ["name": "FerhanSub", "surname": "AkkanSub"])
// testObject.obje.append(testSubObject)
let realm = try! Realm()
func deleteDatabase() {
try! realm.write {
realm.deleteAll()
}
}
func delete<T: Object>(selectedType: T.Type) {
try! realm.write {
let object = realm.objects(selectedType)
realm.delete(object)
}
}
func delete<T: Object>(selectedType: T.Type, index: Int) {
try! realm.write {
let object = realm.objects(selectedType)
realm.delete(object[index])
}
}
func add<T: Object>(_ selectedObject: T) {
do {
try realm.write {
realm.add(selectedObject)
}
} catch let error as NSError {
print(error.localizedDescription)
}
}
// return Diretly object
func getArray<T: Object>(selectedType: T.Type) -> [T]{
let object = realm.objects(selectedType)
var array = [T]()
for data in object {
array.append(data)
}
return array
}
func getObject<T: Object>(selectedType: T.Type, index: Int) -> T{
let object = realm.objects(selectedType)
var array = [T]()
for data in object {
array.append(data)
}
return array[index]
}
// return Result tyle
func getResults<T: Object>(selectedType: T.Type) -> Results<T> {
return realm.objects(selectedType)
}
func getResult<T: Object>(selectedType: T.Type) -> T? {
return realm.objects(selectedType).first
}
func createJsonToDB<T: Object>(jsonData data: Data, formatType: T.Type) {
// let data = "{\"name\": \"San Francisco\", \"cityId\": 123}".data(using: .utf8)!
let realm = try! Realm()
try! realm.write {
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
realm.create(formatType, value: json, update: .modified)
} catch {
print("Json parsing error line 65")
}
}
}
}

Swift app crashing when attempting to fetch contacts

I am creating an onboarding portion of an app which gets the user's contacts to check which already have the app to add as friends. I'm using the CNContact framework. I have created several methods I'm using to get a full list of the users' contacts, check if they have the app, and enumerate them in a UITableView. However, when the view loads, the app crashes with the error "A property was not requested when contact was fetched." I already make a fetch request with the keys CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey, ad CNContactImageDataKey. I've included all my code here:
import Foundation
import Contacts
import PhoneNumberKit
struct ContactService {
static func createContactArray() -> [CNContact] {
var tempContacts = [CNContact]()
let store = CNContactStore()
store.requestAccess(for: .contacts) { (granted, error) in
if let _ = error {
print("failed to request access to contacts")
return
}
if granted {
let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey, CNContactImageDataKey]
let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
request.sortOrder = CNContactSortOrder.familyName
do {
try store.enumerateContacts(with: request, usingBlock: { (contact, stop) in
tempContacts.append(contact)
})
} catch {
print("unable to fetch contacts")
}
print("created contact list")
}
}
return tempContacts
}
static func createFetchedContactArray(contactArray: [CNContact], completion: #escaping ([FetchedContact]?) -> Void) -> Void {
var temp = [FetchedContact]()
getNumsInFirestore { (nums) in
if let nums = nums {
for c in contactArray {
let f = FetchedContact(cnContact: c, numsInFirebase: nums)
temp.append(f)
}
return completion(temp)
} else {
print("Error retrieving numbers")
}
}
return completion(nil)
}
static func getNumsInFirestore(_ completion: #escaping (_ nums : [String]?) -> Void ) -> Void {
var numsInFirebase = [String]()
let db = FirestoreService.db
db.collection(Constants.Firestore.Collections.users).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting user documents: \(err)")
completion(nil)
} else {
for doc in querySnapshot!.documents {
let dict = doc.data()
numsInFirebase.append(dict[Constants.Firestore.Keys.phone] as! String)
}
completion(numsInFirebase)
}
}
}
static func getPhoneStrings(contact: CNContact) -> [String] {
var temp = [String]()
let cnPhoneNums = contact.phoneNumbers
for n in cnPhoneNums {
temp.append(n.value.stringValue)
}
return temp
}
static func hasBump(contact: CNContact, completion: #escaping (_ h: Bool) -> Void ) -> Void {
let contactNums = ContactService.getPhoneStrings(contact: contact)
ContactService.getNumsInFirestore { (nums) in
if let nums = nums {
return completion(contactNums.contains(where: nums.contains))
} else {
print("Error retrieving numbers from firestore")
return completion(false)
}
}
}
static func anyContactsWithBump(completion: #escaping (_ yes: Bool) -> Void) {
let contacts = createContactArray()
var tempBool = false
let workgroup = DispatchGroup()
for c in contacts {
workgroup.enter()
hasBump(contact: c) { (has) in
if has {
tempBool = true
}
workgroup.leave()
}
}
workgroup.notify(queue: .main) {
completion(tempBool)
}
}
}
Then I call the methods in the view controller class:
import UIKit
import Contacts
import FirebaseDatabase
import Firebase
import FirebaseFirestore
class AddContactsVC: UIViewController {
var fetchedContactsWithBump: [FetchedContact] = []
override func viewDidLoad() {
let cnContacts = ContactService.createContactArray()
var contactsWithBump = [CNContact]()
let workgroup = DispatchGroup()
for contact in cnContacts {
workgroup.enter()
ContactService.hasBump(contact: contact) { (has) in
if has {
contactsWithBump.append(contact)
}
workgroup.leave()
}
}
workgroup.notify(queue: .main) {
print("Number of contacts with Bump: \(contactsWithBump.count)")
ContactService.createFetchedContactArray(contactArray: contactsWithBump) { (fetchedContacts) in
if let fetchedContacts = fetchedContacts {
self.fetchedContactsWithBump = fetchedContacts
} else {
print("Error creating fetchedContacts array.")
}
}
}
I also get the message "Error creating fetchedContacts array" in the console, so I know something is going wrong with that method, I'm just not sure what. Any help is appreciated!
Edit: The exception is thrown at 3 points: 1 at the first line of my FetchedContact init method, which looks like this:
init(cnContact: CNContact, numsInFirebase: [String]) {
if cnContact.imageDataAvailable { self.image = UIImage(data: cnContact.imageData!) }
It also points to the line let f = FetchedContact(cnContact: c, numsInFirebase: nums) in createFetched contact array, and finally at my completion(numsInFirebase) call in getNumsInFirestore.
To start with
let contacts = createContactArray()
will always return an empty array.
This function has a return statement outside the closure so will immediately return an empty array.
Change createContactArray to use a completion handler like the other functions you have to populate contacts from inside the closure.

Hold reference to downloaded DynamoDB data

I have a class holding a DynamoDB model (I cut the # of variables for brevity, but they're all Optional Strings:
import AWSCore
import AWSDynamoDB
#objcMembers class Article: AWSDynamoDBObjectModel, AWSDynamoDBModeling {
var _articleSource: String?
class func dynamoDBTableName() -> String {
return "article"
}
class func hashKeyAttribute() -> String {
return "_articleId"
}
class func rangeKeyAttribute() -> String {
return "_articleUrl"
}
override class func jsonKeyPathsByPropertyKey() -> [AnyHashable: Any] {
return [
"_articleSource" : "articles.articleSource",
]
}
}
In my View Controller, I'm downloading data from the table and storing each article in an array like this:
let dynamoDbObjectMapper = AWSDynamoDBObjectMapper.default()
var allArticles = [AnyObject]()
func getArticles(completed: #escaping DownloadComplete) {
let scanExpression = AWSDynamoDBScanExpression()
scanExpression.limit = 50
self.dynamoDbObjectMapper.scan(Article.self, expression: scanExpression).continueWith(block: { (task:AWSTask<AWSDynamoDBPaginatedOutput>!) -> Any? in
if let error = task.error as NSError? {
print("The request failed. Error: \(error)")
} else if let paginatedOutput = task.result {
for article in paginatedOutput.items as! [Article] {
self.allArticles.append(article)
}
}
return(self.allArticles)
})
completed()
}
When I try to work with the data that should be stored in allArticles the array is empty. However, the array holds articles when I break execution in the download block where articles are being appended. How can I hold reference to the downloaded data? My use of a completion block was my attempt.
Edit: allArticles is of type [AnyObject] because I'm attempting to store objects from 3 different classes total in the same array to make it easier to work with in a TableView
The array wasn't empty after all, I just didn't realize this was all async (duh...)
I just needed:
DispatchQueue.main.async {
self.tableView.reloadData()
}
in place of completed() in the getArticles() func