构建一个 iOS 应用并上传到 TestFlight 
目标 
- 提交一个commit到Github仓库的main分支后.bashgit push
- 观察到Xcode Cloud自动开始构建应用, 并将构建结果发布到TestFlight上.
前提条件 
- 你的Apple ID已加入苹果开发者计划
- 一个Github账户
创建一个 Xcode 项目 
创建一个新的Bundle ID 
每一个苹果应用都有一个ID, 苹果称之为Bundle ID. 这个ID可以在苹果开发者网站中申请, 具体的网址为 Apple Developer > Account > Certificates, IDs & Profiles > Identifiers. 
创建一个App Store Connect App 
想要在App Store发布应用, 就必须用到 App Store Connect 网站. 之后像是什么更新应用, 添加TestFlight测试者, 查看Xcode Cloud日志, 全部都是在这个网站上操作的.
- 在 Apps 页面点击 New App.
- 填写 New App 表格. 你可能会问 SKU 怎么写, 其实可以随便写, 我一般直接填入Bundle ID. 
创建Xcode项目 
- 打开Xcode, 创建一个新的项目.
- 然后将此项目的Bundle ID修改为我们新创建的Bundle ID. 修改位置在左侧导航栏>app.xcodeproj>Signing & Capabilities>Signing>Bundle Identifier. 
- 修改Bundle ID后,Xcode会自动获取Profile. 点击Provisioning Profile右边的i图标, 如果全部是打勾, 则说明获取成功. 
- 在 iOS 模拟器上运行该项目, 保证该项目可以正常运行.
手动构建并上传 
注意
在设置 Xcode Cloud 自动构建之前, 请务必先完成一次手动上传. 因为这能保证您在设置 Xcode Cloud 自动构建的时候遇到的问题全部都是 Xcode Cloud 造成的, 而不是由于源码或者 TestFlight 导致的.
设置应用图标 
- 这一操作是必须的, 因为没有应用图标将会导致手动上传到TestFlight失败
- 此图片不得有alpha通道. 因为应用图标如有alpha通道, 将导致Xcode Cloud构建. 您可以通过将应用图标转换为jpeg格式来解决这个问题, 因为jpeg格式的图片没有alpha通道.
设置加密方式 
设置 Info.plist 中 ITSAppUsesNonExemptEncryption 键的值.
- 这个键的值反应了当前应用的加密方式, 如果您不知道那是什么, 可以将其设置为 false.
- 这一操作是必须的, 因为如果没有在 Info.plist中设置该键值, 每次发布TestFlight前, 您将需要在App Store Connect中手动设置应用的加密方式.
<dict>
   ...
   <key>ITSAppUsesNonExemptEncryption</key>
   <false/>
</dict>构建, 上传 
- 设置 Run Destination为Any iOS Device
- 点击 顶部菜单栏>Product>Archive以开始构建
- 在构建完成后一个名为 Archives的窗口会自动弹出(该窗口亦可通过顶部菜单栏>Window>Organizer打开). 选择刚刚生成的Archive, 然后点击Distribute App. 如此, 应用便开始上传到TestFlight. 
创建 TestFlight 测试组 
- 当Archive上传完成后, 你就能在 App Store Connect > TestFlight 网页中找到你刚刚上传的项目了.
- 在TestFlight中创建一个内部测试组(INTERNAL TESTING).
- 将测试者的Apple ID加到组里. 这一操作会使得TestFlight向你的Apple ID对应的邮箱发送一封邀请邮件.- 注意, 如果测试者不是当前开发者帐号的成员, 需要先要求他的Apple ID成为开发者帐号的成员.
 
- 注意, 如果测试者不是当前开发者帐号的成员, 需要先要求他的
接受 TestFlight 邀请 
- 测试者首先需要保证自己的设备已安装 TestFlight 应用. 如尚未安装, 请到 App Store 安装 
- 测试者检查其 Apple ID对应的邮箱, 找到来自TestFlight的邀请邮件.
- 点击邮件中的 View in TestFlight按钮, 这将打开TestFlight 应用,TestFlight 应用会询问你是否接受邀请, 点击Accept. 如此测试者将能够在TestFlight 应用中找到测试应用, 测试应用更新时测试者的设备将会收到通知. 
再次上传 
加大版本号 
如果你想上传一个船新版本新的版本到TestFlight, 那么你必须先到 左侧导航栏 > app.xcodeproj > General > Identity 加大应用的版本号. 因为 TestFlight 只接受比当前版本大的 Archive. 
Xcode Cloud 自动构建并上传 
在设置完 Xcode Cloud workflow 后, 每次 Github仓库 的 main 分支变化时, Xcode Cloud 就会自动开始构建 Archive, 完成后自动将其上传到 TestFlight.
自动加大版本号 
根据苹果的文档 Writing custom build scripts | Apple, Xcode Cloud 在构建 Archive 之前, 会运行项目文件夹下的 ci_scripts/ci_post_clone.sh 文件. 所以我们可以在该文件中运行一个js脚本来加大版本号.
#!/bin/sh
export HOMEBREW_NO_INSTALL_CLEANUP=TRUE
# Install node start
brew install node
brew link node
# Install node end
# TODO: Replace "app" with the product name of Xcode project
node changeAppInfo.mjs app#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
/**
 * Alert! Must pass Xcode project name as first param of the script.
 * ```sh
 * # app is Xcode project name
 * node changeAppInfo.mjs app
 * ```
 */
async function main() {
  try {
    const args = process.argv.slice(2);  // Skip the first two elements
    const projectName = args.at(0)
    if(projectName === undefined) {
      throw new Error("[changeAppInfo] Must pass Xcode project name as first param of the script");
    }
    const configPath = path.resolve(`../${projectName}.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 ciBuildNumber = process.env.CI_BUILD_NUMBER
  if(ciBuildNumber === undefined) {
    throw new Error("[changeAppInfo] CI_BUILD_NUMBER is required, but it's undefined");
  }
  const finalBundleVersion = `${bundleVersion}.${ciBuildNumber}`
  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()应用图标没有透明像素 
使用Xcode Cloud构建时, 应用图标不得有alpha通道, 其实就是不能有透明的像素. 你可以通过将应用图标转换为jpeg格式来解决这个问题, 因为jpeg格式的图片没有alpha通道.
创建 Github 仓库 
- 创建一个main分支, 然后将这个分支上传Github仓库.
- (可选)创建一个build_ios分支专门用于触发构建.
创建 Xcode Cloud workflow 
- 创建 workflow的按钮在Xcode>左侧导航栏>最右边那个标签>Cloud>Get Started 
- 创建过程中需要为 workflow增加一个Archive类型的Action. 其中Distribution Preparation需要选为App Store Connect, 因为这个构建最终可能会被发布到App Store 
- 首次设置需要授权 Xcode Cloud 访问 Github 仓库  
- 此时如果我们向 Github仓库的main分支提交一个commit, 应该就会触发Xcode Cloud的自动构建了. 你可以在 App Store Connect > Xcode Cloud 观察构建过程.
构建完成后自动发布到 TestFlight 
上述 workflow 配置了自动构建, 但是没有配置自动发布. 我们现在跳转到 App Store Connect > Xcode Cloud.
打开要编辑的 workflow, 然后增加一个 Post-Action, 类型为 TestFlight Internal Testing. 选择你之前创建的测试组, 然后每次 Xcode Cloud 构建完成后就会自动发布到 TestFlight 了. 