1

In my iOS project, I need to download a file at a specific location on the device, so I can start reading it while it's still being downloaded.

My goal here is to stream an audio file from the start, but I need that audio file to be saved locally at its final location (I don't want the file to be saved in a temporary folder during the download, and then moved to its final location when the download is complete, which apparently is the default behavior).

So here is what I've tried so far:


private var observation: NSKeyValueObservation?

func downloadAudioFile()
{
    let url: URL = URL(string: "https://www.demo.com/audiofile.ogg")!
    let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
        if (error != nil)
        {
            print(error)
        }
        else
        {
            print("download complete")
        }
    }
    
    observation = task.progress.observe(\.fractionCompleted) {(progress, _) in
        print("progression : \(progress)")
    }
    
    task.resume()
}

But since I'm quite a beginner in iOS, I don't know how to save the downloaded data while it's downloading.

How can I achieve that?

Thanks.

matteoh
  • 2,810
  • 2
  • 29
  • 54
  • 1
    Maybe you can instead use downloadTask and the download delegate methods to receive periodic updates on the data being received. check out this delegate method, looks like it could be useful here: https://developer.apple.com/documentation/foundation/urlsessiondownloaddelegate/1409408-urlsession – Scriptable Jul 08 '21 at 12:37
  • @Scriptable Thanks, and yes, it looks interesting but I still don't understand how to make this work. A working example would be really helpful. – matteoh Jul 08 '21 at 13:11
  • OK so thanks to this answer https://stackoverflow.com/a/54964530/3527024, now I manage to download a file and access the data during download. Now I need to know how to append each data block on a local file properly. – matteoh Jul 08 '21 at 14:39
  • 1
    You can check this answer https://stackoverflow.com/a/53662203/526828. It writes a string but it first converts it to `Data` which you already have. So just write data directly. – Matic Oblak Jul 09 '21 at 05:53
  • @MaticOblak Looks good, thanks! – matteoh Jul 09 '21 at 08:57
  • 1
    @matteoh I suggest you answer your own question if you have a nice working solution so that other members may benefit from your findings. Thank You :) – Matic Oblak Jul 09 '21 at 11:14
  • @MaticOblak I will, I just need to finish and test everything before posting my solution – matteoh Jul 09 '21 at 12:54

1 Answers1

2

OK so here is what I did (it might not be the cleanest solution, but it works):

class Downloader : NSObject, URLSessionDelegate, URLSessionDataDelegate
{
    private var session: URLSession!
    private var dataTask: URLSessionDataTask!
    private var fileUrl: URL!

    func download(url: String, fileName: String)
    {
        // get path of directory
        guard let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
            return
        }
        
        // create file url
        fileUrl = directory.appendingPathComponent(fileName)
        
        // starts download
        session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
        var request: URLRequest = try! URLRequest(url: URL(string: url)!)
        request.cachePolicy = .reloadIgnoringLocalCacheData
        dataTask = session.dataTask(with: request)
        dataTask.resume()
    }

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
    {
        writeToFile(data: data)
    }

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse,
                    completionHandler: (URLSession.ResponseDisposition) -> Void)
    {
        completionHandler(URLSession.ResponseDisposition.allow)
    }

    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
    {
        if (error == nil)
        {
            // download complete without any error
        }
        else
        {
            // error during download
        }
    }

    func writeToFile(data: Data)
    {
        // if file exists then write data
        if FileManager.default.fileExists(atPath: fileUrl.path)
        {
            if let fileHandle = FileHandle(forWritingAtPath: fileUrl.path)
            {
                // seekToEndOfFile, writes data at the last of file(appends not override)
                fileHandle.seekToEndOfFile()
                fileHandle.write(data)
                fileHandle.closeFile()
            }
            else
            {
                // Can't open file to write
            }
        }
        else
        {
            // if file does not exist write data for the first time
            do
            {
                try data.write(to: fileUrl, options: .atomic)
            }
            catch
            {
                // Unable to write in new file
            }
        }
    }
}
matteoh
  • 2,810
  • 2
  • 29
  • 54