Working at an agency most of the times means having multiple projects per year. Setting up the development environment over and over again can cost a lot of time. Instead of starting from scratch every time, it makes sense to automate this setup. Even further, by having a standardized flow of creating projects, company wide rules can be applied in order to have a standardized project structure.
There are a lot of tools out there which can be used to create an automation process for Xcode projects. In this post I want to highlight two of those. The first one being cookiecutter and the latter being XcodeGen. I want to demonstrate how these two powerful utilities can be used together to create a fast and convenient way of setting up a Xcode project. If you want to skip all other sections and directly jump to the part where we use these two tool, here is your way.
Set the standards
After deciding to automate the generation of projects we need to think about what standards to set. I will only highlight some external dependencies here. There are some internal implementations of common utilities like networking, logging and accessing UserDefaults
which are specific to our use-cases and therefore not mentioned here.
SwiftLint
For us, the first library which has to be included into the template was SwiftLint. SwiftLint is "a tool to enforce Swift style and conventions". It is a command line tool which scans through your source code and checks if certain rules are followed. There are several built-in ones which can be activated and deactivated. It is even possible to define your own ones. SwiftLint can be hooked into Xcode itself to be run at build time and on our CI/CD tools to let builds fail if certain rules are not followed. It helps us to enforce all developers to write better code.
R.swift
The second library which is a must have in a template is called R.swift. There are a lot of stringly typed APIs in Swift/iOS projects. The most prominent examples are Segues
, and everything related to accessing resources inside the Assets
directory. Stringly typed APIs have the problem that you will not receive any compile time errors. If you typed something wrong your app will most probably only crash at runtime not at compile time. R.swift
comes to the rescue. It will go through all resources and creates enumerations for them to be accessed in code. With that you get strongly typed, compile time checked and autocompleted access to your resources.
Conventional Commits
Besides introducing libraries and common utilities in code, a template should also include some non-code related standards around that - Conventional Commits is one of them. When multiple people working on the same project multiple flavours and workflows are introduced in a common place. And everyone has it's own understanding of writing commit messages. With Conventional Commits
we can introduce a specification for our commit messages to follow certain rules. To have a specific format allows for structured, easy to read messages as well as making them parsable, which allows the automatic creation of changelogs.
Create the template
After deciding what to include into the template it is time to setup everything. At first we need to install the tools to generate our template.
brew install cookiecutter
brew install xcodegen
Use cookiecutter to create files and directories
cookiecutter
is best described by being a search and replace tool for files, their content and directories. It will take a configuration of placeholders, ask for their replacement and then do the work.
Create a directory, which will contain some supporting files and the final directory holding the Xcode template project.
# Create a new directory
mkdir ios-starter
cd ./ios-starter
# Create some supporting files
touch README.md # Readme which will explain the purpose of the template
touch CHANGELOG.md # Changelog for the template itself
touch .gitignore
touch cookiecutter.json # Configuration file for cookiecutter
mkdir hooks # Hooks directory for cookiecutter
mkdir {{cookiecutter.projectName}} # Directory which contains the template project
You may have noticed the strange directory name of our template project. This is a placeholder defined by the cookiecutter.json
file. All placeholders have the format of {{cookiecutter.*}}
. You can define these variables in the configuration file. When we run cookiecutter
it will ask us for all variables defined inside the configuration file and replaces the placeholders with our answers.
Let's have a look inside the configuration file:
{
"projectName": "Example",
"projectDirectory": "{{cookiecutter.projectName|lower|replace(' ', '-')}}-ios",
"teamId": "123456",
"teamName": "Your Team Name",
"companyName": "Your company name",
"bundleIdentifier": "com.example.{{cookiecutter.projectName|lower|replace(' ', '-')}}",
"deploymentTarget": "12.0",
"runCocoaPods": "y",
"runXcodeGen": "y"
}
Since we will have an Xcode template we will ask for common properties like, the project name, the Apple developer team id, bundle identifier and so on. You can define any variable you like. Furthermore it is possible to combine variables to create new ones like it can be seen here with the projectDirectory
and the bundleIdentifier
. These variables now can be used either in directory names or directly in source code files, like for example in an ViewController.swift
:
//
// ViewController.swift
// {{cookiecutter.projectName}}
//
// Created by Automated on {% now 'utc', '%d/%m/%Y' %}.
// Copyright © {% now 'utc', '%Y' %} {{cookiecutter.companyName}}. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
Create your template directory structure
Inside the ./{{cookiecutter.projectDirectory}}
you can now define how your bootstrapped project will look like. We have decided for the following structure:
./{{cookiecutter.projectDirectory}}
├── CHANGELOG.md
├── Podfile
├── README.md
├── commit-msg
├── project.yml
├── {{cookiecutter.projectName}}
│ ├── Assets.xcassets
│ ├── Sources
│ │ ├── AppDelegate.swift
│ │ ├── Base.lproj
│ │ │ └── Main.storyboard
│ │ ├── Extensions
│ │ ├── Helpers
│ │ │ ├── Localization
│ │ │ │ └── Localization.swift
│ │ │ ├── Logging
│ │ │ │ └── Logging.swift
│ │ │ └── UserDefaults
│ │ │ └── UserDefaults.swift
│ │ └── ViewController.swift
│ └── SupportingFiles
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ └── Info.plist
├── {{cookiecutter.projectName}}Tests
│ ├── Info.plist
│ └── {{cookiecutter.projectName}}Tests.swift
└── {{cookiecutter.projectName}}UITests
├── Info.plist
└── {{cookiecutter.projectName}}UITests.swift
At the top level we will find a README
, a CHANGELOG
, a Podfile
(you can use whatever dependency management tool you like), a project.yml
file (for XcodeGen
setup here) and an executable commit-msg
.
Hooks
In the beginning we created a hooks
directory. Here you can place scripts that run before and after the cookiecutter
process runs.
./hooks
├── post_gen_project.sh
└── pre_gen_project.sh
You can even use your cookiecutter
placeholder in those scripts. For example inside the post_gen_project.sh
, where we let XcodeGen
and cocoa-pods
run depending on the variables set when the process has started. Additionally we set up a git repository and move our commit-msg
script to the hook directory of git. This small script ensures that the conventional commit
specification is met. Otherwise we will reject every commit to the repository.
#! /bin/bash
{%- if cookiecutter.runXcodeGen == 'y' %}
xcodegen
{%- endif %}
{%- if cookiecutter.runCocoaPods == 'y' %}
pod install
{%- endif %}
{%- if cookiecutter.runXcodeGen == 'y' %}
xed .
{%- endif %}
git init
mv commit-msg .git/hooks/commit-msg
chmod u+x .git/hooks/commit-msg
printf 'all done - enjoy 🤓'
XcodeGen to create a Xcode project
XcodeGen is a handy tool which creates Xcode project files out of the file structure on your file system in combination with a project.yml
file. We can now combine the cookiecutter
placeholders inside the project.yml
file. With XcodeGen
it is even possible to strip out the .xcodeproj
completely from your versioning system, since everyone can generate it with XcodeGen
. This will solve those frustrating moments were you have to resolve merge conflicts inside the Xcode project files. To get a better understanding about the different variables inside the project.yml
file, I definitely recommend to read through the documentation to get a better understanding of how to write those configuration files.
Let it run !
We have come a long way - from defining the standards, setting up the placeholders to creating the project.yml
files for the Xcode project. Now it is time to run everything.
We can either point cookiecutter
to a directory or any git repository only. Keep in mind that the project will be generated in the directory you are currently in.
cookiecutter <directoryPath>
# or
cookiecutter <git url>
Answer all the questions and after a few seconds you should have a brand new Xcode project set up for you !
Conclusion
We invested quite a lot of time into the setup of the template project. But now we have a standardized structure and workflow to create Xcode projects. If a new needs to be created, only a few questions need to be answered and voilà we can start within seconds without the hustle of thinking how to set up a new project.
If you want to take a look at the setup in code, you can find it on my GitHub account here.