An operation runs an asynchronous task which starts with a request and ends with either success or failure. At high level an operation has the same interface across all Amplify platforms. Some operations are quick, like getting a pre-signed URL because the size of the response is very small. Other operations like uploading a large file will take longer and offer progress updates so that the UI can show the user the current status. All operations can also be cancelled which may be necessary for various purposes.
A basic operation is created with a Request
and a result listener which is a closure which will receive a Result
which will either contain the value for Success
or Failure
. Let’s see how we would run an operation named FastOperation
. It will take in a Request
and a closure to listen for the Result
. See the code below.
let request = FastOperationRequest(numbers: [1, 2, 3])
let operation = FastOperation(request: request) { result in
switch result {
case .success(let result):
print("Result: \(result.value)")
case .failure(let error):
print("Result: \(error)")
}
}
operation.start()
A request is created and the initializer takes in a series of numbers. The operation is created with the request and a closure is provided to listen for the result. The Result
type is an enum to a switch statement can be used to access the value for Success
or Failure
.
For operations which take a longer time to run there is another listener which can provide updates as the work is in process. It is used as the InProcess
generic type which is often the Progress
type built into the platform which can provide the total unit count with the value for completed unit count being updated while work is progressing. The percentage can be collected from Progress
with the fractionCompleted
property.
Below is code which works with LongOperation
which uses a LongOperationRequest
and ends with LongOperationResult
.
let request = LongOperationRequest(steps: 10, delay: 0.1)
let operation = LongOperation(request: request,
progressListener: { progress in
let percent = Int(progress.fractionCompleted * 100)
print("Progress: \(percent)")
}, resultListener: { result in
switch result {
case .success:
print("Result: Success")
case .failure(let error):
print("Result: \(error)")
}
})
Notice that this operation includes a progress listener which uses the Progress
type. As the work progresses this listener will be executed every time the value for completed unit count is updated. The Request
used by this operation specifies the number of steps and delay for each step.
This code includes 10 steps with a short delay. The progress listener is called multiple times and the print statement shows the percent of completion. Eventually the result listener will be called and will be printed as well. These progress updates can be used to update UI or to log this activity.
For a long running operation which could be using resources like battery or network resources it may be helpful to cancel operations if the result is no longer needed. A user may be scrolling through a list which shows images and these images are being provided by an operation in the Storage category. If the row which would show an image is scrolled off the screen the operation which is requesting the image can be cancelled. Inside the operation is a network request which can be accessed with a task. This task can be cancelled to stop the request and release resources. These resources could be used for other network requests which are pending.
With previous long operation we can update the progress listener to cancel the request once it reaches 50% completed with this revised code below in a Swift Playground.
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
func die() {
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
print("End.")
PlaygroundPage.current.finishExecution()
}
}
let cancelEnabled = true
// pre-declare cancel closure to define once operation is defined
var cancel: (() -> Void)?
let request = LongOperationRequest(steps: 10, delay: 0.1)
let operation = LongOperation(request: request,
progressListener: { progress in
let percent = Int(progress.fractionCompleted * 100)
print("Progress: \(percent)")
if cancelEnabled && percent >= 50 {
// cancel halfway through the steps
cancel?()
die()
}
}, resultListener: { result in
switch result {
case .success:
print("Result: Success")
case .failure(let error):
print("Result: \(error)")
}
die()
})
cancel = {
operation.cancel()
}
When cancelEnabled
is set to true the operation will be cancelled. After a short delay the playground page will finish execution. At the start of every step of the operation there is a check to see if the operation has been cancelled which prevents it from proceeding. Instead it finishes without calling the result listener.
There are many supported operations in the Amplify iOS library across the categories. While operations work in the same way there are differences which can be handled using generic types. The actual type for Request
, Success
and Failure
are defined by each implementation of an operation. Each unique request can support any properties which are needed along with the initializer. Every Success
type can be any type, such as Int or even a custom struct. The Failure
type should conform to AmplifyError
. When working with a specific operation these generic types will be known so the necessary values can be provided for the request when it is initialized and the listeners can receive the result that is expected.
There is also the InProcess
generic type which is often the Progress
type. For the operations which support it, the listener passes an instance which can be used for updates which are in process with the operation.