跳到主要内容
教程目录

把 Swift Package 变成能装到 iPhone 的 iOS App

用 Swift Package 或 Swift Playgrounds 写的 App,代码能编译,却没法直接装到 iPhone 上,也打不出 ipa,在 Mac 上也是一样。这篇在 Windows 上把这样的包转成一个 iOS App 工程,装到真机调试,再打包上架。之后你照常改包里的代码,App 工程跟着一起变。

开始前准备好一部 iPhone 和数据线、Apple 开发者账号下的证书,以及装好 KXBuild 和 VS Code 插件的电脑,还没装的按安装页操作。

哪些包能转

能转的是可执行 target 本身就是 App 的包,也就是可执行 target 里有 @main struct …: App 这样的 SwiftUI 入口,或者 UIKit 的 UIApplicationDelegate。其他几种情况这样处理。

  • 只提供库的包。它是给 App 用的,不转换。在 App 工程里添加 Swift Package 依赖即可。
  • 命令行程序。可执行 target 里没有 App 入口,iOS 上跑不了命令行程序,也不转换。
  • Swift Playgrounds 的 App 项目。目前不能直接转换,先整理成普通的包,做法见后文。

下面用一个最小的包演示,目录是 D:\projects\Hello。Hello 是 App,HelloCore 是 App 用到的一个库 target。

1Hello\
2├─ Package.swift
3└─ Sources\
4   ├─ Hello\
5   │  └─ HelloApp.swift
6   └─ HelloCore\
7      └─ Greeting.swift
 1// swift-tools-version:5.9
 2import PackageDescription
 3
 4let package = Package(
 5    name: "Hello",
 6    platforms: [.iOS(.v16)],
 7    products: [
 8        .executable(name: "Hello", targets: ["Hello"])
 9    ],
10    targets: [
11        .executableTarget(name: "Hello", dependencies: ["HelloCore"]),
12        .target(name: "HelloCore")
13    ]
14)
 1import SwiftUI
 2import HelloCore
 3
 4@main
 5struct HelloApp: App {
 6    var body: some Scene {
 7        WindowGroup {
 8            Text(Greeting.text)
 9        }
10    }
11}

在 VS Code 中「文件 → 打开文件夹」打开 Package.swift 所在目录。打开 .swift 文件就有补全和错误提示,SwiftUI、UIKit 的 API 按 iOS 补全。转换前可以先在包目录编译检查一次。

1kxbuild build .

转为 App 工程

以下两种做法任选其一。

  • VS Code 插件。命令面板运行 KXBuild: 将 Swift Package 转为 App 工程,或者点 KXBuild 视图里标着「点击生成 App 工程」的那一行。按提示填 App 名和 Bundle ID,App 名留空就用可执行 target 的名字。完成后新工程自动设为启动项目。
  • 命令行。在 Package.swift 所在目录执行下面的命令。之后在 KXBuild 视图中点一下 Hello 工程节点,把它设为启动项目。
1kxbuild project spm2app . -n Hello -b com.yourname.hello

Bundle ID 换成你自己的反向域名,后面生成描述文件、在 App Store Connect 建 App 都用它。转换完成后包目录下多了一个和 App 同名的子目录,里面就是 App 工程。

 1Hello\
 2├─ Package.swift
 3├─ Sources\
 4│  ├─ Hello\
 5│  └─ HelloCore\
 6└─ Hello\
 7   ├─ .gitignore
 8   ├─ Hello.xcodeproj
 9   └─ Hello\
10      └─ Assets.xcassets

App 工程里没有复制源码,它直接编译 Sources\Hello 目录,包里的其他 target 按本地包引用。所以改包里的代码就是改 App,这个子目录要和包一起提交到版本库。子目录里自带的 .gitignore 已经排除了构建产物和证书目录。

示例里 App 用到了 HelloCore,而包里没有暴露它的库产品,App 工程引用不到。这时转换会在 Package.swift 的 products 里补一行,插件会弹出提示,命令行输出的 manifestChanges 里也会列出来。转换完成后检查并提交 Package.swift 的这处改动。

