What is the equivalent of "DatabaseHandle" in firestore? - swift

I am converting the Firebase Realtime database functions to Firestore Database, I am not able to get the equivalent for "DatabaseHandle" as in the below code to configure the database, I need to convert the database for the chat application , this is the tutorial I have been following tutorial reference!
fileprivate var _refHandle: DatabaseHandle?
deinit {
if let refHandle = _refHandle {
self.ref.child("messages").removeObserver(withHandle: refHandle)
}
}
func configureDatabase() {
//ref = Firestore.firestore() // Listen for new messages in the Firebase database
_refHandle = self.ref.collection("messages").observe(.childAdded, with: { [weak self] (snapshot) -> Void in
guard let strongSelf = self else { return }
strongSelf.messages.append(snapshot)
strongSelf.clientTable.insertRows(at: [IndexPath(row: strongSelf.messages.count-1, section: 0)], with: .automatic)
})
}

There is lot of differences in both the database but you can find most of the similarities. Like DatabaseHandle is equivalent to "CollectionReference" and observer's are equivalent to "Listeners".
Rather than following any code, you must refer to official documentation which has a snippet for all features and then you will able to make the difference.
Doc: https://firebase.google.com/docs/firestore
Let me know if you need any more clarification.

Related

Firestore async issue

I'm calling a Firestore query that does come back, but I need to ensure completion before moving on with the rest of the code. So I need a completion handler...but for the life of me I can't seem to code it.
// get user info from db
func getUser() async {
self.db.collection("userSetting").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let userTrust = document.data()["userTrust"] as! String
let userGrade = document.data()["userGrade"] as! String
let userDisclaimer = document.data()["userDisclaimer"] as! String
var row = [String]()
row.append(userTrust)
row.append(userGrade)
row.append(userDisclaimer)
self.userArray.append(row)
// set google firebase analytics user info
self.userTrustInfo = userTrust
self.userGradeInfo = userGrade
}
}
}
}
Called by:
internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
db = Firestore.firestore()
Database.database().isPersistenceEnabled = true
Task {
do {
let userInfo = await getUser()
}
} return true }
I used a Task as didFinishLauncingWithOptions is synchronous and not asynchronous
However, the getUser() still isn't completed before didFinishLauncingWithOptions moves on.
I need the data from getUser as the very next step uses the data in the array, and without it I get an 'out of bounds exception' as the array is still empty.
Also tried using dispatch group within the func getUser(). Again with no joy.
Finally tried a completion handler:
func getUser(completion: #escaping (Bool) -> Void) {
self.db.collection("userSetting").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
let userTrust = document.data()["userTrust"] as! String
let userGrade = document.data()["userGrade"] as! String
let userDisclaimer = document.data()["userDisclaimer"] as! String
var row = [String]()
row.append(userTrust)
row.append(userGrade)
row.append(userDisclaimer)
self.userArray.append(row)
// set google firebase analytics user info
self.userTrustInfo = userTrust
self.userGradeInfo = userGrade
completion(true)
}
}
}
}
Nothing works. The getUser call isn't completed before the code moves on. Can someone please help. I've searched multiple times, looked at all linked answers but I can not make this work.I'm clearly missing something easy, please help
read this post: Waiting for data to be loaded on app startup.
It explains why you should never wait for data before returning from
function application(_:didFinishLaunchingWithOptions).
To achieve what you need, you could use your first ViewController as a sort of splashscreen (that only shows an image or an activity indicator) and call the function getUser(completion:) in the viewDidLoad() method the ViewController.
Example:
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
MyFirestoreDatabaseManager.shared.getUser() { success in
if success {
//TODO: Navigate to another ViewController
} else {
//TODO: Show an error
}
}
}
}
Where obviously MyFirestoreDatabaseManager.shared is the object on which you defined the getUser(completion:) method.
(In your example, I think that you defined that function in the AppDelegate. In that case, you should mark your getUser(completion:) method and all related variables as static. Then replace MyFirestoreDatabaseManager.shared with AppDelegate).
Not 100% sure what you would like to accomplish as I can't see all your code, but try something similar to this, replacing Objects for what you are trying to return from the documents.
You don't want your user's data spread across multiple documents. With Firebase you pay for every document you have to get. Ideally you want all your user's settings within one firebase document. Then create a UserInfo struct that you can decode to using the library CodeableFirebase or the decoder of your choice.
// Create user struct
struct UserInfo: Codable {
var userId: String
var userTrust: String
var userGrade: String
var userDisclaimer: String
}
// get user info from db and decode using CodableFirebase
func getUser() async throws -> UserInfo {
let doc = try await self.db.collection("users").document("userIdHere")
let userInfo = try FirestoreDecoder().decode(UserInfo.self, from: doc.data())
return UserInfo
}
Then you can do this...
Task {
do {
let userInfo = try await getUser()
let userTrust = userInfo.userTrust
let userGrade = userInfo.userGrade
let userDisclaimer = userInfo.userDisclaimer
}
}

