In this blog post, we'll explore an awesome Swift code snippet that demonstrates a lightweight and easy-to-use asynchronous process handler. By leveraging async/await and the Foundation framework, this code enables you to execute processes asynchronously and retrieve their output data effortlessly. Let's dive into the code and discover its powerful features step by step.
Works only inside MacOS app! We don't have access to such capabilities on iOS!
The code
import Foundation
struct ProcessHandler {
let executableURL: URL
let launchPath: String
struct ProcessCommand {
typealias Argument = String
var arguments: [Argument]
}
enum ProcessError: Error {
case failure(code: Int32, reason: String?)
}
@MainActor
func run(process: ProcessCommand) async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global(qos: .userInitiated)
.async {
let task = Process()
let pipe = Pipe()
let errorPipe = Pipe()
task.standardOutput = pipe
task.arguments = process.arguments
task.executableURL = executableURL
task.launchPath = launchPath
task.standardError = errorPipe
do {
try task.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
continuation.resume(returning: data)
} catch {
let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
let errorString = String(data: errorData, encoding: .utf8)
let returnCode = task.terminationStatus
continuation.resume(throwing: ProcessError.failure(code: returnCode, reason: errorString))
}
}
}
}
}
// Example, like Foundation Notification.Name
extension ProcessHandler.ProcessCommand {
static let getSimulators = ProcessHandler.ProcessCommand(arguments: ["xcrun", "simctl", "list", "-j"])
}
// simple usage case
do {
let handler = ProcessHandler(executableURL: URL(fileURLWithPath: "/bin/zsh"), launchPath: "/usr/bin/env")
let data = try await handler.run(process: .getSimulators)
// make something with data!
} catch {
print(error)
}
How it works
- Defining the ProcessHandler Structure: The
ProcessHandler
structure is the heart of the code. It simplifies process handling by encapsulating the executable URL and launch path as properties, making it straightforward to specify the process to be executed. - ProcessCommand Nested Structure: Inside the
ProcessHandler
, we find the nestedProcessCommand
structure. It simplifies the specification of process arguments by providing a clean representation for command-line argument strings. This structure empowers you to define process arguments with ease. - ProcessError Enumeration: The
ProcessError
enumeration ensures seamless handling of process-related errors. It includes a single case,failure
, which allows you to easily access the process return code (code
) and an optional error message (reason
) for convenient debugging and error handling. run(process:)
Method: Therun
method is where the magic happens. With Swift's new concurrency model and theasync
keyword, this method provides a seamless and efficient way to execute processes asynchronously. Thethrows
keyword signifies that it can throw an error if necessary.
Inside therun
method, we create awithCheckedThrowingContinuation
, which acts as a bridge between the callback-based and async/await paradigms. This powerful construct enables us to resume execution and return values or errors once the asynchronous operation completes, making our code more readable and maintainable.
The actual process execution takes place on a global dispatch queue with a quality of service (QoS) set to.userInitiated
. This ensures that the process runs smoothly in the background without affecting the main thread's responsiveness, providing a seamless user experience.
Within the background queue, we create a newProcess
instance and establishPipe
instances for capturing standard output and standard error. By configuring the process with the provided arguments, executable URL, and launch path, we set the stage for a smooth process execution.
Next, we run the process usingtask.run()
. If the execution succeeds, we read the output data from the standard output pipe and resume the continuation with the retrieved data. In case of an error, we read the error data from the standard error pipe, create an error message string, capture the process return code, and resume the continuation with a descriptive error object, ensuring that you can easily handle process failures.- Example Extension: To demonstrate the code's simplicity, we've included an extension showcasing a real-life usage scenario. The
ProcessHandler.ProcessCommand
structure offers a static property,.getSimulators
, which represents a set of arguments for retrieving a list of available simulators usingxcrun simctl list -j
. This extension allows you to easily reuse and adapt the code for various process requirements. - To experience the power of this lightweight process handler, simply create an instance of
ProcessHandler
, providing the executable URL and launch path as parameters. Then, invoke therun
method asynchronously, passing the predefined.getSimulators
process. Await the result, and the output data will be conveniently stored in thedata
constant, ready for further processing according to your needs.
Conclusion
Congratulations! You've just explored an awesome and easy-to-use Swift code snippet for handling asynchronous processes. By leveraging async/await and the Foundation framework, this lightweight implementation simplifies process execution and data retrieval. With clear explanations and code annotations, you're now well-equipped to enhance your projects with efficient process handling capabilities. This code serves as a solid foundation for writing powerful tooling that can significantly aid iOS developers in their day-to-day tasks. Get ready to boost your productivity and streamline your development workflows with this amazing process handling code!