Set up resumable uploads for iOS

Upload large media files to FastPix from iOS apps with resumable chunked uploads and track the progress.

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.


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)

  1. Open your Xcode project.
  2. Navigate to the “File” menu and select “Swift Packages” > “Add Package Dependency…
  3. In the search bar, enter the URL for the FastPixUploadSDK repository: https://github.com/FastPix/iOS-Uploads
  4. Locate the desired version of the SDK. By default, the latest version will be displayed.
  5. 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
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.

1func createDirectUpload() async throws -> URL {
2
3 let parameters: [String: Any] = [
4 "corsOrigin": "*",
5 "newAssetSettings": [
6 "accessPolicy": "public",
7 "generateSubtitles": true,
8 "normalizeAudio": true,
9 "maxResolution": "1080p",
10 "mediaQuality": "standard"
11
12 ]
13 ]
14
15 guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters) else {
16 throw NSError(domain: "com.example.app", code: 500, userInfo: [
17 NSLocalizedDescriptionKey: "Failed to serialize parameters"
18 ])
19 }
20
21 let request = try {
22 var req = try URLRequest(url: fullURL(forEndpoint: "upload"))
23 req.httpBody = jsonData
24 req.httpMethod = "POST"
25 req.addValue("application/json", forHTTPHeaderField: "Content-Type")
26 req.addValue("application/json", forHTTPHeaderField: "Accept")
27
28 let credentials = "\(FASTPIX_ACCESS_TOKEN_ID):\(FASTPIX_SECRET_KEY)"
29 let basicAuthCredential = Data(credentials.utf8).base64EncodedString()
30 req.addValue("Basic \(basicAuthCredential)", forHTTPHeaderField: "Authorization")
31
32 return req
33 }()
34
35 let (data, response) = try await urlSession.data(for: request)
36 guard let httpResponse = response as? HTTPURLResponse else {
37 throw CreateUploadError(message: "Invalid HTTP response")
38 }
39
40 if (200...299).contains(httpResponse.statusCode) {
41 do {
42 if let jsonDictionary = try JSONSerialization.jsonObject(with: data) as? [String: Any],
43 let dataDic = jsonDictionary["data"] as? [String: Any],
44 let urlString = dataDic["url"] as? String,
45 let uploadUrl = URL(string: urlString) {
46 return uploadUrl
47 } else {
48 print("Failed to parse upload URL from response.")
49 return URL(string: "")!
50 }
51 } catch {
52 print("Error decoding response: \(error.localizedDescription)")
53 return URL(string: "")!
54 }
55 } else {
56 let errorMessage = String(decoding: data, as: UTF8.self)
57 throw CreateUploadError(message: "Upload POST failed: HTTP \(httpResponse.statusCode):\n\(errorMessage)")
58 }
59}
60
61/// Generates a full URL for a given endpoint in the FastPix Video public API
62private func fullURL(forEndpoint endpoint: String) throws -> URL {
63 let fullPath = "https://api.fastpix.com/v1/on-demand/\(endpoint)"
64 guard let url = URL(string: fullPath) else {
65 throw CreateUploadError(message: "Bad endpoint: \(endpoint)")
66 }
67 return url
68}



Step 3: Start your upload

1import fp_swift_upload_sdk
2
3// Initialize the uploader
4var uploader = Uploads()
5
6Task {
7 do {
8 // Get the upload URL from your backend
9 let createUploadURL = try await self.myServerBackend.createDirectUpload()
10
11 // Start the upload
12 uploader.uploadFile(
13 file: file!,
14 endpoint: createUploadURL.absoluteString,
15 chunkSizeKB: Int(chunkSize.text ?? "") ?? 0
16 )
17
18 } catch {
19 print("Failed to create upload URL: \(error.localizedDescription)")
20 }
21}

  • 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.


