Download multiple URLs using Alamofire 5 - ios

I want to download the file from the URL Array and put the results stored in another Array.
Ex.
let urls = [URL1,URL2,URL3,....]
for url in urls {
AF.download(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers:
headers, interceptor: nil, to: destination).response { (responseData) in
self.urlLoad.append(responseData.fileURL!)
completion(self.urlLoad)
}
}
What I am having a problem with right now is the download and get the same result. I don't know what to do. Please guide me

You haven’t shared your destination, but the question is how that closure was defined and how you built the URL that was returned. But you want to give it a DownloadRequest.Destination closure that returns a unique path for each URL. For example, you can tell it to put the downloads in the “caches” folder like so:
let urls = [
"https://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg",
"https://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg",
"https://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg",
"https://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg",
"https://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg",
"https://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg"
].compactMap { URL(string: $0) }
let folder = try! FileManager.default
.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("images")
for url in urls {
let destination: DownloadRequest.Destination = { _, _ in
let fileURL = folder.appendingPathComponent(url.lastPathComponent)
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
AF.download(url, to: destination).response { responseData in
switch responseData.result {
case .success(let url):
guard let url = url else { return }
self.urlLoad.append(url)
case .failure(let error):
print(error)
}
}
}
Or, alternatively:
for url in urls {
AF.download(url) { _, _ in
(folder.appendingPathComponent(url.lastPathComponent), [.removePreviousFile, .createIntermediateDirectories])
}.response { responseData in
switch responseData.result {
case .success(let url):
guard let url = url else { return }
self.urlLoad.append(url)
case .failure(let error):
print(error)
}
}
}

Related

How to get partially downloaded data in Alamofire when network connection was lost

I am trying to download mp3 file from server. When the network connection was lost (I turn off my wifi connection explicitly) on device, the download request is failed which is fine but i can't get the partially downloaded data.
I have found there is a property called resumeData in responseData but it always returns nil.
I am using Alamofire 5.2.0. Here is my code.
func startTask(urlString: String) {
guard let url = URL(string: urlString) else { return }
let requestDestination: DownloadRequest.Destination = { _, _ in
let documentsURL = self.localFilePathForUser(url: url)!
return (documentsURL, [.removePreviousFile])
}
AF.download(url, to: requestDestination)
.validate()
.downloadProgress { [weak self] progress in
print("Progress: \(progress.fractionCompleted)")
}
.response { [weak self] responseData in
switch responseData.result {
case .success(_):
print("Download success")
case .failure(let error):
print("Download failed: ", error.localizedDescription)
print("Downloaded data: ", responseData.resumeData?.count.byteSize) // always get nil
}
}
}
func localFilePathForUser(url: URL) -> URL? {
guard let userId = UserManager.shared.currentUser?.id else { return nil }
guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
let newUrl = documentsPath.appendingPathComponent("\(userId)").appendingPathComponent(url.lastPathComponent).deletingPathExtension().appendingPathExtension("mp3")
return newUrl
}
Found the solution. I just cast the AFError to NSError and retrieve the resumableData from userInfo using the key named NSURLSessionDownloadTaskResumeData. You can also find this useful. Here is the working code
func startTask(urlString: String) {
guard let url = URL(string: urlString) else { return }
let requestDestination: DownloadRequest.Destination = { _, _ in
let documentsURL = self.localFilePathForUser(url: url)!
return (documentsURL, [.removePreviousFile])
}
AF.download(url, to: requestDestination)
.validate()
.downloadProgress { [weak self] progress in
print("Progress: \(progress.fractionCompleted)")
}
.response { [weak self] responseData in
switch responseData.result {
case .success(_):
print("Download success")
case .failure(let error):
print("Download failed: ", error.localizedDescription)
if let resumabledata = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
print("Resumable Data found: ", resumabledata.count)
} else {
print("No resumable Data")
}
}
}
}

URLSession moving slow when caching a firebase video url

