Swift: Crash when loading a Table many times - ios

I have following issue:
In my App, you have online recipes now imagine a TabViewController. On the first two pages of this TabViewController you have a view displaying recipes stored on the Realtime Database of Firebase. On the third one you have a simple view with some buttons, but no Firebase used and imported. The problem now is, when I slam the Bottom Bar multiple times and therefore switch the TabViewController multiple times in a second the app crashes. This probably because of Firebase reloading everytime since there is a TabViewController change, maybe resulting in a overload.
Now I get following error:
Fatal error: Index out of range: file /AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.8/swift/stdlib/public/core/ContiguousArrayBuffer.swift, line 444
2020-05-22 16:44:28.057640+0200 GestureCook[10451:3103426] Fatal error: Index out of range: file /AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.8/swift/stdlib/public/core/ContiguousArrayBuffer.swift, line 444
It highlights this code let recipe = myRecipes[indexPath.row] with the index out of range. Now how can I either reduce load on the server or avoid this error?
The reason why there is an increased load is probably since I have to fetch multiple recipes from different locations at once like this simplified example:
// dict is a list of recipe IDs
// And the GetRecipeService.getRecipe is a service which gets a recipe using a .observeSingleEvent (this causes these requests)
for child in dict {
GetRecipeService.getRecipe(recipeUID: child.key) { recipe in
self.myRecipes.append(recipe ?? [String:Any]())
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
How could I reduce load? Is there something as multipath udpates in Firebase, but just as a get, so I don't have to load 10-20 recipes with a .observeSingleEvent?

First of all the DispatchQueue block is at the wrong place. It must be inside the closure
GetRecipeService.getRecipe(recipeUID: child.key) { recipe in
self.myRecipes.append(recipe ?? [String:Any]())
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
To manage multiple asynchronous requests in a loop there is an API: DispatchGroup
let group = DispatchGroup()
for child in dict {
group.enter()
GetRecipeService.getRecipe(recipeUID: child.key) { recipe in
self.myRecipes.append(recipe ?? [String:Any]())
group.leave()
}
}
group.notify(queue: .main) {
self.tableView.reloadData()
}

Related

Apple Core Data saving duplicates of entities when initializing entities

I am currently building an iOS app that utilizes Core Data to cache Firebase queries, so that my app won't be hitting the Firebase server multiple times for the same query.
My main issue right now is with fetching what will be labeled as IdeaEntity, where the initial fetch will result in the published array of IdeaEntity storing duplicates of the IdeaEntity elements. I will post images below, along with the code snippet for how I'm fetching IdeaEntity arrays from Core Data. However, when re-opening the app (when you close it down via app preview), the duplicates seem to disappear.
iOS Simulator 16.2; Showing proof of duplicate IdeaEntity
// Fetch either from Firebase or Core Data
func fetchIdeas() {
let request = NSFetchRequest<IdeaEntity>(entityName: "IdeaEntity")
do {
self.savedIdeas = try appContainer.viewContext.fetch(request)
if self.savedIdeas.isEmpty {
self.initialFetchIdeas()
}
} catch {
print("ERROR - \(error)")
}
}
// Initial Function to fetch IdeaEntity
private func initialFetchIdeas() {
do {
let uid = self.firebaseService.fetchUserID()
guard !uid.isEmpty else { throw AltyrErrors.cannotFetchUid }
self.firebaseService.fetchUser(withUid: uid) { [weak self] user in
guard let self = self else { return }
self.firebaseService.fetchIdeas(user: user) { ideas in
guard !(ideas.isEmpty) else { return }
ideas.forEach { idea in
let newIdea = IdeaEntity(context: self.appContainer.viewContext)
// ... Insert code for inserting parameters for `newIdea`
if let feedback = idea.feedback {
let newFeedback = FeedbackEntity(context: self.appContainer.viewContext)
// ... Insert code for inserting parameters for `newIdea` Feedback object
newIdea.feedback = newFeedback
}
}
self.save()
}
}
} catch {
// TODO: Error handling...
print("ERROR - \(error)")
}
}
Couple of assumptions:
FirebaseService functions are assumed to be 100% functional, as in they output the needed items in a proper manner.
The above functions are in a class named CoreDataService, which is a class that inherits from ObservableObject
savedProjects is a Published array of IdeaEntity
If there is a better manner of fetching with another library, a different method for fetching IdeaEntity, need any other code snippets / signatures, or anything else, please do let me know.
I had tried switching around fetchIdea with initialFetchIdeas within the private init (since CoreDataService is a singleton) to see if fetchIdea was missing anything that initialFetchIdeas had, though the result is the same (e.g., duplicate entities).
Edit: TL;DR of the solution, just use NSCache if you have really complex objects. It's not worth the effort to utilize Core Data.

How to run a Firestore query inside a map function in Swift

I am new to SwiftUI and Firebase and I am trying to build my first app. I am storing Game documents in Firestore and one of the fields is an array containing the user ids of the players as you can see in the image.
Game data structure
That being said, I am trying to list all games of a given user and have all the players listed in each one of the cells (the order is important).
In order to create the list of games in the UI I created a GameCellListView and a GameCellViewModel. The GameCellViewModel should load both the games and the array of users that correspond to the players of each game. However I am not being able to load the users to an array. I have to go through the players array and query the database for each Id and append to a User array; then I should be able to return this User array. Since I'm using a for loop, I can't assign the values to the array and then return it. I tried using map(), but I can't perform a query inside of it.
The goal is to load that "all" var with a struct that receives a game and its players GamePlayers(players: [User], game: Game)
It should look something like the code snippet below, but the users array always comes empty. This function runs on GameCellViewModel init.
I hope you can understand my problem and thank you in advance! Been stuck on this for 2 weeks now
func loadData() {
let userId = Auth.auth().currentUser?.uid
db.collection("games")
.order(by: "createdTime")
.whereField("userId", isEqualTo: userId)
.addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
self.games = querySnapshot.documents.compactMap { document in
do {
let extractedGame = try document.data(as: Game.self)
var user = [User]()
let users = extractedGame!.players.map { playerId -> [User] in
self.db.collection("users")
.whereField("uid", isEqualTo: playerId)
.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
user = documents.compactMap { queryDocumentSnapshot -> User? in
return try? queryDocumentSnapshot.data(as: User.self)
}
}
return user
}
self.all.append(GamePlayers(players: users.first ?? [User](), game: extractedGame!))
return extractedGame
}
catch {
print(error)
}
return nil
}
}
}
}
There are a lot of moving parts in your code and so to isolate points of failure would require seeing additional code so just be aware of that upfront. That said, if you are relatively new to Firestore or Swift then I would strongly suggest you first get a handle on this function using basic syntax. Once you're comfortable with the ins and outs of async looping then I would suggest refactoring the code using more advanced syntax, like you have here.
Your function requires performing async work within each loop iteration (of each document). You actually need to do this twice, async work within a loop within a loop. Be sure this is what you really want to do before proceeding because there may be cleaner ways, which may include a more efficient NoSQL data architecture. Regardless, for the purposes of this function, start with the most basic syntax there is for the job which is the Dispatch Group in concert with the for-loop. Go ahead and nest these until you have it working and then consider refactoring.
func loadData() {
// Always safely unwrap the user ID and never assume it is there.
guard let userId = Auth.auth().currentUser?.uid else {
return
}
// Query the database.
db.collection("games").whereField("userId", isEqualTo: userId).order(by: "createdTime").addSnapshotListener { (querySnapshot, error) in
if let querySnapshot = querySnapshot {
// We need to loop through a number of documents and perform
// async tasks within them so instantiate a Dispatch Group
// outside of the loop.
let dispatch = DispatchGroup()
for doc in querySnapshot.documents {
// Everytime you enter the loop, enter the dispatch.
dispatch.enter()
do {
// Do something with this document.
// You want to perform an additional async task in here,
// so fire up another dispatch and repeat these steps.
// Consider partitioning these tasks into separate functions
// for readability.
// At some point in this do block, we must leave the dispatch.
dispatch.leave()
} catch {
print(error)
// Everytime you leave this iteration, no matter the reason,
// even on error, you must leave the dispatch.
dispatch.leave()
// If there is an error in this iteration, do not return.
// Return will return out of the method itself (loadData).
// Instead, continue, which will continue the loop.
continue
}
}
dispatch.notify(queue: .main) {
// This is the completion handler of the dispatch.
// Your first round of data is ready, now proceed.
}
} else if let error = error {
// Always log errors to console!!!
// This should be automatic by now without even having to think about it.
print(error)
}
}
}
I also noticed that within the second set of async tasks within the second loop, you're adding snapshot listeners. Are you really sure you want to do this? Don't you just need a plain document get?

