My experience of setting up Continuous Delivery with Xcode Cloud for a fresh project
I’ve started properly building Garner, my career journalling app now. I feel like I’ve done enough to explore the idea, I feel confident using Swift now and I’m growing conscious that the longer I sit not putting something in people’s hands the more time I spend disconnected from the reality of the value the app will bring to users.
This weekend I sat down and thought about what I need in place to start my journey towards that reality of having an app running on people’s iPhones, iPads and Macs so that I can get feedback on how to make things better and stop living in my head.
In my previous job at a software consultancy my life revolved around solving this problem. We would get hired to digitally transform a client’s business and often the first step to doing so was to bring in processes that allowed them to ship continuously in a safe way so they could get feedback and iterate faster.
Often the way we did this was to set up a Continuous Delivery (CD) pipeline where code could go from being merged into the main branch and within minutes would be pushed live.
I’m very familiar with how to do this for web projects and have had some experiences a couple of years ago with Expo and React Native for mobile but I haven’t set up a CD pipeline for a native Swift based app before.
My experiences with any form of CD pipeline for iOS came from that time using Expo which saw me burn through Github Action credits because of how long it took and I was concerned that with building an app for iOS, iPadOS and MacOS that this would become a bigger problem due to Mac runners on Github costing 10 times that of Linux ones.
I then read that Apple provide 25 hours of Xcode Cloud build time as part of the Apple Developer programme for free every month and given how small my app will be at the start I decided to give that service a try.
The Pros and Cons of Xcode Cloud (on paper)
Before I started setting everything up I decided to draw up a pros and cons list of using such an approach.
Pros
- I’ll have a stand alone compute hour budget for the app as it’s the only app I’m building and my Github Actions compute budget is shared across all my repos
- Xcode Cloud is cheaper than GitHub Actions (based on $4.80 an hour for Mac Github Actions runner vs 100 hours of Xcode Cloud for $50)
- I don’t have to rely on an external tool like Fastlane’s Match to handle signing certificates as Xcode Cloud sorts all that provided you have your project to automatically handle signing for you
- I can get the app published to TestFlight directly through Xcode Cloud so no need to manage API keys for publishing to TestFlight
- I can handle the pipeline configuration from within Xcode so I don’t need to leave my IDE to set it up or check on the status of builds
- I don’t have to write code for the build steps or manage dependencies so the pipeline shouldn’t require much attention after initial setup
- Should I need to move off the platform I should be able to go back to the other methods such as using Fastlane via Github Actions
Cons
- I’m putting my eggs in Apple’s basket but as I’m building for Apple’s platforms this is happening anyways
- The configuration is hidden away from me so there is a chance that I could end up with sub-optimal workflows that end up eating into my build time budget
- If I want to create custom steps to do things such as capture screenshots and create app store assets using Fastlane I will have to break out of the default configuration and this could prove unreliable
- Github Actions is battle tested and has a lot of tools that work with that ecosystem
I didn't see too many issues with using Xcode Cloud for this initial setting up of a CD pipeline from this analysis. It may be that I have to move away from it as my needs grow but for now it provides me with a convenient solution.
Going from new Xcode project to app running on device via TestFlight
Having decided on using Xcode Cloud I then set about creating the Xcode project for the app and the infrastructure around it to ensure that I could push code to main and get those changes in someone’s hands within a short amount of time.
Ensuring that Source Code Management is working
The first step was creating the Git repository where I would store the app’s source code. This was pretty straightforward, I created a new repository in my Github like I would do any other coding project.
After that I created a new Multiplatform app project in Xcode and configured that project to use the Github repository as a remote.
At that point I had a basic project that would run and a means to manage the source code. Pretty standard stuff but an important foundation to ensure is in place.
Setting up Xcode Cloud
In order for Xcode Cloud to work you need to tell it where your source code is stored and authorise it via whatever service you use to store it. I use Github to store my code so this meant installing the Xcode Cloud application on my Github profile and giving it authorisation to manage my repositories for me.
I initially messed this stage up. Being the security conscious person I am I decided to limit the Xcode Cloud application to only being able to access the one repo it would need instead of giving it full access to every repo.
However it turns out if you do this then the next step in the Xcode Cloud wizard in Xcode will fail with a very unhelpful error of “This Operation Could Not Be Completed” but the underlying cause is that Xcode Cloud’s Github application requires admin permissions — https://developer.apple.com/documentation/xcode/requirements-for-using-xcode-cloud#Source-control-requirements
After I corrected the application’s permissions in Github I was then able to move forward and start creating the first pipeline. The initial pipeline that Xcode Cloud suggests will build the app for the different platforms and then store that build as an artefact for later steps.
This of course was not what I wanted, I wanted to have the app built and published to TestFlight so that I could then have people use it and give me feedback.
So I went looking in the Workflow editor for how I would set up publishing to TestFlight and under “Post-Actions” I could see “TestFlight Internal Testing” greyed out.
It turns out that there’s a bit of hidden set up that is needed to unlock publishing to TestFlight which is to add an App Record in App Store Connect for the app.
Once this set up is done then you have the option as part of the Archive Action to configure “TestFlight (Internal Testing Only)” in the “Distribution Preparation” section and you can create the a “Post-Action” to publish a new build in TestFlight with the Archive and assign it to a group.
With this extra App Record step done I then had an app publishing to TestFlight right?
Of course not! Is this your first time working with CD Pipelines? The first run never passes
Fixing your Xcode Cloud setup to actually publish to TestFlight
I was a little foolish, I had assumed that the default template for a new app would have been configured so that it could be published to TestFlight but it’s only configured to get something running on a device in development mode.
In order to publish to TestFlight you need an app icon and that default template doesn’t include any icons. This means when the Xcode Cloud build step runs it’ll chuck the following errors at you:
- Missing Info.plist value. A value for the Info.plist key ‘CFBundleIconName’ is missing in the bundle
- Missing required icon file. The bundles does not contain an app icon for iPad of exactly ‘152x152’ pixels, in .png format for iOS versions ≥10.0
- Missing required icon file. This bundles does not contain an app icon for iPhone/ iPod Touch of exactly ‘120x120’ pixels, in .png format for iOS versions >10.0
This is solved easily by creating the app icons at the various different sizes. To do this I used a free app on the Mac App Store called Icondary that allowed me to generate the App Icon assets needed.
However after doing so I then had another error as it turns out that the 1024x1024 app icon cannot contain transparency so I had to set a background for that icon size.
After doing this though I was able to get the archive step passing and the build was made available in TestFlight.
Actually getting your builds into people’s TestFlight apps automatically
Of course, having the build from Xcode Cloud in TestFlight doesn’t mean it’s actually available for people to download in their TestFlight app.
Instead you have some additional information around encryption and whether or not you plan to distribute your app in France to fill out so your build will be flagged as “Missing Compliance”.
This can be automated by adding the “App Uses Non-Exempt Encryption” property to Info.plist and setting it to NO.
Finally once that value is configured you will be able to push up a change, have Xcode Cloud build the app, create a new build in TestFlight and distribute that to your group of testers.
A note on CloudKit
If you enable CloudKit for your app you may find that Xcode Cloud complains that your entitlements file doesn’t have com.apple.developer.icloud-container-environment
set to Production
.
This is easy to fix, you just add the entitlement to your Entitlements file but I found that this caused me issues because your local development will then run against the Production database in iCloud and you can’t amend the schema of the Production database.
This means that no data gets synced because there’s no schema and the way to add that schema is to have your app run against the Development database and then promote the changes.
You can make your life a little easier by breaking your Entitlements file into two files, one for local development and one for release. You can then update your build settings to have the debug and release schemas point to the appropriate files.
I found this Stack Overflow answer useful for this https://stackoverflow.com/a/53984551
My thoughts on setting up a CD pipeline with Xcode Cloud
Generally I found the experience to be quite good. Aside from the initial Git repo interaction I was for the most part able to setup a CD pipeline fully within Xcode.
This contrasts to my usual experiences with something like Github Actions where I’ve had have my IDE, the Github Actions tab of the repo and the Github Actions documentation available in different windows as I stumble my way to correct set of commands to get a passing build.
I do think that Apple could do a better job of streamlining the TestFlight side of things for Xcode Cloud though.
They want to paint the picture of an app developer being able to have an idea and get it into the hands of users quickly but in reality there’s a lot of stuff that could be automated to ensure that a new app can be published and made available with minimal setup.
Adding a step in the Xcode Cloud setup wizard to ask if the user wants a placeholder app icon generated and if they plan on using non-exempt encryption at the start would fulfil that ideal of being able to turn on Xcode Cloud and go from having an app running on the developer’s device to it being made available for testing across the world.
But I get that there’s a lot of variables that come into play with app development so this “story” isn’t going to meet everyone’s needs but I think it’s a common enough one to warrant a little more effort.
Next Steps for me
Now I have a CD pipeline that will publish the changes I make to my app to TestFlight I will move onto three things:
- Creating a basic app that can be tested
- Creating a website for the app with a sign up form that I can use to have people to register to test that basic app
- Creating a #buildInPublic campaign around the app so that I can get people to that website and signing up for testing it