Alexito's World

A world of coding 💻, by Alejandro Martinez

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.

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.

Scripts with async operations

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.

If you liked this article please consider supporting me