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 3 of 4 of this article. Click here to view the first page.

Linting Your Project

In your previous scripts, you automated some operations on the resources used by the project. In this script, you’ll run a validation on the project itself using SwiftLint and interrupt the build process as a whole based on a remote configuration.

If you’re not familiar with SwiftLint, it’s a command-line tool that allows you to validate your Swift code against a set of rules to make sure it’s organized properly. It helps developers to quickly spot any formatting issues in their code and always keep the code organized and visually clean.

You’ll need to install SwiftLint on your machine before you start working on this script. The GitHub page provides different ways to install it. It’s best not to install it as a pod in the project, but instead to install it directly on the machine.

Once you finish installing it, open a terminal window and navigate through the command line to the project’s folder. Then, run the following command:

swiftlint --config com.raywenderlich.swiftlint.yml

You’ll see the result on the terminal with this message:

......ContentView.swift:42:1: warning: Line Length Violation: Line should be 120 characters or less: currently 185 characters (line_length)
Done linting! Found 1 violation, 0 serious in 2 files.
Note: com.raywenderlich.swiftlint.yml is a SwiftLint configuration file to configure which rules to be used and which to be ignored. Feel free to go through the tool’s
documentation
to learn more how to configure it.

This lint violation is intentional. Your script will run the same operation directly in the build process and check the number of violations found. If it doesn’t pass a certain threshold, your build process will continue. If it’s exceeded, the build will stop with an error.

Create a new Swift file in the Scripts folder and name it LintingWithConfig.swift. Add the following in the new file:

import Foundation
import Combine

@main
enum LintingWithConfig {
  static func main() {
    startLinting(allowedWarnings: 1)
  }

  static func startLinting(allowedWarnings: Int = 0) {
  }
}

The code adds the standard main() that you saw in all the previous scripts and is only calling startLinting(allowedWarnings:). You’ll do the first set of work on this method.

Utilizing the Shell

As mentioned earlier, Shell.swift provides a function to execute shell commands from Swift and returns the result of this command as a string.

So, to run the same command you just executed in a shell, all you need to do is add the following line in startLinting(allowedWarnings:):

let lintResult = shell("swiftlint --config com.raywenderlich.swiftlint.yml")
print(lintResult)

Add this new script to your build phases in a new run script phase and reorder it to the top. Remember to add the Swift file to the input files list, or you could hard code the file directly. You also need to compile Shell.swift with it.

xcrun --sdk macosx swiftc \
  -parse-as-library $SCRIPT_INPUT_FILE_0 Scripts/Shell.swift -o CompiledScript
./CompiledScript

Build and open the log to see its output.

SwiftLint result printed by the run script phase that you just added

Controlling the Exit

You received the result of the command as a string. Add the following to the end of startLinting(allowedWarnings:):

var logResult = lintResult
  .components(separatedBy: "Done linting!").last ?? "Found 0"
logResult = logResult.trimmingCharacters(in: CharacterSet(charactersIn: " "))
  .components(separatedBy: " ")[1]
let foundViolations = Int(logResult) ?? 0

The first line separates the result by the text “Done linting!”, since you’re only interested in the part after you only take the last part of the array. To get the number of violations, the code separates the last part into single words and takes only the second word, which contains the number of violations found.

Next, you want to compare the found violations against the allowed number.

Add the following:

if foundViolations > allowedWarnings {
  print("""
    Error: Violations allowed exceed limit. Limit is \(allowedWarnings) \
    violations, Found \(foundViolations)!
    """)
  exit(1)
}

exit(0)

If the number exceeds the allowed, you print an informative message then exit the execution with an error.

exit(_:) allows you to terminate an execution. Passing any value other than zero means that there was an error and that is why the execution was terminated. Passing zero means that the operation finished everything required and the execution ended normally. In situations when you’re using scripts within scripts, you’ll use those numbers to identify one error from the other.

In main(), change the value sent to startLinting(allowedWarnings:) to 0 and build.

Build failed when the allowed violations is reduced to 0

As expected, your build process stopped with an error.

Loading Remote Configuration

You don’t want to hard code the allowed violations in your script. Ideally, you never want to have any violations and the ideal number should be zero. SwiftLint also provides that. But in larger projects that have a large team and a complex CI/CD pipeline, keeping this number as a strict zero would be inefficient. Allowing yourself and your team some flexibility would be very nice. A simple example for this is when you want to apply a new linting rule that would execute on thousands of lines of old code and you don’t want to update all of it in one go. You’ll want to take it one step at a time while keeping everything else under control.

For the next part, you’ll load the allowed violations limit from an external file in an asynchronous request before you execute the SwiftLint command.

At the end of the file, add this structure:

struct LintConfig: Codable {
  let allowedWarnings: Int
}

Replace the content of main() with the following:

var cancelHandler: AnyCancellable?
let group = DispatchGroup()
// 1
guard let projectDir = ProcessInfo.processInfo.environment["SRCROOT"] else {
  return
}

let configURL = URL(fileURLWithPath: "\(projectDir)/AllowedWarnings.json")
// 2
let publisher = URLSession.shared.dataTaskPublisher(for: configURL)
let configPublisher = publisher
  .map(\.data)
  .decode(type: LintConfig.self, decoder: JSONDecoder())
  .eraseToAnyPublisher()
// 3
group.enter()
cancelHandler = configPublisher.sink { completion in
  // 4
  switch completion {
  case .failure(let error):
    print("\(error)")
    group.leave()
    exit(1)
  case .finished:
    print("Linting Config Loaded")
  }
} receiveValue: { value in
  // 5
  startLinting(allowedWarnings: value.allowedWarnings)
}
// 6
group.wait()
cancelHandler?.cancel()

Here’s what this does:

  1. Fetch the path of the project to build the path of the file that has the allowed violations limit. Normally, you’d want to have this file on a remote server, but for this tutorial, you’ll treat this file as the remote location.
  2. Create a combine publisher to load the contents of the file and map this publisher to the LintConfig structure you defined in the previous step.
  3. Call enter() on the DispatchGroup object you defined, then fetch the value from the publisher.
  4. If the publisher failed to provide the value for any reason, exit the execution with an error to interrupt the build process.
  5. Use the value received by the publisher to call startLinting(allowedWarnings:).
  6. Call wait() on the DispatchGroup object to force the execution to wait.

Using DispatchGroup is very important here since combine is calling the request asynchronously. Without it, your script will not wait for the publisher to receive any value and will just finish execution before running your SwiftLint step. Calling group.leave() when the publisher receives the data isn’t needed since startLinting(allowedWarnings:) calls exit(0) at the end.

Build and open the log. Your build will succeed and will show the same info as when you hard-coded the limit through code.

From Finder, open the file AllowedWarnings.json and change its contents to:

{"allowedWarnings":0}

Build again. As expected, the build will fail because the config file doesn’t allow any violations.