I have NSMutableDictionary Array lists that I want to download. I am using cellForRowAtIndexPath to download each of them. However, when the cellForRowAtIndexPath runs, all the zip files downloaded in parallel, which causes the app to hang, the UI to freeze, CPU use to go through the roof.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:updateAllCell = tableView.dequeueReusableCellWithIdentifier("updateAllCell") as! updateAllCell!
let row = self.bookArray.objectAtIndex(indexPath.row) as! NSMutableDictionary
self.updateBookList(row, progressView: cell.downloadProgressView, labelView: cell.lblDownloadPercent)
}
func updateBookList(bookData: NSMutableDictionary, progressView: UIProgressView, labelView: UILabel) {
let source = Utility().getContentsDirectory().stringByAppendingString("/\(fileName).zip")
Alamofire.download(.GET, source, destination: destination)
.progress { bytesRead, totalBytesRead, totalBytesExpectedToRead in
println(totalBytesRead) // update progressView and labelView
}
.response { request, response, _, error in
println(response)
}
}
Can they downloaded one by one sequentially? Thanks.
The problem you are facing is that the download calls are being made as soon as the table requests the cell in question, since Alamofire does everything asynchronously (and if it didn't you would be waiting for the files to download before you would even see the cells).
What you want to do is implement a stack that will queue your requests and you pop the next request as soon as the previous one is finished.
Related
I trying to show file size before downloading in my AlertView after cell click:
var fileSize = 0
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let url = URL(string: "http://ex.com/\(indexPath.row).mp3")!
getDownloadSize(url: url, completion: { (size, error) in
if error != nil {
print("An error occurred when retrieving the download size: \(error!.localizedDescription)")
} else {
print("The download size is \(size).")
self.fileSize = Int(size)
}
})
// Create the alert controller
let alertController = UIAlertController(title: nil, message: "Size - \(self.fileSize) MB", preferredStyle: UIAlertControllerStyle.actionSheet)
etc
}
func getDownloadSize(url: URL, completion: #escaping (Int64, Error?) -> Void) {
let timeoutInterval = 5.0
var request = URLRequest(url: url,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: timeoutInterval)
request.httpMethod = "HEAD"
URLSession.shared.dataTask(with: request) { (data, response, error) in
let contentLength = response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
completion(contentLength, error)
}.resume()
}
But when I click on cell first time I get Size - 0 MB. And if I click next time on any cell I get size from previous url(previous cell). How to fix it?
The URL data task is asynchronous. Your code is not waiting until it has completed before displaying the dialog.
It's like sending somebody to the garden to count how many apples he sees lying on the ground, so you can put that number onto a piece of paper. You tell that person to go counting and put the result into a box, where currently the number zero is stored. As soon as the person leaves, you are opening the box and writing down the number you see. Yet the person hasn't even arrived in the garden, nor has the person counted any apples yet. When the person returns, he does as you told him, put the number of counted apples in the box. Now you send the person again, this time counting the numbers or pears he can see in the garden. And again, as soon as the person left, you open up the box and write down the number you see there, but that is still the number from counting apples as the person didn't even have a chance to update that number.
The block you feed to getDownloadSize() will run once the result has been fetched, but the code below that call runs immediately after calling getDownloadSize(), it will not wait until the block has executed.
And it wouldn't be a good idea to change that since fetching the result can take anything from a few seconds to several minutes. If the execution of code would block until the result has been fetched, your entire application would block for that time as well (your UI would completely freeze, no user interaction would be possible).
You are checking the filesize in the didSelectRowAt method, which only gets called when you select the row. You should lazy load a variable in the cell in the cellForRowAt method instead. But be careful to not do the download on the main thread.
I have to populate a TableView with some data fetched with an URLSession task. The source is an XML file, so i parse it into the task. The result of parsing, is an array that i pass to another function that populate another array used by TableView Delegates.
My problem is that TableView Delegates are called before task ends, so tha table is empty when i start the app, unless a data reloading (so i know that parsing and task work fine).
Here is viewDidLoad function. listOfApps is my TableView
override func viewDidLoad() {
super.viewDidLoad()
fetchData()
checkInstalledApps(apps: <ARRAY POPULATED>)
listOfApps.delegate = self
listOfApps.dataSource = self
}
}
fetchData is the function where i fetch the XML file and parse it
func fetchData() {
let myUrl = URL(string: "<POST REQUEST>");
var request = URLRequest(url:myUrl!)
request.httpMethod = "POST"
let postString = "firstName=James&lastName=Bond";
request.httpBody = postString.data(using: String.Encoding.utf8);
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
self.parser = XMLParser(data: data!)
self.parser.delegate = self
}
task.resume()
}
while checkInstalledApps is the function where i compose the array used by TableView Delegates.
func checkInstalledApps(apps: NSMutableArray){
....
installedApps.add(...)
installedApps.add(...)
....
}
So, for example, to set the number of rows i count installedApps elements
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (installedApps.count == 0) {
noApp = true
return 1
}
return installedApps.count
}
that are 0. Obviously, if i reload data, it's all ok.
My problem is the async call: first of that i used an XML accessible via GET request, so i can use XMLParser(contentsOf: myUrl) and the time is not a problem. Maybe if the XML will grow up, also in this way i will have some trouble, but now i've to use a POST request
I've tried with DispatchGroup, with a
group.enter() before super.viewDidLoad
group.leave() after task.resume()
group.wait() after checkInstalledApps()
where group is let group = DispatchGroup(), but nothing.
So, how can i tell to the tableview delegate to wait the task response and the next function?
thanks in advance
I would forget about DispatchGroup, and change a way of thinking here (you don't want to freeze the UI until the response is here).
I believe you can leave the fetchData implementation as it is.
In XMLParserDelegate.parserDidEndDocument(_:) you will be notified that the XML has been parsed. In that method call checkInstalledApps to populate the model data. After that simply call listOfApps.reloadData() to tell the tableView to reload with the new data.
You want to call both checkInstalledApps and listOfApps.reloadData() on the main thread (using DispatchQueue.main.async {}).
Also keep listOfApps.delegate = self and listOfApps.dataSource = self in viewDidLoad as it is now.
The cleaner way is to use an empty state view / activityIndicator / loader / progress hud (whatever you want), informing the user that the app is fetching/loading datas,
After the fetch is done, just reload your tableview and remove the empty state view / loader
Your problem is caused by the fact that you currently have no way in knowing when the URLSession task ended. The reloadData() call occurs almost instantly after submitting the request, thus you see the empty table, and a later table reload is needed, though the new reload should be no sooner that the task ending.
Here's a simplified diagram of what happens:
Completion blocks provide here an easy-to-implement solution. Below you can find a very simplistic (and likely incomplete as I don't have all the details regarding the actual xml parsing) solution:
func fetchData(completion: #escaping () -> Void) {
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
self.parser = XMLParser(data: data!)
self.parser.delegate = self
completion()
}
override func viewDidLoad() {
super.viewDidLoad()
fetchData() { [weak self] in self?.tableView.reloadData() }
}
Basically the completion block will complete the xml data return chain.
I have a UITableView with about 1000 rows. I also have a timer running every 6 seconds that fetches data from a web service. Each time I call reloadData() there is a blip - my app freezes very noticeably for a brief moment. This is very evident when scrolling.
I tried fetching about 400 rows only and the blip disappears. Any tips how to get rid of this while still fetching the 1000 rows?
var items: [Item] = []
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
OperationQueue.main.addOperation {
strongSelf.items = itemsFromResponse
strongSelf.itemsTableView.reloadData()
}
}
}
UITableViewDataSource code:
extension ItemViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath)
cell.textLabel?.text = items[indexPath.row].name
return cell
}
}
The problem is being caused because you are storing the items from the response and then updating the table view from the same OperationQueue, meaning that the UI thread is being blocked while your array is being updated. Using an operation queue in itself is not an optimal way to schedule tasks if you do not need fine grain control over the task (such as cancelling and advanced scheduling, like you don't need here). You should instead be using a DispatchQueue, see here for more.
In order to fix your issue, you should update your array from the background completion handler, then update your table.
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
strongSelf.items = itemsFromResponse
// update the table on the main (UI) thread
DispatchQueue.main.async {
strongSelf.itemsTableView.reloadData()
}
}
}
You should also maybe look into a more efficient way to fetch new data, because reloading the entire dataset every 6 seconds is not very efficient in terms of data or CPU on the user's phone.
The problem is you are reloading data every 6 seconds, so if the data is so big you're reloading 1000 rows every 6 seconds. I recommend you request the data and compare if there's new data so in that case you need to reload data or you simply ask to refresh once. For example:
var items: [Item] = []
Timer.scheduledTimer(withTimeInterval: 6, repeats: true) { [weak self] _ in
guard let strongSelf = self else { return }
Alamofire.request(urlString, method: method, parameters: params) { response in
// parse the response here and save it in array called itemsFromResponse
OperationQueue.main.addOperation {
if(strongSelf.items != itemsFromResponse){
strongSelf.items = itemsFromResponse
strongSelf.itemsTableView.reloadData()
}
}
}
I am wondering if there is any way to keep reloading a tableView as items are being downloaded.
There are images and info associated with those images. When the user first opens the app, the app begins downloading images and data using HTTP. The user can only see downloaded items in the tableView as they're being downloaded if he/she keeps leaving the viewController and coming back to it.
I have tried doing something like this:
while downloading {
tableView.reloadData()
}
, however, this uses too much memory and it crashes the app.
How can I asynchronously populate a tableView with images and data as they are being downloaded while still remaining in the tableViewController?
P.S. If you're interested in which libraries or APIs I'm using, I use Alamofire to download and Realm for data persistence.
The correct and usual way to do this is reload data into table, than delegate the single cell to load asyncronously the image from a link.
In swift, you can extend UIImage
extension UIImageView {
func imageFromUrl(urlString: String) {
if let url = NSURL(string: urlString) {
let request = NSURLRequest(URL: url)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {
(response: NSURLResponse?, data: NSData?, error: NSError?) -> Void in
if let imageData = data as NSData? {
self.image = UIImage(data: imageData)
}
}
}
}
}
And in your CellForRowAtIndexPath load the image from link using something like this
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
...
cell.image.imageFromUrl(dataArray[indexPath.row].imageUrl)
return cell
}
I have a UITable-View with a List of Users and their Profile Pictures.
I am loading the pictures (http://api/pictures/{userid}) one by one for each player asynchronous:
func loadImageAsync(imageUrl: URL, completionHandler handler: #escaping (_ success: Bool, _ image: UIImage?) -> Void){
DispatchQueue.global(qos: .userInitiated).async { () -> Void in
if let imgData = try? Data(contentsOf: imageUrl), let img = UIImage(data: imgData) {
DispatchQueue.main.async(execute: { () -> Void in
handler(true, img)
})
} else {
handler(false, nil)
}
}
In the completion handler in the cellForRowAt-Index-Fuction, I am setting the pictures.
loadImageAsync(imageUrl: imageUrl!, label: ip) { (success, image, backlabel) -> Void in
if(success){
cell.profilePictureView.image = image
}
}
However, when I scroll very fast, some pictures get loaded in the wrong cells.
To prevent reuse-issues, I am "resetting" the image view after every reuse:
override func prepareForReuse() {
profilePictureView.image = UIImage(named: "defaultProfilePicture")
}
But why are still some images loaded false when scrolling fastly?
hmmm, this is what I thought too.
__Update:
So, I extended the function with a Label Parameter (type Any), that is returned back as it was put in the function. I tried to compare the parameter (is used the indexpath) with the current indexpath. Actually, this should work - shouldn't it?!
loadImageAsync(imageUrl: imageUrl!, label: ip) { (success, image, backlabel) -> Void in
cell.loader.stopAnimating()
if (backlabel as! IndexPath == indexPath) {
//set image...
But however, it doesn't show any effect. Do you know why or have any other solutions to fix this?
The issue is that if you scroll fast, the download may take long enough that by the time it's complete, the cell in question has scrolled off the screen and been recycled for a different indexPath in your data model.
The trick is to ask the table view for the cell at that indexPath in the completion block and only install the image if you get a cell back:
loadImageAsync(imageUrl: imageUrl!, label: ip, for indexPath: IndexPath) { (success, image, backlabel) -> Void in
if(success){
let targetCell = tableview.cell(for: indexPath)
targetCell.profilePictureView.image = image
}
}
EDIT:
Redefine your loadImageAsync function like this:
func loadImageAsync(imageUrl: URL,
indexPath: IndexPath,
completionHandler handler: #escaping (_ success: Bool,
_ image: UIImage?,
_ indexPath: IndexPath ) -> Void) { ... }
EDIT #2
And by the way, you should really save your images to disk and load them from there rather than loading from the internet each time. I suggest using a hash of the image URL as a filename.
Modify loadImageAsync as follows:
Check to see if the file already exists on disk. If so, load it and return it.
If the file does not exist, do the async load, and then save it to disk using the hash of the URL as a filename, before returning the in-memory image.
Because your completionHandler can be called after the cell has been reused for the next user, and possibly another image request for the cell has been fired. The order of events (reuse/completion) is not predictable, and in fact a later async request could complete before an earlier one.