I initially asked this question, got the answer, and in the comments #LeoDabus said:
NSData(contentsOf: url) it is not mean to use with non local resources
urls
He suggested I use URLSession which I did, but the response is very slow. I'm wondering am I doing something wrong. The video is 2mb if that makes any difference.
Inside the the session's completionHandler I tried updating the returned data on the main queue but there was a scrolling glitch while doing that. Using DispatchQueue.global().async there is no scrolling glitch but it seems like it takes longer return
// all of this occurs inside my data model
var cachedURL: URL?
let videoUrl = dict["videoUrl"] as? String ?? "" // eg. "https://firebasestorage.googleapis.com/v0/b/myApp.appspot.com/o/abcd%277920FHqFBkl7D6j%2F-MC65EFG_qT0KZbdtFhU%2F48127-8C29-4666-96C9-E95BE178B268.mp4?alt=media&token=bf85dcd1-8cee-428e-87bc-91800b7316de"
guard let url = URL(string: videoUrl) else { return }
useURLSessionToCacheVideo(url)
func useURLSessionToCacheVideo(_ url: URL) {
let lastPathComponent = url.lastPathComponent
let cachesDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
let file = cachesDir.appendingPathComponent(lastPathComponent)
if FileManager.default.fileExists(atPath: file.path) {
self.cachedURL = file
print("url already exists in cache")
return
}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error { return }
if let response = response as? HTTPURLResponse {
guard response.statusCode == 200 else {
return
}
}
guard let data = data else {
return
}
DispatchQueue.global().async { // main queue caused a hiccup while scrolling a cv
do {
try data.write(to: file, options: .atomic)
DispatchQueue.main.async { [weak self] in
self?.cachedURL = file
}
} catch {
print("couldn't cache video file")
}
}
}).resume()
}
You should write the file from the session's background thread:
func useURLSessionToCacheVideo(_ url: URL) {
let lastPathComponent = url.lastPathComponent
let fileURL = try! FileManager.default
.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(lastPathComponent)
if FileManager.default.fileExists(atPath: fileURL.path) {
self.cachedURL = fileURL
print("url already exists in cache")
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode,
let data = data
else {
return
}
do {
try data.write(to: fileURL, options: .atomic)
DispatchQueue.main.async { [weak self] in
self?.cachedURL = fileURL
}
} catch {
print("couldn't cache video file")
}
}.resume()
}
This also accepts any 2xx HTTP response code.
That having been said, I’d suggest using a download task, which reduces the peak memory usage and writes the data to the file as you go along:
func useURLSessionToCacheVideo(_ url: URL) {
let lastPathComponent = url.lastPathComponent
let fileURL = try! FileManager.default
.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(lastPathComponent)
if FileManager.default.fileExists(atPath: fileURL.path) {
self.cachedURL = fileURL
print("url already exists in cache")
return
}
URLSession.shared.downloadTask(with: url) { location, response, error in
guard
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode,
let location = location
else {
return
}
do {
try FileManager.default.moveItem(at: location, to: fileURL)
DispatchQueue.main.async { [weak self] in
self?.cachedURL = fileURL
}
} catch {
print("couldn't cache video file")
}
}.resume()
}
Personally, rather than having this routine update cachedURL itself, I'd use a completion handler pattern:
enum CacheError: Error {
case failure(URL?, URLResponse?)
}
func useURLSessionToCacheVideo(_ url: URL, completion: #escaping (Result<URL, Error>) -> Void) {
let lastPathComponent = url.lastPathComponent
let fileURL = try! FileManager.default
.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(lastPathComponent)
if FileManager.default.fileExists(atPath: fileURL.path) {
completion(.success(fileURL))
return
}
URLSession.shared.downloadTask(with: url) { location, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
guard
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode,
let temporaryLocation = location
else {
DispatchQueue.main.async {
completion(.failure(CacheError.failure(location, response)))
}
return
}
do {
try FileManager.default.moveItem(at: temporaryLocation, to: fileURL)
DispatchQueue.main.async {
completion(.success(fileURL))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}.resume()
}
And call it like so:
useURLSessionToCacheVideo(url) { result in
switch result {
case .failure(let error):
print(error)
case .success(let cachedURL):
self.cachedURL = cachedURL
}
}
That way, the caller is responsible for updating cachedURL, it now knows when it's done (in case you want to update the UI to reflect the success or failure of the download), and your network layer isn't entangled with the model structure of the caller.

Swift How to Unzip file from URL with Alamofire and SSZipArchive

func downLoad(fileName:String) {
let urlString : String = "\(myurl)\(fileName)"
var localPath: NSURL?
let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory)
Alamofire.download(urlString, method: .get, encoding: JSONEncoding.default, to: destination)
.downloadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
print("Progress: \(progress.fractionCompleted)")
}
.validate { request, response, temporaryURL, destinationURL in
// Custom evaluation closure now includes file URLs (allows you to parse out error messages if necessary)
return .success
}
.responseJSON { response in
debugPrint(response)
print(response.destinationURL?.path)
print(response.destinationURL?.absoluteString)
let unzipDirectory = self.unzipPath(fileURL:fileName)
let success = SSZipArchive.unzipFile(atPath: (response.destinationURL?.path)!, toDestination: unzipDirectory!)
print(success)
if !success {
return
}
}
}
func unzipPath(fileName:String) -> String? {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let url = NSURL(fileURLWithPath: path)
let pathComponent = url.appendingPathComponent("test\(fileName)")
do {
try FileManager.default.createDirectory(at: pathComponent!, withIntermediateDirectories: true, attributes: nil)
} catch {
return nil
}
return pathComponent?.absoluteString
}
i get right path in response.destinationURL
but success is false
i tried atPath : to response.destinationURL?.path and
response.destinationURL?.absoluteString
but failed too
What i am doing wrong
destinationURL?.path return this
Optional("/Users/MyUser/Library/Developer/CoreSimulator/Devices/3FBAD207-E5AB-4FC1-8199-2269A1249D97/data/Containers/Data/Application/CB1C2EF5-3100-430B-B869-774C09B8EA7F/Documents/testFile.zip")
response.destinationURL?.absoluteString
return this
Optional("file:///Users/MyUser/Library/Developer/CoreSimulator/Devices/3FBAD207-E5AB-4FC1-8199-2269A1249D97/data/Containers/Data/Application/CB1C2EF5-3100-430B-B869-774C09B8EA7F/Documents/testFile.zip")
i think this is correct URL
why failed unzip?
By looking at your code, the path you are using for the source is perfect.
For the destination path, try updating it to
func unzipPath(fileName:String) -> String? {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString
let pathWithComponent = path.appendingPathComponent("test\(fileName)")
do {
try FileManager.default.createDirectory(atPath: pathWithComponent, withIntermediateDirectories: true, attributes: nil)
} catch {
return nil
}
return pathWithComponent
}
Try and share the results.

