The power of Continuous Delivery for iOS apps

John Sundell shares his thoughts on how using continuous integration and continuous delivery makes all team members aware of the project’s current status, its high-level plans and goals, and how this benefits all parties involved.

Arguably one of the most challenging aspects of growing a team beyond a single person is making sure that everyone shares the same context. Not only do all team members need to be aware of the project’s high-level plans and goals — effectively sharing the exact status of in-progress features and milestones can really be essential in order for a team to work well together.

From design to implementation

As an example, let’s say that a designer has been working on a new screen for an app, and that the team has decided that it’s time to start implementing that new screen in code. The designer hands off their design and all of its assets to the developers, who start building the new screen in Xcode. Since this is estimated to take a week or two, the designer then moves on to the next task, which — depending on the size of the company — might involve working with a whole new group of developers, product managers, testers, and other designers.

However, this kind of handoff is almost never a clean break. Chances are high that the design delivered to the developers won’t answer all of their questions, and some kind of detail often gets “lost in translation” along the way — not due to incompetence, but because we haven’t yet established a shared context. What we’d ideally want, is for a designer/developer handoff to not actually be a handoff at all — but rather just another step in a continuous feedback loop.

If we can get a designer’s feedback on the implementation of their designs before we’ve finished working on them, chances are much higher that we’ll be able to act on that feedback, and course-correct before we’ve passed the point of having to ship what we’ve built anyway. “We’ll just fix it in the next release” can be a quite demoralizing thing to hear over and over again.

Continuous integration

Within the iOS developer community, we’ve long since recognized the value of continuous feedback — through the rising popularity of continuous integration. Using a service like Bitrise, we’re able to quickly get feedback on any code change pushed to our remote repository — by building the app for all supported devices, and running our automated test suite, linters, and other tooling, on the result.

Add peer review using something like GitHub Pull Requests on top of that, and we’ve established our continuous feedback loop — in which code gets written, reviewed, tested and integrated into the main code base, as one ongoing process. Not only does that make our workflow smoother, it also acts as a safety net for mistakes. Bugs can get caught before they’re even merged in, version control conflicts can be resolved and re-tested, and architectural inconsistencies can be adjusted.

It’s fair to say that once you’ve worked on a project that uses continuous integration, it’s very hard to go back to working without it. So how can we extend the continuous integration model to not only be a workflow for developers, but to also involve other members of our team — such as designers, product managers, or external stakeholders? Enter continuous delivery.

Continuous delivery

What if a green build and a passing test suite wasn’t the end of our continuous integration process — and what if we also actually delivered every build to our team? That way we could bring not only developers, but everyone on our team, into our feedback loop. Designers could see the exact state of their implemented designs, product managers could be able to make more accurate plans, and external stakeholders (such as customers for an agency or a freelancer) could transparently see the exact state of their project at any time.

While continuous delivery has become a more and more common practice for things like web and backend development, on iOS things are a little bit more tricky — since we can’t really deploy every single change we make onto the App Store, or practically even TestFlight (depending on the volume of changes). Thankfully, Bitrise has a solution to this problem.


By adding an Xcode Archive & Export for iOS step to a Bitrise workflow, a new IPA (app) artifact will be archived and saved for each build. Chaining that step to a Deploy to step will, in turn, take that IPA file and deploy it to Bitrise’s internal testing service. Once that’s done, everyone you invite to that app on Bitrise will get access to those builds.

Code signing

The final piece of the puzzle is to make sure that the deployed app is code signed in a way that enables it to be run on all team members’ devices. For that there are four main options:

  1. If your company has an Enterprise Certificate from Apple, it can be used to deploy apps internally within your team. Make sure that the app is set up to be signed using your enterprise certificate within the Code Signing tab in the Bitrise Workflow Editor.
  2. Use an iOS Auto Provision step within your Bitrise workflow. That’ll automatically register any new devices with the Apple Developer Portal, and will make sure that the app’s provisioning profile is up to date before deploying it.
  3. Use the Fastlane Match step within your workflow to download and install certificates and provisioning profiles through fastlane match.
  4. Manually manage your app’s code signing.

Once the above pieces are in place, Bitrise will automatically manage the continuous delivery process going forward. Team members will be notified when a new build is available, and will easily be able to get those builds onto their devices.

Beyond beta

While we’ve now established a smooth process for ensuring that everyone on our team is aware of the current state of the app we’re building — the true power of continuous delivery really comes from the routines that it’ll make us start forming. Since we’re now actually shipping every single change that we make, even though it’s only internally within our team for now, it raises the bar for what level of quality every change needs to meet.

