Skip to content

Commit

Permalink
Use URLSession delegate methods to allow background URLSessions in Tr…
Browse files Browse the repository at this point in the history
…ansloaditKit (#39)

* Started migrating to a delegate based URLSession task system

* ensure callbacks are called and removed when no longer needed

* CI fixes

* Swift tools bump

* Various test fixes
  • Loading branch information
donnywals authored Nov 22, 2024
1 parent 0373b5a commit a14850d
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 42 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.3
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand Down
27 changes: 27 additions & 0 deletions Sources/TransloaditKit/TransloaditAPI+URLSessionDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation

extension TransloaditAPI: URLSessionDataDelegate {
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let completionHandler = callbacks[task] else {
return
}

defer { callbacks[task] = nil }

if let error {
completionHandler.callback(.failure(error))
return
}

guard let response = task.response else {
completionHandler.callback(.failure(TransloaditAPIError.incompleteServerResponse))
return
}

completionHandler.callback(.success((completionHandler.data, response)))
}

public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
callbacks[dataTask]?.data.append(data)
}
}
67 changes: 40 additions & 27 deletions Sources/TransloaditKit/TransloaditAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,23 @@ enum TransloaditAPIError: Error {
case couldNotFetchStatus
case couldNotCreateAssembly(Error)
case assemblyError(String)
case incompleteServerResponse
}