1import fp_swift_upload_sdk
2
3var uploader = Uploads()
4
5uploader.progressHandler = { [weak self] progress in
6 // Ensure UI updates are done on the main thread
7 DispatchQueue.main.async {
8 guard let self = self else { return }
9
10 // Show and style progress UI
11 self.progressBar.layer.cornerRadius = 10
12 self.progressBar.isHidden = false
13 self.label_progress.isHidden = false
14
15 // Update progress bar and label
16 self.progressBar.progress = progress
17 self.label_progress.text = String(format: "%.0f%%", progress * 100)
18
19 // If upload is complete
20 if progress == 1.0 {
21 self.button_pause.isHidden = true
22 self.button_abort.isHidden = true
23 self.button_upload.isHidden = false
24
25 // Show success alert
26 let alertController = UIAlertController(
27 title: "Success",
28 message: "The video file uploaded successfully.",
29 preferredStyle: .alert
30 )
31 let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
32 alertController.addAction(okAction)
33 self.present(alertController, animated: true, completion: nil)
34 }
35 }
36}


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
    1var uploader = Uploads()

  • Call pause() to temporarily stop the upload
    1uploader.pause()

  • Call resume() to continue the upload from where it stopped
    1uploader.resume()

Below is an example implementation of the iOS Upload SDK. You can customize the UI and components based on your app’s design.


