Skip to content

构建一个 iOS 应用并上传到 TestFlight

效果演示

  1. 提交一个commitGithub仓库main分支.
    bash
    git push
  2. 观察到Xcode Cloud自动开始构建应用, 并将构建结果发布到TestFlight上.

前提条件

  1. 你的Apple ID已加入苹果开发者计划
  2. 一个Github账户

创建一个 Xcode 项目

创建一个新的Bundle ID

每一个苹果应用都有一个ID, 苹果称之为Bundle ID. 这个ID可以在苹果开发者网站中申请, 具体的网址为 Apple Developer > Account > Certificates, IDs & Profiles > Identifiers. picture 0

创建一个App Store Connect App

想要在App Store发布应用, 就必须用到 App Store Connect 网站. 之后像是什么更新应用, 添加TestFlight测试者, 查看Xcode Cloud日志, 全部都是在这个网站上操作的.

  1. 在 Apps 页面点击 New App.
  2. 填写 New App 表格. 你可能会问 SKU 怎么写, 其实可以随便写, 我一般直接填入Bundle ID. picture 1

创建Xcode项目

  1. 打开Xcode, 创建一个新的项目.
  2. 然后将此项目的Bundle ID修改为我们新创建的Bundle ID. 修改位置在 左侧导航栏 > app.xcodeproj > Signing & Capabilities > Signing > Bundle Identifier. picture 2
  3. 修改Bundle ID后, Xcode会自动获取Profile. 点击 Provisioning Profile 右边的i图标, 如果全部是打勾, 则说明获取成功. picture 3
  4. 在 iOS 模拟器上运行该项目, 保证该项目可以正常运行.

手动构建并上传

首次上传

  1. 设置 Run DestinationAny iOS Device
  2. 点击 顶部菜单栏 > Product > Archive 以开始构建
  3. 在构建完成后一个名为 Archives 的窗口会自动弹出(该窗口亦可通过 顶部菜单栏 > Window > Organizer 打开). 选择刚刚生成的 Archive, 然后点击 Distribute App. 如此, 应用便开始上传到TestFlight. picture 0
  4. Archive上传完成后, 你就能在 App Store Connect > TestFlight 网页中找到你刚刚上传的项目了.
  5. TestFlight中设置该版本的加密方式, 我一般选未加密. 如果不想每次都手动设置加密方式的话, 我们可以在Info.plist中设置应用加密方法.
    xml
    <dict>
     ...
     <key>ITSAppUsesNonExemptEncryption</key>
     <false/>
    </dict>
  6. TestFlight中创建一个内部测试组(INTERNAL TESTING). 把你的iPhone上使用的Apple ID加到组里, 然后你就能在你的注册Apple ID所使用的邮箱中找到一封来自TestFlight的邀请. 点击View in TestFlight这将打开TestFlight, TestFlight会询问你是否接受邀请, 点击Accept. 如此以后你就能在你的iPhoneTestFlight应用中找到测试应用, 测试应用更新时你的iPhone会收到通知. picture 5

加大版本号

如果你想上传一个船新版本新的版本到TestFlight, 那么你必须先到 左侧导航栏 > app.xcodeproj > General > Identity 加大应用的版本号. 因为 TestFlight 只接受比当前版本大的 Archive. picture 2

Xcode Cloud 自动构建并上传

在设置完 Xcode Cloud workflow 后, 每次 Github仓库main 分支变化时, Xcode Cloud 就会自动开始构建 Archive, 完成后自动将其上传到 TestFlight.

创建 Github 仓库

创建一个main分支, 然后将这个分支上传Github仓库.

创建 Xcode Cloud workflow

  1. 创建 workflow 的按钮在 Xcode > 左侧导航栏 > 最右边那个标签 > Cloud > Get Startedpicture 1
  2. 创建过程中需要为 workflow 增加一个 Archive 类型的 Action. 其中 Distribution Preparation 需要选为 App Store Connect, 因为这个构建最终可能会被发布到 App Storepicture 10
  3. 此时如果我们向 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 了. picture 9

自动加大版本号

根据苹果的文档 Writing custom build scripts | Apple, Xcode Cloud 在构建 Archive 之前, 会运行项目文件夹下的 ci_scripts/ci_post_clone.sh 文件. 所以我们可以在该文件中运行一个js脚本来加大版本号.

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()

应用图标

使用Xcode Cloud构建时, 应用图标不得有alpha通道, 其实就是不能有透明的像素. 你可以通过将应用图标转换为jpeg格式来解决这个问题, 因为jpeg格式的图片没有alpha通道.

参考材料