apple_kit

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

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