Downloading to camera roll using alamofire

I'm using this function to download videos to a file called downloads using alamofire.
How would I edit it so it saves videos to the camera roll
func downloadVideoToCameraRoll() {
let destination: DownloadRequest.DownloadFileDestination = { _, response in
let pathComponent = response.suggestedFilename!
var documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let directoryURL: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let folderPath: URL = directoryURL.appendingPathComponent("Downloads", isDirectory: true)
let fileURL: URL = folderPath.appendingPathComponent(pathComponent)
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
Alamofire.download(firstId, method: .get, parameters: nil, encoding: JSONEncoding.default, to: destination)
.downloadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
self.progresss.setProgress(Float(progress.fractionCompleted), animated: true)
//print("Progress: \(progress.fractionCompleted)")
}
.validate { request, response, temporaryURL, destinationURL in
// Custom evaluation closure now includes file URLs (allows you to parse out error messages if necessary)
return .success
}
.responseJSON { response in
debugPrint(response)
print(response.temporaryURL!)
print(response.destinationURL!)
}
You can use this in swift 3:
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: urlToYourVideo)
}) { saved, error in
if saved {
print("Saved")
}
}
Noted: Need to import Photos
Try the code below
func downloadVideoToCameraRoll() {
let destination: DownloadRequest.DownloadFileDestination = { _, response in
let pathComponent = response.suggestedFilename!
var documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let directoryURL: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
return (directoryURL, [.removePreviousFile, .createIntermediateDirectories])
}
Alamofire.download(firstId, method: .get, parameters: nil, encoding: JSONEncoding.default, to: destination)
.downloadProgress(queue: DispatchQueue.global(qos: .utility)) { progress in
self.progresss.setProgress(Float(progress.fractionCompleted), animated: true)
//print("Progress: \(progress.fractionCompleted)")
}
.validate { request, response, temporaryURL, destinationURL in
// Custom evaluation closure now includes file URLs (allows you to parse out error messages if necessary)
return .success
}
.responseJSON { response in
debugPrint(response)
print(response.temporaryURL!)
print(response.destinationURL!)
saveVideoTo(destinationURL)
}
func saveVideoTo(_ videoUrl:Url?){
if videoUrl != nil {
PHPhotoLibrary.sharedPhotoLibrary().performChanges({ () -> Void in
let createAssetRequest: PHAssetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(NSURL(string: videoUrl)!)!
createAssetRequest.placeholderForCreatedAsset
}) { (success, error) -> Void in
if success {
//saved successfully
}
else {
//error occured
}
}
}
}

Check if a file already exists before downloading it with Alamofire + suggestedDownloadDestination

How can I check if a given file has been already downloaded before re-downloading it by using Alamofire? I'm using suggestedDownloadDestination so Alamofire will automatically choose the name of the file and save it in the choosen directory, for example the .CachesDirectory. The problem is that the value given by suggestedDownloadDestination is of type DownloadFileDestination which will return a NSURL only by calling him with the request's response, but in this way I could not ever know the file path without performing a request before.
This is the code I currently use to download a file with Alamofire:
Alamofire.download(.GET, downloadLink, destination: destination).progress {
bytesRead, totalBytesRead, totalBytesExpectedToRead in
}.response {
request, response, data, error in
guard error == nil else {
return
}
// This will give me the file path but we're already in a Request!
print("\(destination(NSURL(string: "")!, response!))")
}
What am I missing?
Not sure if you figured this out yet, but you can create an extension over Alamofire.DownloadRequest like:
extension Alamofire.DownloadRequest {
open class func suggestedDownloadDestination(
for directory: FileManager.SearchPathDirectory = .documentDirectory,
in domain: FileManager.SearchPathDomainMask = .userDomainMask,
with options: DownloadOptions)
-> DownloadFileDestination
{
return { temporaryURL, response in
let destination = DownloadRequest.suggestedDownloadDestination(for: directory, in: domain)(temporaryURL, response)
return (destination.destinationURL, options)
}
}
}
Now you can specify in the options parameter if you want the file to be overwritten:
let destination = DownloadRequest.suggestedDownloadDestination(for: .cachesDirectory,
in: .userDomainMask,
with: [DownloadRequest.DownloadOptions.removePreviousFile])
Here is the solution which I used
Download a file
check if it exist if so just return the path otherwise
func downloadDocumentFile(filePath: String,onDownloadProgress: #escaping(_ progress: Double) -> Void,onError: #escaping(_ errorMessage: String) -> Void,onSuccess: #escaping(_ destinationUrl: URL) -> Void){
guard let url = URL(string: filePath) else {
onError("Couldn't create url from passed file path")
assertionFailure()
return
}
let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory, in: .userDomainMask)
Alamofire.download(url, to: destination)
.downloadProgress { (progress) in
onDownloadProgress(progress.fractionCompleted)
}
.responseData(queue: .main) { (response) in
switch response.result {
case .success:
if let destinationUrl = response.destinationURL {
onSuccess(destinationUrl)
}else {
onError("Couldn't get destination url")
assertionFailure()
}
case .failure(let error):
// check if file exists before
if let destinationURL = response.destinationURL {
if FileManager.default.fileExists(atPath: destinationURL.path){
// File exists, so no need to override it. simply return the path.
onSuccess(destinationURL)
print()
}else {
onError(error.localizedDescription)
assertionFailure()
}
}else {
onError(error.localizedDescription)
assertionFailure()
}
}
}
}

Resources