Teabyte

Mobile app development for iOS and Swift

Automate Your Xcode Project Setup

2019-08-10

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
//
//  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.

post_gen_project.sh
#! /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.