Don't load information from Parse Again - ios

In my cellForRowAtIndexPath, I load information from Parse into each cell. The only problem with this is every time I refresh a cell, the Parse information gets loaded again. Should I move the code out of the cellForRowAtIndexPath and put it somewhere else or should I surround it in an if statement? Please tell me if any more information or any code is needed. Thanks!
Here is my code to refresh...
func refresh() {
print("View appeared")
self.posts.removeAll(keepCapacity: false)
let postQuery = PFQuery(className: "Post")
postQuery.whereKey("type", equalTo: "world")
postQuery.orderByDescending("createdAt")
postQuery.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let objects = objects as? [PFObject] {
self.posts = objects
self.table.reloadData()
}
})
}

Load your data in viewDidLoad. Then access it in cellForRowAtIndex. Refresh the data in some other methods, maybe in pull down to fresh functionality, or a fresh button event.

Related

UITableview does not respond while loading data from firebase

My Tableview does not respond to any touches when it starts loading data from firebase. (It already shows the cells, but does not react) After a while it does the scrolling you tried to do when the tableview wasn't reacting.
var filteredData = [archivCellStruct]()
func firData() {
filteredData.removeAll()
var databaseRef : DatabaseReference!
databaseRef = Database.database().reference()
databaseRef.child("Aufträge").child("Archiv").queryOrderedByKey().observe(.childAdded, with:{
snapshot in
let snap = snapshot.value as? NSDictionary
//extracting the data and appending it to an Array
self.filteredData.append(//myData)
self.tableView.reloadData()
})
}
So it is working for smaller amounts of data (tableview rows) but with big delay(and I am already filtering the data to limit the data displayed in the tableView).
I think it has something to do with the tableView.reloadData() (maybe userinteraction is disabled while reloading?)
everytime you have to reload your tableview asynchronously -
var filteredData = [archivCellStruct]()
func firData() {
filteredData.removeAll()
var databaseRef : DatabaseReference!
databaseRef = Database.database().reference()
databaseRef.child("Aufträge").child("Archiv").queryOrderedByKey().observe(.childAdded, with:{
snapshot in
let snap = snapshot.value as? NSDictionary
//extracting the data and appending it to an Array
self.filteredData.append(//myData)
DispatchQueue.main.async { //change this in you code
self.tableView.reloadData()
}
})
}
With the firebase you can go manual way:
UseCase:
struct UseCaseName {
struct Request {}
struct Response {
let firebaseCallbackData: [FirebaseModelType]
}
struct ViewModel {
let data: [DisplayType]
}
}
ViewController:
var filteredData: [DisplayType]! = []
override func viewWillAppear() {
// this one can make you trouble, adjust the observation logic to whatever you need. `WillAppear` can fire multiple times during the view lifecycle
super.viewWillAppear()
interractor?.observe()
}
func showData(viewModel: Scene.Usecase.ViewModel) {
// this is where the different approach begins
tableView.beginUpdates()
var indexPaths = [NSIndexPath]()
for row in (filteredData.count..<(FilteredData.count + viewModel.data.count)) {
indexPaths.append(NSIndexPath(forRow: row, inSection: 0))
}
filteredData += viewModel.data
tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: .Automatic)
tableView.endUpdates()
}
Interractor:
func observe(request: Scene.UseCase.Request) { /* signup for updates with observe(_ eventType: DataEventType, with block: #escaping (DataSnapshot) -> Void) -> UInt */
callback is something like { DataSnapshot in presenter.presentData(response: Scene.Usecase.Response())
}
Presenter:
func presentData(response: Scene.UseCase.Response) {
/* format for representation */
DispatchQueue.main.async {
controller.present(viewModel: Scene.UseCase.ViewModel())
}
}
Sorry for the separation of the flow, I've got addicted to this way.
Also I'm assuming, that the data in the firebase is not modified, but added (because of observe(.childAdded, part. If I'm wrong, please edit your question to reflect that. Another assumption is that you have the single section. Don't forget to change the inSection: 0 to the proper section. I'm to lazy and SO isn't that friendly for mobile devices
This way only appends new values sent by Firebase and works faster
EDIT: on another answer.
DispatchQueue.main.async { //change this in you code
self.tableView.reloadData()
}
Sometimes it's not good to reload all items. It depends on the count however. If my assumptions are correct, it'll be better to update separate cells without reloading the whole table on change. Quick example: something like a chat with the Firebase "backend". explanation: adding a single row will work faster then reloadData() anyway, but the difference is heavy only when you call those methods often. With the chat example, the difference may be huge enough in case the chat is spammy to optimize the UITableView reload behaviour in your controller.
Also it'll be nice to see the code, which affects the methods. It maybe a threading issue, like the John Ayers told in the comments. Where do you call func firData()?

Refreshing table view with a UIRefreshControl in swift

I am working on an iOS app in which I have a UITableView which needs to be refreshed on command. I have added a UIRefreshControl object and hooked up a function to it so that whenever I drag down on the table view, my refresh code is supposed to take place. The code fragment is below:
#IBAction func refresh(sender: UIRefreshControl?) {
self.tableView.setContentOffset(CGPointMake(0, self.tableView.contentOffset.y-self.refreshControl!.frame.size.height), animated: true)
sender?.beginRefreshing()
if (profileActivated) {
self.matches = []
let dynamoDBObjectMapper = AWSDynamoDBObjectMapper.defaultDynamoDBObjectMapper()
let queryExpression = AWSDynamoDBScanExpression()
queryExpression.limit = 100;
// retrieving everything
dynamoDBObjectMapper.scan(DDBMatch.self, expression: queryExpression).continueWithBlock({ (task:AWSTask!) -> AnyObject! in
if task.result != nil {
let paginatedOutput = task.result as! AWSDynamoDBPaginatedOutput
//adding all to the matches array
for item in paginatedOutput.items as! [DDBMatch] {
self.matches.append(item)
}
//code to filter and sort the matches array...
self.tableView.reloadData()
}
return nil
})
self.tableView.reloadData()
}
sender?.endRefreshing()
self.tableView.setContentOffset(CGPointMake(0, self.tableView.contentOffset.y - (0.5*self.refreshControl!.frame.size.height)), animated: true)
}
Unfortunately, this code does not quite work right. Whenever I drag down on the refresh control, my table populates but then immediately goes away, and then refreshes about 10-15 seconds later. I inserted some print statements, and the data is all there, it just does not appear for a long time, and I am having trouble making it appear as soon as it is retrieved. I am new to iOS programming so any help would be appreciated.
Thanks in advance

CellForRowAtIndexPath called before ViewWillAppear finished running

I have an application that pulls information from a Parse database, and displays it in a UITableView. I pull the information from parse in the viewWillAppear function, and i display it in the tableView(cellForRowAtIndexPath) function. Sometimes i receive an error because the array that stores the Parse information has a length of 0, and i try to access information at an index outside of the bounds of the array. I believe this is because the cellForRowAtIndexPath is getting called before the viewWillAppear is finished running. Is this possible or is my error definitely coming from somewhere else?
EDIT: The error does not occur every time, and i cannot find a way to reproduce it
override func viewWillAppear(animated: Bool) {
//begin ignoring events until the information is finished being pulled
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
resultsArray.removeAll(keepCapacity: false)
//run query
let query = PFQuery(className: "Answers")
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if let objects = objects {
//append information to the resultsArray
}
}
self.tableView.reloadData()
}
}
//information is now pulled, so allow interaction
UIApplication.sharedApplication().endIgnoringInteractionEvents()
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! answerCell
// THIS IS WHERE THE ERROR OCCURS
resultsArray[indexPath.row].imageFile.getDataInBackgroundWithBlock { (data, error) -> Void in
//set image within cell
}
return cell
}
I would suggest that you load your data from Parse into a temporary array and then assign this to your property array right before you call reloadData - this will avoid any potential race conditions and remove the need for the removeAll which is potentially a big part of your problem;
override func viewWillAppear(animated: Bool) {
//begin ignoring events until the information is finished being pulled
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
//run query
let query = PFQuery(className: "Answers")
query.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
var localArray=[SomeType]()
if let objects = objects {
//append information to the localArray
}
}
self.resultsArray=localArray
self.tableView.reloadData()
}
}
//information is now pulled, so allow interaction
UIApplication.sharedApplication().endIgnoringInteractionEvents()
}
Looks like in viewWillAppear you have a background block findObjectsInBackgroundWithBlock that has some work to do in a different thread (AKA off the main thread), that means that viewWillAppear will finish while the block will get a callback.
This explains why cellForRowAtIndexPath is being called after viewWillAppear finishes, because of the callback block.
That means that everything is alright and viewWillAppear actually do finish a legit "run".
You can actually put a breaking point inside the callback method (in viewWillAppear) and a breaking point inside cellForRowAtIndexPath and see when the callback happens while cellForRowAtIndexPath is being called.
If you need a different method from Parse perhaps you should look in their documentation.
Actually if your callback not access to self.tableView, everything will go on as you think as usual. You can have a try.
It happened to me when I access to the view on the screen in init method viewDidLoad method called before init ends.
Anyway, you should know that fact. And you access to your tableView in callback (called before viewWillAppear finishing) which needs cellForRowAtIndexPath.

