Skip to content

Build an iOS App then publish to TestFlight

This article shows how to build and publish an iOS App to TestFlight manually or automatically with Xcode Cloud.

Prerequisites

  1. An Apple ID that has joined Apple Developer Program
  2. A Github account

Create a Xcode project

  1. Create a Bundle ID at Apple Developer website
  2. Create an App App Store Connect

    Note: The term "App" here means a project we create at App Store Connect rather an App on your phone.

  3. Use the newly created Bundle ID to create a Xcode project.
  4. Make sure signing of the Xcode project is valid.
  5. Run the App, make sure it works.
  6. Git init, then push this repo to Github.

Manually build

Basic workflow

  1. Set Run Destination as Any iOS Device
  2. Click Product > Archive to start building
  3. After building completed, a small window with a title Archives will be opened. Click Distribute App, then waiting upload complete.
  4. After upload complete, you should be able to find this build at App Store Connect > TestFlightpicture 0

Increase version

If you want upload your build to TestFlight again, then you must update version at app.xcodeproj > General > Identity first. picture 2

Automatically build

After setting up Xcode Cloud workflow, your App will automatically build and publish to TestFlight by Xcode Cloud after you push a commit to Github.

Note: Until now (2024-01-07) you can only create your first Xcode Cloud Workflow with Xcode rather than App Store Connect > Xcode Cloud

Basic workflow

You can find the setup workflow button as following picture showed. You can also find more detailed guide at Configuring your first Xcode Cloud workflow | Applepicture 1

Increase version

We will use a js script to automatically increase version. Since Xcode Cloud will run ci_scripts/ci_post_clone.sh script before building, we should place our custom script there.

You can find more about Xcode Cloud custom build scripts here -> Writing custom build scripts | Apple

sh
#!/bin/sh

# Note: Xcode will run ci_post_clone.sh at ci_scripts directory

export HOMEBREW_NO_INSTALL_CLEANUP=TRUE

# Install node start
brew install node
brew link node
# Install node end

# Change App info
node changeAppInfo.mjs
js
#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";

async function main() {
  try {
    // TODO: Replace config path. Note: Xcode will run ci_post_clone.sh at ci_scripts directory
    const configPath = path.resolve('../app.xcodeproj/project.pbxproj')
    await increaseVersion(configPath)
  }
  catch (error) {
    console.error(error)
    process.exit(1)
  }
}

/**
 * Increase version automatically in project.pbxproj file
 * Build #26 1.2 -> 1.2.26
 * Build #27 1.2 -> 1.2.27
 */
async function increaseVersion(configPath) {
  const configText = await fs.readFile(configPath, { encoding: 'utf-8' })
  print(`Changing ${configPath}`)

  const regex = /MARKETING_VERSION = (.+?);/g
  const versionMatch = configText.match(regex)
  if (versionMatch === null) {
    throw new Error(`Cant get iOS bundle version in ${configPath}, terminate build`)
  }

  const bundleVersion = versionMatch[0].replace('MARKETING_VERSION = ', '').replace(';', '')

  const finalBundleVersion = `${bundleVersion}.${process.env.CI_BUILD_NUMBER}`
  print(`Overwrite version: ${bundleVersion} -> ${finalBundleVersion}`)

  const updatedConfigText = configText
    .replace(regex, `MARKETING_VERSION = ${finalBundleVersion};`)

  await fs.writeFile(configPath, updatedConfigText, { encoding: 'utf-8' })
}

function print(message) {
  console.log(`[changeAppInfo] ${message}`)
}

main()

App Icon

You App Icon can not have alpha channel, otherwise Xcode Cloud will failed to build.

(Optional) Other notes

  1. Use build_ios branch to trigger build instead of main branch.
    bash
    git checkout build_ios && git rebase main && git push && git checkout main