Just like how continuous integration most often results in fewer broken master branches, and higher code quality through things like automated testing, linting and peer review — when we start getting used to continuously delivering our app, we can get the same effect, but on the app’s overall user experience. And once we’ve stabilized the quality of our user experience to not be degraded during development, and then fixed right before a release — some really interesting things become possible.

Imagine being able to release our app at any time. No long release processes, no stopping feature development for weeks to fix a large number of bugs, and no intense periods of overtime work to meet a specific release window. The ultimate goal of continuous delivery is instead to get so used to shipping the app that it can be done based on any commit currently in the master branch.

Feature flags

To be able to ship an app at any time, we won’t only need to make changes to our process and the way we integrate and deliver code, we might also need to make some actual code changes. In particular, for every release we’ll most likely have code within our repository that’s still not ready for production. It can be a feature that’s not yet fully implemented, a UI change that needs to be propagated throughout the app, or something that needs to be coordinated with some form of marketing effort.

Rather than having to manually take out such work-in-progress code before making a release (which wouldn’t scale very well as we might ramp up our continuous delivery efforts), we can make use of feature flags to dynamically enable or disable a given code path within our app.

While there are a number of ways that feature flags can be implemented on iOS, a quick and easy way to get started could be to simply create a set of static Bool constants that are attached to some kind of FeatureFlags type. The values of those constants could either be hard-coded, or based on some other value, such as the configuration that our app was built with — like in this Swift struct:

struct FeatureFlags {
    static let allowLandscapeMode = true
    static let enableSocialFeature = false
    static let useRoundImageStyle = AppConfig.isForInternalUse
Copy code

With the above in place, we can now easily check whether a given feature should be enabled, and if not we’ll exit out of any code paths that activate it — such as here where we verify that FeatureFlags.enableSocialFeature is true before adding a social view to a view controller we’re building:

extension ArticleViewController {
    func addSocialViewIfNeeded() {
        guard FeatureFlags.enableSocialFeature else {

        let socialView = SocialView()
        socialView.delegate = self
Copy code

While the above is a very simple version of a feature flag system, which could include more advanced functionality — like dynamically downloading the value for each flag from a remote server, or enabling team members to tweak their values — starting out simple is key, and even a first iteration (like the one above) can get us quite far along the way.

Once we’re able to turn specific code paths on or off, we can easily hide any feature that shouldn’t be exposed to end users, while still keeping it active for development — and possibly even for internal testing as well. That’s not only really convenient for developers, but also moves us further towards true continuous delivery, when we’ll be able to ship our app to production at any time.


Continuous delivery can be an incredibly powerful addition to an app development team’s workflow. By bringing everyone, not only developers, into the process of seeing a new feature come to life in the app — we can much easier establish a shared context among the team, reduce the need for extra meetings and syncing, and avoid misunderstandings.

And then, once we’re used to continuously delivering our code internally within our team, chances are also high that we’ll be able to keep iterating on that process to one day be able to ship a new version of our app to the App Store at any time — based on any commit from our repository’s master branch. That’ll not only encourage us to always keep our app in a high-quality state, but could also drastically reduce the amount of overhead needed to ship our code to our end users.

What do you think? Does your team currently use some form of continuous delivery, or is it something you’ll try out? Let me know on Twitter @johnsundell. And if you’re looking for a service to use for your continuous integration/delivery needs, look no further than Bitrise — it’s my personal favorite.

Thanks for reading! 🚀

No items found.
The Mobile DevOps Newsletter

Explore more topics

App Development

Learn how to optimize your mobile app deployment processes for iOS, Android, Flutter, ReactNative, and more

Bitrise & Community

Check out the latest from Bitrise and the community. Learn about the upcoming mobile events, employee spotlights, women in tech, and more

Mobile App Releases

Learn how to release faster, better apps on the App Store, Google Play Store, Huawei AppGallery, and other app stores

Mobile DevOps

Learn Mobile DevOps best practices such as DevOps for iOS, Android, and industry-specific DevOps tips for mobile engineers

Mobile Testing & Security

Learn how to optimize mobile testing and security — from automated security checks to robust mobile testing and more.

Product Updates

Check out the latest product updates from Bitrise — Build Insights updates, product news, and more.

The Mobile DevOps Newsletter

Join 1000s of your peers. Sign up to receive Mobile DevOps tips, news, and best practice guides once every two weeks.