Skip to content

Commit b75b779

Browse files
authored
BackgroundTasks API support (#251)
1 parent f1dff05 commit b75b779

File tree

10 files changed

+190
-2
lines changed

10 files changed

+190
-2
lines changed

Sources/SwiftQueue/JobBuilder.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,12 @@ public final class JobBuilder {
7979
/// Repeat job a certain number of time and with a interval between each run
8080
/// Limit of period to reproduce
8181
/// interval between each run. Does not affect the first iteration. Please add delay if so
82-
public func periodic(limit: Limit = .unlimited, interval: TimeInterval = 0) -> Self {
82+
public func periodic(limit: Limit = .unlimited, interval: TimeInterval = 0, allowBackground: Bool = false) -> Self {
8383
assert(limit.validate)
8484
assert(interval >= 0)
8585
info.maxRun = limit
8686
info.interval = interval
87+
info.allowBackground = allowBackground
8788
return self
8889
}
8990

Sources/SwiftQueue/JobInfo.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public struct JobInfo {
3838
/// Override job when scheduling a job with same uuid
3939
var override: Bool
4040

41-
//// Including job that are executing when scheduling with same uuid
41+
/// Including job that are executing when scheduling with same uuid
4242
var includeExecutingJob: Bool
4343

4444
/// Set of identifiers
@@ -65,6 +65,9 @@ public struct JobInfo {
6565
/// Time between each repetition of the job
6666
var interval: TimeInterval
6767

68+
/// Allow job to run in background task
69+
var allowBackground: Bool
70+
6871
/// Number of run maximum
6972
var maxRun: Limit
7073

@@ -131,6 +134,7 @@ public struct JobInfo {
131134
createTime: Date(),
132135
interval: -1.0,
133136
maxRun: .limited(0),
137+
allowBackground: false,
134138
retries: .limited(0),
135139
runCount: 0,
136140
requireCharging: false,
@@ -154,6 +158,7 @@ public struct JobInfo {
154158
createTime: Date,
155159
interval: TimeInterval,
156160
maxRun: Limit,
161+
allowBackground: Bool,
157162
retries: Limit,
158163
runCount: Double,
159164
requireCharging: Bool,
@@ -176,6 +181,7 @@ public struct JobInfo {
176181
self.createTime = createTime
177182
self.interval = interval
178183
self.maxRun = maxRun
184+
self.allowBackground = allowBackground
179185
self.retries = retries
180186
self.runCount = runCount
181187
self.requireCharging = requireCharging

Sources/SwiftQueue/JobInfoSerializer+Decodable.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ extension JobInfo: Decodable {
7474
case createTime = "createTime"
7575
case interval = "runCount"
7676
case maxRun = "maxRun"
77+
case allowBackground = "allowBackground"
7778
case retries = "retries"
7879
case runCount = "interval"
7980
case requireCharging = "requireCharging"
@@ -99,6 +100,7 @@ extension JobInfo: Decodable {
99100
let createTime: Date = try container.decode(Date.self, forKey: .createTime)
100101
let interval: TimeInterval = try container.decode(TimeInterval.self, forKey: .interval)
101102
let maxRun: Limit = try container.decode(Limit.self, forKey: .maxRun)
103+
let allowBackground: Bool = try container.decode(Bool.self, forKey: .allowBackground)
102104
let retries: Limit = try container.decode(Limit.self, forKey: .retries)
103105
let runCount: Double = try container.decode(Double.self, forKey: .runCount)
104106
let requireCharging: Bool = try container.decode(Bool.self, forKey: .requireCharging)
@@ -121,6 +123,7 @@ extension JobInfo: Decodable {
121123
createTime: createTime,
122124
interval: interval,
123125
maxRun: maxRun,
126+
allowBackground: allowBackground,
124127
retries: retries,
125128
runCount: runCount,
126129
requireCharging: requireCharging,
@@ -149,6 +152,7 @@ extension JobInfo: Encodable {
149152
try container.encode(createTime, forKey: .createTime)
150153
try container.encode(interval, forKey: .interval)
151154
try container.encode(maxRun, forKey: .maxRun)
155+
try container.encode(allowBackground, forKey: .allowBackground)
152156
try container.encode(retries, forKey: .retries)
153157
try container.encode(runCount, forKey: .runCount)
154158
try container.encode(requireCharging, forKey: .requireCharging)

Sources/SwiftQueue/JobInfoSerializer+V1.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ internal extension JobInfo {
9191
dict[JobInfoKeys.params.stringValue] = self.params
9292
dict[JobInfoKeys.createTime.stringValue] = dateFormatter.string(from: self.createTime)
9393
dict[JobInfoKeys.runCount.stringValue] = self.runCount
94+
dict[JobInfoKeys.allowBackground.stringValue] = self.allowBackground
9495
dict[JobInfoKeys.maxRun.stringValue] = self.maxRun.rawValue
9596
dict[JobInfoKeys.retries.stringValue] = self.retries.rawValue
9697
dict[JobInfoKeys.interval.stringValue] = self.interval
@@ -114,6 +115,7 @@ internal extension JobInfo {
114115
dictionary.assign(JobInfoKeys.createTime.stringValue, &self.createTime, dateFormatter.date)
115116
dictionary.assign(JobInfoKeys.interval.stringValue, &self.interval)
116117
dictionary.assign(JobInfoKeys.maxRun.stringValue, &self.maxRun, Limit.fromRawValue)
118+
dictionary.assign(JobInfoKeys.allowBackground.stringValue, &self.allowBackground)
117119
dictionary.assign(JobInfoKeys.retries.stringValue, &self.retries, Limit.fromRawValue)
118120
dictionary.assign(JobInfoKeys.runCount.stringValue, &self.runCount)
119121
dictionary.assign(JobInfoKeys.requireCharging.stringValue, &self.requireCharging)

Sources/SwiftQueue/SqOperation.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ internal final class SqOperation: Operation {
3838

3939
let dispatchQueue: DispatchQueue
4040

41+
var nextRunSchedule: Date? = nil
42+
4143
override var name: String? { get { return info.uuid } set { } }
4244
override var queuePriority: QueuePriority { get { return info.priority } set { } }
4345
override var qualityOfService: QualityOfService { get { return info.qualityOfService } set { } }
@@ -225,6 +227,7 @@ extension SqOperation: JobResult {
225227
}
226228

227229
// Schedule run after interval
230+
nextRunSchedule = Date().addingTimeInterval(info.interval)
228231
dispatchQueue.runAfter(info.interval, callback: { [weak self] in
229232
self?.info.runCount += 1
230233
self?.run()
@@ -259,6 +262,7 @@ extension SqOperation {
259262
extension SqOperation {
260263

261264
fileprivate func retryInBackgroundAfter(_ delay: TimeInterval) {
265+
nextRunSchedule = Date().addingTimeInterval(delay)
262266
dispatchQueue.runAfter(delay) { [weak self] in
263267
self?.info.retries.decreaseValue(by: 1)
264268
self?.run()

Sources/SwiftQueue/SwiftQueueManager.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,25 @@ public final class SwiftQueueManager {
118118
}
119119
return count
120120
}
121+
}
122+
123+
internal extension SwiftQueueManager {
124+
125+
func getAllAllowBackgroundOperation() -> [SqOperation] {
126+
return manage.values
127+
.flatMap { $0.operations }
128+
.compactMap { $0 as? SqOperation }
129+
.filter { $0.info.allowBackground }
130+
}
121131

132+
func getOperation(forUUID: String) -> SqOperation? {
133+
for queue: SqOperationQueue in manage.values {
134+
for operation in queue.operations where operation.name == forUUID {
135+
return operation as? SqOperation
136+
}
137+
}
138+
return nil
139+
}
122140
}
123141

124142
internal struct SqManagerParams {
@@ -220,6 +238,7 @@ public final class SwiftQueueManagerBuilder {
220238
params.dispatchQueue = dispatchQueue
221239
return self
222240
}
241+
223242
/// Get an instance of `SwiftQueueManager`
224243
public func build() -> SwiftQueueManager {
225244
return SwiftQueueManager(params: params, isSuspended: isSuspended)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// The MIT License (MIT)
2+
//
3+
// Copyright (c) 2017 Lucas Nelaupe
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
#if canImport(BackgroundTasks)
24+
import BackgroundTasks
25+
#endif
26+
27+
@available(iOS 13.0, *)
28+
/// Extension of SwiftQueueManager to support BackgroundTask API from iOS 13.
29+
public extension SwiftQueueManager {
30+
31+
/// Register task that can potentially run in Background (Using BackgroundTask API)
32+
/// Registration of all launch handlers must be complete before the end of applicationDidFinishLaunching(_:)
33+
/// https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler/3180427-register
34+
func registerForBackgroundTask(forTaskWithUUID: String) {
35+
BGTaskScheduler.shared.register(forTaskWithIdentifier: forTaskWithUUID, using: nil) { [weak self] task in
36+
if let operation = self?.getOperation(forUUID: task.identifier) {
37+
task.expirationHandler = {
38+
operation.done(.fail(SwiftQueueError.timeout))
39+
}
40+
operation.handler.onRun(callback: TaskJobResult(actual: operation, task: task))
41+
}
42+
}
43+
}
44+
45+
/// Call this method when application is entering background to schedule jobs as background task
46+
func applicationDidEnterBackground() {
47+
for operation in getAllAllowBackgroundOperation() {
48+
operation.scheduleBackgroundTask()
49+
}
50+
}
51+
52+
/// Cancel all possible background Task
53+
func cancelAllBackgroundTask() {
54+
for operation in getAllAllowBackgroundOperation() {
55+
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: operation.info.uuid)
56+
}
57+
}
58+
}
59+
60+
@available(iOS 13.0, *)
61+
internal extension SqOperation {
62+
63+
func scheduleBackgroundTask() {
64+
let request = BGProcessingTaskRequest(identifier: info.uuid)
65+
66+
request.requiresNetworkConnectivity = info.requireNetwork.rawValue > NetworkType.any.rawValue
67+
request.requiresExternalPower = info.requireCharging
68+
request.earliestBeginDate = nextRunSchedule
69+
70+
do {
71+
try BGTaskScheduler.shared.submit(request)
72+
} catch {
73+
logger.log(.verbose, jobId: info.uuid, message: "Could not schedule BackgroundTask")
74+
}
75+
}
76+
}
77+
78+
@available(iOS 13.0, *)
79+
private class TaskJobResult: JobResult {
80+
81+
private let task: BGTask
82+
private let actual: JobResult
83+
84+
init(actual: JobResult, task: BGTask) {
85+
self.actual = actual
86+
self.task = task
87+
}
88+
89+
public func done(_ result: JobCompletion) {
90+
actual.done(result)
91+
92+
switch result {
93+
case .success:
94+
task.setTaskCompleted(success: true)
95+
case .fail:
96+
task.setTaskCompleted(success: false)
97+
}
98+
}
99+
}

SwiftQueue.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Pod::Spec.new do |s|
1818
s.requires_arc = true
1919

2020
s.source_files = 'Sources/SwiftQueue/**.swift'
21+
s.ios.source_files = 'Sources/ios/*.swift', 'Sources/SwiftQueue/**.swift'
2122

2223
s.ios.dependency 'ReachabilitySwift', '~> 4.3'
2324
s.tvos.dependency 'ReachabilitySwift', '~> 4.3'

SwiftQueue.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
1E68ED598FA15EF36B431571 /* Constraint+Deadline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E68EF96F1DE038FFF94FCD8 /* Constraint+Deadline.swift */; };
3434
1E68EDC2230D2FACC61E7868 /* Constraint+Delay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E68E940C8373221E3EDC771 /* Constraint+Delay.swift */; };
3535
1E68EE914B5B51CDB2BD7C4D /* Constraint+UniqueUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E68E002DC39E9BBE4A0A06A /* Constraint+UniqueUUID.swift */; };
36+
618E9CE4230E1F1300856158 /* SwiftQueueManager+BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 618E9CE2230E1F1000856158 /* SwiftQueueManager+BackgroundTask.swift */; };
3637
61A65B7722ABD172009AC9B7 /* Constraint+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A65B7622ABD172009AC9B7 /* Constraint+Timeout.swift */; };
3738
61A65B7822ABD175009AC9B7 /* Constraint+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A65B7622ABD172009AC9B7 /* Constraint+Timeout.swift */; };
3839
61A65B7922ABD175009AC9B7 /* Constraint+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A65B7622ABD172009AC9B7 /* Constraint+Timeout.swift */; };
@@ -118,6 +119,7 @@
118119
1E68E82C1E071C7AA1AC4BB6 /* Constraint+Network.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Constraint+Network.swift"; path = "SwiftQueue/Constraint+Network.swift"; sourceTree = "<group>"; };
119120
1E68E940C8373221E3EDC771 /* Constraint+Delay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Constraint+Delay.swift"; path = "SwiftQueue/Constraint+Delay.swift"; sourceTree = "<group>"; };
120121
1E68EF96F1DE038FFF94FCD8 /* Constraint+Deadline.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Constraint+Deadline.swift"; path = "SwiftQueue/Constraint+Deadline.swift"; sourceTree = "<group>"; };
122+
618E9CE2230E1F1000856158 /* SwiftQueueManager+BackgroundTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "SwiftQueueManager+BackgroundTask.swift"; path = "ios/SwiftQueueManager+BackgroundTask.swift"; sourceTree = "<group>"; };
121123
61A65B7622ABD172009AC9B7 /* Constraint+Timeout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Constraint+Timeout.swift"; path = "SwiftQueue/Constraint+Timeout.swift"; sourceTree = "<group>"; };
122124
95A210DDE517DF37DE5A7EC1 /* JobInfoSerializer+V1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "JobInfoSerializer+V1.swift"; path = "SwiftQueue/JobInfoSerializer+V1.swift"; sourceTree = "<group>"; };
123125
95A2169E6968B5F155D5B612 /* JobInfoSerializer+Decodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "JobInfoSerializer+Decodable.swift"; path = "SwiftQueue/JobInfoSerializer+Decodable.swift"; sourceTree = "<group>"; };
@@ -232,6 +234,7 @@
232234
OBJ_7 /* Sources */ = {
233235
isa = PBXGroup;
234236
children = (
237+
618E9CE2230E1F1000856158 /* SwiftQueueManager+BackgroundTask.swift */,
235238
61A65B7622ABD172009AC9B7 /* Constraint+Timeout.swift */,
236239
1D8479A61F82A84B00B3BBFB /* Constraint.swift */,
237240
1D8479A21F82A84A00B3BBFB /* SwiftQueue.swift */,
@@ -414,6 +417,7 @@
414417
D0B7C9AE1E4717DE474FD804 /* SqOperationQueue.swift in Sources */,
415418
95A21FA05AABFA4A59096E8E /* SwiftQueueLogger.swift in Sources */,
416419
95A21FA0723161F6908B133B /* Constraint+Charging.swift in Sources */,
420+
618E9CE4230E1F1300856158 /* SwiftQueueManager+BackgroundTask.swift in Sources */,
417421
95A21B7D3F013BAF87216CF0 /* JobInfoSerializer+Decodable.swift in Sources */,
418422
95A215B786349300D06194AF /* JobInfoSerializer+V1.swift in Sources */,
419423
);

Tests/SwiftQueueTests/SwiftQueueManagerTests.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,52 @@ class SwiftQueueManagerTests: XCTestCase {
197197
XCTAssertNotEqual(Limit.unlimited, Limit.limited(-1))
198198
}
199199

200+
public func testGetAllAllowBackgroundOperation() {
201+
let (type, job) = (UUID().uuidString, TestJob())
202+
203+
let id = UUID().uuidString
204+
let id2 = UUID().uuidString
205+
206+
let group = UUID().uuidString
207+
let group2 = UUID().uuidString
208+
209+
let creator = TestCreator([type: job])
210+
211+
let persister = PersisterTracker(key: UUID().uuidString)
212+
213+
let manager = SwiftQueueManagerBuilder(creator: creator).set(isSuspended: true).set(persister: persister).build()
214+
215+
JobBuilder(type: type).periodic(allowBackground: false).parallel(queueName: group).schedule(manager: manager)
216+
JobBuilder(type: type).periodic(allowBackground: false).parallel(queueName: group2).schedule(manager: manager)
217+
218+
JobBuilder(type: type).singleInstance(forId: id).periodic(allowBackground: true).parallel(queueName: group).schedule(manager: manager)
219+
JobBuilder(type: type).singleInstance(forId: id2).periodic(allowBackground: true).parallel(queueName: group2).schedule(manager: manager)
220+
221+
let result = manager.getAllAllowBackgroundOperation()
222+
223+
XCTAssertEqual(2, result.count)
224+
XCTAssertTrue([id, id2].contains(result[0].info.uuid))
225+
XCTAssertTrue([id, id2].contains(result[1].info.uuid))
226+
}
227+
228+
229+
public func testGetOperation() {
230+
let (type, job) = (UUID().uuidString, TestJob())
231+
let id = UUID().uuidString
232+
let creator = TestCreator([type: job])
233+
let persister = PersisterTracker(key: UUID().uuidString)
234+
let manager = SwiftQueueManagerBuilder(creator: creator).set(isSuspended: true).set(persister: persister).build()
235+
236+
for _ in 0..<100 {
237+
JobBuilder(type: type).parallel(queueName: UUID().uuidString).schedule(manager: manager)
238+
}
239+
240+
JobBuilder(type: type).singleInstance(forId: id).parallel(queueName: UUID().uuidString).schedule(manager: manager)
241+
242+
let operation = manager.getOperation(forUUID: id)
243+
244+
XCTAssertNotNil(operation)
245+
XCTAssertEqual(id, operation?.info.uuid)
246+
}
247+
200248
}

0 commit comments

Comments
 (0)