apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

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