1import UIKit
2import PhotosUI
3import fp_swift_upload_sdk
4import Foundation
5import Network
6
7class SelectUploadVideoViewController: UIViewController, PHPickerViewControllerDelegate, UITextFieldDelegate, UploadProgressDelegate, UploadSDKErrorDelegate, UIDocumentPickerDelegate {
8
9
10 @IBOutlet weak var thumbNailImage: UIImageView!
11 @IBOutlet weak var button_upload: UIButton!
12 @IBOutlet weak var progressBar: UIProgressView!
13 @IBOutlet weak var label_progress: UILabel!
14 @IBOutlet weak var button_selectVideo: UIButton!
15 @IBOutlet weak var errorLabel: UILabel!
16 @IBOutlet weak var buttonsStack: UIStackView!
17 @IBOutlet weak var button_pause: UIButton!
18 @IBOutlet weak var chunkSize: UITextField!
19 @IBOutlet weak var uploadedChunk: UILabel!
20 @IBOutlet var button_abort: UIButton!
21
22 var isOfflineModeEnabled = false
23 var uploadIsPaused = false
24 var file : URL?
25 var pauseBtnClicked = false
26 var uploader = Uploads()
27 var picje = VideoPickerConfiguration()
28 var thumGenerater = ThumbnailImageGenerater()
29 var authenticatedURL: URL? = nil
30 var videoInputURL: URL? = nil
31
32 let myServerBackend = UploadURLGenerater(urlSession: URLSession(configuration: URLSessionConfiguration.default))
33 private var prepareTask: Task<Void, Never>? = nil
34 public var isPaused = false
35 private var assetRequestId: PHImageRequestID? = nil
36
37 let session = URLSession.shared
38 var requestData = [String: Any]()
39 var chunks : Int = 0
40 var resData = [String]()
41 var urlIndex = 0
42 var chunksList: [String] = []
43 var chunksArray: [Data] = []
44 var signedURL: URL?
45 var objectName: String?
46 var uploadId: String?
47 let monitor = NWPathMonitor()
48 var isInternetAvailable = false
49 var chunkAttempt = 0
50
51 override func viewDidLoad() {
52 super.viewDidLoad()
53 chunkSize.delegate = self
54 uploader.progressDelegate = self
55 uploader.errorDelegate = self
56 button_abort.isHidden = true
57 pickerConfig()
58 title = "Create New Upload"
59 button_upload.isHidden = false
60 button_pause.isHidden = true
61 PHPhotoLibrary.authorizationStatus(for: .readWrite)
62 button_upload.isEnabled = false
63 button_upload.backgroundColor = .lightGray
64
65 // Set the progress handler
66 progressBar.isHidden = true
67 label_progress.isHidden = true
68 uploader.progressHandler = { [weak self] progress in
69
70 // Update UI with the progress value
71 DispatchQueue.main.async {
72 self?.progressBar.layer.cornerRadius = 10
73 self?.progressBar.isHidden = false
74 self?.label_progress.isHidden = false
75
76 // Update progress bar or any other UI element
77 self?.progressBar.progress = progress
78 self?.label_progress.text = String(format: "%.0f%%", progress * 100)
79 if progress == 1.0 {
80 self?.button_pause.isHidden = true
81 self?.button_abort.isHidden = true
82 self?.button_upload.isHidden = false
83 let alertController = UIAlertController(title: "success", message: "The video file uploaded successfully.", preferredStyle: .alert)
84 let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
85 alertController.addAction(okAction)
86 self?.present(alertController, animated: true, completion: nil)
87 }
88 }
89 }
90
91 startMonitoring()
92
93 NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
94
95 chunkSize.attributedPlaceholder = NSAttributedString(
96 string: "Enter chunk size",
97 attributes: [
98 .foregroundColor: UIColor.white // Change to your desired color
99 ]
100 )
101 }
102
103 func didUpdateProgressText(_ text: String) {
104 DispatchQueue.main.async {
105 self.uploadedChunk.text = text
106 }
107 }
108
109 func uploadSDKDidFail(with error: String) {
110 DispatchQueue.main.async {
111 self.displayErrorAlert(error: error)
112 }
113 }
114
115 func textFieldShouldReturn(_ textField: UITextField) -> Bool {
116 textField.resignFirstResponder() // Dismisses the keyboard
117 return true
118 }
119
120 func startMonitoring() {
121 monitor.pathUpdateHandler = { path in
122 DispatchQueue.main.async { [self] in
123 self.isInternetAvailable = path.status == .satisfied
124 if self.isInternetAvailable {
125 if uploadIsPaused {
126 if self.chunkAttempt < chunksArray.count {
127
128 }
129 }
130 } else {
131 }
132 }
133 }
134 let queue = DispatchQueue(label: "NetworkMonitor")
135 monitor.start(queue: queue)
136 }
137
138 @objc func appDidBecomeActive() {
139 startMonitoring()
140 }
141
142 func pickerConfig() {
143 let alert = UIAlertController(title: "Select Media Type", message: nil, preferredStyle: .actionSheet)
144
145 // Option to pick Video
146 alert.addAction(UIAlertAction(title: "Pick Video", style: .default, handler: { _ in
147 let config = self.picje.pickerConfig() // video-only config
148 let picker = PHPickerViewController(configuration: config)
149 picker.delegate = self
150 picker.modalPresentationStyle = .overCurrentContext
151 self.present(picker, animated: true)
152 }))
153
154 // Option to pick Audio
155 alert.addAction(UIAlertAction(title: "Pick Audio", style: .default, handler: { _ in
156 let audioPicker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.audio])
157 audioPicker.allowsMultipleSelection = false
158 audioPicker.delegate = self
159 self.present(audioPicker, animated: true)
160 }))
161
162 alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
163
164 self.present(alert, animated: true)
165 }
166
167 @IBAction func button_pauseVideo(_ sender: UIButton) {
168 sender.isEnabled = false
169 if sender.isSelected {
170 sender.setTitle("Pause", for: .normal)
171 uploadIsPaused = false
172 pauseBtnClicked = true
173 uploader.resume()
174 sender.backgroundColor = .systemGreen
175
176 } else {
177 sender.setTitle("Resume", for: .normal)
178 uploadIsPaused = true
179 pauseBtnClicked = false
180 startMonitoring()
181 uploader.pause()
182 sender.backgroundColor = .systemRed
183 }
184
185 sender.isSelected.toggle()
186 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
187 sender.isEnabled = true
188
189 }
190 }
191
192 @IBAction func button_uploadVideo(_ sender: UIButton) {
193 button_upload.isHidden = true
194 button_pause.isHidden = false
195 button_abort.isHidden = false
196 self.progressBar.isHidden = false
197 self.label_progress.isHidden = false
198 self.label_progress.text = String(format: "%.0f%%", 0.0000000 * 100)
199
200 Task {
201 let createUploadURL = try await self.myServerBackend.createDirectUpload()
202 uploader.uploadFile(file: file!, endpoint: createUploadURL.absoluteString, chunkSizeKB: Int(chunkSize.text ?? "") ?? 0)
203 }
204 }
205
206 @IBAction func button_abort(_ sender: UIButton) {
207 button_pause.isHidden = true
208 button_abort.isHidden = true
209 button_upload.isHidden = false
210 uploader.abort()
211 sender.backgroundColor = .red
212 }
213
214 @IBAction func button_selectVideo(_ sender: UIButton) {
215 pickerConfig()
216 }
217
218 func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
219 dismiss(animated: true, completion: nil)
220 guard let result = results.first else {
221 return
222 }
223
224 if let assetRequestId = self.assetRequestId {
225 PHImageManager.default().cancelImageRequest(assetRequestId)
226 }
227 let tempDir = FileManager.default.temporaryDirectory
228 let tempFile = URL(string: "upload-\(Date().timeIntervalSince1970).mp4", relativeTo: tempDir)!
229
230 guard let assetIdentitfier = result.assetIdentifier else {
231 print("No Asset ID for chosen asset")
232 return
233 }
234
235 guard let assetIdentitfier = result.assetIdentifier else {
236 print("No Asset ID for chosen asset")
237 return
238
239 }
240
241 let options = PHFetchOptions()
242 options.includeAssetSourceTypes = [.typeUserLibrary, .typeCloudShared]
243 let phAssetResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetIdentitfier], options: options)
244 guard let phAsset = phAssetResult.firstObject else {
245 return
246 }
247
248 let exportOptions = PHVideoRequestOptions()
249 exportOptions.isNetworkAccessAllowed = true
250 exportOptions.deliveryMode = .highQualityFormat
251 assetRequestId = PHImageManager.default().requestExportSession(forVideo: phAsset, options: exportOptions, exportPreset: AVAssetExportPresetHighestQuality, resultHandler: {(exportSession, info) -> Void in
252 DispatchQueue.main.async { [weak self] in
253 guard let exportSession = exportSession else {
254 return
255 }
256 self?.thumGenerater.extractThumbnailAsync(exportSession.asset) { thumbnailImage in
257 guard let thumbImage = thumbnailImage else { return }
258 DispatchQueue.main.async {
259 self?.thumbNailImage.image = UIImage(cgImage: thumbImage)
260 self?.thumbNailImage.isHidden = false
261 self?.button_selectVideo.isUserInteractionEnabled = false
262 }
263 }
264 }
265 })
266
267 result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier , completionHandler: { [ weak self] url, error in
268 guard let fileURL = url else {
269 return
270 }
271 let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
272 guard let targetURL = documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent) else { return }
273 self?.file = (documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent))!
274
275 do {
276 if FileManager.default.fileExists(atPath: targetURL.path) {
277 try FileManager.default.removeItem(at: targetURL)
278 } else {
279 // print("file path not exists")
280
281 }
282
283 try FileManager.default.copyItem(at: fileURL, to: targetURL)
284 self?.videoInputURL = targetURL
285 DispatchQueue.main.async {
286 self?.button_upload.isEnabled = true
287 self?.button_upload.backgroundColor = .green
288 }
289 } catch {
290 self?.displayErrorAlert(error: error.localizedDescription)
291 }
292 })
293 }
294
295 func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
296 guard let fileURL = urls.first else { return }
297
298 // Start accessing security-scoped resource
299 let didStartAccessing = fileURL.startAccessingSecurityScopedResource()
300 defer {
301 if didStartAccessing {
302 fileURL.stopAccessingSecurityScopedResource()
303 }
304 }
305
306 // Confirm access granted
307 guard didStartAccessing else {
308 print("Failed to access security-scoped resource")
309 self.displayErrorAlert(error: "Unable to access selected file due to security permissions.")
310 return
311 }
312
313 // Now safe to use fileURL
314 print("Picked audio file: \(fileURL.lastPathComponent)")
315
316 // Copy to local directory
317 let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
318 guard let targetURL = documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent) else { return }
319 self.file = documentsDirectory?.appendingPathComponent(fileURL.lastPathComponent)
320
321 do {
322 if FileManager.default.fileExists(atPath: targetURL.path) {
323 try FileManager.default.removeItem(at: targetURL)
324 }
325 try FileManager.default.copyItem(at: fileURL, to: targetURL)
326
327 self.videoInputURL = targetURL // Or self.audioInputURL
328 DispatchQueue.main.async {
329 self.button_upload.isEnabled = true
330 self.button_upload.backgroundColor = .green
331 }
332 } catch {
333 self.displayErrorAlert(error: error.localizedDescription)
334 }
335 }
336
337 func pickerDidCancel(_ picker: PHPickerViewController) {
338 dismiss(animated: true, completion: nil)
339 }
340
341 // Display an error message
342 func displayErrorAlert(error: String) {
343 let alertController = UIAlertController(title: "Error", message: error, preferredStyle: .alert)
344 let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in
345 DispatchQueue.main.async {
346 self?.thumbNailImage.isHidden = false
347 self?.button_selectVideo.isUserInteractionEnabled = true
348 if error == "The current upload was aborted. Please try uploading a new video" {
349 self?.navigationController?.popViewController(animated: true)
350 }
351 }
352 }
353 alertController.addAction(okAction)
354 present(alertController, animated: true, completion: nil)
355 }
356
357 deinit {
358 monitor.cancel()
359 NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
360 }
361}
362
363class UploadURLGenerater {
364
365 public func presentErrorAlert(on viewController: UIViewController, withTitle title: String, message: String) {
366 let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
367 let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
368 alertController.addAction(okAction)
369 viewController.present(alertController, animated: true, completion: nil)
370 }
371
372 func createDirectUpload() async throws -> URL {
373
374 let parameters: [String: Any] = [
375 "corsOrigin": "*",
376 "pushMediaSettings": [
377 "accessPolicy": "public",
378 "generateSubtitles": true,
379 "normalizeAudio": true,
380 "maxResolution": "1080p"
381 ]
382 ]
383
384 guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters) else {
385 throw NSError(domain: "com.example.app", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to serialize parameters"])
386 }
387
388 let request = try {
389 var req = try URLRequest(url:fullURL(forEndpoint: "upload"))
390 req.httpBody = jsonData
391 req.httpMethod = "POST"
392 req.addValue("application/json", forHTTPHeaderField: "Content-Type")
393 req.addValue("application/json", forHTTPHeaderField: "accept")
394
395 let basicAuthCredential = "\(FASTPIX_ACCESS_TOKEN_ID):\(FASTPIX_SECRET_KEY)".data(using: .utf8)!.base64EncodedString()
396 req.addValue("Basic \(basicAuthCredential)", forHTTPHeaderField: "Authorization")
397 return req
398 }()
399
400
401
402 let (data, response) = try await urlSession.data(for: request)
403 let httpResponse = response as! HTTPURLResponse
404 print("Response",httpResponse.statusCode)
405 var uploadUrl: URL?
406 if (200...299).contains(httpResponse.statusCode) {
407 do {
408 // Assuming responseData is the Data object you want to convert to a dictionary
409 if let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
410 var dataDic = jsonDictionary["data"] as? NSDictionary
411 uploadUrl = URL(string: dataDic?.value(forKey: "url") as? String ?? "")
412 } else {
413 print("Failed to convert data to dictionary")
414 }
415 } catch {
416 print("Error converting data to dictionary: \(error.localizedDescription)")
417 }
418 guard let uploadUrl = uploadUrl else { return URL(string: "")!}
419 return uploadUrl
420 } else {
421 throw CreateUploadError(message: "Upload POST failed: HTTP \(httpResponse.statusCode):\n\(String(decoding: data, as: UTF8.self))")
422 }
423 }
424
425 /// Generates a full URL for a given endpoint in the FastPix Video public API
426 private func fullURL(forEndpoint: String) throws -> URL {
427 guard let url = URL(string: "https://venus-v1.fastpix.dev/on-demand/\(forEndpoint)") else {
428 throw CreateUploadError(message: "bad endpoint")
429 }
430 return url
431 }
432
433 // https://dev-api.fastpix.com
434 let urlSession: URLSession
435 let jsonEncoder: JSONEncoder
436 let jsonDecoder: JSONDecoder
437
438 //Gowtham Venus
439 let FASTPIX_ACCESS_TOKEN_ID = "ACCESS TOKEN"
440 let FASTPIX_SECRET_KEY = "SECRET KEY"
441
442 init(urlSession: URLSession) {
443 self.urlSession = urlSession
444 self.jsonEncoder = JSONEncoder()
445 self.jsonEncoder.keyEncodingStrategy = JSONEncoder.KeyEncodingStrategy.convertToSnakeCase
446 self.jsonDecoder = JSONDecoder()
447 self.jsonDecoder.keyDecodingStrategy = JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase
448 }
449
450 convenience init() {
451 self.init(urlSession: URLSession(configuration: URLSessionConfiguration.default))
452 }
453}
454
455struct CreateUploadError : Error {
456 let message: String
457}
458
459fileprivate struct CreateUploadPost: Codable {
460 var newAssetSettings: NewAssetSettings = NewAssetSettings()
461 var corsOrigin: String = "*"
462}
463
464fileprivate struct NewAssetSettings: Codable {
465 var playbackPolicy: [String] = ["public"]
466 var passthrough: String = "Extra video data. This can be any data and it's for your use"
467 var mp4Support: String = "standard"
468 var normalizeAudio: Bool = true
469 var test: Bool = false
470}
471
472fileprivate struct CreateUploadResponse: Decodable {
473 var url: String
474 var id: String
475 var timeout: Int64
476 var status: String
477}
478
479fileprivate struct CreateUploadResponseContainer: Decodable {
480 var data: CreateUploadResponse
481}

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:

