Using Swift Scripts with Xcode

Learn how to run Swift scripts as part of the Xcode build phase, giving you control to configure or validate your app while building your project. By Ehab Amer.

5 (10) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Incrementing the App Version

The next script you’ll create is designed to increment the version number of your app with each build. While you probably won’t ever have cause to increment the version number so frequently, this demonstrates how you could do it.

There are two values in the project’s Info.plist that represent the version:

  • Bundle version: Represented by a number
  • Bundle version string (short): Represented by two or three numbers like 1.12 (Major.Minor) or 1.3.16 (Major.Minor.Patch)

In this script, you’ll increment the bundle version with every build and the short string only when the build is a release build.

Create a new Swift file under the Scripts folder named IncBuildNumber.swift. Add the following to it:

import Foundation

@main
enum IncBuildNumber {
  static func main() {
    // 1
    guard let infoFile = ProcessInfo.processInfo
      .environment["INFOPLIST_FILE"]
    else {
      return
    }
    guard let projectDir = ProcessInfo.processInfo.environment["SRCROOT"] else {
      return
    }
    // 2
    if var dict = NSDictionary(contentsOfFile:
      projectDir + "/" + infoFile) as? [String: Any] {
      guard 
        let currentVersionString = dict["CFBundleShortVersionString"]
          as? String,
        let currentBuildNumberString = dict["CFBundleVersion"] as? String,
        let currentBuildNumber = Int(currentBuildNumberString)
      else {
        return
        }
      // 3
      dict["CFBundleVersion"] = "\(currentBuildNumber + 1)"
      // 4
      if ProcessInfo.processInfo.environment["CONFIGURATION"] == "Release" {
        var versionComponents = currentVersionString
          .components(separatedBy: ".")
        let lastComponent = (Int(versionComponents.last ?? "1") ?? 1)
        versionComponents[versionComponents.endIndex - 1] = 
          "\(lastComponent + 1)"
        dict["CFBundleShortVersionString"] = versionComponents
          .joined(separator: ".")
      }
      // 5
      (dict as NSDictionary).write(
        toFile: projectDir + "/" + infoFile, 
        atomically: true)
    }
  }
}
  1. First, you need the location of the Info.plist file for the current project, which Xcode stored in the environment variable INFOPLIST_FILE, and the path of the project from SRCROOT.
  2. Next, with those values, you have the path of the .plist file. Read the file as a dictionary and fetch the two version values currently stored in it.
  3. Increment the build number.
  4. If the current build configuration is Release, break down the short version string and increment the last digit in it.
  5. Overwrite the .plist file with the dictionary after all the changes.

Go to the project’s Build Phases and add a new run script phase. You can copy the contents of the previous build script tab, but remember to add the new IncBuildNumber.swift file to the Input Files list.

Note: Reordering this script is important because you want to update the version numbers before Xcode reads the values from the plist file.
Note: If you leave the phases with their default name Run Script, it will be confusing if there are several. You can rename them by clicking on the title to make it editable.

Build the project a few times and check Info.plist with each build. You’ll see the Bundle version value change.

Change the Build Configuration of the project’s scheme to Release instead of Debug and build the project again.

Changing the build configuration to release

You’ll see the two values updated in the .plist file.

Version values updated in the info.plist file

Changing the App Icon

The third script you’ll implement changes the AppIcon based on your current build configuration. In most projects, you’d have a different icon set for each configuration, but for this one, you’ll do something a little more sophisticated. You’ll alter the images directly and rewrite them.

In the current Scripts folder, ImageOverlay.swift provides addOverlay(imagePath:text:), which is a function for macOS using AppKit to add a text overlay in the bottom left corner on an existing image file. You’ll use it in this script.

Also, there is Shell.swift. It allows you to execute shell commands from Swift. You’ll learn about this in more detail in the next script.

Create a new Swift file in the same folder named AppIconOverlay.swift and add the following:

import Foundation

@main
enum AppIconOverlay {
  static func main() {
    // 1
    guard
      let srcRoot = ProcessInfo.processInfo.environment["SRCROOT"],
      let appIconName = ProcessInfo.processInfo
        .environment["ASSETCATALOG_COMPILER_APPICON_NAME"],
      let targetName = ProcessInfo.processInfo.environment["TARGET_NAME"]
    else {
      return
    }
    // 2
    let appIconsPath =
      "\(srcRoot)/\(targetName)/Assets.xcassets/\(appIconName).appiconset"
    let assetsPath =
      "\(srcRoot)/\(targetName)/Assets.xcassets/"
    let sourcePath =
      "\(srcRoot)/Scripts/AppIcon.appiconset"

    // 3
    _ = shell("rm -r \(appIconsPath)")
    _ = shell("cp -r \(sourcePath) \(assetsPath)")

    // 4
    guard let images =
      try? FileManager.default.contentsOfDirectory(atPath: appIconsPath)
    else {
      return
    }
    // 5
    let config = ProcessInfo.processInfo.environment["CONFIGURATION"] ?? ""
    // 6
    for imageFile in images {
      if imageFile.hasSuffix(".png") {
        let fileURL = URL(fileURLWithPath: appIconsPath + "/" + imageFile)
        addOverlay(imagePath: fileURL, text: "\(config.prefix(1))")
      }
    }
  }
}
  1. As with the previous script, you read the environment variables. Here, you need the path of the project file, the target name and the name of the AppIcon assets.
  2. Then, you define the paths you’ll use: the path to the AppIcon assets, the path to the assets folder as a whole and the path of the original unmodified assets present in the scripts folder.
  3. Delete the icon assets from the project and copy the unmodified ones from Scripts through shell commands. This is like a reset to the images so you don’t keep adding overlays on top of each other.
  4. Load the list of files present in the AppIcons assets folder so you can modify them one by one.
  5. Fetch the current build configuration.
  6. Loop over the files and, if the file is a PNG image, add an overlay of the first letter of the current configuration on top of it.

Add a new script to the build phases like the previous two and move it to the top — but this time, you want to include the three Swift files, Shell.swift, OverlayLabel.swift and ImageOverlay.swift, directly in the script and the AppIconOverlay.swift file as part of the Input Files. The shell script in the new run phase should be:

xcrun --sdk macosx swiftc \
  -parse-as-library $SCRIPT_INPUT_FILE_0 Scripts/Shell.swift \
  Scripts/OverlayLabel.swift Scripts/ImageOverlay.swift -o CompiledScript
./CompiledScript
Note: Reordering this script is important because you want to change the images before Xcode copies the images to the compiled bundle.

Along with your input file, you’re passing all the needed Swift files for your code to compile successfully. This means you’re not limited to writing one file, and you can create your own reusable code for your scripts.

Build the project once on Debug and once on Release. Observe the changes on the AppIcon.

Simulator screenshots showing the 2 different App Icons

Note: With Xcode 13.0, you’ll have to delete the app from the simulator to see the icon change.