How to refactor duplicate Firestore document IDs in Swift? - ios

I'm doing my very first IOS app using Cloud Firestore and have to make the same queries to my database repeatedly. I would like to get rid of the duplicate lines of code. This is examples of func where documents ID are duplicated. Also I using other queries as .delete(), .addSnapshotListener(), .setData(). Should I refactor all that queries somehow or leave them because they were used just for one time?
#objc func updateUI() {
inputTranslate.text = ""
inputTranslate.backgroundColor = UIColor.clear
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { [self] (document, error) in
if let document = document, document.exists {
let document = document
let label = document.data()?.keys.randomElement()!
self.someNewWord.text = label
// Fit the label into screen
self.someNewWord.adjustsFontSizeToFitWidth = true
self.checkButton.isHidden = false
self.inputTranslate.isHidden = false
self.deleteBtn.isHidden = false
} else {
self.checkButton.isHidden = true
self.inputTranslate.isHidden = true
self.deleteBtn.isHidden = true
self.someNewWord.adjustsFontSizeToFitWidth = true
self.someNewWord.text = "Add your first word to translate"
updateUI()
}
}
}
#IBAction func checkButton(_ sender: UIButton) {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { (document, error) in
let document = document
let label = self.someNewWord.text!
let currentTranslate = document!.get(label) as? String
let translateField = self.inputTranslate.text!.lowercased().trimmingCharacters(in: .whitespaces)
if translateField == currentTranslate {
self.inputTranslate.backgroundColor = UIColor.green
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
self.inputTranslate.backgroundColor = UIColor.clear
updateUI()}
} else {
self.inputTranslate.backgroundColor = UIColor.red
self.inputTranslate.shakingAndRedBg()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
self.inputTranslate.backgroundColor = UIColor.clear
self.inputTranslate.text = ""
}
}
}
}
func deletCurrentWord () {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { (document, err) in
let document = document
if let err = err {
print("Error getting documents: \(err)")
} else {
let array = document!.data()
let counter = array!.count
if counter == 1 {
// The whole document will deleted together with a last word in list.
let user = Auth.auth().currentUser?.email
self.db.collection(K.FStore.collectionName).document(user!).delete() { err in
if let err = err {
print("Error removing document: \(err)")
} else {
self.updateUI()
}
}
} else {
// A current word will be deleted
let user = Auth.auth().currentUser?.email
let wordForDelete = self.someNewWord.text!
self.db.collection(K.FStore.collectionName).document(user!).updateData([
wordForDelete: FieldValue.delete()
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
self.updateUI()
}
}
}
}
}
}
Another query example
func loadMessages() {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.addSnapshotListener { (querySnapshot, error) in
self.messages = []
if let e = error {
print(e)
} else {
if let snapshotDocuments = querySnapshot?.data(){
for item in snapshotDocuments {
if let key = item.key as? String, let translate = item.value as? String {
let newMessage = Message(key: key, value: translate)
self.messages.append(newMessage)
}
}
DispatchQueue.main.async {
self.messages.sort(by: {$0.value > $1.value})
self.secondTableView.reloadData()
let indexPath = IndexPath(row: self.messages.count - 1, section: 0)
self.secondTableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}
}
}
}

enum Error {
case invalidUser
case noDocumentFound
}
func fetchDocument(onError: #escaping (Error) -> (), completion: #escaping (FIRQueryDocument) -> ()) {
guard let user = Auth.auth().currentUser?.email else {
onError(.invalidUser)
return
}
db.collection(K.FStore.collectionName).document(user).getDocument { (document, error) in
if let error = error {
onError(.noDocumentFound)
} else {
completion(document)
}
}
}
func updateUI() {
fetchDocument { [weak self] error in
self?.hideShowViews(shouldHide: true, newWordText: nil)
} completion: { [weak self] document in
guard document.exists else {
self?.hideShowViews(shouldHide: true, newWordText: nil)
return
}
self?.hideShowViews(shouldHide: false, newWordText: document.data()?.keys.randomElement())
}
}
private func hideShowViews(shouldHide: Bool, newWordText: String?) {
checkButton.isHidden = shouldHide
inputTranslate.isHidden = shouldHide
deleteBtn.isHidden = shouldHide
someNewWord.adjustsFontSizeToFitWidth = true
someNewWord.text = newWordText ?? "Add your first word to translate"
}
The updateUI method can easily be refactored using a simple guard statement and then taking out the common code into a separate function. I also used [weak self] so that no memory leaks or retain cycles occur.
Now, you can follow the similar approach for rest of the methods.
Use guard let instead of if let to avoid nesting.
Use [weak self] for async calls to avoid memory leaks.
Take out the common code into a separate method and use a Bool flag to hide/show views.
Update for step 3:
You can create methods similar to async APIs for getDocument() or delete() etc and on completion you can update UI or perform any action. You can also create a separate class and move the fetchDocument() and other similar methods in there and use them.

Related

Animation in cell stopping when other cell's animation finishes

I'm having an issue with the animations in my TableViewCells. Whenever someone crosses of an item on their list, a progress bar animates from 0.0 to 1.0 in 4 seconds:
func startAnimation() {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
if items![indexRow!].checked {
self.delegate?.changeButton(state: false, indexSection: indexSection!, indexRow: indexRow!, itemID: itemID!)
self.progressBar.setProgress(0.0, animated: false)
self.checkBoxOutlet.setBackgroundImage(#imageLiteral(resourceName: "checkBoxOUTLINE "), for: .normal)
} else {
self.checkBoxOutlet.setBackgroundImage(#imageLiteral(resourceName: "checkBoxFILLED "), for: .normal)
self.tempState = true
UIView.animate(withDuration: 4.0, animations: {
self.progressBar.setProgress(1.0, animated: true)
}) { (finished: Bool) in
self.workItem = DispatchWorkItem {
self.delegate?.changeButton(state: true, indexSection: self.indexSection!, indexRow: self.indexRow!, itemID: self.itemID)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3.3, execute: self.workItem!)
}
}
}
This works, and the animation performs nicely. However, whenever multiple items are being checked quickly after each other, the animation stops when the first animation triggered is completed. Here is a screenrecord of the issue.
As you can see, there are 2 major issues:
The animation of the other cells stop abruptly
Cells that aren't supposed to be deleted get deleted.
I suspect that the issues lies in the delegate method that gets triggered, and not the animation here. This is my delegate method (which updates data in Firestore):
func changeButton(state: Bool, indexSection: Int?, indexRow: Int?, itemID: String?) {
if let indexSection = indexSection, let indexRow = indexRow {
sections[indexSection].items[indexRow].checked = state
}
let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred()
if let itemID = itemID {
let itemRef = db.collection(K.FStore.lists).document(currentListID!).collection(K.FStore.sections).document("\(indexSection!)").collection(K.FStore.items).document(itemID)
if sections[indexSection!].items[indexRow!].checked {
itemRef.updateData([
K.Item.isChecked: true,
K.Item.checkedBy: currentUserID!,
K.Item.dateChecked: Date()
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
if let indexSection = indexSection, let indexRow = indexRow {
if self.sections[indexSection].items != nil {
let item = self.sections[indexSection].items[indexRow]
let itemRef = self.db.collection(K.FStore.lists).document(self.currentListID!).collection(K.FStore.sections).document("\(indexSection)").collection(K.FStore.items).document(item.itemID!)
itemRef.getDocument { (document, error) in
if let document = document, document.exists {
// Get the properties of the item
let name = document.data()?[K.Item.name] as? String
let uid = document.data()?[K.Item.uid] as? String
let category = document.data()?[K.Item.categoryNumber] as? Int
let isChecked = document.data()?[K.Item.isChecked] as? Bool
let dateCreated = document.data()?[K.Item.date] as? Date
let dateChecked = document.data()?[K.Item.dateChecked] as? Date
let checkedBy = document.data()?[K.Item.checkedBy] as? String
self.db.collection(K.lists).document(self.currentListID!).collection(K.FStore.sectionsChecked).document("\(category!)").collection(K.FStore.items).addDocument(data: [
K.Item.name: name,
K.Item.isChecked: isChecked,
K.Item.categoryNumber: category,
K.Item.date: dateCreated,
K.Item.dateChecked: dateChecked,
K.Item.checkedBy: checkedBy,
K.Item.uid: uid,
K.Item.dateDeleted: Date()
]) { err in
if let err = err {
print("Error adding document: \(err)")
} else {
let cell = self.tableView.cellForRow(at: IndexPath(item: indexRow, section: indexSection)) as? TaskCell
if let cell = cell {
cell.progressBar.setProgress(0.0, animated: false)
}
// If successful, delete the item in the normal collection
itemRef.delete() { err in
if let err = err {
print("Error removing document: \(err)")
} else {
print("Document successfully removed!")
}
}
}
}
}
}
}
}
}
} else {
itemRef.updateData([
K.Item.isChecked : false
]) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
}
}
Does it have to do with the refreshing of the cell? I'm quite lost at this point and don't know what I'm doing wrong.
If anyone could help me out that'd be absolutely great.

Using dispatch to wait for data to be appended to array?

Im trying to download data from Firestore, append it to one array to be sorted and then append it to another array once all the data has been sorted. I have a dispatchgroup which manages all the data is being downloaded before moving on however I don't know how to or where to append the sorted array ( temparray1 / temparray2)to the master array (closeunisSameCourse/nearbyUnis). I have tried using dispatch.wait to wait for both temp arrays to be appended but it just hangs.
func nearbyUnisSameCourse(completion: #escaping (_ success: Bool) -> Void) {
DispatchQueue.main.async {
self.spinner.startAnimating()
}
self.dispatchGroup.enter()
service.loadUniversityAndCourse { (uni, course) in
defer{ self.dispatchGroup.leave() }
let nearbyUnis = ClosestUnis()
let closeUniArray = nearbyUnis.getClosestUnis(University: uni)
for uni in closeUniArray {
let UniRef = Firestore.firestore().collection("User-Universities").document(uni)
self.dispatchGroup.enter()
UniRef.getDocument { (snapshot, error) in
defer{ self.dispatchGroup.leave() }
if let error = error{
print(error.localizedDescription)
}
else {
//append their data to an array
guard let data = snapshot?.data() else {return}
let stringArray = Array(data.keys)
for user in stringArray {
self.dispatchGroup.enter()
let usersRef = Firestore.firestore().collection("users").document(user)
usersRef.getDocument { (snapshot, error) in
defer{ self.dispatchGroup.leave() }
if let error = error {
print(error.localizedDescription)
}
else {
let data = snapshot?.data()
if let dictionary = data as [String:AnyObject]? {
let Info = UserInfo(dictionary: dictionary)
if Info.Course == course {
print(Info.username!)
self.tempArray1.append(Info)
self.tempArray1.sort { (time1, time2) -> Bool in
return Double(time1.Created!.seconds) > Double(time2.Created!.seconds)
}
self.closeunisSameCourse.append(contentsOf: self.tempArray1)
self.tempArray1.removeAll()
}
else {
self.tempArray2.append(Info)
print(Info.username!)
self.tempArray2.sort { (time1, time2) -> Bool in
return Double(time1.Created!.seconds) > Double(time2.Created!.seconds)
}
}
}
}
}
//end of for user loop
}
//outside user for loop
print("now appending")
self.nearbyUnis.append(contentsOf: self.tempArray2)
print(self.nearbyUnis.description)
self.tempArray2.removeAll()
}}}}
self.dispatchGroup.notify(queue: .main) {
print("Finished")
//self.spinner.stopAnimating()
//print(self.nearbyUnis.description)
self.tableView.reloadData()
completion(true)
}
}
}
fixed by sorting by name
else {
self.nearbyUnis.append(Info)
print(Info.username!)
self.nearbyUnis.sort { (time1, time2) -> Bool in
return Double(time1.Created!.seconds) >
Double(time2.Created!.seconds)}
self.nearbyUnis.sort { (uni1, uni2) -> Bool in
return uni2.University! > uni1.University!}
}

Swift: For loop to synchronous one by one until all function response

I'm new to swift and practicing my best.
I have main function appointmentCall() when it executes and in response I may get multiple appointments. Then I pass appointmentId to appointmentDetail function for more details.
All I want to is how can I set For loop to synchronous process. Means it will not execute next appointment until first is finished. At the moment it executes all appointments.
I need appointment one by one executes all function once finished executes next appointment.
AppointmentCall
-> AppoinmentDetail -> processDetail -> Completed.
Code:
func appointmentCall(_ selectedDate:Date) {
DataProvider.main.serviceGetAppointment(date: selectedDate, callback: {success, result in
do{
if(success){
print(result as! Data)
let decoder = JSONDecoder()
let response = try decoder.decode(ResponseData.self, from: result! as! Data)
if let appointments = response.appointments {
self.appData = appointments.map { AppointmentDownloadModel(appointmentModel: $0)}
}
for eachApp in self.appData {
self.appointmentDetail(AppId: appId)
}
self.tableView.reloadData()
return true
}else{
return false
}
}catch let error {
DataProvider.main.token = nil
print(error as Any)
return false
}
})
}
func appointmentDetail(AppId: Int){
DataProvider.main.serviceGetAppointmentDetail(Id: AppId , callback: {success, result in
do{
if(success){
let decoder = JSONDecoder()
let resp = try decoder.decode(AppointmentDetail.self, from: result! as! Data)
self.AppDetailData = resp
self.processDetail(appId: AppId)
return true
}else{
return false
}
}catch let error {
print(error as Any)
return false
}
})
}
func processDetail(appId: Int) {
guard let detail = AppDetailData, AppDetailData?.appointmentId == appId else {
return
}
for firmParam in (detail.sectionList ?? []) {
for firmItem in firmParam.items! {
if firmItem.actionParamData != nil {
let str = firmItem.actionParamData
let param = str?.components(separatedBy: ":")
let final = param![1].replacingOccurrences(of: "}", with: "")
let fmId = final.components(separatedBy: ",")
let frmId = fmId[0]
self.firmDetails(actionParamData: Int(frmId) ?? 0)
}
//pdf download
if firmItem.actionType == 2 {
if firmItem.actionUrl != nil {
self.contentLength(link: firmItem.actionUrl!)
let fileURL = URL(fileURLWithPath: firmItem.actionUrl ?? "")
let fileTitle = firmItem.textField ?? ""
self.downloadPDFTask(pdfURL: firmItem.actionUrl ?? "")
}
}
}
}
}
If you use a recursive function with completion handler it would ensure you could essentially loop over an array and call a function to do something and know that it has completed before you do the same to your next value. See below to high level generic example
func doSomething()
{
// Call this function with the first index in the array
firstFunction(index: 0)
// Now were here and we know that every value in the member variable array has been processed by "secondFunction"
}
func firstFunction(index: Int)
{
let value = memberVariableArray[index]
secondFunction(String: value) { (returnValueFromSecondFunction) in
// do something with the return value from the second function
// Recall this function with the next index (if we have another one)
let newIndex = index + 1
if newIndex < memberVariableArray.count
{
self.firstFunction(index: newIndex)
}
}
}
func secondFunction(valueToDoSomethingWith: String, completion: #escaping (String) -> Void)
{
// Do something here
// Complete function
completion("The Value you want back")
}

Optional Still Returning Nil After Assigning Value

I am working on a similar feature to 'liking/unliking a post'.
I have an MVVM architecture as;
struct MyStructModel {
var isLiked: Bool? = false
}
class MyStructView {
var isLiked: Bool
init(myStructModel: MyStructModel) {
self.isLiked = myStructModel.isLiked ?? false
}
}
I successfully get the value of whether the post is liked or not here;
func isPostLiked(documentID: String, completion: #escaping (Bool) -> Void) {
guard let authID = auth.id else { return }
let query = reference(to: .users).document(authID).collection("liked").document(documentID)
query.getDocument { (snapshot, error) in
if error != nil {
print(error as Any)
return
}
guard let data = snapshot?.data() else { return }
if let value = data["isLiked"] as? Bool {
completion(value)
} else {
completion(false)
}
}
}
func retrieveReviews(completion: #escaping([MyStructModel]) -> ()) {
var posts = [MyStructModel]()
let query = reference(to: .posts).order(by: "createdAt", descending: true)
query.getDocuments { (snapshot, error) in
if error != nil {
print(error as Any)
return
}
guard let snapshotDocuments = snapshot?.documents else { return }
for document in snapshotDocuments {
if var post = try? JSONDecoder().decodeQuery(MyStructModel.self, fromJSONObject: document.decode()) {
// isLiked is nil here...
self.isPostLiked(documentID: post.documentID!) { (isLiked) in
post.isLiked = isLiked
print("MODEL SAYS: \(post.isLiked!)")
// isLiked is correct value here...
}
posts.append(post)
}
completion(posts)
}
}
}
However, when it gets to my cell the value is still nil.
Adding Cell Code:
var post: MyStructView? {
didSet {
guard let post = post else { return }
print(post.isLiked!)
}
}
Your isLiked property is likely nil in your cells because the retrieveReviews function doesn't wait for the isPostLiked function to complete before completing itself.
You could easily solve this issue by using DispatchGroups. This would allow you to make sure all of your Posts have their isLiked value properly set before being inserted in the array, and then simply use the DispatchGroup's notify block to return all the loaded posts via the completion handler:
func retrieveReviews(completion: #escaping([MyStructModel]) -> ()) {
var posts = [MyStructModel]()
let query = reference(to: .posts).order(by: "createdAt", descending: true)
query.getDocuments { [weak self] (snapshot, error) in
guard let self = self else { return }
if error != nil {
return
}
guard let documents = snapshot?.documents else { return }
let dispatchGroup = DispatchGroup()
for document in documents {
dispatchGroup.enter()
if var post = try? JSONDecoder().decodeQuery(MyStructModel.self, fromJSONObject: document.decode()) {
self.isPostLiked(documentID: post.documentID!) { isLiked in
post.isLiked = isLiked
posts.append(post)
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
completion(posts)
}
}
}

Completion block for nested requests

I am trying to build methods with completion blocks for nested requests. The issue is that a completion block catches to early for parent requests (meaning that the child requests haven't actually completed yet). So far I haven't found a way for a child request to communicate back to the parent request other than what I've done in my example below (which is to count the amount of child requests have completed and compare it against the expected amount of requests).
The example below is working against a Firestore database. Imagine a user has multiple card games (decks) with each multiple cards. I'm grateful for any help how to build better completion blocks for cases like these:
func fetchCardsCount(uid: String, completion: #escaping (Int) -> ()) {
let db = Firestore.firestore()
var decksCount = Int()
var cardsCount = Int()
db.collection("users").document(uid).collection("decks").getDocuments { (deckSnapshot, err) in
if let err = err {
print("Error fetching decks for user: ", err)
} else {
guard let deckSnapshot = deckSnapshot else { return }
deckSnapshot.documents.forEach({ (deck) in
let dictionary = deck.data() as [String: Any]
let deck = FSDeck(dictionary: dictionary)
db.collection("users").document(uid).collection("decks").document(deck.deckId).collection("cards").getDocuments(completion: { (cardSnapshot, err) in
if let err = err {
print("Error fetching cards for deck: ", err)
} else {
guard let cardSnapshot = cardSnapshot else { return }
decksCount += 1
cardsCount += cardSnapshot.count
if decksCount == deckSnapshot.count {
completion(cardsCount)
}
}
})
})
}
}
}
Here is the solution, using DispatchGroup, found with #meggar's help in the comments:
func fetchCardsCount(uid: String, completion: #escaping (Int) -> ()) {
let db = Firestore.firestore()
var cardsCount = Int()
let group = DispatchGroup()
group.enter()
db.collection("users").document(uid).collection("decks").getDocuments { (deckSnapshot, err) in
if let err = err {
print("Error fetching decks for user: ", err)
} else {
guard let deckSnapshot = deckSnapshot else { return }
deckSnapshot.documents.forEach({ (deck) in
let dictionary = deck.data() as [String: Any]
let deck = FSDeck(dictionary: dictionary)
group.enter()
db.collection("users").document(uid).collection("decks").document(deck.deckId).collection("cards").getDocuments(completion: { (cardSnapshot, err) in
if let err = err {
print("Error fetching cards for deck: ", err)
} else {
guard let cardSnapshot = cardSnapshot else { return }
cardsCount += cardSnapshot.count
}
group.leave()
})
})
}
group.leave()
}
group.notify(queue: .main) {
completion(cardsCount)
}
}

Resources