I’m scuffling with backgroundSession.Set
capabilities to background fetch and processing, set backgroundSession with URLSessionConfiguration.background.backgroundSession?.uploadTask
is sweet, and
func urlSession(_ session: URLSession, activity: URLSessionTask, didCompleteWithError error: Error?)` is invoked ultimately with out error.
When attempting to check background (placing the app within the background), urlSessionDidFinishEvents(forBackgroundURLSession
is just not triggered nor handleEventsForBackgroundURLSession
in appDelegate.
(checked additionally getting back from background to foreground)
What am I lacking?
class AppDelegate: NSObject, UIApplicationDelegate {
let gcmMessageIDKey = "gcm.message_id"
var backgroundCompletionHandler: (() -> Void)?
var userService: UserService?
func software(_ software: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
FirebaseConfiguration.shared.setLoggerLevel(FirebaseLoggerLevel.debug)
Messaging.messaging().delegate = self
UNUserNotificationCenter.present().delegate = self
AnalyticsManager.startMonitoringNetwork()
return true
}
func software(_ software: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
if let messageID = userInfo[gcmMessageIDKey] {
print("Message ID software: (messageID)")
}
print(userInfo)
completionHandler(UIBackgroundFetchResult.newData)
}
func software(_ app: UIApplication,
open url: URL,
choices: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
return GIDSignIn.sharedInstance.deal with(url)
}
func software(_ software: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
print("☠️ handleEventsForBackgroundURLSession")
userService?.apiClient.backgroundSessionCompletionHandler = completionHandler
}
}
@essential
struct WhisperApp: App {
var userService = UserService()
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
@StateObject var appCoordinator = AppCoordinator()
@ObservedObject var loginViewModel = LogInViewModel()
var physique: some Scene {
WindowGroup {
if appCoordinator.isLoggedIn {
CustomTabBarView(userService: userService)
.environmentObject(appCoordinator)
} else {
LoginView(viewModel: loginViewModel)
.environmentObject(appCoordinator)
.onAppear {
delegate.userService = userService
}
}
}
}
}
last class URLSessionAPIClient: NSObject, ServiceProtocol {
non-public let progress: PassthroughSubject<(id: Int, progress: Double), By no means> = .init()
non-public var session: URLSession
non-public var baseUrl: String
non-public var decoder: JSONDecoder
non-public let tokenExpiredSubject = PassthroughSubject()
non-public var cancellables = Set()
non-public var retryCount = 0
non-public var fireBaseManager: FireBaseManager
non-public var backgroundSession: URLSession?
non-public var tempFileURL: URL?
non-public var topic = PassthroughSubject()
var tokenExpiredPublisher: AnyPublisher {
tokenExpiredSubject.eraseToAnyPublisher()
}
var backgroundSessionCompletionHandler: (() -> Void)?
init(
baseUrl: String = GlobalConstants.baseURL,
sessionConfiguration: URLSessionConfiguration = .default,
decoder: JSONDecoder = JSONDecoder(),
firebaseManager: FireBaseManager = FireBaseManager()
) {
self.baseUrl = baseUrl
self.session = URLSession(configuration: sessionConfiguration)
self.decoder = decoder
self.fireBaseManager = firebaseManager
tremendous.init()
let backgroundConfig = URLSessionConfiguration.background(withIdentifier: "backgroundUpload")
self.backgroundSession = URLSession(configuration: backgroundConfig, delegate: self, delegateQueue: nil)
}
non-public func saveRequestBodyToTemporaryFile(_ physique: Information) throws -> URL {
let tempDirectory = FileManager.default.temporaryDirectory
let tempFileURL = tempDirectory.appendingPathComponent(UUID().uuidString)
attempt physique.write(to: tempFileURL)
self.tempFileURL = tempFileURL
return tempFileURL
}
non-public func uploadLargeFile2(request: URLRequest, fileURL: URL) -> AnyPublisher {
print("☠️ inside uploadLargeFile with native URLSession")
let topic = PassthroughSubject()
guard let httpBody = request.httpBody else {
return Fail(error: NSError(area: "URLError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))
.eraseToAnyPublisher()
}
guard let temporaryFileURL = attempt? saveRequestBodyToTemporaryFile(httpBody) else {
return Fail(error: NSError(area: "URLError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"]))
.eraseToAnyPublisher()
}
var requestTest = request
requestTest.httpBody = nil
let activity = backgroundSession?.uploadTask(with: requestTest, fromFile: temporaryFileURL)
activity?.resume()
return topic
.decode(kind: R.self, decoder: decoder)
.eraseToAnyPublisher()
}
non-public func sendDataTask(request: URLRequest) -> AnyPublisher {
return session.dataTaskPublisher(for: request)
.subscribe(on: DispatchQueue.world(qos: .background))
.tryMap { [weak self] knowledge, response -> R in
guard let self, let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
if httpResponse.statusCode == 401 {
throw APIError.expiredToken
}
guard (200...299).accommodates(httpResponse.statusCode) else {
throw APIError.invalidResponse
}
retryCount = 0
return attempt self.decoder.decode(R.self, from: knowledge)
}
.tryCatch { [weak self] error -> AnyPublisher in
guard let self else { throw error }
if error as? APIError == .expiredToken, retryCount < 3 {
retryCount += 1
return self.refreshTokenAndRetryLarge(request: request)
}
throw error
}
.eraseToAnyPublisher()
}
func sendRequest(_ request: T) -> AnyPublisher {
guard let urlRequest = urlRequest(from: request, baseURL: baseUrl) else {
return Fail(error: APIError.invalidData)
.eraseToAnyPublisher()
}
let isLargeFileR = request.urlPath.flatMap { isLargeFile(url: $0) } ?? false
if isLargeFileR {
// Use the trail because the file URL
guard URL(string: request.path) != nil else {
return Fail(error: APIError.invalidData).eraseToAnyPublisher()
}
guard let file = request.urlPath else { return Fail(error: APIError.invalidData)
.eraseToAnyPublisher()}
print("☠️ uploadLargeFile")
return uploadLargeFile2(request: urlRequest, fileURL: file)
// return uploadLargeFile(request: urlRequest, fileURL: file)
} else {
print("☠️ sendDataTask")
return sendDataTask(request: urlRequest)
}
}
non-public func refreshTokenAndRetryLarge(request: URLRequest) -> AnyPublisher {
print("☠️ refreshTokenAndRetryLarge refreshTokenAndRetry")
// Try to refresh the token
return refreshToken()
.flatMap { [weak self] _ -> AnyPublisher in
guard let self else {
return Fail(error: APIError.invalidData).eraseToAnyPublisher()
}
// As soon as token is refreshed, retry the unique request
return self.sendDataTask(request: request)
}
.eraseToAnyPublisher()
}
non-public func refreshTokenAndRetry(request: T) -> AnyPublisher {
print("☠️ refreshTokenAndRetry")
return refreshToken()
.flatMap { [weak self] _ -> AnyPublisher in
guard let self else {
return Fail(error: APIError.invalidData).eraseToAnyPublisher()
}
return self.sendRequest(request)
}
.eraseToAnyPublisher()
}
non-public func refreshToken() -> AnyPublisher {
return Future { promise in
let currentUser = Auth.auth().currentUser
print("☠️ refreshToken")
currentUser?.getIDTokenForcingRefresh(true) { idToken, error in
if error != nil {
promise(.failure(APIError.invalidData))
} else if let token = idToken {
CredentialManager.shared.setValue(token, kind: .token)
promise(.success(()))
} else {
promise(.failure(APIError.invalidData))
}
}
}
.eraseToAnyPublisher()
}
}
non-public extension URLSessionAPIClient {
func urlRequest(from request: any HTTPRequest, baseURL: String) -> URLRequest? {
guard let url = URL(string: baseURL + request.path) else {
return nil
}
var urlRequest = URLRequest(url: url)
urlRequest.timeoutInterval = GlobalConstants.requestTimeoutInterval
urlRequest.httpMethod = request.methodology.rawValue
request.headers?.forEach { urlRequest.addValue($0.worth, forHTTPHeaderField: $0.key) }
if request.authRequirement == .requiresAuth {
urlRequest.addValue(CredentialManager.shared.getValue(.token), forHTTPHeaderField: GlobalConstants.googleTokenHeaderKey)
urlRequest.addValue(CredentialManager.shared.getValue(.gadget), forHTTPHeaderField: GlobalConstants.deviceTokenHeaderKey)
urlRequest.addValue(CredentialManager.shared.getValue(.electronic mail), forHTTPHeaderField: GlobalConstants.userEmailKey)
}
urlRequest.httpBody = request.physique
if let httpBody = urlRequest.httpBody {
urlRequest.setValue("(String(describing: httpBody.rely))", forHTTPHeaderField: "Content material-Size")
}
return urlRequest
}
}
extension URLSessionAPIClient {
func isLargeFile(url: URL) -> Bool {
// Examine if the request accommodates a legitimate file URL within the path
// In any other case, examine the file dimension for big information
do {
let fileSize = attempt FileManager.default.attributesOfItem(atPath: url.path)[.size] as? NSNumber
if let dimension = fileSize, dimension.intValue > 200 * 1024 * 1024 { // 200MB
return true
}
} catch {
print("Error checking file dimension: (error)")
}
return false
}
}
extension URLSessionAPIClient: URLSessionDelegate, URLSessionTaskDelegate,
URLSessionDataDelegate {
// Deal with the completion of the duty
func urlSession(_ session: URLSession, activity: URLSessionTask, didCompleteWithError
error: Error?) {
if let error = error {
print("☠️ Add failed with error: (error.localizedDescription)")
// Deal with the error right here (e.g., notify the consumer, retry the add, and so on.)
} else {
print("☠️ Add accomplished efficiently.")
// You may deal with the profitable add right here
}
if let tempFileURL = self.tempFileURL {
attempt? FileManager.default.removeItem(at: tempFileURL)
}
// Name the background session completion handler to complete the duty
if let completionHandler = backgroundSessionCompletionHandler {
completionHandler()
}
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
print("☠️ Background duties completed")
self.backgroundSessionCompletionHandler?()
self.backgroundSessionCompletionHandler = nil
}
func urlSession(_ session: URLSession, uploadTask: URLSessionUploadTask, didSendBodyData bytesSent: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
print("☠️ Add Progress: (progress * 100)%")
}
}