How to get file size before downloading from url? - ios

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.

Related

Swift - TableView datasource from URLSession task

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.

Swift 3 DispatchQueue.global (download images from Firebase and UI frozen)

I have a problema with this method:
func DownloadImages(uid: String, indice: Int) {
DispatchQueue.global(qos: .background).async {
let refBBDD = FIRDatabase.database().reference().child("users").child(uid)
refBBDD.observeSingleEvent(of: .value, with: { snapshot in
let snapshotValue = snapshot.value as? NSDictionary
let profileImageUrl = snapshotValue?.value(forKey: "profileImageUrl") as! String
let storage = FIRStorage.storage()
var reference: FIRStorageReference!
if(profileImageUrl == "") {
return
}
print("before")
reference = storage.reference(forURL: profileImageUrl)
reference.downloadURL { (url, error) in
let data = NSData(contentsOf: url!)
let image = UIImage(data: data! as Data)
print("image yet dowload ")
self.citas[indice].image = image
DispatchQueue.main.async(execute: { () -> Void in
self.tableView.reloadRows(at: [IndexPath(row: indice, section: 0)], with: .none)
//self.tableView.reloadData()
print("image loaded")
})
}
print("after")
})
}
}
I want to download images in background mode. I want follow using app, but the UI has frozen until methods not entry in reloadRows.
Is it possible run in true background mode and can i follow using the app??
Trace program:
before
after
before
after
...
before
after
before
after
image yet dowload --> here start UI frozen
image loaded
image yet dowload
image yet dowload
...
image yet dowload
image yet dowload
image loaded
image loaded
...
image loaded
image loaded
image yet dowload ------> here UI is not frozen
image loaded
The problem is caused by this line: let data = NSData(contentsOf: url!).
That constructor of NSData should only be used to read the contents of a local URL (a file path on the iOS device), since it is a synchronous method and hence if you call it to download a file from a URL, it will be blocking the UI for a long time.
I have never used Firebase, but looking at the documentation, it seems to me that you are using the wrong method to download that file. You should be using func getData(maxSize size: Int64, completion: #escaping (Data?, Error?) -> Void) -> StorageDownloadTask instead of func downloadURL(completion: #escaping (URL?, Error?) -> Void), since as the documentation states, the latter only "retrieves a long lived download URL with a revokable token", but doesn't download the contents of the URL itself.
Moreover, you shouldn't be force unwrapping values in the completion handler of a network request. Network requests can often fail for reasons other than a programming error, but if you don't handle those errors gracefully, your program will crash.
Your problem is in this line let data = NSData(contentsOf: url!) and let's now see what apple says about this method below
Don't use this synchronous method to request network-based URLs. For
network-based URLs, this method can block the current thread for tens
of seconds on a slow network, resulting in a poor user experience, and
in iOS, may cause your app to be terminated.
So it clearly states that this method will block your User Interface.

Asynchronously Load Images in UITableView While Downloading

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
}

Alamofire: How to download zip files sequentially within UITableView

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.

swift shouldPerformSegueWithIdentifier get data async

Im making a very basic app which has a search field to get data that is passed to a tableview.
What I want to do is run an Async task to get the data and if the data is succesfully fetched go to the next view, during the loading the screen must not freeze thats why the async part is needed.
When the user pressed the searchbutton I run the following code to get data in my
override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
method.
var valid = true
let searchValue = searchField.text
let session = NSURLSession.sharedSession()
let url = NSURL(string: "https://someapi.com/search?query=" + searchValue!)
let task = session.dataTaskWithURL(url!, completionHandler: {(data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
if let theData = data {
dispatch_async(dispatch_get_main_queue(), {
//for the example a print is enough, later this will be replaced with a json parser
print(NSString(data: theData, encoding: NSUTF8StringEncoding) as! String)
})
}
else
{
valid = false;
print("something went wrong");
}
})
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
task.resume()
return valid;
I removed some code that checks for connection/changes texts to show the app is loading data to make the code more readable.
This is the part where I have the problem, it comes after all the checks and im sure at this part I have connection etc.
What happens is it returns true (I can see this because the view is loaded), but also logs "Something went wrong" from the else statement.
I Understand this is because the return valid (at the last line) returns valid before valid is set to false.
How can I only return true (which changes the view) if the data is succesfully fetched and dont show the next view if something went wrong?
Because you want the data fetching to be async you cannot return a value, because returning a value is sync (the current thread has to wait until the function returns and then use the value). What you want instead is to use a callback. So when the data is fetched you can do an action. For this you could use closures so your method would be:
func shouldPerformSegue(identifier: String, sender: AnyObject?, completion:(success:Bool) -> ());
And just call completion(true) or completion(false) in your session.dataTaskWithURL block depending on if it was successful or not, and when you call your function you give a block for completion in which you can perform the segue or not based on the success parameter. This means you cannot override that method to do what you need, you must implement your own mechanism.

Resources