0

I'm trying to download a zip file to the user's phone storage in an iOS app. Is it possible to do this without NSURLSession?

Adrienne
  • 2,540
  • 1
  • 29
  • 39
  • 2
    Why you don't want to use `NSURLSession` exactly? There is `Data(contentsOf:)`, but it wouldn't surprise me that's doing a `NSURLSession` behind the code. – Larme Feb 23 '21 at 08:54
  • Do you want to natively program sockets, or maybe use SwiftDSSocket library https://github.com/csujedihy/SwiftDSSocket ? – Andreas Oetjen Feb 23 '21 at 08:58
  • @Larme I'm trying to download a zip file on an app extension and and keep getting a "A server with specified hostname could not be found" so am trying to explore other ways of downloading the file. – Adrienne Feb 23 '21 at 10:35
  • I would have said that the error is elsewhere, not on URLSession, it's just that it can't find the server. In an AppExtension? And inside the app, the same URL works? What's your URL exactly? See https://stackoverflow.com/questions/42996709/ios-error-code-1003-a-server-with-the-specified-hostname-could-not-be-found Are you allowed to make a request? – Larme Feb 23 '21 at 13:05
  • @Aspen you should always use URLSession downloadTask to to asynchronously download external resources to disk. – Leo Dabus Feb 23 '21 at 15:26
  • This is a very long shot but... Are you by chance missing whitelisting of domains in your extension plist? Usually missing "App Transport Security Settings -> Allow Arbitrary Loads -> TRUE" or check https://stackoverflow.com/a/30732693/526828 – Matic Oblak Feb 23 '21 at 15:33

1 Answers1

1

Yes, there are multiple tools but you should still try and use URL session.

A very easy way to do this is using Data. But it blocks your thread so you need to work a bit with queues to make it work properly (Otherwise your app MAY crash).

A very simple, non-safe, thread-blocking way would be:

func saveFile(atRemoteURL remoteURL: URL, to localURL: URL) {
    let data = try! Data(contentsOf: remoteURL)
    try! data.write(to: localURL)
}

But doing it a bit more stable should look something like this:

private func downloadIteam(atURL url: URL, completion: ((_ data: Data?, _ error: Error?) -> Void)?) {
    let queue = DispatchQueue(label: "downloading_file")
    queue.async {
        do {
            let data = try Data(contentsOf: url)
            completion?(data, nil)
        } catch {
            completion?(nil, error)
        }
    }
}

private func saveDataToDocuments(_ data: Data, to: String, completion: ((_ resultPath: URL?, _ error: Error?) -> Void)?) {
    let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(to)
    let queue = DispatchQueue(label: "saving_file")
    queue.async {
        do {
            let folderPath: String = path.deletingLastPathComponent().path
            if !FileManager.default.fileExists(atPath: folderPath) {
                try FileManager.default.createDirectory(atPath: folderPath, withIntermediateDirectories: true, attributes: nil)
            }
            try data.write(to: path)
            completion?(path, nil)
        } catch {
            completion?(nil, error)
        }
    }
}

public func downloadFileAndSaveItToDocuments(urlToRemoteFile: URL, completion: @escaping (_ file: (relativePath: String, fullPath: URL)?, _ error: Error?) -> Void) {
    
    let fileName = urlToRemoteFile.lastPathComponent
    let relativePath = "downloads/" + fileName
    
    func finish(fullPath: URL?, error: Error?) {
        DispatchQueue.main.async {
            if let path = fullPath {
                completion((relativePath, path), error)
            } else {
                completion(nil, error)
            }
        }
    }
    
    downloadIteam(atURL: urlToRemoteFile) { (data, error) in
        guard let data = data else {
            completion(nil, error)
            return
        }
        saveDataToDocuments(data, to: relativePath) { (url, saveError) in
            finish(fullPath: url, error: saveError ?? error)
        }
    }
}

I hope the code is self-documented enough.

Matic Oblak
  • 16,318
  • 3
  • 24
  • 43
  • You should never use `Data(contentsOf: URL)` to download remote resources not even from a background thread – Leo Dabus Feb 23 '21 at 15:15
  • Also if the zip file is too large downloading it all to memory will crash the app. OP should use URLSession downloadTask method which also supports downloading/resuming from the background. – Leo Dabus Feb 23 '21 at 15:18
  • @LeoDabus Yes. To both. But you CAN download it. And it is without NSURLSession which was the request in this thread. And in some cases this can actually be quite convenient in debugging phase. So even that "never" can be changed to "never in production". – Matic Oblak Feb 23 '21 at 15:21