Swift/UnwrapDiscover about Swift, iOS and architecture patterns

Cocoapods script phases

August 10, 2021

A few years ago Cocoapods landed a long waited functionality: script phases.

Back at the time the blog did not exist so with a little delay let's have a look on how using them through two examples: SwiftLint and SwiftGen.

This is an updated version of an article I wrote some time ago in French 🇫🇷

SwiftLint

Let's start by defining a script called "🚨 SwiftLint" right into our Podfile. We will make it execute before code compilation.

target :myApp do
  pod SwiftLint
  pod ...

  script_phase {
    :name => '🚨 SwiftLint',
    :script => '"${PODS_ROOT}/SwiftLint/swiftlint"',
    :execution_position => :before_compile
  }
end

Once we run pod install Cocoapods add the script phase into our xcodeproj build phases. SwiftLint will then be executed by Xcode (or xcodebuild) when compiling.

And... that's all 🤷‍♂️. As we did not define any input nor output files the script phase will get executed every time we compile.

Make sure to surround environment variable with double quotes (")

SwiftGen

target :myApp do
  pod SwiftGen
  pod ...

  script_phase {
    :name => '🛠️ SwiftGen (Generate resources)',
    :script => '"${PODS_ROOT}/SwiftGen/bin/swiftgen" config run --config .swiftgen.yml && touch .swiftgen.yml"',
    :execution_position => :before_compile,
    :input_files => ['.swiftgen.yml'],
    :output_files => ['Generated/Assets.generated.swift', 'Generated/Fonts.generated.swift']
  }
end  

Here we added two new parameters:

  • input_files to define which files, when modified, trigger the script to run while building
  • output_files to define files the script generate

You can also use {input/output}_file_lists to define input/output files

You might wonder why adding .swiftgen.yml as input file and touching it? Script execution is based on both input and output files. If output files exist then script won't run again unless input files changed. So to make our script run every time we have to hack a little bit.

Going deeper

Up until know we defined our script phases in Podfile. While convenient at first it can quickly increase our file length.

To mitigate this issue and make our code more readable let's move our scripts into a dedicated file: Phasesfile (name is arbitrary).

module ScriptPhase
  def self.swiftlint
    {
      :name => '🚨 SwiftLint',
      :script => '"${PODS_ROOT}/SwiftLint/swiftlint"',
      :execution_position => :before_compile
    }
  end

  def self.swiftgen
    {
      :name => '🛠️ SwiftGen (Generate resources)',
      :script => Script.swiftgen,
      :execution_position => :before_compile,
      :input_files => ['.swiftgen.yml'],
      :output_file_lists => ['.swiftgen.outputs.xcfilelist']
    }
  end
end

module Script
  def self.swiftgen
    <<~EOS
      "${PODS_ROOT}/SwiftGen/bin/swiftgen" config run --config .swiftgen.yml
      touch .swiftgen.yml
    EOS
  end
end 

We can now refactor our Podfile:

load 'Phasesfile'

target :myApp do
  script_phase(Phase.swiftlint)
  script_phase(Phase.swiftgen)

  pod SwiftLint
  pod SwiftGen
  pod ...
end

Not only did we improve its readability but we can now also share our Phasesfile with other projects if we want to.

Conclusion

Cocoapods script phases are nothing but simple and handy by giving you the ability to include your custom build steps in your Podfile.

This feature is not yet available in Swift Package Manager but should come as part of SE-303 Extensible Build Tools.

While probably providing more functionality than Cocoapods script phases counterpart it will also be harder to read.