Running custom method of the main thread

class func loadData(
onCompletition: #escaping ([LocationInfo])->Void){
let workingQueue = DispatchQueue.global(qos:.utility)
let completitionQueue = DispatchQueue.main
workingQueue.sync {
print("\n Data fetch started \n")
let root = FIRDatabase.database().reference()
let locationSummary = root.child("LocSummary")
locationSummary.observeSingleEvent(of: .value,with: { (snapshot) in
for item in snapshot.children{
let locationInfo = LocationInfo(snapshot: item as! FIRDataSnapshot)
FirebaseDataController.resultsArray.append(locationInfo)
}
completitionQueue.async {
print("\n data fetch completed \n ")
onCompletition(FirebaseDataController.resultsArray)
print("After on completion method")
}
})
}
}
The problem I have no is that, every time I want to access the data inside the results array I have to go through this functions completion handler. Which is not something I can do all the time specially when I want to work with table views and such (I have a seperate class to handle all DB interactions and many other classes to handle table view interactions).
My objective is to run this code at the start of the application may be through the AppDelegate and have a populated array that I can call anytime I want access to data.
To do this I think I need to run this code on the main thread. I tried that by substituting the workingQueue with the main thread but the application keeps crashing.
Is there is anything that I can do about this?
Your application will crash only if you call this synchronously on main thread when launching application because you cause a deadlock. If you run this function in application:didFinishLaunching: method of AppDelegate you should be able to use safely this implementation:
class func loadData(onCompletition: #escaping ([LocationInfo])->Void){
let completitionQueue = DispatchQueue.main
print("\n Data fetch started \n")
let root = FIRDatabase.database().reference()
let locationSummary = root.child("LocSummary")
locationSummary.observeSingleEvent(of: .value,with: { (snapshot) in
for item in snapshot.children{
let locationInfo = LocationInfo(snapshot: item as! FIRDataSnapshot)
FirebaseDataController.resultsArray.append(locationInfo)
}
completitionQueue.async {
print("\n data fetch completed \n ")
onCompletition(FirebaseDataController.resultsArray)
print("After on completion method")
}
})
}
Your guess that you need to perform this operations on the main thread is wrong - there is no reason to do that, in fact, you will end up with stalled user interface. Fetch the data, and then asynchronously update user interface on main thread.
Since you want to call the method on main thread.
First thing you remove all the code related to DispatchQueue.
Then call the loadData using - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