1import fp_swift_upload_sdk
2
3class ViewController: UIViewController, UploadSDKErrorDelegate {
4 var uploader = Uploads()
5
6 override func viewDidLoad() {
7 super.viewDidLoad()
8 uploader.errorDelegate = self
9 }
10}

Changelog

All notable changes to this project will be documented in this file.

[1.0.3]

Changed

  • Code standardization updates applied across the SDK to align with best practices and strengthen overall stability.

[1.0.2]

Compatibility

  • The SDK is now fully compatible with both old and new versions of Xcode (before and after Xcode 26) and Swift (Swift 5.x and Swift 6.x including Swift 6.3.2).
  • No functionality has been changed. All fixes are only compatability ones, updates to ensure the SDK builds and runs correctly across all supported Xcode and Swift versions.

Fixed

  • Fixed invalid try (tuple) syntax in uploadFile that caused a Swift 6.3.2 compiler crash (SILGen segfault) under Xcode 26.
  • Fixed customizedChunkSize incorrectly storing 0 when chunkSizeKB: 0 was passed. Both nil and 0 now correctly fall back to the SDK default (16384 KB).
  • Fixed chunk size validation thresholds that were written in MB (< 5, > 500) but compared against a value stored in KB, causing valid chunk sizes and the SDK default to always fail validation.
  • Fixed FileHandle on iOS 13.0–13.3 silently returning empty Data due to a missing availability check. Legacy seek(toFileOffset:) and readData(ofLength:) are now used as a fallback on iOS < 13.4.
  • Fixed missing fileHandle.closeFile() fallback in deinit for iOS < 13.0.
  • Fixed force-unwrap crash on VideoChunkProcessor(fileURL:) returning nil when the file could not be opened.
  • Fixed force-unwrap crash on currentChunk in requestChunk.
  • Fixed NotificationCenter.addObserver being called off the main thread when uploadFile is invoked from a background thread.

