Since the introduction of Swift Package Manager (SPM) support for iOS projects, I’ve been avoiding the use of cocoapods in my newer projects. This works great for incorporating packages into the app, however there is very little info out there on using SPM to add executable dependencies that are not included in the app itself. A key example of this is build tools that use a
Run script Build Phase (eg. SwiftFormat, R.swift, SwiftLint)
Today I’ll provide a walkthrough of using SPM to add such tools to your project.
In order to define our build tools using SPM we’re going to:
- Define what tools we need available
- Run these tools in a
Run Scriptbuild phase from our xcodeproj
Setting up our dependencies
First we’re going to create a folder called
BuildTools in the same directory as
Next, in the new
BuildTools folder, create a
Package.swift file with the following content:
// swift-tools-version:5.1 import PackageDescription let package = Package( name: "BuildTools", platforms: [.macOS(.v10_11)], dependencies: [ //Define any tools you want available from your build phases //Here's an example with SwiftFormat .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.41.2"), ] )
Run Script Build phase
For many build tools, the easiest use case is to run them automatically on building your main app target.
Alternatively, if you’d rather choose when to run the scripts, you could define this in a test target (which you can build whenever you want the script to run)
- Click on your project in the file list, choose your target under
TARGETS, click the
- Add a new
Run Script Phaseby clicking on the plus icon in the top-left of the tab
- Move the
Run Script Phaseto above your
Compile Sourcesphase (Note: depending on the nature of the tool, you might want a different order)
- Expand the run script and paste the following:
cd BuildTools #Temporarily uncomment the following line to update your packages (based on the versions defined in BuildTools/Package.swift) #swift package update #Run your build tool - using swiftformat as an example swift run -c release swiftformat "$SRCROOT"
All done! SwiftFormat will now run whenever you build the target (keeping your source code nice and neat!)
Worked example for R.swift
R.swift is a helpful codegen tool that generates strong typed resource accessors. Using these generated accessors means that the existence of your assets is checked at compile time, and you get auto-completion!
Firstly, we would add R.swift to our
// swift-tools-version:5.1 import PackageDescription let package = Package( name: "BuildTools", platforms: [.macOS(.v10_11)], dependencies: [ .package(url: "https://github.com/mac-cain13/R.swift", from: "5.1.0"), ] )
Next we would add a
Run script build phase, dragging it before ‘Compile Sources’:
cd BuildTools swift run -c release rswift generate "$SRCROOT/R.generated.swift"
R.Swift uses Input / Output files so we’ll add these to the run script:
Now build the target, and the
R.generated.swift file will be created.
Next add this file to your Xcode Project. Make sure to untick ‘copy if needed’ – this will ensure the project points to the original file (which will get automatically updated)
The generated file (R.generated.swift) refers to types in a module called
R.Swift.Library, so we’ll add this to our target:
- Go to
File -> Swift Packages -> Add Package Dependency
- Paste the url of the ‘R.swift.Library‘ repository:
- Select version: ‘up to next major’ and press Next
- Tick any targets that you’ll use
That’s it! 🎉 You’ve set up R.Swift, and the generated file will be automatically updated when you build your target.
I am quite satisfied with this approach for the following reasons:
- Your build tool dependencies are defined in a
Package.swiftfile and this can be checked into source control.
- Other than the Run Script build phase, this requires zero changes to your xcodeproj
- The tools will automatically be downloaded when someone builds the project the first time (no installation required). It also ensures the correct version is used.
The very first build will take a while as the dependency is downloaded and built, however the
swift run command intelligently reuses the build unless a new version is available and
swift package update is called.
Please comment if you have any suggestions or feedback 😃