Best way to use findObjectsInBackgroundWithBlock when moving data around

I have a Tableview that gets data with findObjectsInBackgroundWithBlock in viewDidLoad and passes that data to a Detail View Controller no problem.
Im having trouble managing the flow of findObjectsInBackgroundWithBlock. Here is a example: I have a like button on the detail view and when pressed it increments the UILabel and displays it. It also then gets the object in Parse then increments and saves it... Everything good.
#IBAction func likeButtonPressed(sender: AnyObject) {
print("likeButtonPressed()")
// Adding the like to label
mixLike!++
var stringForCount: String = String(mixLike!)
mixLikeLabel.text = stringForCount
// Saving the like back to Parse
var query = PFQuery(className: "musicMixes")
query.whereKey("info", equalTo: mixNameLabel.text)
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!, error:NSError!) -> Void in
if error == nil {
for object in objects {
//var votes = object["votes"] as! Int
let mixObject:PFObject = object as! PFObject
mixObject.incrementKey("votes", byAmount: 1)
mixObject.saveInBackgroundWithTarget(nil, selector: nil)
print("mixObjectSaved")
}
} else {
print("Error getLikeCount()")
}
print("sending Notification...")
NSNotificationCenter.defaultCenter().postNotificationName("reload", object: nil)
print("sent Notification...")
}
} // likeButtonPressed End
I also then call a NSNotification back to the Table View so the Table View can update the likes to match the users like click on the detail view (See bellow)
The NSNotification calls this function in the Table View, which removes the like array, grabs the new likes again and then reloads the Table View.
# objc func reloadTableData(notification: NSNotification){
print("Notification Recived, Removing Likes and Reloading. reloadTableData()...")
self.mixLikeArray.removeAll()
//self.stringForCountArray.removeAll()
print("Like array Data removed, getting data again...")
var query = PFQuery(className: "musicMixes")
query.orderByAscending("date")
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!,error: NSError!) -> Void in
if error == nil {
for object in objects {
let mixLike = object["votes"] as! Int
self.mixLikeArray.append(mixLike)
print("New mixLikeArray data is \(self.mixLikeArray)")
}
} else {
print("error getting like object")
}
}
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
}
I see three issues wrong with how this works at the moment. likeButtonPressed() Is sometimes sending the NSNotification before mixObject.saveInBackgroundWithTarget is finished. Meaning that the incremented like won't be displayed on the table view.
Secondly if I was to click like then click back to tableview swiftly the app will crash. This is because I'm guessing both likeButtonPressed() and the NSNotification function still has not been completed.
Also in # objc func reloadTableData(notification: NSNotification) once again the
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
Is being called before the findObjectsInBackgroundWithBlock is being completed? Anyway round this?
How would you suggest I can remodel this to work efficiently? Im pretty new to coding and a bit rusty with designing the best ways to do things... I know the concept behind completion handlers could I use these? I know that Parse likes to work in the background though hhhmmmm.....
to fix your reloadTableData problem, you should trigger the reload once the parse block is done executing, which means moving this line
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
inside the block
query.findObjectsInBackgroundWithBlock { (objects:[AnyObject]!,error: NSError!) -> Void in
if error == nil {
for object in objects {
let mixLike = object["votes"] as! Int
self.mixLikeArray.append(mixLike)
print("New mixLikeArray data is \(self.mixLikeArray)")
}
dispatch_async(dispatch_get_main_queue(),{
self.allTableView.reloadData()
});
} else {
print("error getting like object")
}
}
That will ensure that it gets triggered once parse is done updating objects. Currently its triggering before that while the block is executing. It also means that it won't reload if you get an error as you probably need to handle that differently anyway.
As for your problem of the notification happening before the saving is complete, you are calling . saveInBackgroundWithTarget but don't seem to send anything into it. You could use saveInBackgroundWithBlock and then use dispatch_group dispatch_group_enter, dispatch_group_leave, and dispatch_group_notify inside the block to make your program wait till everything is done being saved before sending the notification.
So you would create a dispatch_group
dispatch_group_t group = dispatch_group_create();
And then call it dispatch_group_enter in the for loop through the objects
for object in objects {
dispatch_group_enter(group);
let mixObject:PFObject = object as! PFObject
.....
}
Then call dispatch_group_leave on the mixObject.saveInBackgroundWithBlock
and wrap the notification in dispatch_group_notify
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 4
NSNotificationCenter.defaultCenter().postNotificationName("reload", object: nil)
});
Something like that
It sounds more daunting than it is, here's a Ray Wenderlich tutorial to bring you up to speed on how to use it, if your not familiar