1    products: [
2        .executable(name: "Hello", targets: ["Hello"]),
3        .library(
4            name: "HelloCore",
5            targets: [ "HelloCore" ]
6        ),
7    ],

手边还没有证书时,可以先不签名编译一次,确认工程没问题。

1kxbuild build .\Hello\Hello.xcodeproj -D CODE_SIGNING_ALLOWED=NO

Swift Playgrounds 的 App 项目

Swift Playgrounds 建的 App 项目是一个 .swiftpm 目录,Package.swift 里有 import AppleProductTypes,用 .iOSApplication 描述 App。这种包目前不能直接转换,运行转换会提示暂不支持。做法是另建一个普通的包,把源码复制过去再转换,原来的 .swiftpm 不动,在 iPad 上照常打开。

下面以 D:\projects\PGApp.swiftpm 为例。先把其中的 .swift 文件连同子文件夹复制到新包的 Sources\PGApp 目录,Package.swift 除外。

1robocopy D:\projects\PGApp.swiftpm D:\projects\PGApp\Sources\PGApp *.swift /S /XF Package.swift

再在 D:\projects\PGApp 下新建 Package.swift,写入下面的内容,最低 iOS 版本照原项目的 platforms 填。

 1// swift-tools-version: 5.9
 2import PackageDescription
 3
 4let package = Package(
 5    name: "PGApp",
 6    platforms: [.iOS(.v16)],
 7    targets: [
 8        .executableTarget(name: "PGApp")
 9    ]
10)

然后按上一节转换。原项目 .iOSApplication 里写的 Bundle ID 用 -b 带上,版本号、显示名称、图标在下一节的编辑器里改。原项目有 Assets.xcassets 的,把里面的图片集和颜色集复制到新 App 工程的 Assets.xcassets 中。

1cd D:\projects\PGApp
2kxbuild project spm2app . -b com.yourname.pgapp

改名称、图标和权限说明

名称与版本号

在 KXBuild 视图中右键 Hello target 选「KXBuild: 编辑目标设置」,General 页可以改 Display Name、Bundle Identifier、版本号和最低 iOS 版本,Ctrl+S 保存。各字段见工程设置编辑器。

工程设置编辑器的 General 页

图标

图标在 App 工程的 Hello\Hello\Assets.xcassets 里。在 KXBuild 视图中点工程节点下的资源目录打开它,把 AppIcon 的占位图换成一张 1024×1024 的 PNG,见资源目录编辑器。

权限说明

转换出的工程没有 Info.plist 文件,构建时自动生成。用到相机、相册、定位这类权限时,把说明文字写成 INFOPLIST_KEY_ 开头的构建设置,构建后会出现在 App 的 Info.plist 里。以相机为例,以下两种做法任选其一。

  • VS Code 插件。在工程设置编辑器切到 Build Settings 页,在值单元格上右键选「添加用户自定义设置」,名称填 INFOPLIST_KEY_NSCameraUsageDescription,值填给用户看的说明,Ctrl+S 保存。
  • 命令行。在包目录执行下面的命令。
1kxbuild project add-setting -k INFOPLIST_KEY_NSCameraUsageDescription -v "用于扫描二维码" -t Hello -P .\Hello\Hello.xcodeproj

装到 iPhone 上调试

真机运行需要 Apple Development 证书(.p12)和包含这部手机 UDID 的 Development 描述文件。在 App 工程目录 Hello\Hello 下新建 keychain 文件夹,把两个文件放进去,再新建 password.properties,写一行「文件名=密码」。想让所有工程共用的,放到 KXBuild 安装目录下的 keychain 文件夹。证书的获取方式见证书与签名。

1development.p12=123456

用数据线接上 iPhone,在 KXBuild 视图的「设备」节点选中它。第一次连接要安装设备驱动、在手机上点信任并打开开发者模式,见设备准备。

确认启动项目是 Hello,在 Sources\Hello 的代码里打断点,按 F5。KXBuild 构建、签名、安装、启动 App 并停在断点上,HelloCore 这类库 target 里的代码同样能断点。改完代码先 Shift+F5 停止再按 F5。变量查看、调试控制台和崩溃定位见 在 Windows 上真机调试 iOS App。

