Try out the new code from the Swift Package Manager team
Apple has unveiled a new collection of open-source utility code for Swift developers, grown out of its Swift Package Manager project. The collection contains some interesting new data types (OrderedSet
– hurray!), some tools to make command line programs easier to write, and some helpers for common tasks like temporary files and SHA hashing.
Apple describes the code with a big warning that bears repeating before we continue: “This product consists of unsupported, unstable API. These APIs are implementation details of the package manager. Depend on it at your own risk” Still, who could resist having a quick play? Let’s see what we have…
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Start by creating a new Swift project:
cd Desktop
to change to your Desktop directory, if you aren’t there already.mkdir UtilityTest
and cd UtilityTest
to make a directory and change into it.swift package init --type=executable
to generate a new command-line project.Now modify open Package.swift in the UtilityTest directory and modify it to this:
// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "UtilityTest",
dependencies: [
.package(url: "https://github.com/apple/swift-package-manager.git", from: "0.5.0"),
],
targets: [
.target(
name: "UtilityTest",
dependencies: ["SPMUtility"])
]
)
That brings in the Swift Package Manager project, which is where Apple’s new utility code lives for the time being.
Finally, run swift package generate-xcodeproj
to download our lone dependency and generate an Xcode project. When that finishes, open it in Xcode, then open main.swift inside the Sources > UtilityTest folder, and give it these three imports:
import Foundation
import Basic
import SPMUtility
That’s the setup complete, so let’s try it out!
First up, we have a new OrderedSet
type that gives all the performance of sets without their annoying habit of forgetting the order you add things. The code for this might surprise you with its simplicity: it’s a data type that combines an array and a set so that one can be used for storing order while the other is used for look ups.
Here’s how a regular set looks:
let set = Set(["Anna", "Olaf", "Elsa", "Kristoff", "Anna"])
When printed out, that contains ["Elsa", "Kristoff", "Anna", "Olaf"]
– all the order has been lost. Moving over to an ordered set is now trivial:
let set = OrderedSet(["Anna", "Olaf", "Elsa", "Kristoff", "Anna"])
Moving on, there’s a new SortedArray
type that ensures new data is always insert into the correct alphabetical position. For example:
var array = SortedArray<String>()
array.insert("Hans")
array.insert("Kristoff")
array.insert("Anna")
array.insert("Sven")
When that code finishes, the array will contain Anna, Hans, Kristoff, Sven. (Some trivia for you: the names Hans, Kristoff, Anna, and Sven were chosen to honor the Danish author Hans Christian Andersen, who wrote the book Frozen was based on – the Snow Queen.)
Obviously SortedArray
might cause a serious performance slowdown compared to using a regular array and sorting only once, so use it wisely.
Writing command line apps has its own set of complexities, particularly if you want them to be cross-platform across macOS and Linux. Fortunately, Apple’s new utility code takes away a lot of the pain of dealing with POSIX compliance, wrapping it all up in some great little helpers.
For example, a common task is to run another process, wait for its result, then take some action based on that result. Thanks to the new Process
type, this is surprisingly easy – here’s some code to list your Applications directory:
do {
let process = Process(arguments: ["ls", "-l", "/Applications"])
try process.launch()
let result = try process.waitUntilExit()
let output = try result.utf8Output()
print(output)
} catch {
print("Error while reading the Applications directory")
}
You can also read errors (stderr
), exit status, and more.
Another brilliantly simple helper is TemporaryFile
: it’s a class that instantiates a new file you can write to freely, but as soon as your file object goes out of scope – e.g. your method returns or your loop ends – it gets deleted. (Note: it’s down to the operating system when the file actually gets deleted.)
In its most simple form here’s how TemporaryFile
works:
let file = try TemporaryFile()
let data = Data("Hello, world!".utf8)
file.fileHandle.write(data)
You can also add your own prefix or suffix to the filename if you want, like this:
let file = try TemporaryFile(prefix: "hws")
And if you want to know where the file is being stored, you can read its path like this:
print(file.path.asString)
Another great little helper is Version
, which is perfect for handling semver checks in your code. You can create a new Version
instance from major, minor, and patch numbers, then compare instances, like this:
let currentVersion = Version(1, 5, 0)
let newestVersion = Version(2, 0, 1)
if currentVersion < newestVersion {
print("Upgrade is available!")
}
You can also create versions from strings in the format “X.Y.Z-prereleasedata”, but this returns an optional:
if let version = Version(string: "2.0.0-alpha4") {
print("You're running v\(version.major)")
}
Finally, there’s a new SHA256
class for computing the SHA-2 hash of strings. While I wish it were a simple String
extension, that’s just because I’m lazy – here’s how to use it:
let sha = SHA256("Time doesn't exist; clocks exist.")
print(sha.digestString())
There are a few common things any command-line app wants to do, and Apple’s utilities have gone a long way to making them easier.
First, there’s a new ArgumentParser
class that’s responsible for reading arguments from the command-line and making them available to your program. You start by creating an instance of the class, like this:
let parser = ArgumentParser(commandName: "swearcheck", usage: "filename [--input naughty_words.txt]", overview: "Swearcheck checks a file of code for swearing, because let's face it: you were angry coding last night.")
That provides ArgumentParser
with some basic information to show if the user runs the program using -h
or --help
. You can then go ahead and add positional or optional arguments, like this:
let input = parser.add(option: "--input", shortName: "-i", kind: String.self, usage: "A filename containing naughty words in your language", completion: .filename)
let filename = parser.add(positional: "filename", kind: String.self)
That first one will look for both “--input filename.txt” and “-i filename.txt”, whereas the second one will look just for a loose value to be read.
Once you’ve configured your parser you can go ahead and use it. Most of the time you’ll want to use the input from the command line (dropping the first item, because that contains the program name), like this:
let args = Array(CommandLine.arguments.dropFirst())
let result = try parser.parse(args)
Finally, you can read individual arguments by calling get()
with the argument you created, like this:
if let wordsFilename = result.get(input) {
print("Using \(wordsFilename) for the list of naughty words to check for.")
} else {
print("Using 'JavaScript' in lieu of a list of naughty words.")
}
ArgumentParser
will throw errors if values were missing or incorrect, so you should really wrap the whole thing in a do
/catch
block, like this:
do {
let parser = ArgumentParser(commandName: "swearcheck", usage: "filename [--input naughty_words.txt]", overview: "Swearcheck checks a file of code for swearing, because let's face it: you were angry coding last night.")
let input = parser.add(option: "--input", shortName: "-i", kind: String.self, usage: "A filename containing naughty words in your language", completion: .filename)
let filename = parser.add(positional: "filename", kind: String.self)
let args = Array(CommandLine.arguments.dropFirst())
let result = try parser.parse(args)
guard let codeFile = result.get(filename) else {
throw ArgumentParserError.expectedArguments(parser, ["filename"])
}
print("Scanning \(codeFile) for sweary code…")
if let wordsFilename = result.get(input) {
print("Using \(wordsFilename) for the list of naughty words to check for.")
} else {
print("Using 'JavaScript' in lieu of a list of naughty words.")
}
} catch ArgumentParserError.expectedValue(let value) {
print("Missing value for argument \(value).")
} catch ArgumentParserError.expectedArguments(let parser, let stringArray) {
print("Missing arguments: \(stringArray.joined()).")
} catch {
print(error.localizedDescription)
}
Moving on, the TerminalController
class gives you more control over the terminal window, as you might have guessed from its name. This includes things like printing text in color, clearing the current line, and measuring the terminal width in characters.
To demonstrate this, we could write a simple text string user super cool colors, and by “super cool” I really mean “the kind of thing you wrote when you were 14”:
let colors: [TerminalController.Color] = [.red, .green, .yellow, .cyan, .white]
if let stdout = stdoutStream as? LocalFileOutputByteStream {
let tc = TerminalController(stream: stdout)
for (index, letter) in "Hello, world!".enumerated() {
tc?.write(String(letter), inColor: colors[index % colors.count], bold: true)
}
}
Beautiful.
This has been a pretty long article, and we’ve covered probably only about 25% of what the new library offers. Hopefully you can see there’s a heck of a lot here, and this was just me covering the highlights. That being said, it feels very much like some of these things could easily go on to Swift Evolution if Apple were so inclined – OrderedSet
might be a quick win, for example!
Before you starting adding this code to big projects, remember Apple’s warning: it's unstable and unsupported, at least for now. While it’s interesting to see their direction of travel, and always great to see new open source code, keep in mind that it can change dramatically at any point in the future!
SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.