Understanding how to correctly execute CKQueryOperation

I'm working with CloudKit for the first time and am having trouble executing a CKQueryOperation to query all records of a given type. It doesn't help that Apple has deprecated most of the stuff I've found online and that their documentation for these things are completely blank besides the func declaration. I think I've got the "skeleton" of the code done but am unsure of what goes into the .recordMatchedBlock and the .queryResultsBlock.
I have a func queryAllNotes() which should query all records in the public database of type "Notes" and return an array of tuples of the note's title and its associated cloudID, which is just the unique recordName given to it when it is added to the database.
Here's the code for queryAllNotes() :
private func queryAllNotes() -> [(title: String, cloudID: String)] {
/*
query all notes in the cloud DB into an array to populate
the tableView
*/
var resultArray: [(title: String, cloudID: String)] = []
//set the cloud database to .publicCloudDatabase
let container = CKContainer.default()
let cloudDB = container.publicCloudDatabase
let pred = NSPredicate(value: true) //true -> return all records
let query = CKQuery(recordType: "Notes", predicate: pred)
let queryOperation = CKQueryOperation(query: query)
queryOperation.database = cloudDB
queryOperation.resultsLimit = 100
queryOperation.recordMatchedBlock = { (record: CKRecord) in
let noteTitle = record["Title"] as! String
let noteCloudID = record.recordID.recordName
resultArray.append((noteTitle, noteCloudID))
}
queryOperation.queryResultBlock = { (cursor, error) in
}
return resultArray
}
To my understanding the .recordMatchedBlock is called for every record returned by the query so I think it is complete but I could be very wrong. In regards to the .queryResultBlock, my understanding is that the query technically only return one record at a time and this block basically tells the query to run again for the next record for all records within the .resultLimit. How can I structure this query? I am keen to understand what each of these blocks do.
Also this is for a macOS app; I don't know if the code is different for macOS vs iOS but I thought I should include this just in case.
Also I'm getting an error saying "Type of expression is ambiguous without more context" which I'm assuming is because I haven't completed setting up my query. If it's for a different reason could also explain why this is happening.
Edit
I call this func inside of viewDidLoad() like so:
//array var for the array that is used to populate the tableView
var noteRecords: [(title: String, cloudID: String)] = []
override func viewDidLoad() {
super.viewDidLoad()
// do additional setup here
// set serachField delegate
searchField.delegate = self
// set tableView delegate and data source
tableView.delegate = self
tableView.dataSource = self
// load all NoteRecords in public cloud db into noteRecords
noteRecords = queryAllNotes()
}
With the new async pattern it has become much easier to fetch data from CloudKit.
Instead of CKQueryOperation you call records(matching:resultsLimit:) directly and map the result to whatever you like.
A possible error is handed over to the caller.
func queryAllNotes() async throws -> [(title: String, cloudID: String)] {
//set the cloud database to .publicCloudDatabase
let container = CKContainer.default()
let cloudDB = container.publicCloudDatabase
let pred = NSPredicate(value: true) //true -> return all records
let query = CKQuery(recordType: "Notes", predicate: pred)
let (notesResults, _) = try await cloudDB.records(matching: query,
resultsLimit: 100)
return notesResults
.compactMap { _, result in
guard let record = try? result.get(),
let noteTitle = record["Title"] as? String else { return nil }
return (title: noteTitle, cloudID: record.recordID.recordName)
}
}
And use it
override func viewDidLoad() {
super.viewDidLoad()
// do additional setup here
// set serachField delegate
searchField.delegate = self
// set tableView delegate and data source
tableView.delegate = self
tableView.dataSource = self
// load all NoteRecords in public cloud db into noteRecords
Task {
do {
noteRecords = try await queryAllNotes()
tableView.reloadData()
} catch {
print(error)
}
}
}
Please watch the related video from WWDC 2021 for detailed information about the async CloudKit APIs and also the Apple examples on GitHub.
Side note:
Rather than a tuple use a struct. Tuples as data source array are discouraged.

