Hey guys. I'm getting a fatal error whenever I scroll table view cell out of visible space. The error reads as such, "fatal error: Index out of range." The number of sections is getting its value from the count of an array declared directly in my class that consists of URLs, and the number of rows is 1. Why am I getting this error?
Fatal Error when scrolling in UITableView
Are you sure you didn't mean to return 1 as the number of sections, and the array count as the number of rows (in the one and only section)? This would be more usual than what you said above.
The index-out-of-error is probably what you'd get if you mixed up row and section indexes.
I set the sections and rows the way I did for a different look. I did try the opposite to see if that would fix the issue, but it still happens.
Finally found the issue. I create a cell for each URL in my array, and after it assigns one to each cell it removes the last one so that it won't duplicate. It seems the "override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell" function runs every time the cells come into view. Thus, when you've assigned all parts of the array and it tries to pull another, and if it doen't know how to handle the situation, it crashes. I'm still working on the solution, which I'll post once I have it, but if anyone else has this issue, here's the cause!
Do you want to show the code ? Which index is Index out of range ?
What do you remove ? if the cell exists, you could just change its content, not delete and recreate.
Here's my script. The index that is out of reach is the URLS array. When even one cell isn't visible, the override function at line 52 runs again once it's visible. But because the array is empty, it crashes. I've tested this by removing the '"URLS.Remove(at: 0)" statement at line 56, and set the number of sections to 20 for plenty of scrolling. When I did this, it never crashed, and I could see from a print statement in one of functins in my cell that it kept on running the override function (line 52).
import UIKit
var numTitle = 1
let docDir = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
class VideoListTableViewController: UITableViewController {
let docURLs = try! FileManager.default.contentsOfDirectory(at: docDir, includingPropertiesForKeys: nil
var URLS : [URL] = []
override func viewDidLoad() {
super.viewDidLoad()
/
self.clearsSelectionOnViewWillAppear = false
/
/
print ("\(docURLs.count) URLS ON STARTUP")
URLS = docURLs
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
/
}
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 4
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 4
}
/
override func numberOfSections(in tableView: UITableView) -> Int{
return docURLs.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell") as! VideoTableViewCell
cell.url = URLS[0]
URLS.remove(at: 0)
cell.setDate()
cell.setTitle()
cell.setImage()
cell.setSize()
print (docURLs.count)
if (URLS.count == 0){
print ("no more urls") // used for debugging
}
return cell
}
Er, don't do that.
You can't assume that the "tableView(_:cellForRowAt:)" method is called exactly once for each row. It's called more often than that.
You can't assume that "tableView(_:cellForRowAt:)" is called for rows in row-number order. It isn't.
That means you can't get the "next" entry from your URLs array in this method. Instead, you must look at "indexPath.row" and always use the element of the URLs array that corresponds to that row index. That also mean's you must not remove the entry from URLs each time.
The point is that your table view has a data source, in this case a view controller, which is responsible for configuring cells for rows on request. Given row, get cell.
Got it. Here's the fix.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell") as! VideoTableViewCell
cell.url = URLS[indexPath.section]
cell.setDate()
cell.setTitle()
cell.setImage()
cell.setSize()
return cell
}As the IndexPath instance starts at 0 just like an array, I simple set the the URLS array element integer call as the same as the indexPath section integer (as the number of sections is the count of URLS). I do have another question though. I've have 10 URLS in the array, and there's a small lag when I scroll. Is there a way to reduce the lag?
Well, yes and no and yes.
The problem is that creating your cell is expensive. At the very least, you're referencing something (I assume a file) at each URL, and then you're perhaps extracting an image from the file (etc, etc), and it's unreasonably expensive to do that "inline" when the table is being scrolled — which is what the above code does.
As an alternative, you could do the expensive accesses in advance, when you first populate the URLs array, so that the text, image, whatever, is ready to be set in the cell on demand. That's a reasonable approach if your table is going to be small (a few rows), but if the number of rows grows, you'll find that you still have the same slowdown, just in a different place. Or, if you cache all that information, then you may use up a lot of memory, which means that your app isn't going to play well with others (or, in the extreme case, crash due to excessive memory demands).
The solution is to change your programming pattern. Basically, what you need to do is to create the cells with placeholder information — cheap placeholder information — then start a background operation that does the more expensive retrieval. You do this in the background because you don't ever want to hold your main thread up with that amount of work. As each row's worth of information is constructed, you send it back to the main thread for display in the table.
What the user will see is a table that scrolls smoothly, but has a short delay before the real information appears. If you decide it's worth the programming effort, you can combine this asynchronous pattern with cacheing of a few rows, and that will improve the user experience.
Note that there's no quick fix here. Doing expensive things takes time, and you've just got to deal with that.
There have been WWDC videos over the years that deal with this subject. For example:
developer.apple.com/videos/play/wwdc2012/211/
but there may also be other tutorials out there on the web that might help if you search around a bit.
AWESOME! Thanks Quincy. Makes sense. Took another look at the CPU usage during runtime, and it says that "Other Processes" uses up to 77% of the total capacity. That's simply when I scroll around a lot!!! I'm working on implenting a new Operation Queue for each function in the cell as needed now. For anyone who's trying the same, here's a tutorial I found in swift:
http://www.appcoda.com/ios-concurrency/
I'm still going through everything, but this explains a lot.
So, here's my setImage function:
func setImage() {
let video = url
print ("\(video) AT SET IMAGE")
let ImageQueue = OperationQueue()
let operation = BlockOperation(block: {
let asset = AVAsset(url: video)
let generatory : AVAssetImageGenerator = AVAssetImageGenerator.init(asset: asset)
var time = asset.duration
time.value = 0
let cgImage = try! generatory.copyCGImage(at: time, actualTime: nil)
self.firstFrame.image = UIImage(cgImage: cgImage)
})
ImageQueue.addOperation(operation)
}After implenting simply this, it's a LOT faster. That said, it doesn't work for what I need it to do. What's happening is it doesn't always set the right image, and it doesn't always display the UIImage (I think its still processing it). It is a little better if you set the QueuePriority to a higher value, but it's still glitchy. What I'm going to do is convert the first frame of the video to a photo (i.e. a PNG or JPEG) and set that image to the UIImage in the cell. I still have to figure out how exactly to do that, but that's probably going to be the best thing. Thanks again Quincy!