CellForRowAtIndexPath called before ViewWillAppear finished running - ios

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.

Related

How to ensure the order of list shown in UITableView when getting data for cell from UIDocument

My app fetches data via FileManager for each cell in UITableView. The cells need data from a file which requires open UIDocument objects. However, it seems like code inside open completion handler get executed non predictably, so the cells don't get displayed in the order I wish.
How do I solve this problem? I appreciate if anyone gives any clue.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
fetchCellsData()
}
func fetchCellsData() {
do {
let files = getUrlsOfFiles() //files in a certain order
for file in files {
let document = MyDocument(fileURL: file) //MyDocument subclassing UIDocument
//open to get data for cell
document.open { (success) in
if !success {
print("File not open at %#", file)
return
}
let data = populateCellData(document.fileInfo)
self.cellsData.append(data)
self.tableView.reloadData()
/// tried below also
// DispatchQueue.main.async {
// self.cellsData.append(data)
// self.tableView.reloadData()
// }
}
document.close(completionHandler: nil)
}
} catch {
print(error)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! MyCell
let fileInfo = cellsData[indexPath.row].fileInfo
//assign fileInfo into MyCell property
return cell
}
I expect cells get rendered in the order of 'files', however, it seems like the order is a bit unpredictable presuming that it's due to the fact that 'cellForRowAt' gets called before it knows about the full set of 'cellsData'.
From Apple documentation on UIdocument . Apple doc:
open(completionHandler:)
Opens a document asynchronously.
Which means that even if you trigger document.open in the right order, nothing guarantees that the completionHandler sequence will be in the same order, this is why the order is unpredictible.
However, you know that they will eventually all get done.
What you could do is :
1 - place all your datas from opened document into another list
2 - order this list in accordance to your need
3 - place this list into cellsData (which I assume is bound to your tableViesDatasource)
var allDatas: [/*your data type*/] = []
...
do {
// reset allDatas
allDatas = []
let files = getUrlsOfFiles()
for file in files {
let document = MyDocument(fileURL: file)
document.open { (success) in
if !success {
print("File not open at %#", file)
return
}
let data = populateCellData(document.fileInfo)
self.allDatas.append(data) // "buffer" list
self.tableView.reloadData()
}
document.close(completionHandler: nil)
}
} catch { ...
Then you change cellsData to a computed property like so :
var cellsData: [/*your data type*/] {
get {
return allDatas.order( /* order in accordance to your needs */ )
}
}
this way, each time a new data is added, the list is orderer before being redisplayed.
Discussion
this is the easier solution regarding the state of your code, however this may not be the overall prefered solution. For instance in your code, you reload your tableview each time you add a new value, knowing that there will be more data added after, which is not optimised.
I suggest you to read on Dispatch Group, this is a way to wait until all asynchronous operation your triggered are finished before executing certain actions (such as reloading your tableview in this case) (Readings: Raywenderlich tuts)

How to asynchronously load and update several parts of a UITableView?

I have a table view that is made up of three parts. One part is passed in from the previous view controller, but the other two parts need to be loaded asynchronously. I am displaying "placeholder loading spinners" in the areas that are waiting for HTTP responses. When one section returns, I try updating the table data, but I'm finding that I can get into a situation where both responses return around the same time and try to update the table at the same time, resulting in a crash. It seems like I need to apply some sort of lock and queue so that it does not crash from multiple asynchronous requests trying to update the table at the same time.
I would like to know, what is the iOS best practice for safely loading/updating partial sections of a UITableView asynchronously. I'm not looking for a code sample. Rather, I'm looking for the terminology and method calls that are used to achieve this.
If you're using different sections(and a static number of sections), try reloading them instead of reloading the table view. When an API returns, update its respective section:
[self.tableView reloadSections: withRowAnimation:]
Short answer: main thread. More specifically:
Update your data model on the main thread
Reload table view data on the main thread (in fact, do all UI stuff on the main thread, always)
If you do the above, you should have no issue.
If you're using something like NSURLConnection, you can specify the queue to which the completion proc should be dispatched when data is received (that'd be NSOperationQueue.mainQueue()). If you're doing something else that ends up executing on a different thread, you can dispatch back to the main thread with something like performSelectorOnMainThread or dispatch_async to dispatch_get_main_queue.
You can reload just particular sections (via reloadSections:withRowAnimation:) or even just certain rows (reloadRowsAtIndexPaths:withRowAnimation:), but I wouldn't bother with any of that unless/until there's an issue (e.g., slow performance or flicker due to excessive redraw). Start off just reloading the whole table until you've observed that you need to do otherwise.
I know you said you're not looking for a code sample, but I just can't help myself; I communicate better in code than in words.
Main thing is tableView:cellForRowAtIndexPath:, which makes a URL request (via NSURLConnection). The completion proc (which is dispatched to the main queue) parses some JSON, updates the model, and reloads the table. That's it.
class ViewController: UIViewController, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
private var appIds = [ "391439366", "549762657", "568903335", "327630330", "281796108", "506003812" ]
private var ratings = [String : Int]() // AppID : RatingCount
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.appIds.count;
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let aCell = UITableViewCell(style: .Value2, reuseIdentifier: "RatingCell")
let appId = appIds[indexPath.row]
aCell.textLabel?.text = appId
if let count = self.ratings[appId] {
// Already got rating count for this app - display it.
aCell.detailTextLabel!.text = String(count)
aCell.accessoryView = nil
}
else {
// Don't have rating count: go get it.
self.getNumberOfRatingsForAppID(appId) {
success, number in
if success {
// Update model and reload table.
self.ratings[appId] = number
self.tableView.reloadData()
}
}
// Progress indicator while we wait for data.
let spinner = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
spinner.startAnimating()
aCell.accessoryView = spinner
}
return aCell
}
typealias GetRatingsCompletion = (Bool, Int) -> ()
func getNumberOfRatingsForAppID( appID: String, completion: GetRatingsCompletion ) {
let appStoreURL = NSURL(string: "https://itunes.apple.com/lookup?id=\(appID)")
let request = NSURLRequest(URL: appStoreURL!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue() ) {
response, data, error in
guard data != nil else {
completion( false, 0 )
return
}
if let
jsonResult = (try? NSJSONSerialization.JSONObjectWithData(data!, options:[])) as? NSDictionary,
results = jsonResult["results"] as? NSArray,
result = results[0] as? NSDictionary,
numberOfRatings = result["userRatingCountForCurrentVersion"] as? Int
{
completion( true, numberOfRatings )
return
}
completion( false, 0 )
}
}
}

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.

