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.
Related
I encountered difficulties when loading the Collection views nested in table view cells. The content inside cells would only show after scrolling the table a couple of times. My approach was to use DispatchGroup() in order to fetch the data in a background thread but it didn't work. What is there to do in order to show all the information at once without scrolling through table?
ViewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
_tableView.isHidden = true
_tableView.dataSource = nil
_tableView.delegate = nil
SVProgressHUD.show()
view.backgroundColor = UIColor.flatBlack()
getData()
dispatchGroup.notify(queue: .main) {
SVProgressHUD.dismiss()
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
}
}
UICollectionView and UITableView datasource / OtherMethods
func getData(){
dispatchGroup.enter()
backend.movieDelegate = self
backend.actorDelegate = self
backend.getMoviePopularList()
backend.getMovieTopRatedList()
backend.getMovieUpcomingList()
backend.getPopularActors()
backend.getMovieNowPlayingList()
dispatchGroup.leave()
}
func transferMovies(data: [String:[MovieModel]]) {
dispatchGroup.enter()
popularMovies = data
dispatchGroup.leave()
}
func transferActors(data: [ActorModel]) {
dispatchGroup.enter()
popularActors = data
dispatchGroup.leave()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "DiscoverCell") as? DiscoverViewCell else { return UITableViewCell()}
cell.categoryLabel.text = cell.categories[indexPath.item]
//categories[indexPath.item]
cell._collectionView.delegate = self
cell._collectionView.dataSource = self
cell._collectionView.tag = indexPath.row
cell._collectionView.reloadData()
self.setUpCell(cell)
return cell
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MovieCell", for: indexPath) as? MovieCollectionViewCell else { return UICollectionViewCell()}
if collectionView.tag == 0{
if let movieDetails = popularMovies["Popular"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
} else if collectionView.tag == 1{
if let movieDetails = popularMovies["Top rated"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
} else if collectionView.tag == 2{
if let movieDetails = popularMovies["Upcoming"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
} else if collectionView.tag == 3{
cell.movieTitleLabel.text = popularActors?[indexPath.item].name ?? ""
cell.moviePicture.image = popularActors?[indexPath.item].poster
}
} else if collectionView.tag == 4{
if let movieDetails = popularMovies["Now playing"]?[indexPath.item] {
cell.updateMovieCollectionCell(movie: movieDetails)
}
}
return cell
}
MovieCollectionViewCell
class MovieCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var moviePicture: UIImageView!
#IBOutlet weak var movieTitleLabel: UILabel!
func updateMovieCollectionCell(movie: MovieModel){
moviePicture.image = movie.poster
movieTitleLabel.text = movie.name
}
}
DiscoverViewCell
class DiscoverViewCell: UITableViewCell {
#IBOutlet weak var categoryLabel: UILabel!
#IBOutlet weak var _collectionView: UICollectionView!
let categories = ["Popular", "Top Rated", "Upcoming", "Popular People", "Now playing"]
#IBAction func seeMoreAction(_ sender: Any) {
}
}
My intention is to show a loading animation until all the data is fetched and the display the table view cells containing the collection views with fetched data from web.
The desired result should look like this when opening the app
From what I can tell, you're using dispatchGroup incorrectly.
To summarize notify():
it runs the associated block when all currently queued blocks in the group are complete
the block is only run once and then released
if the group's queue is empty, the block is run immediately
The issue I see is that with the way your fetch code is written, the group thinks its queue is empty when you call notify. So the notify() block is run immediately and then you only see the cells populate when they are reloaded during scrolling.
There are two ways to populate a dispatchGroup's queue:
call DispatchQueue.async() and pass the group to directly enqueue a block and associate it with the group
manually call enter() when a block begins and leave() when it ends, which increases/decreases an internal counter on the group
The first way is safer, since you don't have to keep track of the blocks yourself, but the second one is more flexible if you don't have control over what queue a block is run on for example.
Since you're using enter/leave, you need to make sure that you call enter() for each separate work item (in your case, the asynchronous calls to backend), and only call leave() when each one those work items completes. I'm not sure how you're using the delegate methods, but there doesn't seem to one for each backend call, since there are 5 different calls and only 2 delegate methods. It also doesn't look like the delegate methods would be called if an error happened in the backend call.
I would recommend changing the backend calls to use completion blocks instead, but if you want to stick with the delegate pattern, here's how you might do it:
func getData(){
backend.movieDelegate = self
backend.actorDelegate = self
dispatchGroup.enter()
backend.getMoviePopularList()
dispatchGroup.enter()
backend.getMovieTopRatedList()
dispatchGroup.enter()
backend.getMovieUpcomingList()
dispatchGroup.enter()
backend.getPopularActors()
dispatchGroup.enter()
backend.getMovieNowPlayingList()
dispatchGroup.notify(queue: .main) {
SVProgressHUD.dismiss()
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
}
}
func transferPopularMovies(data: [MovieModel]) {
popularMovies = data
dispatchGroup.leave()
}
func transferTopRatedMovies(data: [MovieModel]) {
topRatedMovies = data
dispatchGroup.leave()
}
func transferUpcomingMovies(data: [MovieModel]) {
upcomingMovies = data
dispatchGroup.leave()
}
func transferActors(data: [ActorModel]) {
popularActors = data
dispatchGroup.leave()
}
func transferNowPlayingMovies(data: [MovieModel]) {
nowPlayingMovies = data
dispatchGroup.leave()
}
Don't forget to call the delegate methods even if there is an error to make sure the enter/leave calls are balanced. If you call enter more often than leave, the notify block never runs. If you call leave more often, you crash.
Try this...After getting your data from backend and assigning to moviedetails, set your delegate and datasource to self and reload table.
Set this line into the bottom of getData() function and run
self._tableView.isHidden = false
self._tableView.dataSource = self
self._tableView.delegate = self
self._tableView.reloadData()
and remove from viewdidload()
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 )
}
}
}
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.
var noteList:NSMutableArray = NSMutableArray() // declaration of `noteObjects`
query.findObjectsInBackgroundWithBlock { ( objects, error) -> Void in
if (error == nil) {
let temp: NSArray = objects as NSArray!
self.noteList = temp.mutableCopy() as! NSMutableArray
self.tableView.reloadData()
}else {
print(error!.userInfo)
}
}
//populating noteObjects
i have a tableView who's datasource is an Array 'noteObjects' its type is NSMutableArray , so the problem is that every time i open my tableView my 'noteObjects' array's Value is 0 but then it automatically changes to desired value , how can i say this ? i did this in different stages of my tableViewController -
i printed the noteObjects.count in ViewDidload
override func viewDidLoad() {
super.viewDidLoad()
print("\(noteObjects.count) viewDidlaod3") }
output :
0 viewDidlaod3
inside cellForRowAtindexPath i printed this
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
print("\(noteObjects.count) in cellForAtIndexPath")
return cell }
output :
18 in cellForAtIndexPath
and if i move to another View then again open this tableView it keeps giving the sae result , (note object.count = 0 at first)
i want to use that noteObjects.count so that i can confirm that if the tableView's datasource is empty so i can show a message , but when am using this then its always showing that my tableview is empty because at first noteObjects.count is 0
if details provided above is not enough then please let me know i'll fix it
The api call 'findeObjectsInBackground' is asynchronous meaning the closure is executed later in time when result is obtained in a different thread. So getting back to the main thread and reloading the table view when data is ready will solve the problem. You can read more about these type of closures as they are very common in iOS.
query.findObjectsInBackgroundWithBlock { ( objects, error) -> Void in
if (error == nil) {
let temp: NSArray = objects as NSArray!
dispatch_async(dispatch_get_main_queue(), {
self.noteList = temp.mutableCopy() as! NSMutableArray
self.tableView.reloadData()
})
}
else {
print(error!.userInfo)
}
}
use a if statement after the synchronisation method
if noteObjects.count < 1 {
let Alert = UIAlertView(title: "No Data to Display", message: "ther's nothing bro you should pull to refresh", delegate: self, cancelButtonTitle: "OKAY Got IT")
Alert.show()
}
just moved my code from viewDidLoad to viewDidAppear and it worked For me
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.fetchAllObjectsFromLocalDataStore()
print("\(noteObjects.count) view appears")
if noteObjects.count < 1 {
let Alert = UIAlertView(title: "No Data to Display", message: "ther's nothing bro you should pull to refresh", delegate: self, cancelButtonTitle: "OKAY Got IT")
Alert.show()
}
}
now noteObject.count is getting check after updating its Value
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!