继续开发,改代码和加依赖

转换只做一次,之后照常在包里开发。按改动的类型处理。

  • 改代码、在 Sources\Hello 里新建或删除 .swift 文件。直接构建或按 F5,App 工程跟着这个目录走,不用做别的。
  • 改包里库 target 的代码。同样直接构建。
  • 给 App 的可执行 target 新增依赖。Package.swift 里改完之后,App 工程也要加上同一个依赖,否则构建报 no such module。

新增的依赖是包里的另一个 target 时,先在 Package.swift 的 products 里给它加一个库产品,例如 .library(name: "HelloExtra", targets: ["HelloExtra"])。然后以下两种做法任选其一。

  • VS Code 插件。命令面板运行 KXBuild: 添加 Swift Package 依赖…,选择包目录 D:\projects\Hello 作为本地包,产品选 HelloExtra。
  • 命令行。在包目录执行下面的命令。
1kxbuild project add-package -P .\Hello\Hello.xcodeproj --path . --product HelloExtra

新增的依赖是远程包时,App 工程加同一个包和产品,版本规则和 Package.swift 里写的保持一致。以下两种做法任选其一。

  • VS Code 插件。命令面板运行 KXBuild: 添加 Swift Package 依赖…,输入包的 Git 地址,选版本规则和产品。
  • 命令行。在包目录执行下面的命令,以 Alamofire 为例。
1kxbuild project add-package -P .\Hello\Hello.xcodeproj --url https://github.com/Alamofire/Alamofire.git --from 5.9.0 --product Alamofire

打包 ipa 上架

上架用 Apple Distribution 证书和 App Store 类型的描述文件,放进同一个 keychain 目录,在 password.properties 里加一行密码。然后以下两种做法任选其一。

  • VS Code 插件。在 KXBuild 视图的「构建配置」节点切到 Release,命令面板运行 KXBuild: 构建。
  • 命令行。在包目录执行下面的命令。
1kxbuild build .\Hello\Hello.xcodeproj -c Release

完成后生成 Hello\build\Hello.ipa。在 App Store Connect 建 App、上传 ipa、填写资料和提交审核的完整流程见在 Windows 上上架 App Store。

常见问题

提示 only provides libraries

包里只有库产品,没有 App。这种包不转换,新建一个 App 工程再把它作为本地包添加进去,见 Swift 技术栈。

提示 has no app to convert

可执行 target 里找不到 App 入口,是命令行程序。确认入口文件里有 @main 标记的 App 结构体,或者 UIApplicationDelegate。

包里有多个 App

命令行用 --executable 指定要转的那个,每个 App 各转一次,工程分别建在各自的子目录里。

1kxbuild project spm2app . -n HelloPro -b com.yourname.hellopro --executable HelloPro

提示 choose another place with -o

可执行 target 的源码目录就是包根目录(Package.swift 里写了 path: "."),工程建在包里会被当成 App 源码一起编译。用 -o 把工程放到包目录外,例如 -o ..。

转换输出里有 warnings

可执行 target 在 Package.swift 里写的几样设置不会带到 App 工程,输出里会逐条列出。

  • resources。资源会拷进 App,但 App 里没有 Bundle.module,读取资源的代码改用 Bundle.main。
  • swiftSettings、linkerSettings 等编译设置。在工程设置编辑器的 Build Settings 页里重新设置。
  • 构建插件。不会在 App 工程里运行。

链接报 …vpfi 未定义

Debug 构建在链接阶段报 undefined symbol: …vpfi,是开源 Swift 编译器的已知问题。@State private var count = 0 这类带初始值的属性,所在结构体的 init 写在另一个文件里就会出现。App 的代码在包里,属于本地包,默认按文件编译。设置环境变量 KXBUILD_SWIFT_WHOLE_MODULE=all 改为整模块编译,重开 VS Code 后再构建即可。

1setx KXBUILD_SWIFT_WHOLE_MODULE all

能不能重新转换一次

可以加 --force 在原目录重新生成,但整个 App 工程会重建,之前在工程设置里改过的内容和换过的图标都会还原。只是加了依赖的,按上文添加即可,不必重新转换。