Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,16 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.8.1")),
.package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.4.2")),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "MSLFoundation",
dependencies: []
dependencies: [
.product(name: "Logging", package: "swift-log"),
]
),
.target(
name: "MSLCombine",
Expand Down
76 changes: 76 additions & 0 deletions Sources/MSLFoundation/BackgroundTaskManager/AsyncOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import BackgroundTasks

/// Create another class that conforms to this class to help manage your async operation
/// https://www.avanderlee.com/swift/asynchronous-operations/
open class AsyncOperation: Operation {
private let lockQueue: DispatchQueue

public init(label: String) {
self.lockQueue = DispatchQueue(label: label, attributes: .concurrent)
}

override public var isAsynchronous: Bool {
return true
}

private var _isExecuting = false
override public private(set) var isExecuting: Bool {
get {
return self.lockQueue.sync { () -> Bool in
return self._isExecuting
}
}
set {
willChangeValue(forKey: "isExecuting")
self.lockQueue.sync(flags: [.barrier]) {
self._isExecuting = newValue
}
didChangeValue(forKey: "isExecuting")
}
}

private var _isFinished = false
override public private(set) var isFinished: Bool {
get {
return self.lockQueue.sync { () -> Bool in
return self._isFinished
}
}
set {
willChangeValue(forKey: "isFinished")
self.lockQueue.sync(flags: [.barrier]) {
self._isFinished = newValue
}
didChangeValue(forKey: "isFinished")
}
}

override open func cancel() {
super.cancel()

self.finish()
}

override public func start() {
super.start() // calls main()

guard !self.isCancelled else {
self.finish()
return
}

self.isFinished = false
self.isExecuting = true
}

override open func main() {
fatalError("Subclasses must implement `main` without overriding super.")
}

public func finish() {
guard self.isExecuting else { return }

self.isExecuting = false
self.isFinished = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import BackgroundTasks
import Combine
import Foundation
import Logging
import UIKit

private let logger: Logger = {
var logger = Logger(label: "\(#file)")
logger.logLevel = .info
return logger
}()

public enum EnqueueType {
/// Replace the existing task with the provided one
case replace

/// Keep the existing task instead of using the provided one
case keep
}

public final class BackgroundTaskManager {
private let taskIdentifier: String

private let queue = OperationQueueManager()

/// The current background refresh task that woke up the application
private var backgroundTask: BGAppRefreshTask?

/// Create a new BackgroundTaskManager with a unique identifier. This unique identifier will be used when tasks are executed in the background.
public init(
taskId: String
) {
self.taskIdentifier = taskId

BGTaskScheduler.shared.register(
forTaskWithIdentifier: self.taskIdentifier,
using: nil
) { task in
guard let task = task as? BGAppRefreshTask else { return }
self.handleBackgroundTask(task)
}

// Observe the app entering the foreground
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleActivate(_:)),
name: UIScene.willEnterForegroundNotification,
object: nil
)

// Observe the app entering the background
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleDeactivate(_:)),
name: UIScene.willDeactivateNotification,
object: nil
)

self.queue.addListener(self)
}

deinit {
self.queue.removeListener(self)
}
}

// MARK: Public Functions

public extension BackgroundTaskManager {
/// Adds a new task to the manager. EnqueueType can be used to either `keep` or `replace` a provider that
/// has already been register with the same `identifier`.
func register(type: EnqueueType = .keep, provider: OperationWorkProvider) {
self.queue.register(type: type, provider: provider)
}

/// Removes a task from the background manager.
func unregister(provider: OperationWorkProvider) {
self.queue.unregister(provider: provider)
}

/// Begin runing registered tasks.
func start() {
self.queue.start()
}

/// Prevent the BackgroundTaskManager from running any tasks.
func stop() {
self.queue.stop()
}
}

// MARK: Helpers

extension BackgroundTaskManager {
@objc private func handleActivate(_ notification: Notification) {
BGTaskScheduler.shared.cancel(
taskRequestWithIdentifier: self.taskIdentifier
)
}

@objc private func handleDeactivate(_ notification: Notification) {
self.scheduleBackgroundTasks()
}

private func scheduleBackgroundTasks() {
let backgroundTask = BGAppRefreshTaskRequest(identifier: self.taskIdentifier)
backgroundTask.earliestBeginDate = self.queue.nextRunDate

do {
try BGTaskScheduler.shared.submit(backgroundTask)

BGTaskScheduler.shared.getPendingTaskRequests { tasks in
let details = tasks.map(\.description).joined(separator: "\n")
logger.debug("\(tasks.count) background tasks scheduled:\n\(details)")
}
} catch {
logger.error("Failed to schedule background tasks!")
logger.error("\(error.localizedDescription)")
}
}

private func handleBackgroundTask(_ task: BGAppRefreshTask) {
defer {
// Schedule the next background task
self.scheduleBackgroundTasks()
}

logger.info("App woke up for background refresh task: \(task.description)")

self.backgroundTask = task

self.queue.start()

task.expirationHandler = {
logger.info("Background refresh task expired")

self.queue.stop()

self.backgroundTask = nil
}
}
}

extension BackgroundTaskManager: QueueManagerListener {
func didCompleteQueue() {
logger.info("Background task completed 1 round of work")
}

func didSleepQueue() {
logger.info("Background work did finish")

self.backgroundTask?.setTaskCompleted(success: true)
self.backgroundTask = nil
}
}
Loading
Loading