Changed

  • Replaced duplicated chunk-success logic in submitHttpRequest with a shared handleChunkSuccess() method.
  • All closures (emit, upload task completion, asyncAfter) now capture self weakly via [weak self] to prevent retain cycles.
  • UploadEvent associated error values updated from bare Error to any Error for Swift 5.7+ and Swift 6 compatibility.

Added

  • Sendable conformance added to UploadEvent, UploadsDelegate, UploadProgressDelegate, and UploadSDKErrorDelegate for Swift 6 strict concurrency compliance.

Removed

  • Removed unstable () async throws -> String existential type from validateUserInput endpoint check, which is unsupported across Swift versions.
  • Removed unused variable var response = 429 and duplicate if let httpResponse rebinding inside submitHttpRequest.

[1.0.1]

Changed

  • Domain migration: fastpix.iofastpix.com
    • Direct Upload API endpoint updated from https://api.fastpix.io/v1/on-demand/upload to https://api.fastpix.com/v1/on-demand/upload.
    • README references for dashboard, documentation, and API links updated to use .com domains.

Note: Existing .io domains will continue functioning temporarily, but migration to .com endpoints is strongly recommended to avoid future disruptions.

[1.0.0]

Added

  • Chunking: Files are automatically split into chunks (configurable, default size is 16MB/chunk).
  • Pause and Resume: Allows temporarily pausing the upload and resuming after a while.
  • Upload Lifecycle Callbacks: Track the entire upload process using callback functions to monitor upload lifecycle.
  • Retry: Individual chunks are retried up to 5 times with exponential backoff to recover from temporary network failures.
  • Error Handling: Comprehensive error management to notify users of issues during uploads.
  • Customizability: Options to customize chunk size and retry attempts.
  • Swift Package Manager Support: SDK is installable via SPM using the repo URL.
  • Implemented support for Google Cloud Storage resumable uploads and chunked client uploads.
  • Added retry mechanism with exponential backoff for GCS upload failures based on retryable status codes.
  • Enabled support for user-provided signed URLs for resumable uploads with externally generated session URIs.
  • Updated the API endpoint from https://v1.fastpix.io/on-demand/uploads to https://api.fastpix.io/v1/on-demand/upload.