Swift Scripting: Automatic Version Increments in Xcode

Posted 7/20/2017.

Goal: Automate version increments for a Xcode project using a Swift script.

Almsot a year ago, I wrote a tutorial on how to automate Xcode project version increments with a shell script. You can find that here. I realized recently, why am I using a bash script when I could do the same thing in Swift?

In this tutorial we'll increment the version number of a Xcode single view application using a Swift script. We'll cover how to make the script executable, accept command line input, and execute shell commands from our Swift file.

First we need to create our single view application in Xcode. I named mine "ScriptTest" and put it in my Documents folder. Next, open Terminal and navigate to where you just put your new Xcode project. In your text editor of choice, create a new file called "increment-version.swift" and add the following line:

#!/usr/bin/env swift

Great, we have our empty Swift script. Now let's make it executable. Type the following into Terminal:

chmod +x increment-version.swift

"chmod" is the command for modifying file and directory access permissions. "+x" makes the file executable.

Our goal is to increment the version number of our application in its Info.plist file. Because iOS apps typically use semantic versioning (major version, minor version, patch e.g. 2.1.0), it's not as simple as incrementing one number.

The first step is to ask the user of the script for the new version that will be written to Info.plist. To do this we'll call the readLine() function. Add the following to your script:

print("Enter new version number...")

if let newVersion = readLine() {
    print("Updated to \(newVersion)")
}

You can now run the script in terminal and type in a version number at the prompt.

./increment-version.swift

Note that readLine() returns an optional, so we need to unwrap it.

Next we'll use the defaults command to write to Info.plist. To do this we'll need to execute a terminal command from our Swift file using a Process. Add the following at the top of the file below #!/usr/bin/env swift:

import Foundation

@discardableResult
func executeCommand(_ args: String...) -> Int32 {
    let task = Process()
    task.launchPath = "/usr/bin/env"
    task.arguments = args
    task.launch()
    task.waitUntilExit()
    return task.terminationStatus
}

Note that we need to import Foundation, which includes Process. We also declare that we may not be interested in the output by specifying @discardableResult.

This function accepts a variadic parameter of type String to specify the command's arguments. To try it out, we'll run the ls -la command to list the contents of the current directory. Update the code as follows, save, and run:

if let newVersion = readLine() {
    print("Updated to \(newVersion)")
    executeCommand("ls", "-la")
}

Now let's replace ls -la with the command we actually want to run that will update our Info.plist. First, we'll need the path of the current directory so we can get the path to Info.plist. Let's add a constant for this path at the top of the file:

let currentDirectoryPath = FileManager.default.currentDirectoryPath

If you've been following along, your Info.plist file will be in the "ScriptTest" directory. Let's add this path right below currentDirectoryPath.

let sourceDirectoryPath = "\(currentDirectoryPath)/ScriptTest"

Now that we have the paths, we're ready to use the defaults command to write the new version to the plist. This will create a binary file that we then need to convert using plutil. Add the following below the executeCommand function:

func updateInfoPlist(withVersion version: String) {
    let filenamePath = "\(sourceDirectoryPath)/Info"
    let extensionPath = "\(filenamePath).plist"
    executeCommand("defaults", "write", filenamePath, "CFBundleShortVersionString", version)
    executeCommand("plutil", "-convert", "xml1", extensionPath)
}

The last step is to call this function in our script. Replace the code at the bottom of the file with the following:

if let newVersion = readLine() {
    updateInfoPlist(withVersion: newVersion)
    print("Updated to \(newVersion)")
}

And that's it. We've replaced our bash script with one written entirely in Swift! Here's the full script:

#!/usr/bin/env swift

import Foundation

let currentDirectoryPath = FileManager.default.currentDirectoryPath
let sourceDirectoryPath = "\(currentDirectoryPath)/ScriptTest"

@discardableResult
func executeCommand(_ args: String...) -> Int32 {
    let task = Process()
    task.launchPath = "/usr/bin/env"
    task.arguments = args
    task.launch()
    task.waitUntilExit()
    return task.terminationStatus
}

func updateInfoPlist(withVersion version: String) {
    let filenamePath = "\(sourceDirectoryPath)/Info"
    let extensionPath = "\(filenamePath).plist"
    executeCommand("defaults", "write", filenamePath, "CFBundleShortVersionString", version)
    executeCommand("plutil", "-convert", "xml1", extensionPath)
}

print("Enter new version number...")

if let newVersion = readLine() {
    updateInfoPlist(withVersion: newVersion)
    print("Updated to \(newVersion)")
}