Don't load information from Parse Again

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.

Pull to Refresh: data refresh is delayed

I've got Pull to Refresh working great, except when the table reloads there is a split second delay before the data in the table reloads.
Do I just have some small thing out of place? Any ideas?
viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.getCloudKit()
}
handleRefresh for Pull to Refresh:
func handleRefresh(refreshControl: UIRefreshControl) {
self.objects.removeAll()
self.getCloudKit()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
refreshControl.endRefreshing()
})
}
Need the data in two places, so created a function for it getCloudKit:
func getCloudKit() {
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil { // There is no error
for play in results! {
let newPlay = Play()
newPlay.color = play["Color"] as! String
self.objects.append(newPlay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
} else {
print(error)
}
}
}
tableView:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
let object = objects[indexPath.row]
if let label = cell.textLabel{
label.text = object.matchup
}
return cell
}
This is how you should do this:
In your handleRefresh function, add a bool to track the refresh operation in process - say isLoading.
In your getCloudKit function just before reloading the table view call endRefreshing function if isLoading was true.
Reset isLoading to false.
Importantly - Do not remove your model data before refresh operation is even instantiated. What if there is error in fetching the data? Delete it only after you get response back in getCloudKit function.
Also, as a side note, if I would you, I would implement a timestamp based approach where I would pass my last service data timestamp (time at which last update was taken from server) to server and server side would return me complete data only there were changes post that timestamp else I would expect them to tell me no change. In such a case I would simple call endRefreshing function and would not reload data on table. Trust me - this saves a lot and gives a good end user experience as most of time there is no change in data!

Resources