Upload SDK for iOS
The FastPix iOS Upload SDK helps you to upload videos from your iOS app to the FastPix platform. It’s built to handle large files, manage network interruptions, and give you full control over the upload process all with minimal setup.
Step 1
Step 1: Install the SDK
Step 2
Create an upload URL
Step 3
Start your upload
Step 4
Track progress with progressHandler
Step 5
Pause and resume uploads
Why use the FastPix iOS upload SDK?
The FastPix iOS SDK is built for reliability, flexibility, and performance when uploading large video files from your app. Key features include:
- Chunked uploads: Automatically breaks large videos into smaller chunks for smoother transfers. You can customize the chunkSize to fit your app’s needs.
- Resumable uploads: If the network drops or the app closes, uploads resume from where they left off no need to start over.
- Pause and resume support: Give users control by allowing uploads to be paused mid-way and resumed later without data loss.
Step 1: Install the SDK
Prerequisites
- An existing Xcode project.
- Swift Package Manager (SPM) installed on your development machine.
Installing using Swift Package Manager (SPM)
- Open your Xcode project.
- Navigate to the "File" menu and select "Swift Packages" > "Add Package Dependency..."
- In the search bar, enter the URL for the FastPixUploadSDK repository:
https://github.com/FastPix/iOS-Uploads
- Locate the desired version of the SDK. By default, the latest version will be displayed.
- Click "Add Package" to integrate the FastPixUploadSDK into your project.
Importing the SDK package
Once the package dependency is added, you can import the FastPixUploadSDK module:
import fp_swift_upload_sdk
Step 2: Create an upload URL
To fully integrate the FastPixUploadSDK into your iOS application, follow these steps:
Generating a signed direct upload URL
- Implement server-side logic to interact with FastPix's API to create a signed URL.
- You will need an access token from FastPix (Token ID and Secret Key), generated in your FastPix dashboard.
- After making the request, FastPix returns a signed URL used for secure uploads.
func createDirectUpload() async throws -> URL {
let parameters: [String: Any] = [
"corsOrigin": "*",
"newAssetSettings": [
"accessPolicy": "public",
"generateSubtitles": true,
"normalizeAudio": true,
"maxResolution": "1080p"
]
]
guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters) else {
throw NSError(domain: "com.example.app", code: 500, userInfo: [
NSLocalizedDescriptionKey: "Failed to serialize parameters"
])
}
let request = try {
var req = try URLRequest(url: fullURL(forEndpoint: "upload"))
req.httpBody = jsonData
req.httpMethod = "POST"
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.addValue("application/json", forHTTPHeaderField: "Accept")
let credentials = "\(FASTPIX_ACCESS_TOKEN_ID):\(FASTPIX_SECRET_KEY)"
let basicAuthCredential = Data(credentials.utf8).base64EncodedString()
req.addValue("Basic \(basicAuthCredential)", forHTTPHeaderField: "Authorization")
return req
}()
let (data, response) = try await urlSession.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw CreateUploadError(message: "Invalid HTTP response")
}
if (200...299).contains(httpResponse.statusCode) {
do {
if let jsonDictionary = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let dataDic = jsonDictionary["data"] as? [String: Any],
let urlString = dataDic["url"] as? String,
let uploadUrl = URL(string: urlString) {
return uploadUrl
} else {
print("Failed to parse upload URL from response.")
return URL(string: "")!
}
} catch {
print("Error decoding response: \(error.localizedDescription)")
return URL(string: "")!
}
} else {
let errorMessage = String(decoding: data, as: UTF8.self)
throw CreateUploadError(message: "Upload POST failed: HTTP \(httpResponse.statusCode):\n\(errorMessage)")
}
}
/// Generates a full URL for a given endpoint in the FastPix Video public API
private func fullURL(forEndpoint endpoint: String) throws -> URL {
let fullPath = "https://api.fastpix.io/v1/on-demand/\(endpoint)"
guard let url = URL(string: fullPath) else {
throw CreateUploadError(message: "Bad endpoint: \(endpoint)")
}
return url
}
Step 3: Start your upload
import fp_swift_upload_sdk
// Initialize the uploader
var uploader = Uploads()
Task {
do {
// Get the upload URL from your backend
let createUploadURL = try await self.myServerBackend.createDirectUpload()
// Start the upload
uploader.uploadFile(
file: file!,
endpoint: createUploadURL.absoluteString,
chunkSizeKB: Int(chunkSize.text ?? "") ?? 0
)
} catch {
print("Failed to create upload URL: \(error.localizedDescription)")
}
}
- Uploader initialization:
var uploader = Uploads()
prepares for a new session. - Async upload trigger: Runs inside
Task
for async handling. - Signed URL retrieval: Calls
createDirectUpload()
from your backend. - File upload start: Uses
uploadFile()
to initiate upload.
Add this code in your upload functionality module within your iOS app project. It could be in a service layer or directly within a controller or view model where the upload operation is triggered, ensuring it is properly integrated with your app's workflow.
Step 4: Track progress with progressHandler
You can show real-time progress updates during the upload by using the progressHandler
callback provided by the SDK. This allows you to update progress bars, display percentage completion, and notify users when the upload finishes.
import fp_swift_upload_sdk
var uploader = Uploads()
uploader.progressHandler = { [weak self] progress in
// Ensure UI updates are done on the main thread
DispatchQueue.main.async {
guard let self = self else { return }
// Show and style progress UI
self.progressBar.layer.cornerRadius = 10
self.progressBar.isHidden = false
self.label_progress.isHidden = false
// Update progress bar and label
self.progressBar.progress = progress
self.label_progress.text = String(format: "%.0f%%", progress * 100)
// If upload is complete
if progress == 1.0 {
self.button_pause.isHidden = true
self.button_abort.isHidden = true
self.button_upload.isHidden = false
// Show success alert
let alertController = UIAlertController(
title: "Success",
message: "The video file uploaded successfully.",
preferredStyle: .alert
)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(okAction)
self.present(alertController, animated: true, completion: nil)
}
}
}
Step 5 : Pause and resume uploads
Pause and resumable uploads allow you to temporarily pause a file upload and then pick it up later from where it left off. This is especially useful for large video files that may be interrupted due to network issues or app crashes.
- Create an uploader object
var uploader = Uploads()
- Call pause() to temporarily stop the upload
uploader.pause()
- Call resume() to continue the upload from where it stopped
uploader.resume()
Below is an example implementation of the iOS Upload SDK. You can customize the UI and components based on your app’s design.
import UIKit
import PhotosUI
import fp_swift_upload_sdk
import Foundation
import Network
class SelectUploadVideoViewController: UIViewController, PHPickerViewControllerDelegate, UITextFieldDelegate, UploadProgressDelegate, UploadSDKErrorDelegate, UIDocumentPickerDelegate {
@IBOutlet weak var thumbNailImage: UIImageView!
@IBOutlet weak var button_upload: UIButton!
@IBOutlet weak var progressBar: UIProgressView!
@IBOutlet weak var label_progress: UILabel!
@IBOutlet weak var button_selectVideo: UIButton!
@IBOutlet weak var errorLabel: UILabel!
@IBOutlet weak var buttonsStack: UIStackView!
@IBOutlet weak var button_pause: UIButton!
@IBOutlet weak var chunkSize: UITextField!
@IBOutlet weak var uploadedChunk: UILabel!
@IBOutlet var button_abort: UIButton!
var isOfflineModeEnabled = false
var uploadIsPaused = false
var file : URL?
var pauseBtnClicked = false
var uploader = Uploads()
var picje = VideoPickerConfiguration()
var thumGenerater = ThumbnailImageGenerater()
var authenticatedURL: URL? = nil
var videoInputURL: URL? = nil
let myServerBackend = UploadURLGenerater(urlSession: URLSession(configuration: URLSessionConfiguration.default))
private var prepareTask: Task<Void, Never>? = nil
public var isPaused = false
private var assetRequestId: PHImageRequestID? = nil
let session = URLSession.shared
var requestData = [String: Any]()
var chunks : Int = 0
var resData = [String]()
var urlIndex = 0
var chunksList: [String] = []
var chunksArray: [Data] = []
var signedURL: URL?
var objectName: String?
var uploadId: String?
let monitor = NWPathMonitor()
var isInternetAvailable = false
var chunkAttempt = 0
override func viewDidLoad() {
super.viewDidLoad()
chunkSize.delegate = self
uploader.progressDelegate = self
uploader.errorDelegate = self
button_abort.isHidden = true
pickerConfig()
title = "Create New Upload"
button_upload.isHidden = false
button_pause.isHidden = true
PHPhotoLibrary.authorizationStatus(for: .readWrite)
button_upload.isEnabled = false
button_upload.backgroundColor = .lightGray
// Set the progress handler
progressBar.isHidden = true
label_progress.isHidden = true
uploader.progressHandler = { [weak self] progress in
// Update UI with the progress value
DispatchQueue.main.async {
self?.progressBar.layer.cornerRadius = 10
self?.progressBar.isHidden = false
self?.label_progress.isHidden = false
// Update progress bar or any other UI element
self?.progressBar.progress = progress
self?.label_progress.text = String(format: "%.0f%%", progress * 100)
if progress == 1.0 {
self?.button_pause.isHidden = true
self?.button_abort.isHidden = true
self?.button_upload.isHidden = false
let alertController = UIAlertController(title: "success", message: "The video file uploaded successfully.", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(okAction)
self?.present(alertController, animated: true, completion: nil)
}
}
}
startMonitoring()
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
chunkSize.attributedPlaceholder = NSAttributedString(
string: "Enter chunk size",
attributes: [
.foregroundColor: UIColor.white // Change to your desired color
]
)
}
func didUpdateProgressText(_ text: String) {
DispatchQueue.main.async {
self.uploadedChunk.text = text
}
}
func uploadSDKDidFail(with error: String) {
DispatchQueue.main.async {
self.displayErrorAlert(error: error)
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder() // Dismisses the keyboard
return true
}
func startMonitoring() {
monitor.pathUpdateHandler = { path in
DispatchQueue.main.async { [self] in
self.isInternetAvailable = path.status == .satisfied
if self.isInternetAvailable {
if uploadIsPaused {
if self.chunkAttempt < chunksArray.count {
}
}
} else {
}
}
}
let queue = DispatchQueue(label: "NetworkMonitor")
monitor.start(queue: queue)
}
@objc func appDidBecomeActive() {
startMonitoring()
}
func pickerConfig() {
let alert = UIAlertController(title: "Select Media Type", message: nil, preferredStyle: .actionSheet)
// Option to pick Video
alert.addAction(UIAlertAction(title: "Pick Video", style: .default, handler: { _ in
let config = self.picje.pickerConfig() // video-only config
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
picker.modalPresentationStyle = .overCurrentContext
self.present(picker, animated: true)
}))
// Option to pick Audio
alert.addAction(UIAlertAction(title: "Pick Audio", style: .default, handler: { _ in
let audioPicker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.audio])
audioPicker.allowsMultipleSelection = false
audioPicker.delegate = self
self.present(audioPicker, animated: true)
}))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
self.present(alert, animated: true)
}
@IBAction func button_pauseVideo(_ sender: UIButton) {
sender.isEnabled = false
if sender.isSelected {
sender.setTitle("Pause", for: .normal)
uploadIsPaused = false
pauseBtnClicked = true
uploader.resume()
sender.backgroundColor = .systemGreen
} else {
sender.setTitle("Resume", for: .normal)
uploadIsPaused = true
pauseBtnClicked = false
startMonitoring()
uploader.pause()
sender.backgroundColor = .systemRed
}
sender.isSelected.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
sender.isEnabled = true
}
}
@IBAction func button_uploadVideo(_ sender: UIButton) {
button_upload.isHidden = true
button_pause.isHidden = false
button_abort.isHidden = false
self.progressBar.isHidden = false
self.label_progress.isHidden = false
self.label_progress.text = String(format: "%.0f%%", 0.0000000 * 100)
Task {
let createUploadURL = try await self.myServerBackend.createDirectUpload()
uploader.uploadFile(file: file!, endpoint: createUploadURL.absoluteString, chunkSizeKB: Int(chunkSize.text ?? "") ?? 0)
}
}
@IBAction func button_abort(_ sender: UIButton) {
button_pause.isHidden = true
button_abort.isHidden = true
button_upload.isHidden = false
uploader.abort()
sender.backgroundColor = .red
}
@IBAction func button_selectVideo(_ sender: UIButton) {
pickerConfig()
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
dismiss(animated: true, completion: nil)
guard let result = results.first else {
return
}
if let assetRequestId = self.assetRequestId {
PHImageManager.default().cancelImageRequest(assetRequestId)
}
let tempDir = FileManager.default.temporaryDirectory
let tempFile = URL(string: "upload-\(Date().timeIntervalSince1970).mp4", relativeTo: tempDir)!
guard let assetIdentitfier = result.assetIdentifier else {
print("No Asset ID for chosen asset")
return
}
guard let assetIdentitfier = result.assetIdentifier else {
print("No Asset ID for chosen asset")
return
}
let options = PHFetchOptions()
options.includeAssetSourceTypes = [.typeUserLibrary, .typeCloudShared]
let phAssetResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentitfier], options: options)
guard let phAsset = phAssetResult.firstObject else {
return
}
let exportOptions = PHVideoRequestOptions()
exportOptions.isNetworkAccessAllowed = true
exportOptions.deliveryMode = .highQualityFormat
assetRequestId = PHImageManager.default().requestExportSession(forVideo: phAsset, options: exportOptions, exportPreset: AVAssetExportPresetHighestQuality, resultHandler: {(exportSession, info) -> Void in
DispatchQueue.main.async { [weak self] in
guard let exportSession = exportSession else {
return
}
self?.thumGenerater.extractThumbnailAsync(exportSession.asset) { thumbnailImage in
guard let thumbImage = thumbnailImage else { return }
DispatchQueue.main.async {
self?.thumbNailImage.image = UIImage(cgImage: thumbImage)
self?.thumbNailImage.isHidden = false
self?.button_selectVideo.isUserInteractionEnabled = false
}
}
}
})
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier , completionHandler: { [ weak self] url, error in
guard let fileURL = url else {
return
}
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
guard let targetURL = documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent) else { return }
self?.file = (documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent))!
do {
if FileManager.default.fileExists(atPath: targetURL.path) {
try FileManager.default.removeItem(at: targetURL)
} else {
// print("file path not exists")
}
try FileManager.default.copyItem(at: fileURL, to: targetURL)
self?.videoInputURL = targetURL
DispatchQueue.main.async {
self?.button_upload.isEnabled = true
self?.button_upload.backgroundColor = .green
}
} catch {
self?.displayErrorAlert(error: error.localizedDescription)
}
})
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let fileURL = urls.first else { return }
// Start accessing security-scoped resource
let didStartAccessing = fileURL.startAccessingSecurityScopedResource()
defer {
if didStartAccessing {
fileURL.stopAccessingSecurityScopedResource()
}
}
// Confirm access granted
guard didStartAccessing else {
print("Failed to access security-scoped resource")
self.displayErrorAlert(error: "Unable to access selected file due to security permissions.")
return
}
// Now safe to use fileURL
print("Picked audio file: \(fileURL.lastPathComponent)")
// Copy to local directory
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
guard let targetURL = documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent) else { return }
self.file = documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent)
do {
if FileManager.default.fileExists(atPath: targetURL.path) {
try FileManager.default.removeItem(at: targetURL)
}
try FileManager.default.copyItem(at: fileURL, to: targetURL)
self.videoInputURL = targetURL // Or self.audioInputURL
DispatchQueue.main.async {
self.button_upload.isEnabled = true
self.button_upload.backgroundColor = .green
}
} catch {
self.displayErrorAlert(error: error.localizedDescription)
}
}
func pickerDidCancel(_ picker: PHPickerViewController) {
dismiss(animated: true, completion: nil)
}
// Display an error message
func displayErrorAlert(error: String) {
let alertController = UIAlertController(title: "Error", message: error, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in
DispatchQueue.main.async {
self?.thumbNailImage.isHidden = false
self?.button_selectVideo.isUserInteractionEnabled = true
if error == "The current upload was aborted. Please try uploading a new video" {
self?.navigationController?.popViewController(animated: true)
}
}
}
alertController.addAction(okAction)
present(alertController, animated: true, completion: nil)
}
deinit {
monitor.cancel()
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
}
}
class UploadURLGenerater {
public func presentErrorAlert(on viewController: UIViewController, withTitle title: String, message: String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
alertController.addAction(okAction)
viewController.present(alertController, animated: true, completion: nil)
}
func createDirectUpload() async throws -> URL {
let parameters: [String: Any] = [
"corsOrigin": "*",
"pushMediaSettings": [
"accessPolicy": "public",
"generateSubtitles": true,
"normalizeAudio": true,
"maxResolution": "1080p"
]
]
guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters) else {
throw NSError(domain: "com.example.app", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to serialize parameters"])
}
let request = try {
var req = try URLRequest(url:fullURL(forEndpoint: "upload"))
req.httpBody = jsonData
req.httpMethod = "POST"
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.addValue("application/json", forHTTPHeaderField: "accept")
let basicAuthCredential = "\(FASTPIX_ACCESS_TOKEN_ID):\(FASTPIX_SECRET_KEY)".data(using: .utf8)!.base64EncodedString()
req.addValue("Basic \(basicAuthCredential)", forHTTPHeaderField: "Authorization")
return req
}()
let (data, response) = try await urlSession.data(for: request)
let httpResponse = response as! HTTPURLResponse
print("Response",httpResponse.statusCode)
var uploadUrl: URL?
if (200...299).contains(httpResponse.statusCode) {
do {
// Assuming responseData is the Data object you want to convert to a dictionary
if let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
var dataDic = jsonDictionary["data"] as? NSDictionary
uploadUrl = URL(string: dataDic?.value(forKey: "url") as? String ?? "")
} else {
print("Failed to convert data to dictionary")
}
} catch {
print("Error converting data to dictionary: \(error.localizedDescription)")
}
guard let uploadUrl = uploadUrl else { return URL(string: "")!}
return uploadUrl
} else {
throw CreateUploadError(message: "Upload POST failed: HTTP \(httpResponse.statusCode):\n\(String(decoding: data, as: UTF8.self))")
}
}
/// Generates a full URL for a given endpoint in the FastPix Video public API
private func fullURL(forEndpoint: String) throws -> URL {
guard let url = URL(string: "https://venus-v1.fastpix.dev/on-demand/\(forEndpoint)") else {
throw CreateUploadError(message: "bad endpoint")
}
return url
}
// https://dev-api.fastpix.io
let urlSession: URLSession
let jsonEncoder: JSONEncoder
let jsonDecoder: JSONDecoder
//Gowtham Venus
let FASTPIX_ACCESS_TOKEN_ID = "ACCESS TOKEN"
let FASTPIX_SECRET_KEY = "SECRET KEY"
init(urlSession: URLSession) {
self.urlSession = urlSession
self.jsonEncoder = JSONEncoder()
self.jsonEncoder.keyEncodingStrategy = JSONEncoder.KeyEncodingStrategy.convertToSnakeCase
self.jsonDecoder = JSONDecoder()
self.jsonDecoder.keyDecodingStrategy = JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase
}
convenience init() {
self.init(urlSession: URLSession(configuration: URLSessionConfiguration.default))
}
}
struct CreateUploadError : Error {
let message: String
}
fileprivate struct CreateUploadPost: Codable {
var newAssetSettings: NewAssetSettings = NewAssetSettings()
var corsOrigin: String = "*"
}
fileprivate struct NewAssetSettings: Codable {
var playbackPolicy: [String] = ["public"]
var passthrough: String = "Extra video data. This can be any data and it's for your use"
var mp4Support: String = "standard"
var normalizeAudio: Bool = true
var test: Bool = false
}
fileprivate struct CreateUploadResponse: Decodable {
var url: String
var id: String
var timeout: Int64
var status: String
}
fileprivate struct CreateUploadResponseContainer: Decodable {
var data: CreateUploadResponse
}
Handling network throttling
Network throttling is the intentional slowing down of internet speeds by an Internet Service Provider (ISP) to manage network congestion.
FastPixUploadSDK is designed to handle network throttling efficiently, ensuring smooth video uploads even when network conditions are less than ideal. It automatically detects and adapts to bandwidth throttling, optimizing video upload speeds within the allowed data limits to ensure that the upload process continues without disruption.
Error handling
Proper error handling ensures the SDK gracefully handles failures and provides meaningful feedback.
Common error scenarios:
- Network failures
- Timeout errors
- Invalid data or server responses
- Permission issues
Implementation example:
import fp_swift_upload_sdk
class ViewController: UIViewController, UploadSDKErrorDelegate {
var uploader = Uploads()
override func viewDidLoad() {
super.viewDidLoad()
uploader.errorDelegate = self
}
}
This guide walks you through everything you need to integrate direct video uploads into your iOS app using the FastPix iOS Upload SDK: from SDK installation and signed URL generation to resumable uploads, progress tracking, and error handling.
Updated 1 day ago