Swift 2 + Parse: Array index out of range

SOMETIMES THE REFRESH WORKS SOMETIMES IT DOESN'T
I have a UITableViewController which is basically a news feed. I have also implemented a pull to refresh feature. However sometimes when I pull to refresh it gives me the error
'Array index out of range'.
I know this means an item it is trying to get does not exist but can you tell me why? Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
refresher = UIRefreshControl()
refresher.attributedTitle = NSAttributedString(string: "Pull to refresh")
refresher.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refresher)
refresh()
tableView.delegate = self
tableView.dataSource = self
}
and the refresh() function:
func refresh() {
//disable app while it does stuff
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
//get username and match with userId
let getUser = PFUser.query()
getUser?.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let users = objects {
//clean arrays and dictionaries so we dont get indexing error???
self.messages.removeAll(keepCapacity: true)
self.users.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in users {
if let user = object as? PFUser {
//make userId = username
self.users[user.objectId!] = user.username!
}
}
}
})
let getPost = PFQuery(className: "Posts")
getPost.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if error == nil {
if let objects = objects {
self.messages.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in objects {
self.messages.append(object["message"] as! String)
self.usernames.append(self.users[object["userId"] as! String]!)
self.tableView.reloadData()
}
}
}
}
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
}
and:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCellWithIdentifier("SinglePostCell", forIndexPath: indexPath) as! PostCell
//ERROR GETS REPORTED ON THE LINE BELOW
myCell.usernamePosted.text = usernames[indexPath.row]
myCell.messagePosted.text = messages[indexPath.row]
return myCell
}
You have a race condition given you are doing two background tasks, where the second depends on values returned from the first. getUser?.findObjectsInBackgroundWithBlockwill return immediately, and getPost.findObjectsInBackgroundWithBlock will start executing. The getPost should be inside the block for getUser, to ensure the sequence is correct.
Similarly, the following two lines should be inside the second block:
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
Given the error line, you probably also have a race condition between the two background tasks and displaying the tableView. I would be inclined to try:
func tableView(tableView:UITableView!, numberOfRowsInSection section:Int) {
return self.refresher.refreshing ? 0 : self.usernames.count
}
This way you won't touch self.usernames until the background refresh is finished (as long as you remember to put endRefreshing inside the second block, which is also put inside the first block).
I Believe that in self.users[user.objectId!] = user.username! the user.ObjectId is some random value assigned by parse which looks like this: "34xcf4". This is why you might be getting 'Array index out of range'.
There are two required methods for configuring a UITableView:
tableView(_:cellForRowAtIndexPath:)
and
tableView(_:numberOfRowsInSection:)
In your code you are presenting only one required method, if you don't implement the second method then it that may cause errors.
Check the documentation at:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDataSource_Protocol/#//apple_ref/occ/intfm/UITableViewDataSource/tableView:cellForRowAtIndexPath:
You are calling self.tableView.reloadData() on every addition to your array and doing so in a background thread.
As a general rule, you should not do UI updates in a background thread. When you clear self.messages and self.usernames, because you are in background thread, nothing prevents the tableview from trying to get a cell at an index that no longer has any data in the array.
If you want to keep your code in the background thread (risky as it may be), you should at least call .beginUpdates before reloading your arrays and wait until they're all done before calling reload and endUpdates.