/// The `TransloaditAPI` class makes API calls, such as creating assemblies or checking an assembly's status.
final class TransloaditAPI {
final class TransloaditAPI: NSObject {

private let basePath = URL(string: "https://api2.transloadit.com")!

enum Endpoint: String {
case assemblies = "/assemblies"
}

private let session: URLSession
private let configuration: URLSessionConfiguration
private let delegateQueue: OperationQueue
private lazy var session: URLSession = {
return URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue)
}()

static private let formatter: DateFormatter = {
let dateFormatter = DateFormatter()
Expand All @@ -36,10 +41,13 @@ final class TransloaditAPI {
}()

private let credentials: Transloadit.Credentials
var callbacks: [URLSessionTask: URLSessionCompletionHandler] = [:]

init(credentials: Transloadit.Credentials, session: URLSession) {
self.credentials = credentials
self.session = session
self.configuration = session.configuration
self.delegateQueue = session.delegateQueue
super.init()
}

func createAssembly(
Expand All @@ -60,13 +68,17 @@ final class TransloaditAPI {
return
}

let task = session.dataTask(with: request) { (data, response, error) in
if let data = data {
let task = session.dataTask(with: request)
callbacks[task] = URLSessionCompletionHandler(callback: { result in
switch result {
case .failure(let error):
completion(.failure(.couldNotCreateAssembly(error)))
case .success((let data, _)):
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let assembly = try decoder.decode(Assembly.self, from: data)

if let error = assembly.error {
completion(.failure(.assemblyError(error)))
} else {
Expand All @@ -76,7 +88,7 @@ final class TransloaditAPI {
completion(.failure(TransloaditAPIError.couldNotCreateAssembly(error)))
}
}
}
})
task.resume()
}

Expand All @@ -98,13 +110,18 @@ final class TransloaditAPI {
return
}

let task = session.dataTask(with: request) { (data, response, error) in
if let data = data {
let task = session.dataTask(with: request)
//task.delegate = self
callbacks[task] = URLSessionCompletionHandler(callback: { result in
switch result {
case .failure(let error):
completion(.failure(.couldNotCreateAssembly(error)))
case .success((let data, _)):
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let assembly = try decoder.decode(Assembly.self, from: data)

if let error = assembly.error {
completion(.failure(.assemblyError(error)))
} else {
Expand All @@ -114,7 +131,7 @@ final class TransloaditAPI {
completion(.failure(TransloaditAPIError.couldNotCreateAssembly(error)))
}
}
}
})
task.resume()
}

Expand Down Expand Up @@ -268,9 +285,12 @@ final class TransloaditAPI {
return request
}

let task = session.dataTask(request: makeRequest()) { result in
let task = session.dataTask(with: makeRequest())
callbacks[task] = URLSessionCompletionHandler(callback: { result in
switch result {
case .success((let data?, _)):
case .failure:
completion(.failure(.couldNotFetchStatus))
case .success((let data, _)):
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
Expand All @@ -279,13 +299,8 @@ final class TransloaditAPI {
} catch {
completion(.failure(.couldNotFetchStatus))
}
case .success((nil, _)):
completion(.failure(.couldNotFetchStatus))
case .failure:
completion(.failure(.couldNotFetchStatus))
}
}

})
task.resume()
}

Expand All @@ -296,9 +311,12 @@ final class TransloaditAPI {
return request
}

let task = session.dataTask(request: makeRequest()) { result in
let task = session.dataTask(with: makeRequest())
callbacks[task] = URLSessionCompletionHandler(callback: { result in
switch result {
case .success((let data?, _)):
case .failure:
completion(.failure(.couldNotFetchStatus))
case .success((let data, _)):
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
Expand All @@ -307,13 +325,8 @@ final class TransloaditAPI {
} catch {
completion(.failure(.couldNotFetchStatus))
}
case .success((nil, _)):
completion(.failure(.couldNotFetchStatus))
case .failure:
completion(.failure(.couldNotFetchStatus))
}
}

})
task.resume()
}
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/TransloaditKit/URLSessionCompletionHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

class URLSessionCompletionHandler {
var data: Data
let callback: (Result<(Data, URLResponse), Error>) -> Void

init(callback: @escaping (Result<(Data, URLResponse), Error>) -> Void) {
self.callback = callback
self.data = Data()
}
}
2 changes: 1 addition & 1 deletion Tests/TransloaditKitTests/Fixtures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ enum Fixtures {
// We make Assembly and AssemblyStatus here because we can access their initializers (via testable import). Which we can't in TransloaditKitTests (on purpose to test public API). We have no need to expose the memberwise initializers from these types to the public API either.

static func makeAssembly() -> Assembly {
Assembly(id: "abc", error: nil, tusURL: URL(string: "https://my-tus.transloadit.com")!, url: URL(string: "https://transloadit.com")!)
Assembly(id: UUID().uuidString, error: nil, tusURL: URL(string: "https://my-tus.transloadit.com")!, url: URL(string: "https://transloadit.com")!)
}

static func makeAssemblyStatus(status: AssemblyStatus.ProcessingStatus) -> AssemblyStatus {
Expand Down
6 changes: 3 additions & 3 deletions Tests/TransloaditKitTests/Support.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ enum Files {

static func storeFile(data: Data) throws -> URL {
let docDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let targetLocation = docDir.appendingPathComponent("myfile.txt")
let targetLocation = docDir.appendingPathComponent("\(UUID().uuidString).txt")
try data.write(to: targetLocation)
return targetLocation
}
Expand Down Expand Up @@ -58,7 +58,7 @@ enum Network {
}

static func prepareForStatusResponse(data: Data) {
let url = URL(string: "www.tus-image-upload-location-returned-for-creation-post.com")!
let url = URL(string: "https://my-tus.transloadit.com/www.tus-image-upload-location-returned-for-creation-post.com")!
MockURLProtocol.prepareResponse(for: url, method: "HEAD") { _ in
MockURLProtocol.Response(status: 200, headers: ["Upload-Length": String(data.count),
"Upload-Offset": "0"], data: nil)
Expand All @@ -73,7 +73,7 @@ enum Network {
}

static func prepareForSuccesfulUploads(url: URL, data: Data, lowerCasedKeysInResponses: Bool = false) {
let uploadURL = URL(string: "www.tus-image-upload-location-returned-for-creation-post.com")!
let uploadURL = URL(string: "https://my-tus.transloadit.com/www.tus-image-upload-location-returned-for-creation-post.com")!
MockURLProtocol.prepareResponse(for: url, method: "POST") { _ in
let key: String
if lowerCasedKeysInResponses {
Expand Down
19 changes: 12 additions & 7 deletions Tests/TransloaditKitTests/TransloaditKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ final class TransloaditKitTests: XCTestCase {

var localAssembly: Assembly!
transloadit.createAssembly(steps: [resizeStep], andUpload: files, completion: { result in

switch result {
case .success(let assembly):
localAssembly = assembly
Expand All @@ -146,13 +145,15 @@ final class TransloaditKitTests: XCTestCase {
wait(for: [startedUploadsExpectation], timeout: 3)

transloadit.stopRunningUploads()

// Restart uploads, continue where left off

fileDelegate.finishUploadExpectation = finishedUploadExpectation
transloadit.start()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [self] in
fileDelegate.startUploadExpectation = nil
fileDelegate.finishUploadExpectation = finishedUploadExpectation
transloadit.start()
}

wait(for: [finishedUploadExpectation], timeout: 5)
wait(for: [finishedUploadExpectation], timeout: 10)

XCTAssert(fileDelegate.finishedUploads.contains(localAssembly))
}
Expand Down Expand Up @@ -186,15 +187,19 @@ final class TransloaditKitTests: XCTestCase {

let secondTransloadit = makeClient()
let secondFileDelegate = TransloadItMockDelegate()

secondTransloadit.fileDelegate = secondFileDelegate
secondFileDelegate.name = "SECOND"



let finishedUploadExpectation = self.expectation(description: "Finished file upload")
finishedUploadExpectation.expectedFulfillmentCount = numFiles

secondFileDelegate.finishUploadExpectation = finishedUploadExpectation
secondTransloadit.start()

DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
secondTransloadit.start()
}

wait(for: [finishedUploadExpectation], timeout: 5)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ struct PhotoPicker: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
configuration.selectionLimit = 30
configuration.filter = .images

let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ import Atlantis
final class MyUploader: ObservableObject {
let transloadit: Transloadit

func upload2(_ urls: [URL]) {
let templateID = "1a84d2f1f2584f92981bda285bbc4e84"

transloadit.createAssembly(templateId: templateID, andUpload: urls, customFields: ["hello": "world"]) { result in
switch result {
case .success(let assembly):
print("Retrieved \(assembly)")
case .failure(let error):
print("Assembly error \(error)")
}
}.pollAssemblyStatus { result in
switch result {
case .success(let assemblyStatus):
print("Received assemblystatus \(assemblyStatus)")
case .failure(let error):
print("Caught polling error \(error)")
}
}
}

func upload(_ urls: [URL]) {
let resizeStep = StepFactory.makeResizeStep(width: 200, height: 100)
transloadit.createAssembly(steps: [resizeStep], andUpload: urls, customFields: ["hello": "world"]) { result in
Expand All @@ -32,7 +52,7 @@ final class MyUploader: ObservableObject {
}

init() {
let credentials = Transloadit.Credentials(key: "my_key", secret: "my_secret")
let credentials = Transloadit.Credentials(key: "Ru1rwq3ITrgSEDtgna6SGZa4yY71YJgW", secret: "Xo6xlnn42cfBkNxLSDWJNCQoSNL0j1aFB9wNyaAR")
self.transloadit = Transloadit(credentials: credentials, session: URLSession.shared)
self.transloadit.fileDelegate = self
}
Expand Down Expand Up @@ -90,7 +110,7 @@ struct TransloaditKitExampleApp: App {

init() {
self.uploader = MyUploader()
// Atlantis.start(hostName: "")
Atlantis.start(hostName: "donnys-macbook-pro-2.local.")
}

var body: some Scene {
Expand Down

0 comments on commit a14850d

Please sign in to comment.