How to download & save the video in device storage in swift

Yasir
5 min readFeb 5, 2023

Download video with progress in swift and save it in device permanent storage with an album.

In this article, we will see how we can download a video using URLSession and get the progress of downloading. And then we will be saving that video in our device permanent storage.

Also, we will be creating an album where we will be saving all those videos.

Complete code is available at the end of this article.

Are you looking for:

  1. How to download video with URLSession in swift?
  2. How to save a video in device permanent storage in swift?
  3. How to save a video in an album in device in swift?
  4. How to get progress of video downloading in swift?

Then, this article is for you.

Frameworks & Tools we will use

  • SwiftUI
  • URLSession
  • Photos
  • AVFoundation
  • XCode
Article Cover

Downloading the video with progress and saving it in device permanent storage with an album

Create a viewModel which conforms to protocol URLSessionDownloadDelegate. So that we can track the progress & completion of downloading.

import Foundation
import Photos
import AVFoundation

class DownloadViewModel: NSObject, ObservableObject, URLSessionDownloadDelegate {
@Published var progress: Float = 0

func downloadVideo(url: URL) {
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let task = session.dataTask(with: url) { (data, response, error) in
guard error == nil else {
return
}
let downloadTask = session.downloadTask(with: url)
downloadTask.resume()
}
task.resume()
}
}

Add these functions to conform the protocol in “DownloadViewModel”.

  • didFinishDownloadingTo will give the address of downloaded url.
  • didWriteData will give the progress of downloading.
class DownloadViewModel: NSObject, ObservableObject, URLSessionDownloadDelegate {

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {

}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {

}
}

Now, update the didFinishDownloadingTo function as below.

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
guard let data = try? Data(contentsOf: location) else {
return
}

let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let destinationURL = documentsURL.appendingPathComponent("myVideo.mp4")
do {
try data.write(to: destinationURL)
saveVideoToAlbum(videoURL: destinationURL, albumName: "MyAlbum")
} catch {
print("Error saving file:", error)
}
}

Now, update the didWriteData function also as below.

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
print(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
DispatchQueue.main.async {
self.progress = (Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
}
}

As of now, you got the downloaded video with a progress. Now we will saving that video in an album of device permanent storage.

Add these functions to “DownloadViewModel”.

private func saveVideoToAlbum(videoURL: URL, albumName: String) {
if albumExists(albumName: albumName) {
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %@", albumName)
let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
if let album = collection.firstObject {
saveVideo(videoURL: videoURL, to: album)
}
} else {
var albumPlaceholder: PHObjectPlaceholder?
PHPhotoLibrary.shared().performChanges({
let createAlbumRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: albumName)
albumPlaceholder = createAlbumRequest.placeholderForCreatedAssetCollection
}, completionHandler: { success, error in
if success {
guard let albumPlaceholder = albumPlaceholder else { return }
let collectionFetchResult = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumPlaceholder.localIdentifier], options: nil)
guard let album = collectionFetchResult.firstObject else { return }
self.saveVideo(videoURL: videoURL, to: album)
} else {
print("Error creating album: \(error?.localizedDescription ?? "")")
}
})
}
}

private func albumExists(albumName: String) -> Bool {
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %@", albumName)
let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
return collection.firstObject != nil
}

private func saveVideo(videoURL: URL, to album: PHAssetCollection) {
PHPhotoLibrary.shared().performChanges({
let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
let albumChangeRequest = PHAssetCollectionChangeRequest(for: album)
let enumeration: NSArray = [assetChangeRequest!.placeholderForCreatedAsset!]
albumChangeRequest?.addAssets(enumeration)
}, completionHandler: { success, error in
if success {
print("Successfully saved video to album")
} else {
print("Error saving video to album: \(error?.localizedDescription ?? "")")
}
})
}

Make sure that you have added these properties in your info list file.

Info.plist

You can try the project. Make sure the video url is mp4 and correct.

struct ContentView: View {
@StateObject var viewModel: DownloadViewModel = .init()

var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Progress: \(viewModel.progress)")
Button {
if let url = URL(string: "http://techslides.com/demos/sample-videos/small.mp4") {
viewModel.downloadVideo(url: url)
}
} label: {
Text("download")
}
}
.padding()
}
}

You might wanted a view model saparatelyl, download manager saparatly and video saver saparatly. And that’s total up to you and your architecture. This article is just to give you the idea.

Complete code

import Foundation
import SwiftUI
import Photos
import AVFoundation

class DownloadViewModel: NSObject, ObservableObject, URLSessionDownloadDelegate {
@Published var progress: Float = 0

func downloadVideo(url: URL) {
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let task = session.dataTask(with: url) { (data, response, error) in
guard error == nil else {
return
}
let downloadTask = session.downloadTask(with: url)
downloadTask.resume()
}
task.resume()
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
guard let data = try? Data(contentsOf: location) else {
return
}

let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let destinationURL = documentsURL.appendingPathComponent("myVideo.mp4")
do {
try data.write(to: destinationURL)
saveVideoToAlbum(videoURL: destinationURL, albumName: "MyAlbum")
} catch {
print("Error saving file:", error)
}
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
print(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
DispatchQueue.main.async {
self.progress = (Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
}
}

private func saveVideoToAlbum(videoURL: URL, albumName: String) {
if albumExists(albumName: albumName) {
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %@", albumName)
let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
if let album = collection.firstObject {
saveVideo(videoURL: videoURL, to: album)
}
} else {
var albumPlaceholder: PHObjectPlaceholder?
PHPhotoLibrary.shared().performChanges({
let createAlbumRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: albumName)
albumPlaceholder = createAlbumRequest.placeholderForCreatedAssetCollection
}, completionHandler: { success, error in
if success {
guard let albumPlaceholder = albumPlaceholder else { return }
let collectionFetchResult = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumPlaceholder.localIdentifier], options: nil)
guard let album = collectionFetchResult.firstObject else { return }
self.saveVideo(videoURL: videoURL, to: album)
} else {
print("Error creating album: \(error?.localizedDescription ?? "")")
}
})
}
}

private func albumExists(albumName: String) -> Bool {
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %@", albumName)
let collection = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
return collection.firstObject != nil
}

private func saveVideo(videoURL: URL, to album: PHAssetCollection) {
PHPhotoLibrary.shared().performChanges({
let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
let albumChangeRequest = PHAssetCollectionChangeRequest(for: album)
let enumeration: NSArray = [assetChangeRequest!.placeholderForCreatedAsset!]
albumChangeRequest?.addAssets(enumeration)
}, completionHandler: { success, error in
if success {
print("Successfully saved video to album")
} else {
print("Error saving video to album: \(error?.localizedDescription ?? "")")
}
})
}
}

struct ContentView: View {
@StateObject var viewModel: DownloadViewModel = .init()

var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Progress: \(viewModel.progress)")
Button {
if let url = URL(string: "http://techslides.com/demos/sample-videos/small.mp4") {
viewModel.downloadVideo(url: url)
}
} label: {
Text("download")
}
}
.padding()
}
}

The End

Hope you learned something new from this article.

You can reach out to me at:

Thank you

--

--

Yasir

Exploring the Swift, SwiftUI, and Apple universe.