Skip to content

Commit fc60abe

Browse files
Some preliminary work on TaskScheduler
1 parent 1435738 commit fc60abe

File tree

3 files changed

+167
-11
lines changed

3 files changed

+167
-11
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ dependencies: [
2020

2121
## Concept
2222

23-
[`URLSession`](https://developer.apple.com/documentation/foundation/urlsession)'s background upload and download facilities are relatively straightforward to get started with. But, they are surprisingly difficult to manage. The core challange is an operation could start and/or complete while your process isn't even running. You cannot just wait for a completion handler or `await` call. This usually means you have to involve peristent storage to juggle state across process launches.
23+
[`URLSession`](https://developer.apple.com/documentation/foundation/urlsession)'s background upload and download facilities are relatively straightforward to get started with. But, they are surprisingly difficult to manage. The core challenge is an operation could start and/or complete while your process isn't even running. You cannot just wait for a completion handler or `await` call. This usually means you have to involve persistent storage to juggle state across process launches.
2424

2525
You also typically need to make use of system-provided API to reconnect your session to any work that has happened between launches. This can be done a few different ways, depending on your type of project and how you'd like your system to work.
2626

@@ -125,6 +125,12 @@ struct YourWidget: Widget {
125125
}
126126
```
127127

128+
### Background Tasks
129+
130+
It's disappointing that the [BackgroundTasks](https://developer.apple.com/documentation/backgroundtasks) framework isn't available for macOS. The library includes some preliminary work to build a nearly source-compatible version of `BGTaskScheduler` that works across all platforms. As of right now, these types aren't public because the work isn't complete.
131+
132+
The macOS implementation is build around [NSBackgroundActivityScheduler](https://developer.apple.com/documentation/foundation/nsbackgroundactivityscheduler), which works very differently internally.
133+
128134
### More Complex Usage
129135

130136
This package is used to manage the background uploading facilities of [Wells](https://github.com/ChimeHQ/Wells), a diagnostics report submission system. You can check out that project for a much more complex example of how to manage background uploads.

Sources/Background/TaskScheduler.swift

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,159 @@ import Foundation
33
import BackgroundTasks
44
#endif
55

6-
enum BackgroundTask {
6+
protocol BackgroundTaskRequest: Hashable {
7+
var identifier: String { get }
8+
var earliestBeginDate: Date? { get }
9+
}
10+
11+
enum BackgroundTaskError: Error {
12+
case unsupportedTaskRequest
13+
}
14+
15+
#if os(iOS) || os(tvOS) || os(visionOS)
16+
struct BackgroundTask {
17+
private let task: BGTask
18+
19+
init(_ task: BGTask) {
20+
self.task = task
21+
}
22+
23+
public var identifier: String { task.identifier }
24+
25+
public var expirationHandler: (@Sendable () -> Void)? {
26+
get {
27+
unsafeBitCast(task.expirationHandler, to: (@Sendable () -> Void)?.self)
28+
}
29+
set { task.expirationHandler = newValue }
30+
}
31+
32+
public func setTaskCompleted(success: Bool) {
33+
task.setTaskCompleted(success: success)
34+
}
35+
}
36+
37+
extension BackgroundTaskRequest {
38+
var bgTaskRequest: BGTaskRequest {
39+
get throws {
40+
switch self {
41+
case is AppRefreshTaskRequest:
42+
let request = BGAppRefreshTaskRequest(identifier: identifier)
43+
44+
request.earliestBeginDate = earliestBeginDate
45+
46+
return request
47+
case let processing as ProcessingTaskRequest:
48+
let request = BGProcessingTaskRequest(identifier: identifier)
49+
50+
request.earliestBeginDate = earliestBeginDate
51+
request.requiresNetworkConnectivity = processing.requiresNetworkConnectivity
52+
request.requiresExternalPower = processing.requiresExternalPower
53+
54+
return request
55+
default:
56+
throw BackgroundTaskError.unsupportedTaskRequest
57+
}
58+
}
59+
}
60+
}
61+
#else
62+
struct BackgroundTask {
63+
public let identifier: String
64+
public let expirationHandler: (@Sendable () -> Void)?
65+
66+
public func setTaskCompleted(success: Bool) {
67+
}
68+
}
69+
#endif
70+
71+
struct AppRefreshTaskRequest: BackgroundTaskRequest {
72+
public let identifier: String
73+
public var earliestBeginDate: Date?
74+
75+
init(identifier: String) {
76+
self.identifier = identifier
77+
}
78+
}
779

80+
struct ProcessingTaskRequest: BackgroundTaskRequest {
81+
public let identifier: String
82+
public var earliestBeginDate: Date?
83+
public var requiresNetworkConnectivity: Bool = false
84+
public var requiresExternalPower: Bool = false
85+
86+
init(identifier: String) {
87+
self.identifier = identifier
88+
}
889
}
990

91+
#if os(iOS) || os(tvOS) || os(visionOS)
1092
final class TaskScheduler: Sendable {
1193
public static let shared = TaskScheduler()
1294

1395
private init() {
1496
}
97+
98+
public func submit(_ task: any BackgroundTaskRequest) throws {
99+
let bgTaskRequest = try task.bgTaskRequest
100+
101+
try BGTaskScheduler.shared.submit(bgTaskRequest)
102+
}
103+
104+
public func register(
105+
forTaskWithIdentifier identifier: String,
106+
using queue: dispatch_queue_t? = nil,
107+
launchHandler: @escaping @Sendable (BackgroundTask) -> Void
108+
) -> Bool {
109+
BGTaskScheduler.shared.register(forTaskWithIdentifier: identifier, using: queue) { @Sendable bgTask in
110+
let task = BackgroundTask(bgTask)
111+
112+
launchHandler(task)
113+
}
114+
}
115+
}
116+
117+
extension BGTaskScheduler {
118+
public func register(
119+
forTaskWithIdentifier identifier: String,
120+
launchHandler: @escaping @Sendable (BGTask) -> Void
121+
) -> Bool {
122+
register(forTaskWithIdentifier: identifier, using: nil, launchHandler: launchHandler)
123+
}
124+
}
125+
126+
#else
127+
final class TaskScheduler: @unchecked Sendable {
128+
public static let shared = TaskScheduler()
129+
130+
private let scheduler = NSBackgroundActivityScheduler(identifier: "com.chimehq.Background")
131+
private let lock = NSLock()
132+
private var requests: [String: any BackgroundTaskRequest] = [:]
133+
private var registrations: [String: HandlerRegistration] = [:]
134+
135+
private struct HandlerRegistration: Sendable {
136+
let handler: @Sendable (BackgroundTask) -> Void
137+
let identifier: String
138+
let queue: dispatch_queue_t?
139+
}
140+
141+
private init() {
142+
}
143+
144+
public func submit<T: BackgroundTaskRequest>(_ task: T) throws {
145+
requests[task.identifier] = task
146+
}
147+
148+
public func register(
149+
forTaskWithIdentifier identifier: String,
150+
using queue: dispatch_queue_t? = nil,
151+
launchHandler: @escaping @Sendable (BackgroundTask) -> Void
152+
) -> Bool {
153+
let registration = HandlerRegistration(handler: launchHandler, identifier: identifier, queue: queue)
154+
155+
registrations[identifier] = registration
156+
157+
return true
158+
}
15159
}
160+
161+
#endif
Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import XCTest
1+
import Testing
22
@testable import Background
33

4-
final class BackgroundTests: XCTestCase {
5-
func testExample() throws {
6-
// XCTest Documentation
7-
// https://developer.apple.com/documentation/xctest
8-
9-
// Defining Test Cases and Test Methods
10-
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
11-
}
4+
struct BackgroundTests {
5+
@Test func testExample() throws {
6+
let request = ProcessingTaskRequest(identifier: "abc")
7+
8+
let scheduler = TaskScheduler.shared
9+
10+
_ = scheduler.register(forTaskWithIdentifier: "abc") { task in
11+
12+
}
13+
14+
try scheduler.submit(request)
15+
}
1216
}

0 commit comments

Comments
 (0)