A RunLoop for your Swift script
In the past few days the Swift community has been awoken again with the promise of a better way of using Swift for scripting, all thanks to swift-sh.
I made a new thing, Swift-scripting with dependencies, no additional files, just your script: https://t.co/JVRbqVkWm2 pic.twitter.com/XOOE77b2oM
— Max Howell (@mxcl) 12 de enero de 2019
As you may remember from other posts I’ve always been keeping an eye on the state of improvements of Swift for scripting. I’ve recently been using Marathon which is a great tool that does everything I need. I think swift-sh has made a big splash in the community because, once installed, feels like part of the swift compiler: $ swift sh foo.swift
But today’s tip applies to Swift scripting no matter the tool you use.
Unless your script is really simple and just entails some synchronous manipulation of data, it’s likely that you’ve found yourself with the impossibility of running asynchronous code.
The usual case is when you want to use some networking API to fetch some data and manipulate it. When you do this in an application environment it just works, but if you try it in a script you will be really disappointed.
import Foundation
URLSession.shared.dataTask(with: url, completionHandler: { _, response, _ in
print(response)
}).resume()
If you run a script with this code you will see how there is no output at all.
If you think about it that’s expected: you’re performing an asynchronous operation but by the time that finished the script already reached the last line and terminated itself.
RunLoop to the rescue
When you are doing this in, for example, an iOS application it works because the asynchronous operation can be awaited thanks to the runloop. The RunLoop is an infinite loop that takes care of handling inputs and interruptions to your program, it keeps it alive indefinitely so asynchronous operations can be finalised. This is what happens when you use UIApplicationMain(_:_:_:_:)
on iOS or NSApplicationMain(_:_:)
on macOS.
For our script to work, we need to use a RunLoop so the network request has a chance to finish.
You could try to use the platform specific “ApplicationMain” methods, but it’s much better to use the RunLoop API directly. It’s quite simple, you just need to add the following line at the end of your script.
RunLoop.main.run()
With this addition you will see the output of the network request printed. Mission accomplished!
But wait, now the script doesn’t terminate.
Exit
Now our script can run asynchronous operations but it hangs for ever, it behaves like if we had an infinite loop at the end of the file, because that’s what we have. We need to manually terminate the script ourselves.
For that we can use the exit()
function. This function accepts an integer parameter that indicates the exit status of the application: exit(EXIT_SUCCESS)
or exit(EXIT_FAILURE)
. We can use this to indicate to the operative system if the script succeeded or not. This may seem useless if you just run the script manually, but I always recommend using it properly as it becomes really useful if eventually you use this script as part of a bigger system.
With this our script will work as intended, it will have a chance to perform the network request and when the work is done it will finish the script.
import Foundation
let url = ...
URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
print(response)
exit(EXIT_SUCCESS)
}).resume()
RunLoop.main.run()
Using GCD
As an alternative to RunLoop.main.run()
you can use dispatchMain()
, a GCD API that has the same effects.
Conclusion
If you’re starting to write scripts in Swift it’s likely that you want to fetch some data from the network. To accomplish that you will need a RunLoop and, luckily for us, Swift’s Foundation makes it really easy.