Automate setting up Xcode
In this post, I will show how to automate setting up Xcode using Homebrew
and
Fastlane
in a way that is easy to extend if you need to automate more later on.
Why automate?
I (and many with me) prefer to automate as many tasks as possible, to reduce the amount of repetitive manual work, reduce the risk of human error and to increase the overall reliability of a certain process.
For a development team, automation can also be used to streamline the setup of a
certain developer environment and make it easy to follow shared conventions. For
instance, swiftlint
can help to enforce code conventions, code snippets can be
used to generate comments and code blocks etc.
External dependencies
When you have external dependencies for building and runing a project, you should consider using dependency and package managers to setup your dependencies with a single command.
To achieve this, we can use tools like Homebrew and Fastlane and compose them in a way that makes the setup process simple and painless.
For instance, you can add a Brewfile
to your project root and add all required
tools to it:
brew "carthage"
brew "swiftgen"
brew "swiftlint"
Your can now install all these tools by typing a single command in the Terminal:
brew bundle
Although homebrew
is a standard tool, you can always make this setup even more
accessible by adding it to Fastlane
. Just add this lane to your Fastfile
:
desc "Setup Xcode"
lane :setup_xcode do |options|
sh "cd .. && brew bundle"
end
Now, you just have to type fastlane setup_xcode
to install all tools. It may
seem like a no-win for now, but the nice thing with this approach is that it
can easily be extended to handle even more tasks.
Linting
Linting is a way to automatically check your code for programmatic and stylistic errors. To do this, you use a code analyzer tool called a linter.
swiftlint
is a great linter for Swift. It triggers warnings and errors if the
code doesn’t follow certain conventions. The standard setup is good, but you can
customize it by adding a .swiftlint.yml
to the project root, in which you can
ignore rules, tweak them to fit your style and add new ones.
When you read the swiftlint readme, it suggests you to add a Run Script Phase
to your target, that looks like this:
if which swiftlint >/dev/null; then
swiftlint
else
echo "SwiftLint does not exist, download from https://github.com/realm/SwiftLint"
fi
However, since swiftlint is such a critical tool, I prefer to have it mandatory. I therefore skip the if/else check and just have:
swiftlint
This means that the app now fails to build whenever swiftlint isn’t available, which means that it has gone from being an optional to a required tool.
If you build packages with Swift Package Manager, you will not have a build phase where you can inject swiftlint. I then inject swiftlint into certain Fastlane lanes, so that linting is done in critical processes, like making new versions of my open-source projects.
Xcode snippets
Xcode snippets let you generate text that you type often. For instance, I use it
to generate MARK
blocks, extension bodies, test suite imports, test templates
etc. They save me a lot of time and makes my code look the same across the entire
code base, with no extra effort.
For instance, I have a snippet that creates a “plain” // MARK -
statement, and
have it defined in a file named mark_plain.codesnippet
:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDECodeSnippetCompletionPrefix</key>
<string>mark_plain</string>
<key>IDECodeSnippetCompletionScopes</key>
<array>
<string>All</string>
</array>
<key>IDECodeSnippetContents</key>
<string>// MARK: - </string>
<key>IDECodeSnippetIdentifier</key>
<string>6A7BC3C9-32A4-4EE8-A8DC-7848BC0E40F3</string>
<key>IDECodeSnippetLanguage</key>
<string>Xcode.SourceCodeLanguage.Swift</string>
<key>IDECodeSnippetTitle</key>
<string>Mark</string>
<key>IDECodeSnippetUserSnippet</key>
<true/>
<key>IDECodeSnippetVersion</key>
<integer>2</integer>
</dict>
</plist>
Since snippets are so powerful, I decided to automate how my team shares these snippets. It was very easy, since snippets are just text files. It therefore basically just involved setting up a shared folder with snippets files and writing a script that copies these files to the correct place.
For my personal projects, I just added a snippet folder to a setup project that I have made public here, then created a script that copies these files to the correct place. It looks like this:
#!/bin/bash
path_src=Snippets
path_dst=~/Library/Developer/Xcode/UserData/CodeSnippets
mkdir $path_dst
for file in $path_src/*.codesnippet; do
cp $file $path_dst/$(basename $file)
done
At work, where the team works on multiple projects that share conventions, I
added a snippet folder to a private repo, then added general and work-specific
snippets to it. After that, I extended setup_xcode
to copy all these code
snippets to their correct place, as such:
desc "Setup Xcode"
lane :setup_xcode do |options|
setup_xcode_tools
setup_xcode_snippets
end
desc "Setup Xcode Tools"
lane :setup_xcode_tools do |options|
sh "cd .. && brew bundle"
end
desc "Setup Xcode snippets"
lane :setup_xcode_snippets do |options|
snippets_path = File.expand_path('../Snippets/*.codesnippet')
snippets_paths = [snippets_path].flatten
snippets = snippets_paths.map { |f| f.include?("*") ? Dir.glob(f) : f }.flatten
target_path = File.expand_path('~/Library/Developer/Xcode/UserData/CodeSnippets')
FileUtils.cp_r(snippets, target_path, remove_destination: true)
end
Running fastlane setup_xcode
will now run brew bundle
AND copy code snippets.
This means that we now have a way to setup Xcode, that we can easily extend with
more tasks whenever we need.
Conclusion
Using shell scripts, dependency managers and Fastlane is a simple and convenient
way to setup Xcode with a single command, which can help you standardize things
in your personal or work environment. Tools like swiftlint
make it easy to enforce
conventions, while code snippets can generate code and comments that should follow
a desired format.
Feel free to check out my setup script for some examples, scripts and code snippets.