2

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()
}
Lance Samaria
  • 11,429
  • 8
  • 67
  • 159

1 Answers1

2

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.

Rob
  • 371,891
  • 67
  • 713
  • 902