Swift2 Huge delay in many UIEvents

I usually don't ask questions here, but this makes no sense.
After I load and parse a Json (with swiftyjson) I call this methods:
//remove a loadin screen
uiviewLoader?.removeFromSuperview()
print("uiview loader removed")
self.collectionView.reloadData()
print("refresh table")
And thats its.
The thing is, it prints boths msgs (both in emulator and in physical device) but it takes like 15 seconds to see the effects of this lines.
Do I need an extra call?? I have the latest xcode..
Thanks for any tips or advices.
****** More Info ******
I load a json in another class. when completed loading and parsing, it calls a method in my viewController
func jsonParseado(res:Bool){
print("json result = \(res)")
if res {
uiviewLoader?.removeFromSuperView()
print("loading uiview removed from superview")
self.collectionView.reloadData()
print("refresh data with new values")
...
else{
...
}
And after I see the results prints, it takes about 10-15 seconds to remove the loading view and show the collection controller.
Ok. luk2302 u were right.
the problem is that Im no longer in the main thread.
I solve the problem with
dispatch_async(dispatch_get_main_queue()) {
self.collectionView.reloadData()
self.uiviewLoader?.removeFromSuperview()
print("reload in main thread..")
}
//this actually works!!
I did load the json with swifty json, wich uses another thread to do so.
but im not sure when I left the main thread...
Thank you all.

Resources