Backgroundable
Backgroundable is a collection of handy classes, extensions and global functions to handle being in the background using Swift.
It's main focus is to add functionalities to existing Operation
s and OperationQueue
s, without adding overheads to the runtime (aka it's fast) nor to the developer (aka there's very little to learn).
It's powerful because it's simple.
Specs
- iOS 10+
- tvOS 10+
- macOS 10.12+
- Swift 4.2+
- Objective-C ready
Executing Code in the Background
Transform this:
let queue = OperationQueue()
var bgTaskId = UIBackgroundTaskInvalid
bgTaskId = UIApplication.shared.beginBackgroundTask { () -> Void in
bgTaskId = UIBackgroundTaskInvalid
}
queue.addOperation(BlockOperation(block: { () -> Void in
//do something in the background
UIApplication.shared.endBackgroundTask(bgTaskId)
}))
Into this:
inTheBackground {
//move to the background and get on with your life
}
Operation Queues
Backgroundable exposes a nifty way to enqueue several operations that should be executed sequentially:
var sequentialOperations = [Operation]()
sequentialOperations.append(AsyncOperation({ (op) in
print("Executing sequential operation 1")
op.finish()
}))
sequentialOperations.append(BlockOperation({
print("Executing sequential operation 2")
//The sequential operations work with any Operation objects, not just AsyncOperations
}))
sequentialOperations.append(BlockOperation({ (op) in
print("Executing sequential operation 3")
op.finish()
}))
OperationQueue.background.addSequentialOperations(sequentialOperations, waitUntilFinished: false)
Background Queue
Backgroundable also provides a global background operation queue (similar to the existing OperationQueue.main
):
OperationQueue.background.addOperation {
//do something
}
This background queue is an instance of the BackgroundQueue
class, which automatically handles background task identifiers. Whenever an operation is enqueued, a background task identifier is generated and whenever the queue is empty, the queue automatically invalidates it.
Sequential operations are guaranteed to be executed one after the other.
Background Queue Delegate
The BackgroundQueue
class accepts a BackgroundQueueDelegate
, which is notified whenever the queue backgroundQueueWillStartOperations(_:)
and when backgroundQueueDidFinishOperations(_:)
.
This is quite handy if you want to show the network activity indicator or save a database or anything else really. The sky is the limit!
Asyncronous Operations
An AsyncOperation
is an easy way to perform asynchronous tasks in an OperationQueue
. It's designed to make it easy to perform long-running tasks on an operation queue regardless of how many times its task needs to jump between threads. Only once everything is done, the AsyncOperation
is removed from the queue.
Say we have an asynchronous function we'd like to execute in the background:
self.loadThingsFromTheInternet(callback: { (result, error) in
//process the result
})
If we wrapped this in an Operation
object, we would have one small problem:
operationQueue.addOperation(BlockOperation({ [weak self] in
//We're on a background thread now; NICE!
self?.loadThingsFromTheInternet(callback: { (result, error) in
//process the result
//who knows in which thread this function returns...
})
//Aaaand... As soon as we call the load function, the operation will already be finished and removed from the queue
//But we haven't finished what we wanted to do!
//And the queue will now start executing its next operation!
//Sigh...
}))
The AsyncOperation
class solves this issue by exposing the operation object itself to its execution block and only changing its isFinished
property once everything is done:
operationQueue.addOperation(AsyncOperation({ [weak self] (op) in
//We're on a background thread now; NICE!
self?.loadThingsFromTheInternet(callback: { (result, error) in
//process the result
//then move to the main thread
onTheMainThread {
//go to the background
inTheBackground {
//do more stuff
//once everything is done, finish
op.finish()
//only now the queue will start working on the next thing
}
}
})
}))
Nice, huh?
Timeouts
There's no way for an AsyncOperation
to know when it's done (hence, we need to call op.finish()
when its work is done). But sometimes, we developers - ahem - forget things.
Thus, in order to cover for the case where op.finish()
may never be called (consequently blocking the OperationQueue
), AsyncOperation
s come with a timeout (defaulting to 10 seconds). After the timeout elapses, the operation is automaticallt finished and removed from the queue.
It may be the case that your AsyncOperation
's workload takes longer than the default timeout. If that's the case, you can define a new timeout like this:
AsyncOperation(timeout: 20, { (op) in
//perform very long task
op.finish()
})
Optionally, you can set the onTimeoutCallback:
when instantiating a new AsyncOperation
to be notified when your operations times out.
Cancelations
As per Apple's documentation, it's always a good idea to check if your operation has been cancelled during the execution of its closure and shortcircuit it prematurely if needed. For example:
AsyncOperation({ (op) in
//do some work
guard !op.isCancelled else { return } //No need to call finish() in this case
//do some more work
})
Uniqueness Policy
The uniqueness policy dictates whether AsyncOperation
s with the same name
should co-exist in a BackgroundQueue
. This is great for deduplicating operations, for example:
@IBAction func refresh(_ sender: UIRefreshControl) {
let op = AsyncOperation(name: "Call to API endpoint /xyz", uniquenessPolicy: .drop) { op in
//make the call to the API
op.finish()
}
OperationQueue.background.addOperation(op)
}
This first time the user activates the refresh control, the operation will be added to the queue as normal, because there are no other operations with the name "Call to API endpoint /xyz"
there yet. But if the user activates the control again before the first call to the API returns, then the .drop
policy will make sure that a second operation is not added to the queue, since there's one operation with that name in there already. If .replace
is set, then the previous operation is cancelled and the new one replaces it.
Neat!
Installation
Cocoapods
pod 'Backgroundable', '~> 1.4'
Then import Backgroundable
where needed.
Carthage
github "BellAppLab/Backgroundable" ~> 1.4
Then import Backgroundable
where needed.
Swift Package Manager
dependencies: [
.package(url: "https://github.com/BellAppLab/Backgroundable", from: "1.4")
]
Then import Backgroundable
where needed.
Git Submodules
cd toYourProjectsFolder
git submodule add -b submodule --name Backgroundable https://github.com/BellAppLab/Backgroundable.git
Then drag the Backgroundable
folder into your Xcode project.
Author
Bell App Lab, [email protected]
Credits
Logo image by Becris from The Noun Project
License
Backgroundable is available under the MIT license. See the LICENSE file for more info.