RadrootsAppleBackgroundTasks.swift (8807B)
1 import Foundation 2 3 #if canImport(BackgroundTasks) && os(iOS) 4 import BackgroundTasks 5 #endif 6 7 public struct RadrootsAppleBackgroundTaskRegistration: Sendable { 8 public let identifier: RadrootsBackgroundTaskIdentifier 9 public let kind: RadrootsBackgroundTaskKind 10 public let handler: @Sendable () async -> Bool 11 12 public init( 13 identifier: RadrootsBackgroundTaskIdentifier, 14 kind: RadrootsBackgroundTaskKind, 15 handler: @escaping @Sendable () async -> Bool 16 ) { 17 self.identifier = identifier 18 self.kind = kind 19 self.handler = handler 20 } 21 } 22 23 public struct RadrootsAppleBackgroundTaskSchedulerAdapters: Sendable { 24 public let now: @Sendable () -> Date 25 public let register: @Sendable (RadrootsAppleBackgroundTaskRegistration) async throws -> Bool 26 public let submit: @Sendable (RadrootsBackgroundTaskRequest) async throws -> Void 27 public let cancel: @Sendable (RadrootsBackgroundTaskIdentifier) async -> Void 28 public let cancelAll: @Sendable () async -> Void 29 public let pendingTasks: @Sendable () async throws -> [RadrootsBackgroundTaskSnapshot] 30 31 public init( 32 now: @escaping @Sendable () -> Date = Date.init, 33 register: @escaping @Sendable (RadrootsAppleBackgroundTaskRegistration) async throws -> Bool, 34 submit: @escaping @Sendable (RadrootsBackgroundTaskRequest) async throws -> Void, 35 cancel: @escaping @Sendable (RadrootsBackgroundTaskIdentifier) async -> Void, 36 cancelAll: @escaping @Sendable () async -> Void, 37 pendingTasks: @escaping @Sendable () async throws -> [RadrootsBackgroundTaskSnapshot] 38 ) { 39 self.now = now 40 self.register = register 41 self.submit = submit 42 self.cancel = cancel 43 self.cancelAll = cancelAll 44 self.pendingTasks = pendingTasks 45 } 46 47 public static var live: Self { 48 #if canImport(BackgroundTasks) && os(iOS) 49 Self( 50 register: { registration in 51 BGTaskScheduler.shared.register( 52 forTaskWithIdentifier: registration.identifier.rawValue, 53 using: nil 54 ) { task in 55 let completion = RadrootsAppleBackgroundTaskCompletion(task: task) 56 let handlerTask = Task { 57 await registration.handler() 58 } 59 task.expirationHandler = { 60 handlerTask.cancel() 61 completion.complete(success: false) 62 } 63 Task { 64 let success = await handlerTask.value 65 completion.complete(success: success) 66 } 67 } 68 }, 69 submit: { request in 70 try BGTaskScheduler.shared.submit(Self.platformRequest(for: request)) 71 }, 72 cancel: { identifier in 73 BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier.rawValue) 74 }, 75 cancelAll: { 76 BGTaskScheduler.shared.cancelAllTaskRequests() 77 }, 78 pendingTasks: { 79 try await Self.pendingPlatformTaskSnapshots() 80 } 81 ) 82 #else 83 Self.unavailable 84 #endif 85 } 86 87 public static let unavailable = Self( 88 register: { _ in 89 throw RadrootsBackgroundTaskError.unavailable("background task scheduling is unavailable on this platform") 90 }, 91 submit: { _ in 92 throw RadrootsBackgroundTaskError.unavailable("background task scheduling is unavailable on this platform") 93 }, 94 cancel: { _ in }, 95 cancelAll: {}, 96 pendingTasks: { 97 throw RadrootsBackgroundTaskError.unavailable("background task scheduling is unavailable on this platform") 98 } 99 ) 100 } 101 102 public final class RadrootsAppleBackgroundTaskScheduler: RadrootsBackgroundTaskScheduler, Sendable { 103 private let adapters: RadrootsAppleBackgroundTaskSchedulerAdapters 104 105 public init(adapters: RadrootsAppleBackgroundTaskSchedulerAdapters = .live) { 106 self.adapters = adapters 107 } 108 109 @discardableResult 110 public func register(_ registration: RadrootsAppleBackgroundTaskRegistration) async throws -> Bool { 111 let registered = try await adapters.register(registration) 112 guard registered else { 113 throw RadrootsBackgroundTaskError.schedulerFailure( 114 "background task registration was rejected" 115 ) 116 } 117 return registered 118 } 119 120 public func submit(_ request: RadrootsBackgroundTaskRequest) async throws -> RadrootsBackgroundTaskSnapshot { 121 try await adapters.submit(request) 122 return try RadrootsBackgroundTaskSnapshot( 123 request: request, 124 submittedAt: adapters.now() 125 ) 126 } 127 128 public func cancel(_ identifier: RadrootsBackgroundTaskIdentifier) async throws { 129 await adapters.cancel(identifier) 130 } 131 132 public func cancelAll() async throws { 133 await adapters.cancelAll() 134 } 135 136 public func pendingTasks() async throws -> [RadrootsBackgroundTaskSnapshot] { 137 try await adapters.pendingTasks() 138 } 139 } 140 141 #if canImport(BackgroundTasks) && os(iOS) 142 private extension RadrootsAppleBackgroundTaskSchedulerAdapters { 143 static func platformRequest(for request: RadrootsBackgroundTaskRequest) -> BGTaskRequest { 144 let platformRequest: BGTaskRequest 145 switch request.kind { 146 case .appRefresh: 147 platformRequest = BGAppRefreshTaskRequest(identifier: request.identifier.rawValue) 148 case .processing: 149 let processingRequest = BGProcessingTaskRequest(identifier: request.identifier.rawValue) 150 processingRequest.requiresNetworkConnectivity = request.requiresNetworkConnectivity 151 processingRequest.requiresExternalPower = request.requiresExternalPower 152 platformRequest = processingRequest 153 } 154 platformRequest.earliestBeginDate = request.earliestBeginDate 155 return platformRequest 156 } 157 158 static func pendingPlatformTaskSnapshots() async throws -> [RadrootsBackgroundTaskSnapshot] { 159 try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[RadrootsBackgroundTaskSnapshot], Error>) in 160 BGTaskScheduler.shared.getPendingTaskRequests { requests in 161 do { 162 let snapshots: [RadrootsBackgroundTaskSnapshot] = try requests.compactMap { request -> RadrootsBackgroundTaskSnapshot? in 163 guard let identifier = try? RadrootsBackgroundTaskIdentifier(request.identifier) else { 164 return nil 165 } 166 let kind: RadrootsBackgroundTaskKind 167 let requiresNetworkConnectivity: Bool 168 let requiresExternalPower: Bool 169 if let processingRequest = request as? BGProcessingTaskRequest { 170 kind = .processing 171 requiresNetworkConnectivity = processingRequest.requiresNetworkConnectivity 172 requiresExternalPower = processingRequest.requiresExternalPower 173 } else { 174 kind = .appRefresh 175 requiresNetworkConnectivity = false 176 requiresExternalPower = false 177 } 178 return try RadrootsBackgroundTaskSnapshot( 179 identifier: identifier, 180 kind: kind, 181 earliestBeginDate: request.earliestBeginDate, 182 submittedAt: Date(), 183 requiresNetworkConnectivity: requiresNetworkConnectivity, 184 requiresExternalPower: requiresExternalPower 185 ) 186 } 187 .sorted { left, right in 188 left.identifier.rawValue < right.identifier.rawValue 189 } 190 continuation.resume(returning: snapshots) 191 } catch { 192 continuation.resume(throwing: error) 193 } 194 } 195 } 196 } 197 } 198 #endif 199 200 #if canImport(BackgroundTasks) && os(iOS) 201 private final class RadrootsAppleBackgroundTaskCompletion: @unchecked Sendable { 202 private let task: BGTask 203 private let lock = NSLock() 204 private var completed = false 205 206 init(task: BGTask) { 207 self.task = task 208 } 209 210 func complete(success: Bool) { 211 lock.lock() 212 defer { lock.unlock() } 213 guard !completed else { 214 return 215 } 216 completed = true 217 task.setTaskCompleted(success: success) 218 } 219 } 220 #endif