How to keep from having repeated items on a tableView due to a function triggered several times?

My issue is that I am querying elements in viewDidLoad and adding to the tableView. On another view, the user can add elements to Parse, that I am querying again on viewDidAppear to display only the newly added elements without requiring the view to re-load.
These elements are class of user with name, age etc...
On the viewDidAppear, I am querying the elements from Parse, going through a messy filtering to find out the ones that are not already displayed on the tableView, and then I trigger my function to add it.
It appears that even though I have removed duplicate items from my array, the function to set up my user (adding his name etc) gets called several times and consequently I still end up with duplicated items on my tableView.
The corresponding codes as below:
override func viewWillAppear(animated: Bool) {
var query = PFUser.query()
query!.whereKey("username", equalTo: PFUser.currentUser()!.username!)
query!.findObjectsInBackgroundWithBlock { (objects, NSError) -> Void in
if let objects = objects as? [PFObject] {
for member in objects {
if member["Network"] != nil {
var acceptedMembers: [String] = member["Network"] as! [String]
self.usernames = acceptedMembers
////
var query2 = PFUser.query()
query2?.findObjectsInBackgroundWithBlock({ (objects2, error2) -> Void in
if let objects2 = objects2 as? [PFUser] {
for otherUser in objects2 {
if contains(self.usernames, otherUser.username!) {
var arrayName1 = [String]()
arrayName1.append(otherUser.username!)
var arrayName2 = [String]()
for n in self.arrayMem {
arrayName2.append(n.name)
dispatch_async(dispatch_get_main_queue(), {
for extra in arrayName1 {
if contains(arrayName2, extra) {
} else {
var arrayName3 = [String]()
arrayName3.append(extra)
let unique3 = NSSet(array: arrayName3).allObjects
self.plusOne = unique3.first as! String
self.nameMember = self.plusOne as String
self.setName()
self.tableView.reloadData()
}
}
})
}
}}}}) }}}}
}
PS: I have tried cleaner solution to remove duplicate, converting my class to Hashable, then Equatable and using a simple function, however it turns up that this solution, messier as it is works more efficiently.
Anyway, the question here touches the function "self.setName()" that gets called repeatedly.
Do you have any idea how could this be fixed ?
Thank you infinitely,
A different approach would be to add a new object to your array after it's added in the other view controller and then add a new row for it into your table view. I would suggest communicating the newly formed object via an NSNotification.
View Controller with Table
Fetch your objects from Parse here, do it once in viewDidLoad
func fetchParseObjects () {}
Add an observer for this class to the NSNotificationCenter, also in viewDidLoad
NSNotificationCenter.defaultCenter().addObserver(self, selector: "objectUpdated:", name: "objectUpdated", object: nil)
Remove the observer in a deinit method
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self, name: "objectUpdated", object: nil)
}
func objectUpdated(notification: NSNotification) {
if let yourKindOfObject = notification.object as? YOUR_TYPE_HERE {
// Add, update or remove an item from the array that holds the original fetched data here based on the object
// Update your tableView accordingly (add, remove or update)
}
Where I have done something similarly I have created a custom object to hold some properties of what object needs to be updated and how it should be updated.
View Controller that Updates
Do what you normally do, but send a notification with a descriptive object to use to update your original data accordingly
NSNotificationCenter.defaultCenter().postNotificationName("objectUpdated", object: YOUR_OBJECT_WITH_UPDATE_DATA)

Resources