iOS project template
This repository contains a template for iOS projects with a framework-oriented architecture approach, preconfigured fastlane lanes, Travis CI jobs and Github integrations of Codecov, HoundCI for SwiftLint and Danger. It provides a starting point for new projects which can be immediately distributed via HockeyApp and Testflight.
This template has three goals:
- It can easily be configured.
- It will contain all the necessary files to build and distribute the app.
- The different parts work independently.
Contact
Follow me on Twitter: @_messeb
Contact me on LinkedIn: @messingfeld
Table of Contents
Introduction
To set up new iOS projects with a ci/cd pipeline is always a little mess. Everyone gives input, but usually, someone of the team members has to care about all the following steps:
- Setup a build server or build service.
- Manage the internal distribution of the app to the product owner and testers.
- Providing the signing certificates and provisioning profiles.
- Configure signing for different release stages.
- Creating ci/cd pipelines for pull request and distribution.
- Add code quality definitions for pull requests, like linting and test coverage rules.
The repository contains an example solutions for all of the points. For every step, it includes one solution.
Xcode Project
The iOS template consists of an Xcode workspace file (iOSProject.xcworkspace) with the project for the app (iOSProject.xcodeproj) and a framework project (Core.xcodeproj), named Core. The structure is:
Project structure
The app project is separated into three main folders: Scenes, Resources, and Configurations. It's only an alternative structure to the default Xcode project.
Scenes: Should be the folder, where the different scenes or modules of the app are placed. The concrete structure depends on the chosen app architecture.
Resources: Contains all assets for the app, like the launch screen or the image assets.
Configurations: Contains all the files which define the build artifacts of the app. It contains the Info.plist and a Builds folder with *.xcconfig files for the different build configurations.
The root folder of the project also includes the AppDelegate.swift and the main.swift files.
AppDelegate.swift: In the AppDelegate the main UIWindow
instance of the app is created, and an empty UIViewController
instance is assigned as the root view controller. Modern app architectures, like MVVM or VIPER, work better with manual creation of the entry point, than using the Main Interface
possibility. Therefore the Main.storyboard
file was deleted and the Main Interface
reference from the project removed:
main.swift: In a default iOS project, the AppDelegate
class is annotated with @UIApplicationMain (scroll down to UIApplicationMain). It replaces the main
function of the project and the manual call of UIApplicationMain(::::). To reenable the possibility to call it manual, the annotation has to be removed, and you have to create a main.swift file. With a main.swift file, it's possible to customize the parameter for the UIApplicationDelegate. You can set an empty instance for unit tests to prevent side effects of parallel code execution in the test host.
Configurations (*.xcconfig files)
The whole project configurations are moved from the project file to *.xcconfig files in the iOSProject/Configurations/Builds
folder. The usage of *.xcconfig files in a project solves two problems:
- Separation of project configuration and file references in the project. Now, only changes of files and folders are made in the project file. Changes of configurations in the project file should always be rejected. This prevents mistaken changes during development.
- Traceability which changes are done in the project configuration during development. The history of changes in a decides *.xcconfig file can be more straightforward analyze, than in a complex project file with additional modifications.
You see in the project build settings that all the configurations are moved from the project to config files. Enable All and Levels:
The configurations are also split in different files:
Application.xcconfig: Contains all configurations which were in the Project section of the build settings. These values are set for all of the targets of the project, iOSProject and iOSProjectTests.
Debug.xcconfig, Staging.xcconfig, Release.xcconfig: Contains the different configuration values for the various app builds variants of the iOSProject target. The target has configured for three app builds, for varying stages and with different bundle identifiers. Same configuration values are extracted to Shared.xcconfig. For the test target exists the equivalent *Test.xcconfig files.
DebugSigning.xcconfig, StagingSigning.xcconfig, ReleaseSigning.xcconfig: Contains the configurations for creating the different build artifacts of the iOSProject target. Like the bundle identifiers or if you want Manual Code Signing.
Target configurations
The *.xcconfig files can assign to project configurations:
With the different configurations, Debug, Staging and Release, you can produce different app artifacts. The app artifacts can distinguish by bundle identifier, display name and signing, because if the different *.xcconfig files.
Debug: Can be used for development. It has an own bundle identifier, the code signing is set to Automatically, and the Team could be set to an (enterprise) developer team account, to which all the developers belong. So, Xcode manages the code signing, and every developer can test the app directly on a real device.
Staging: Can be used for In-House-Testing. With an own bundle identifier and signing information, it can be distributed via an external service, like HockeyApp. If you sign your staging app artifacts for an enterprise release, every member of your company or your client could test the app without submitting to Testflight.
Release: Should produce the app artifacts for the Testflight beta test and the App Store release. It also has it's own bundle identifier and signing configuration.
The whole management of the different app artifacts is done inside the Xcode project, because this maintains the independence from third-party configurations steps, like in fastlane. Switching between different build methods is straightforward. You can build and export the app artifacts via Xcode or use xcodebuild
on the command line, or yet use fastlane. With the extracted signing configurations in the *.Signing.xcconfig files, it's also simple to modify the different app artifacts settings.
Project frameworks
The project workspace also contains a Core
framework. It's a sample integration of a custom framework mainly for separate different code parts in different modules. Dedicated frameworks for different logical components in the app have some advantages:
- The separated code in frameworks makes it easier to manage the whole code basis. If you have additional targets, like for a Today Extension, you only need to import the relevant modules in it and keep the target artifacts smaller. And you don't have to hassle with the Target Membership of the source code files. Keep in mind to enable the App Extension option Allow app extension API only in the project settings of the frameworks to prevent using unsupported features for extensions.
- Because only public members of classes in frameworks are accessible from outside, you usually care more about the public interface. That's a small part of creating cleaner code, but it helps you a lot if you can improve the implementation of features over time and don't have to worry that someone uses modules in an not intended way.
- The frameworks are separated projects of the project workspace and have an own project file. If you add files to the framework, these only effects the framework project file. That makes commits and merges much more comfortable, than handling the whole file references in one project file.
- Framework projects in one workspace make Swift code migration easier to handle, because you migrate your whole project in one step, and not each framework for its own.
- The usage of frameworks affects the build time positive during development, too. Frameworks will only rebuild if you made changes in it.
Project frameworks vs. Carthage / Cocoapods
An alternative to the framework projects in the workspace, you could use Carthage or Cocoapods to build submodules for the project and include them into it. But I decide against that solution, because:
- The development with frameworks in a workspace is much faster and direct than via a dependency management system. You can develop in the same Xcode window, and changes in the framework will directly affect the other parts of the code base. You don't have to run an additional step or change paths for development.
- The frameworks are dedicated to using these at first only in this one app. You don't need some extra release step or specific versioning. Changes in one framework are changes in the app. Of course, the framework could extract later in an own Carthage / Cocoapods project, if it grows to an own project - and then you will lose your git history.
Integration in project
To integrate a framework in your app project, just add it to the Embedded Binaries section of your project target:
Third-party dependencies
The project template uses Carthage as dependency manager. Just follow the instructions in the Adding frameworks to an application to use the dependencies in your app.
If you want to use the Carthage dependencies in one of the project frameworks, you have to add the frameworks also in the app target (Adding frameworks to an application). Also, you have to link the Carthage frameworks to your framework:
Also, you have to add the Carthage folder (relative path from framework: $(PROJECT_DIR)/../Carthage/Build/iOS
) to the environment variables LD_RUNPATH_SEARCH_PATHS
and FRAMEWORK_SEARCH_PATHS
. Have a look at the Shared.xcconfig file of the framework.
Build & sign the app
To build and sign an app artifact the template uses fastlane. Fastlane provides an extensive collection of Ruby scripts to support the daily routines of iOS developer. The most used functionality is probably the abstraction for the command line tool xcodebuild
with the Ruby functions run_tests
and build_ios_app
. Fastlane also delivers fast and regular updates for changes of the abstracted functionality. Perhaps you don't even recognize that the command line arguments of xcodebuild
changed over time if you kept fastlane up to date. The abstraction and the proper maintenance are only two strengths of fastlane which makes it worth to use it.
Used fastlane features
The project template divides responsibilities for the build and distribution process to the Xcode project and the fastlane scripts.
The variant configuration of the different build artifacts is done via the Debug, Staging and Release configuration in the Xcode project. Also, the used signing settings are configured in the different *.xcconfig
files. These configurations could also be done via fastlane using functions like update_app_identifier or the use the Appfile. But if you do that configuration with fastlane, you always need it. With the base configuration already defined in the project, you could build your variants directly with Xcode - if you have installed the signing certificate and the mobile provisioning profile.
To build the different app configuration on a build server fastlane is used. Fastlane is also responsible for the creation of the signing environment and the distribution via HockeyApp and Testflight. The alternative is to create scripts in other languages or do it manually. So if you want to switch from fastlane to another solution, you only have to care about these steps.
These are the aberrations in a project. You have to decide on which step you want to use which tool. There is IMHO no right or wrong way, only personal preferences.
Code signing with fastlane
Fastlane also offers excellent solutions for code signing with match or cert and sigh, but I choose another way to create a signing environment. Instead, that fastlane creates and organizes the certificate and provisioning profiles, I want to create them manually. It's often an use case that these files cannot be generated automatically or managed by fastlane, because different parties with different development setups in the company should work with them, like in-house developer and external IT project houses.
My solution, inspired by Travis CI for iOS from objc.io, is that the certificates and mobile provisioning profiles are saved encrypted in the git repository. And for each distribution configuration (Staging, Release) a pair of them are saved:
Staging: In signing/staging could be kept a certificate for enterprise distribution with the corresponding provisioning profile for ad-hoc or in-house distribution.
Release: signing/release should contain the certificate and mobile provisioning profile for the app store release.
To create a signing environment with pre-shipped certificates and mobile provisioning profiles, you have to care about the following steps.
- Decrypt the certificates and provisioning profiles.
- Create and configure a keychain for the certificate. From my experience, the created keychain should set as default keychain, added to the search list (then it's also displayed in the Keychain Access) and be unlocked.
- Import the certificate to the created keychain.
- Copy the mobile provisioning profile to
/Library/MobileDevice/Provisioning Profiles/
After a build, you should clean up your signing environment. Especially if it's shared build server. Do the following steps:
- Delete the created keychain.
- Delete the provisioning profiles, which were copied to
/Library/MobileDevice/Provisioning Profiles/
. - Delete the unencrypted certificates and mobile provisioning profiles.
I created some ruby methods in fastlane/libs/signing.rb, which use built-in fastlane functions like create_keychain, unlock_keychain and import_file from KeychainImporter to create and delete the signing environment. In the fastlane lane you can directly use it with create_signing_configuration
and clear_signing_configuration
. To encrypt and decrypt the certificates and mobile provisioning profile the template uses the OpenSSL::Cipher::AES256 symmetric algorithm. The implementation is in fastlane/libs/encryption.rb.
To create the appropriate signing environment, with the right certificate and mobile provisioning profile, the appropriate folder is referenced in the fastlane lane.
Fastlane lanes
The Fastfile contains only two type of lanes. One self-explanatory lane for executing the unit tests:
lane :test do
run_tests(scheme: 'iOSProject')
end
And the lanes for building an app artifact, like for the App Store build:
lane :release do
build("../signing/release", "Release" ,"app-store")
upload_to_testflight(skip_submission: true)
end
The build
method is an abstraction to combine the creating / deletion of the signing environment and compile the app. It's calling the created methods from fastlane/libs/signing.rb and the fastlane function build_ios_app. Because of the *.xcconfig
configurations in the Xcode project the right certificate and profile are chosen during the build. For the Staging build its StagingSigning.xcconfig, and for the Release build its ReleaseSigning.xcconfig.
The lanes will be executed in the Makefile targets test
, staging
and release
in the build jobs.
Build server
Nobody really likes to set up and manage a macOS build server. In comparison to other build setups for backend services or even Android apps, you need real hardware or an individual plan of a cloud build service. If you manage your build server on your own, you also have to care about the installed Xcode versions and required project dependencies.
To minimize the manual and predefined setup of your build environment, I provide already a solution with my GitHub project Setup your iOS project environment with a Shellscript, Makefile or Rakefile. It shows you a way how your build server only needs Xcode and Ruby preinstalled. All other dependencies can come with the iOS project. Also, this is the base for this project template.
Travis CI
This template uses Travis CI as build service. Travis CI provides via the .travis.yml file an easy way to configure the build environment for an iOS build. Simple set the language
value to objective-c and the osx_image
to the value of the required Xcode version, e.g. xcode10. More info are in the Building an Objective-C or Swift Project section of the documentation.
To minimize the individual configuration setup for the chosen build service, you should have all required setup steps capsulated in a script, like a Makefile. The Makefile in this project template contains two major parts. It has one setup
target, which installs all the dependencies for the iOS build. The second part are the build targets: pull_request
, staging
& release
-
pull_request
: It checks the linting against the SwiftLint rules (.swiftlint.yml) with the HoundCI service (.hound.yml), runs the unit test with fastlane (Fastfile #test), uploads the code coverage of the unit tests to Codecov (.codecov.yml) and checks the pull request with Danger (Dangerfile). The results of the different services and script are combined in the Github pull request.The reason why all the steps are different Makefile targets is, that it depends on your build server and your git hosting provider, how the steps are executed. E.g., HoundCI works as a GitHub integration and is automatically performed on an updated pull request. So you may have to change the scripts to work with your build server setup, or even change the used service if you are not using GitHub. It's much easier to maintain these small subtasks than one long script.
-
staging
: This target executes the fastlane lane staging. It will create an ad-hoc-signed build and uploads it to HockeyApp -
release
: This target executes the fastlane lane release. It will create an appstore-signed build and uploads it to App Store Connect
Configure build jobs
Travis CI uses the .travis.yml file as configuration for the build jobs. Based on the actions in a git repository, it will execute different jobs. In this project template, there are three build jobs defined: Pull Request, Staging, Release. These should be run on different actions:
-
Pull Request: This job should be executed if a pull request was created or updated. It calls the Makefile target
pull_request
. -
Staging: This job should be executed if a commit or merge was made on the master branch. It calls the Makefile target
staging
. -
Release: This job should be executed if a new tag was created. It calls the Makefile target
release
.
This rules can be defined with the Build Stages. The project has only one build
stage because the Makefile targets do the whole work and the project should not depend on a particular build service. With the if
conditions its possible to define which job should be executed. E.g., the Pull Request will be only executed, if you are on a pull request:
jobs:
include:
- name: Pull Request
if: type = pull_request
stage: build
script: make pull_request
More pieces of information for conditional builds are in the https://docs.travis-ci.com/user/conditional-builds-stages-jobs/ section of Travis CI.
The .travis.yml syntax also has elements for installing the required dependencies (Installing Dependencies) before executing the build steps. You can also override the installation step for the dependencies and provide a custom script. An overwrite will prevent the project interpretation of Travis CI. It would automatically install dependencies because it would run bundle install
or cocoapods install
if it finds a Gemfile
or Podfile
file. First of all, this sounds great, but this works only on Travis CI, and you should be build service independent. So, just the Makefile target setup
will execute and installs all the required dependencies:
install: make setup
Configure the build service
Except for Travis CI, there are a lot of other iOS build services, like Circle CI or bitrise. Each of the services has its own strengths and weaknesses. So, you should look more often in the logs of the used services, if something could work better. If you use Carthage, like in the project template, you will face the missing caching of the Carthage frameworks. So each build takes longer than it's necessary. But you can configure folders, which should be cached between different builds. Have a look at the documentation (Caching Dependencies and Directories).
For the iOS project template its good to cache the selected Ruby version, the Ruby Gems, the Homebrew packages, and the Carthage folder:
cache:
bundler: true
directories:
- $HOME/.rvm/
- $HOME/Library/Caches/Homebrew
- $TRAVIS_BUILD_DIR/Carthage
On other build services, you will face other challenges. So keep in
Connect Travis CI to the GitHub repository
To connect your Github repository to the Travis CI build service, you have to visit https://travis-ci.com. You will log in with a Github Account and then connect one or more repos with Travis CI:
Environment variables
The whole ci/cd pipeline needs credentials for some steps. These should not be publicly available for everyone, even not for every developer. Only the build server should have access to them. The project needs the following environment variables:
FILE_DECRYPTION_KEY: The decryption key for the encrypted certificates and provisioning profiles in ./signing.
FASTLANE_USER: Email address of an App Store Connect user to upload an *.ipa to App Store Connect.
FASTLANE_FASTLANE_PASSWORD: Password of FASTLANE_USER
.
FASTLANE_DONT_STORE_PASSWORD: Flag, that the user credentials should not save in the local keychain of the build server. Should always be 1.
HOCKEYAPP_API_TOKEN: API token to upload an *.ipa to HockeyApp.
CODECOV_TOKEN: Codecov token for the connection to the Codecov project.
DANGER_GITHUB_API_TOKEN Personal access token of a GitHub Bot User account. With this one, the pull request comments will be made.
GITHUB_ACCESS_TOKEN Personal access token of a GitHub account, to not run in rate limits for anonymous user. These will happen for fetching pre-build Carthage frameworks.
In Travis CI you can set the environment variables in the Settings of a project. These will be automatically injected into every new build.
Distribution
During the development of an app, there are different requirements for the distribution of an app. After you implemented a feature, at first the QA department and the product owner should test it. And only after approval, the version should be distributed to the internal tester. Then it should go to a public beta and the App Store release.
To give the QA department and the product owner access to a pre-release app version, the app usually signed as Ad-hoc or In-House build. Instead of sending the *.ipa through email or other ways, Apple supports distributing these versions over an own web server: Distribute in-house apps from a web server. Keep in mind to use this method only in a company-wide solution, and don't publish to the public.
HockeyApp
HockeyApp is a distribution and crash report service, which offers the web server space to download an Ad-hoc or In-House sign build via a website:
You only have to upload the *.ipa to the service, and then you can share the link to your product owner.
Fastlane integration
Fastlane provides with hockey a built-in function to upload an *.ipa to the HockeyApp:
hockey(
api_token: ENV['HOCKEYAPP_API_TOKEN'],
ipa: lane_context[SharedValues::IPA_OUTPUT_PATH],
dsym: lane_context[SharedValues::DSYM_OUTPUT_PATH]
)
You only have to provide an api token to a HockeyApp app. See HockeyApp Account how to create one.
Testflight & App Store
For internal and public beta tests is Testflight from Apple the best way. Because you use an App Store signed build for it and need a review for the external beta test, you are very close to a release in the App Store.
For more information take a lot at the Testflight section on Apple's developer site.
Fastlane integration
With upload_to_testflight fastlane also provides a function to upload an App Store signed *.ipa to App Store Connect. It's used in the fastlane lane release:
lane :release do
build("../signing/release", "Release" ,"app-store")
upload_to_testflight(skip_submission: true)
end
If you want manual submit your app to the public beta test set skip_submission
to true.
To be able to upload an *.ipa
to App Store Connect, fastlane needs user credentials for it. These will be set as environment variables (see Environment variables section). To create an App Store Connect user have a look at the App Store Connect User section.
GitHub Integrations
Github offers a great web interface to work with multiple developers on a project. How you can organize your flow is for example described in Understanding the GitHub flow. Other flows and conventions are listed in my repository messeb/development-conventions.
The most important part during development, (after writing the code), is the code review in your team. There you can review and discuss implemented solutions. Github provides with its pull request feature (Creating a pull request) a dedicated manner to do it. But the review of code is one of the most challenging parts in software development. Besides the discussion about the main feature, there are always some stressful side discussions about code style, test coverage, and some missing pieces.
Therefore GitHub provides APIs for the integration of 3rd party service in their pull request. There is also a whole Marketplace for services which can improve the development workflow: GitHub Marketplace.
The project template uses Danger, HoundCI, and Codecov. With that integrations, you see common warnings and errors already in the pull request, and a reviewer will only check out and review the code when everything else is fine. Therefore it's essential that a ci build is done on any changes of a pull request. It should not be needed to check out a feature branch and build it locally to see that anything obvious is wrong.
Danger
Danger is a tool that runs in the ci pipeline and can comment on pull requests if changes in the pull request violate predefined rules. It could look like this:
The rules a defined in the Dangerfile and it uses a GitHub bot user to create that comments (Creating a bot account for Danger to use). But it also supports GitLab and Bitbucket Server. Take a look in the Danger Getting set up guide.
Danger is a big help to concentrate only on the critical parts of the code changes in a review. Because the creator of the pull request gets this warnings direct after a pull request ci build and these can be fixed before another developer have even a look at the code changes.
Of course, you have to define the rules for the Danger check, but you can add them one time after a discussion in the team, and the same debate will never occur. Good starting points for fundamental rules are the Danger Reference and a GitHub search for "Dangerfile" (click).
The template uses the Ruby version (Gemfile) of Danger because you also define your fastlane lanes also in Ruby. Danger is executed through the Makefile target pull_request on the build server.
HoundCI
HoundCI is a 3rd party service which integrates into the GitHub pull request and checks the Swift source files against linting rules.
It supports SwiftLint, which is the defacto standard for linting Swift files. If you don't use SwiftLint already, you should have a look at the rules reference and see how great it is. After a team agreement, you will have much less discussion about the code style. Also, you can run it locally with your Xcode build, so that the warnings appear during development in Xcode:
For the ci build HoundCI will comment inline in the pull request so you will see the linting error direct:
The configuration of HoundCI is done in the .hound.yml and full documentation of it you will find HoundCI SwitfLint site.
Codecov
Tests are essential as the feature code itself. But it's not easy to see if all code paths are covered. Codecov brings visualization of the code coverage in the current pull request:
You will see what code paths are not covered by the tests in the current pull request code and how the code coverage is changed against the base branch. It uses the code coverage output that xcodebuild
produces. So you need to enable it in the scheme:
The configuration of Codecov will be done in the .codecov.yml file. You will find more information in the Codecov Guide. The upload of the code coverage reports are done in the Makefile target codecov_upload.
Usage
To use the template for your own project, you have to change some configurations and contents of files.
Signing
Create the signing certificates and provisioning profiles for the different builds in the Certificates, Identifiers & Profiles section of the developer portal.
Add your unencrypted signing certificate and mobile provision profile for the Staging
(Folder signing/staging) and Release
(Folder signing/release) versions to their folders and delete other existing files.
Then call in the command line:
$ bundle install
$ bundle exec fastlane encrypt
You will be asked for an encryption key. After entering, the files will be encrypted with it. Remember the key, it will be set as environment variable FILE_DECRYPTION_KEY
for the decryption on the build server.
Configuration
To configure the project for your team and with your bundle identifiers and signing information you have to change only following *.xcconfig
files.
SharedSigning.xcconfig (SharedSigning.xcconfig): Change the DEVELOPMENT_TEAM
to your team id.
DebugSigning.xcconfig (DebugSigning.xcconfig): Change the PRODUCT_BUNDLE_IDENTIFIER
to the bundle identifier for your local development.
StagingSigning.xcconfig (StagingSigning.xcconfig): Change the PRODUCT_BUNDLE_IDENTIFIER
to the bundle identifier for your staging build. Also set CODE_SIGN_IDENTITY
and PROVISIONING_PROFILE_SPECIFIER
to the names of the signing identity and the mobile provisioning profile for the staging build.
ReleaseSigning.xcconfig (ReleaseSigning.xcconfig): Change the PRODUCT_BUNDLE_IDENTIFIER
to the bundle identifier for your App Store build. Also, set CODE_SIGN_IDENTITY
and PROVISIONING_PROFILE_SPECIFIER
to the names of the signing identity and the mobile provisioning profile for the App Store build.
HockeyApp Account
Create a free HockeyApp account on https://hockeyapp.net/ and add a new iOS distribution app on the Dashboard.
Generate an API Token for your created distribution app on the Create API Token page and set the environment variable HOCKEYAPP_API_TOKEN
to that value.
App Store Connect User
To upload the app artifact for the App Store via fastlane, it needs credentials for an App Store Connect app. Create a new App Store connect user in the User and Access section for the usage with fastlane.
It should have the Developer role and only access to the needed apps.
Set the environment variables FASTLANE_USER
with the email address and FASTLANE_PASSWORD
with the password of the App Store Connect user.
GitHub
To use the GitHub integrations, you have to configure accounts and setup some service.
Danger: To use Danger, create a new GitHub Account like my bot (messeb-bot), generate a personal token in the Developer Settings and assign it to the environment variable DANGER_GITHUB_API_TOKEN
HoundCI: Go to the website https://houndci.com and log in with your GitHub Account. Then you can connect your repositories with HoundCI.
Codecov: To integrate Codecov in your visit the Codecov Marketplace site and add it to your repository. On the Codecov site you can then access the Settings page of the repository. Set the value of Repository Upload Token to the environment variable CODECOV_TOKEN
.
Makefile
Uncomment in the Makefile the target of the staging
and release
build to:
staging:
bundle exec fastlane staging
release:
bundle exec fastlane release