Skip to content

Instantly share code, notes, and snippets.

@Idomo
Last active June 29, 2023 22:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Idomo/ece6683348372b54e470fd630b1fb76e to your computer and use it in GitHub Desktop.
Save Idomo/ece6683348372b54e470fd630b1fb76e to your computer and use it in GitHub Desktop.
Files manager that allow sending background POST requests and retry if the user has been force-quitting the app
//
// FilesManager.swift
//
// Created by Ido Monzon on 26.6.2018.
// Copyright © 2018 Ido Monzon. All rights reserved.
//
import UIKit
class FilesManager: NSObject {
static let uploadUrl = URL(string: "https://...")!
static func fullPath(for path: String?) -> String {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
return "\(documentsPath)/\(path ?? "fileName.jpg.multipart")"
}
static let multipartFileNameSuffix = ".multipart"
static let urlSessionIdentifierPrefix = "\(Bundle.main.bundleIdentifier ?? "com.app.bundle").uploadfile."
static func urlSessionConfiguration(for id: String) -> URLSessionConfiguration {
let id = id.ltrim(text: FilesManager.urlSessionIdentifierPrefix) // Trim the identifier prefix if `id` containing it
let configuration = URLSessionConfiguration.background(withIdentifier: FilesManager.urlSessionIdentifierPrefix + id)
configuration.sharedContainerIdentifier = FilesManager.urlSessionIdentifierPrefix
return configuration
}
}
// MARK: - Utils
extension FilesManager {
/// Recreate URLSession with same configuration for file to initiate urlSession(_:task:didCompleteWithError:)
func recreateSession(id: String) {
let multipartFileName = id.ltrim(text: FilesManager.urlSessionIdentifierPrefix)
_ = URLSession(configuration: FilesManager.urlSessionConfiguration(for: multipartFileName), delegate: self, delegateQueue: nil)
}
func uploadFile(name fileName: String) {
var urlRequest = URLRequest(url: FilesManager.uploadUrl)
urlRequest.httpMethod = "POST"
let boundary = urlRequest.generateBoundary()
urlRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
let session = URLSession(configuration: FilesManager.urlSessionConfiguration(for: fileName), delegate: self, delegateQueue: nil)
let filePath = FilesManager.fullPath(for: fileName)
DispatchQueue.main.async {
let fileUrl = NSURL(fileURLWithPath: filePath) as URL
if FileManager.default.fileExists(atPath: fileUrl.path) {
session.uploadTask(with: urlRequest, fromFile: fileUrl).resume()
}
FilesManager.addFileUploadTask(id: session.configuration.identifier, fileName: fileName)
}
}
static func addFileUploadTask(id: String?, fileName: String) {
guard let sessionId = id else { return }
var filesUploadTasks = UserDefaults.standard.stringArray(forKey: UserDefaultsKeys.filesUploadTasks) ?? []
if filesUploadTasks.firstIndex(of: sessionId) == nil { // Append only if doesn't already exists
filesUploadTasks.append(sessionId)
}
UserDefaults.standard.set(filesUploadTasks, forKey: UserDefaultsKeys.filesUploadTasks)
}
static func removeFileUploadTask(id: String?) {
guard let sessionId = id else { return }
var filesUploadTasks = UserDefaults.standard.stringArray(forKey: UserDefaultsKeys.filesUploadTasks)
if let index = filesUploadTasks?.firstIndex(of: sessionId) {
filesUploadTasks?.remove(at: index)
}
UserDefaults.standard.set(filesUploadTasks, forKey: UserDefaultsKeys.filesUploadTasks)
}
static func taskFinishedCompletion(id: String?) {
do {
let fileName = id?.ltrim(text: FilesManager.urlSessionIdentifierPrefix)
let filePath = FilesManager.fullPath(for: fileName)
FilesManager.removeFileUploadTask(id: id)
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
try fileManager.removeItem(atPath: filePath)
}
} catch _ {
}
}
}
// MARK: - URLSessionTaskDelegate
extension FilesManager: URLSessionDataDelegate, URLSessionDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let multipartFileName = session.configuration.identifier?.ltrim(text: FilesManager.urlSessionIdentifierPrefix) else { return }
if let error = error {
let nsError = (error as NSError)
guard nsError.userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] as? Int == NSURLErrorCancelledReasonUserForceQuitApplication else { return }
// File didn't finished upload because of force quit
// Try uploading again from saved file
uploadFile(name: multipartFileName)
} else {
FilesManager.taskFinishedCompletion(id: session.configuration.identifier)
}
}
}
import UIKit
extension String {
/// trim spaces & enters from both sides of the string
func trim() -> String {
return trimmingCharacters(in: .whitespaces).trimmingCharacters(in: .newlines)
}
/// trim specific text from the beginning
func ltrim(text: String) -> String {
return replacingOccurrences(of: text, with: "", options: .anchored)
}
/// trim specific text from the ending
func rtrim(text: String) -> String {
return replacingOccurrences(of: text, with: "", options: [.anchored, .backwards])
}
}
extension URLRequest {
func generateBoundary() -> String {
return "Boundary-\(userId)"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment