RadrootsAppleBackgroundTasks.swift (9710B)
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 unavailable 84 #endif 85 } 86 87 public static let unavailable = Self( 88 register: { _ in 89 throw RadrootsBackgroundTaskError.unavailable 90 }, 91 submit: { _ in 92 throw RadrootsBackgroundTaskError.unavailable 93 }, 94 cancel: { _ in }, 95 cancelAll: {}, 96 pendingTasks: { 97 throw RadrootsBackgroundTaskError.unavailable 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: Bool 112 do { 113 registered = try await adapters.register(registration) 114 } catch let error as RadrootsBackgroundTaskError { 115 throw error 116 } catch { 117 throw RadrootsBackgroundTaskError.schedulerFailure 118 } 119 guard registered else { 120 throw RadrootsBackgroundTaskError.schedulerFailure 121 } 122 return registered 123 } 124 125 public func submit(_ request: RadrootsBackgroundTaskRequest) async throws 126 -> RadrootsBackgroundTaskSnapshot 127 { 128 do { 129 try await adapters.submit(request) 130 } catch let error as RadrootsBackgroundTaskError { 131 throw error 132 } catch { 133 throw RadrootsBackgroundTaskError.schedulerFailure 134 } 135 return try RadrootsBackgroundTaskSnapshot( 136 request: request, 137 submittedAt: adapters.now() 138 ) 139 } 140 141 public func cancel(_ identifier: RadrootsBackgroundTaskIdentifier) async throws { 142 await adapters.cancel(identifier) 143 } 144 145 public func cancelAll() async throws { 146 await adapters.cancelAll() 147 } 148 149 public func pendingTasks() async throws -> [RadrootsBackgroundTaskSnapshot] { 150 do { 151 return try await adapters.pendingTasks() 152 } catch let error as RadrootsBackgroundTaskError { 153 throw error 154 } catch { 155 throw RadrootsBackgroundTaskError.schedulerFailure 156 } 157 } 158 } 159 160 #if canImport(BackgroundTasks) && os(iOS) 161 extension RadrootsAppleBackgroundTaskSchedulerAdapters { 162 fileprivate static func platformRequest(for request: RadrootsBackgroundTaskRequest) 163 -> BGTaskRequest 164 { 165 let platformRequest: BGTaskRequest 166 switch request.kind { 167 case .appRefresh: 168 platformRequest = BGAppRefreshTaskRequest(identifier: request.identifier.rawValue) 169 case .processing: 170 let processingRequest = BGProcessingTaskRequest(identifier: request.identifier.rawValue) 171 processingRequest.requiresNetworkConnectivity = request.requiresNetworkConnectivity 172 processingRequest.requiresExternalPower = request.requiresExternalPower 173 platformRequest = processingRequest 174 } 175 platformRequest.earliestBeginDate = request.earliestBeginDate 176 return platformRequest 177 } 178 179 fileprivate static func pendingPlatformTaskSnapshots() async throws 180 -> [RadrootsBackgroundTaskSnapshot] 181 { 182 try await withCheckedThrowingContinuation { 183 (continuation: CheckedContinuation<[RadrootsBackgroundTaskSnapshot], Error>) in 184 BGTaskScheduler.shared.getPendingTaskRequests { requests in 185 do { 186 let snapshots: [RadrootsBackgroundTaskSnapshot] = try requests.compactMap { 187 request -> RadrootsBackgroundTaskSnapshot? in 188 guard let identifier = try? RadrootsBackgroundTaskIdentifier(request.identifier) 189 else { 190 return nil 191 } 192 let kind: RadrootsBackgroundTaskKind 193 let requiresNetworkConnectivity: Bool 194 let requiresExternalPower: Bool 195 if let processingRequest = request as? BGProcessingTaskRequest { 196 kind = .processing 197 requiresNetworkConnectivity = processingRequest.requiresNetworkConnectivity 198 requiresExternalPower = processingRequest.requiresExternalPower 199 } else { 200 kind = .appRefresh 201 requiresNetworkConnectivity = false 202 requiresExternalPower = false 203 } 204 return try RadrootsBackgroundTaskSnapshot( 205 identifier: identifier, 206 kind: kind, 207 earliestBeginDate: request.earliestBeginDate, 208 submittedAt: Date(), 209 requiresNetworkConnectivity: requiresNetworkConnectivity, 210 requiresExternalPower: requiresExternalPower 211 ) 212 } 213 .sorted { left, right in 214 left.identifier.rawValue < right.identifier.rawValue 215 } 216 continuation.resume(returning: snapshots) 217 } catch { 218 continuation.resume(throwing: error) 219 } 220 } 221 } 222 } 223 } 224 #endif 225 226 #if canImport(BackgroundTasks) && os(iOS) 227 private final class RadrootsAppleBackgroundTaskCompletion: @unchecked Sendable { 228 private let task: BGTask 229 private let lock = NSLock() 230 private var completed = false 231 232 init(task: BGTask) { 233 self.task = task 234 } 235 236 func complete(success: Bool) { 237 lock.lock() 238 defer { lock.unlock() } 239 guard !completed else { 240 return 241 } 242 completed = true 243 task.setTaskCompleted(success: success) 244 } 245 } 246 #endif