How to scale your iOS org: sharing tooling across multiple iOS projects

As your organization grows, you might go from building one iOS app to two, and then you’ll find yourself sharing code between them… and then, before you know it, your team is juggling multiple iOS projects spanning multiple repositories. With this increased scale, differences between projects can often creep in. Maybe you have projects that use SwiftLint, but they each have their linting configured differently. And maybe — just maybe — your developers can never remember which fastlane commands they need to run in which projects since the commands are all named similarly but not identically.

So what can we do about this, and why does it matter? Do we need to keep all of our tooling the same across different projects? And how exactly do we even get these tools and environments to work across different projects?

Why is sharing tooling across projects helpful?

Keeping projects consistent – having standardized build scripts, linting rules, code signing certificates and profiles, and other general configurations – means that you can dramatically lessen the day-to-day maintenance of those things in each project. It lets your developers move between projects with much more ease and confidence because they don’t have to think about different code styles or memorize commands for one project that don’t translate to the next. And when it comes to code signing, well, you already know how painful that can be in one project, let alone multiple. So, setting up one script that can update certs and profiles for everything is incredibly powerful. Standardizing your team’s tooling across projects also has the added benefit of simplifying the onboarding process for new team members so they can get set up and start contributing more quickly.

How far should you go in setting up shared tooling?

Unsurprisingly, this is always a question of relative tradeoffs and prioritization, and ultimately it’s only really answerable by you and your team. But hopefully, the conclusion here can be informed by a better understanding of what can be optimized, and the time and resources needed to put new processes in place.

To that end, let’s take a look at some strategies for how we can pull all of this off.

How to standardize workflows with fastlane

fastlane can play a key role in standardizing workflows across your projects. Thankfully most iOS apps have common needs when it comes to things like building and distributing beta releases, running tests, and shipping to the App Store — so we can build lanes that are reusable across projects pretty naturally. When combined with use of environments fastlane becomes a powerful tool for establishing conventions that scale across your apps.

One particularly wonderful feature that helps make fastlane more generalizable across projects is the ability of Fastfiles to import the contents of other Fastfiles (docs). This means you can store more generic lanes in certain shared Fastfiles and pull them into more specific (perhaps project-specific!) Fastfiles. The imported files can even be fetched from a remote git URL!

Here’s an example of a shared lane hosted in git, imported by an app’s Fastfile.

lane :beta do |options|
  base_name = ENV["BASE_NAME"]
  UI.important "Making iOS Beta for #{base_name}"

  update_signing

  # Build iOS app
  build_app(
    scheme: "#{base_name}_iOS",
  )

  changelog = File.read(ENV["CHANGELOG_PATH"])
  distributeExternally = options[:distribute_externally]
  expirePrevious = distributeExternally == true
  groups = ENV["TESTFLIGHT_GROUPS"]

  upload_to_testflight

  upload_app_privacy_details_to_app_store
end

In the above example, we're making an iOS beta for an app. We're bringing in the precise values for this app through the environment (all of those <code>ENV<code> reads), and the primary distinction is through the <code>BASE_NAME<code> variable. This would be customized per app's environment and, by convention, leads to the scheme that we want to build (which would be <code>{BASE_NAME}_iOS<code> in this case).

Combining a standardized convention for your projects alongside these generic lanes gives a huge amount of power and flexibility. If you ever need to update how a beta is made, you can now easily update all of your projects at once.

Centralizing fastlane: migration tips

If you’re dealing with existing projects and Fastfiles, moving towards this centralized approach can be difficult depending on how your projects are currently configured. Here are a few tips to ease the migration process:

  1. Take a broad look at all of your projects and determine what common lanes would work for them.
  2. Build out the centralized version of those generic lanes in a location that will be imported by each project. While doing this, create the conventions that should be followed across projects. These conventions could be things like defining the consistent scheme names in your Xcode projects and/or determining what environment variables each project will set to be picked up by fastlane.
  3. Adopt slowly, one project at a time.

How to keep code style consistent across projects with SwiftLint

Linting is a great way to establish and enforce conventions around code style, but it can be tricky to implement consistent and consolidated linting across teams and projects. Enter SwiftLint, the gold standard when it comes to (Swift) linting! Like fastlane, SwiftLint also has the ability to import remote configurations into a given config file. This lets you (re)use some of the same linting rules across projects, while also allowing you to customize certain things that may need to be handled uniquely per project.