CKShare - Failed to modify some records error - CloudKit

I'm trying to share a record with other users in CloudKit but I keep getting an error. When I tap one of the items/records on the table I'm presented with the UICloudSharingController and I can see the iMessage app icon, but when I tap on it I get an error and the UICloudSharingController disappears, the funny thing is that even after the error I can still continue using the app.
Here is what I have.
Code
var items = [CKRecord]()
var itemName: String?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = items[indexPath.row]
let share = CKShare(rootRecord: item)
if let itemName = item.object(forKey: "name") as? String {
self.itemName = item.object(forKey: "name") as? String
share[CKShareTitleKey] = "Sharing \(itemName)" as CKRecordValue?
} else {
share[CKShareTitleKey] = "" as CKRecordValue?
self.itemName = "item"
}
share[CKShareTypeKey] = "bundle.Identifier.Here" as CKRecordValue
prepareToShare(share: share, record: item)
}
private func prepareToShare(share: CKShare, record: CKRecord){
let sharingViewController = UICloudSharingController(preparationHandler: {(UICloudSharingController, handler: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
let modRecordsList = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: nil)
modRecordsList.modifyRecordsCompletionBlock = {
(record, recordID, error) in
handler(share, CKContainer.default(), error)
}
CKContainer.default().privateCloudDatabase.add(modRecordsList)
})
sharingViewController.delegate = self
sharingViewController.availablePermissions = [.allowPrivate]
self.navigationController?.present(sharingViewController, animated:true, completion:nil)
}
// Delegate Methods:
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
print("saved successfully")
}
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("failed to save: \(error.localizedDescription)")// the error is generated in this method
}
func itemThumbnailData(for csc: UICloudSharingController) -> Data? {
return nil //You can set a hero image in your share sheet. Nil uses the default.
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return self.itemName
}
ERROR
Failed to modify some records
Here is what I see...
Any idea what could be wrong?
EDIT:
By the way, the error is generated in the cloudSharingController failedToSaveShareWithError method.
Looks like you're trying to share in the default zone which isn't allowed. From the docs here
Sharing is only supported in zones with the
CKRecordZoneCapabilitySharing capability. The default zone does not
support sharing.
So you should set up a custom zone in your private database, and save your share and records there.
Possibly it is from the way you're trying to instantiate the UICloudSharingController? I cribbed my directly from the docs and it works:
let cloudSharingController = UICloudSharingController { [weak self] (controller, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
guard let `self` = self else {
return
}
self.share(rootRecord: rootRecord, completion: completion)
}
If that's not the problem it's something with either one or both of the records themselves. If you upload the record without trying to share it, does it work?
EDIT TO ADD:
What is the CKShareTypeKey? I don't use that in my app. Also I set my system fields differently:
share?[CKShare.SystemFieldKey.title] = "Something"
Try to add this to your info.plist
<key>CKSharingSupported</key>
<true/>

Swift load firebase database data when there is a change in data

I have a tableview set up that is fetching my firebase database info like so below. How do I set up a listener to look for any new database changes? At the moment, while looking at my tableview, I can add a new item to the database but won't see the change on my tableview until i reload that view again.
var markets: [loadMyData] = []
var marketSelectedText = ""
var myKey = [Any]()
override func viewDidLoad() {
super.viewDidLoad()
ref.child("Markets").observeSingleEvent(of: .value, with: { (snapshot) in
guard snapshot.exists() else
{
print("Null Markets")
return
}
let allMarkets = (snapshot.value as! NSMutableDictionary).allKeys
self.myKey = allMarkets
self.tableView.reloadData()
print(self.myKey, " This is the printing of my key")
})
ref.child("Markets").observe(.value, with: { snapshot in
print(snapshot.value as Any, " ref.observe")
self.tableView.reloadData()
})
}
Right now you're using:
ref.child("Markets").observeSingleEvent(of: .value,
This observes the current data under Markets and then stops observing.
If you want to get both the current data and any changes, use observe(DataEventType.value.
Firebase is providing listeners to observe database changes, kindly check the firebase documentation https://firebase.google.com/docs/database/ios/lists-of-data
FIRDataEventTypeChildChanged
this is the event for listening database changes
commentsRef.observe(.childChanged , with: { (snapshot) in
// here you will get the changed node value here in snapshot.value
})

adding the duplicate data to the array: error

my problem is adding the duplicate data to the array
my program it works well before it can be refresh manually but duplicate added to list when manually refreshed
when I check the print, the data is added to the double list
print result
ARRAYLAR : ["EXAMPLE", "EXAMPLE"]
Watch the video for a better understanding of the problem
VÄ°DEO
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
getData()
}
#objc func getData() {
self.konuAdiArray.removeAll(keepingCapacity: false)
self.konuHedefTarihArray.removeAll(keepingCapacity: false)
self.konuTestArray.removeAll(keepingCapacity: false)
self.konuIDArray.removeAll(keepingCapacity: false)
self.veriGirisArray.removeAll(keepingCapacity: false)
Database.database().reference().child("users").child((Auth.auth().currentUser?.uid)!).child("dersler").child(gelenDersID!).child("konular").observe(DataEventType.childAdded) { (snapshot) in
let values = snapshot.value! as! NSDictionary
self.konuAdiArray.append(values["konuAdi"]as! String)
self.konuHedefTarihArray.append(values["konuHedefTarihi"]as! String)
self.konuTestArray.append(values["konuTestHedefi"]as! String)
self.veriGirisArray.append(values["veriGirisSoru"]as! String)
self.konuIDArray.append(snapshot.key)
print("ARRAYLAR : \(self.konuAdiArray)")
self.tableView.reloadData()
}
}
refreshBarButton code
#IBAction func refreshBarButton(_ sender: Any) {
getData()
}
You need to make separate function for observing changes of your data base. When you call getData() you code calls twice of DB changes. So that you subscribe on changes one more time that's why I'd recommend you to make separate function like setDataBaseObserver() which you call only one time in viewDidLoad. If you are updating your data you should make network request or take them from different source (not from data base again). Hope you understand me right!
func setDataBaseObserver() {
Database.database().reference().child("users").child((Auth.auth().currentUser?.uid)!).child("dersler").child(gelenDersID!).child("konular").observe(DataEventType.childAdded) { (snapshot) in
let values = snapshot.value! as! NSDictionary
self.konuAdiArray.append(values["konuAdi"]as! String)
self.konuHedefTarihArray.append(values["konuHedefTarihi"]as! String)
self.konuTestArray.append(values["konuTestHedefi"]as! String)
self.veriGirisArray.append(values["veriGirisSoru"]as! String)
self.konuIDArray.append(snapshot.key)
print("ARRAYLAR : \(self.konuAdiArray)")
self.tableView.reloadData()
}
}
Hope it will help to you!
There's no reason for the manual refresh. You are observing childAdded on a Firebase database, which will continue to update in real time. Whenever you hit the manual refresh, your getData() is adding your controller as an observer again. Either remove the manual refresh control and just let Firebase do its thing (this is what it excels at - realtime updates without manual refresh), or change your childAdded observation to be a one-time data fetch.