There are a few ways to pull this off, depending on how SwiftLint is implemented in your projects. Thankfully there is really good documentation on how to build up configurations that work best for you. Once you're done, you'll be in a place where there is a standard set of rules to apply that are then customized for each project as needed.

Here’s a contrived example of just part of what this could look like:

// parent-config.yml stored in a git repository 
file_length:
  warning: 500
  error: 1200
force_try:
  severity: warning

// child-config.yml stored in an app repo
parent_config: https://myteamserver.com/parent-config.yml
file_length:
  warning: 800
  error: 1000
excluded:
  - Carthage
  - Pods

This example shows a parent config which specifies two rules: <code>file_length<code> and <code>force_try<code>. The parent gets imported by the child via git and the child then layers on rules of its own: overriding the <code>file_length<code> rule and adding specific directories to ignore in its project via the <code>excluded<code> customization. There is much, much more flexibility to be had in SwiftLint so looking over their docs is highly recommended.

Centralizing code style with SwiftLint: migration tips

As with the shared fastlane configuration, getting to the finish line is a process. Here are some helpful hints to get you there:

  • Establish a set of rules for the whole organization to follow. If SwiftLint is new to you, try creating a set of rules from your existing projects with SwiftLint Autodetect.
  • In each project, bring in that parent set of rules and see what's broken. Fix things bit by bit until the errors go away and you're happy with the configuration changes that each project makes.
  • It's super helpful to have these lint checks run as part of your PR process, to ensure the rules are applied consistently and each project can’t gradually go astray of the standards you put in place.

Managing secrets across iOS projects

Secrets are another very specific type of data that need to be shared across projects as well as the developers working on them. These are sensitive pieces of data like signing keys and certificates which are used to distribute your apps to users, and API keys that are used to communicate with external services used by your apps and tools. Unfortunately, somewhat counter to the whole point of this piece, the best way to keep secrets secret is to share them around as little as possible.

The usual approach to limiting the distribution of secrets is keeping them away from your git repo by storing them locally in ignored files (like a <code>.env.secret<code> file that can be loaded into a fastlane environment). But how can you make this work at scale, across entire teams and multiple projects? One approach is to use a shared secrets manager like Doppler or 1Password. They can help keep your whole organization in sync by acting as a secure, central repository of scoped secrets that can be pulled down into local projects, or even by automatically injecting secrets at build time.

Signing certificates and provisioning profiles are a very particular kind of secret to manage in the iOS world. For this kind of information the best-in-class solution is fastlane match. match communicates directly with App Store Connect’s API to generate the certificates and profiles needed for your projects, then stores them away in a private git repository in the cloud (or even another cloud storage destination like Google Cloud Storage or Amazon S3). Anyone with access to the private repository can then load secrets onto their local machine using the fastlane match action.

Continuous Integration (CI) tools like Jenkins, Bitrise, and GitHub Actions often require access to secrets within the jobs and workflows that they run. Giving CI jobs and workflows access to secrets can be accomplished in a couple of different ways:

  1. Adding secrets as environment variables that are passed in through each job’s configuration. Because secrets are passed into each job separately, there’s duplicated effort here if the secrets are required in multiple jobs.
  2. Referencing a value stored in the CI tool’s centralized secrets manager, if one is available. Many CI providers do now provide a secrets manager as part of their offering. When secrets are updated centrally, values don’t need to be updated in multiple places across the jobs that need them. Instead, they hold a reference to the managed value.

Shared tooling can help you adapt to growing iOS needs

The growing pains that can arrive as more iOS projects come online in your organization are a great problem to have – you’re scaling! You can help mitigate some of the pain by establishing conventions and standardizing tooling across teams and projects. Keeping things consistent will help your developers pivot between repositories much more easily, and it ensures best practices spread everywhere in your iOS org. With centralized fastlane configuration, common linting, and securely shared secrets, your projects will run more smoothly and you can focus on the thing that matters — building great apps.

Mobile DevOps

Release better with Runway.

Runway integrates with all the tools you’re already using to level-up your release coordination and automation, from kickoff to submission to release. No more cat-herding, spreadsheets, or steady drip of manual busywork.
request a demo