Windows 上开发第一个 iOS App,SwiftUI 从新建工程到装上 iPhone
这篇从一个空工程开始,在 Windows 上用 SwiftUI 写一个待办清单 App,装到自己的 iPhone 上运行和断点调试,最后打包发给朋友或同事。适合会一点编程、第一次写 iOS 的人,也适合手边只有 Windows 电脑的 iOS 开发者,全程大约一小时。
开始前准备好三样东西。一个 Apple 开发者账号,用来生成证书和描述文件。一部 iPhone 和能传数据的线。一台装好 KXBuild 和 VS Code 插件的电脑,还没装的按安装页操作。
新建工程
以下两种做法任选其一。
- VS Code 插件。按 Ctrl+Shift+P 打开命令面板,运行
KXBuild: 新建项目,模板选「iOS 应用(SwiftUI)」,项目名填MyTodo,Bundle ID 改成你自己的反向域名,例如com.yourname.mytodo,再选一个存放目录。创建完成后选「在新窗口打开」。 - 命令行。在 PowerShell 中执行下面的命令,再用 VS Code「文件 → 打开文件夹」打开
D:\projects\MyTodo。
1kxbuild project create -t app_swiftui -n MyTodo -b com.yourname.mytodo -o D:\projects

打开后左侧 KXBuild 视图列出 MyTodo 工程和同名的 app target。工程里只有三样东西,MyTodoApp.swift 是 App 入口,ContentView.swift 是首页界面,Assets.xcassets 放图标和颜色。下面的代码全部写在 ContentView.swift 里,省去往工程里添加新文件的步骤。
Bundle ID 是 App 在 Apple 那边的唯一标识,后面生成描述文件、在 App Store Connect 建 App 都要用同一个,现在定好,之后不要再改。
第一步,列表界面
先做一个只能看的列表,数据写死在代码里。把 ContentView.swift 的内容整个换成下面这段。
1import SwiftUI
2
3struct TodoItem: Identifiable, Codable {
4 var id = UUID()
5 var title: String
6 var done = false
7}
8
9struct ContentView: View {
10 @State private var items = [
11 TodoItem(title: "买牛奶"),
12 TodoItem(title: "给 App 换个图标"),
13 TodoItem(title: "把 ipa 发给同事", done: true)
14 ]
15
16 var body: some View {
17 NavigationView {
18 List(items) { item in
19 HStack {
20 Image(systemName: item.done ? "checkmark.circle.fill" : "circle")
21 .foregroundColor(item.done ? .green : .secondary)
22 Text(item.title)
23 .strikethrough(item.done)
24 }
25 }
26 .navigationTitle("待办")
27 }
28 }
29}
TodoItem 是一条待办的数据,Identifiable 让 List 能区分每一行,Codable 留给第三步存文件用。@State 标记的数组一变,界面就会自动刷新。
写的过程中你会发现,输入 Text(item.title). 之后会弹出 strikethrough、font、foregroundColor 这些修饰符,Image(systemName:) 的参数、List 的各个初始化方法也都能补全,自己写的类型按 F12 可以跳到定义。写错的地方会画红线,问题面板里有完整的错误信息。第一次打开 Swift 文件时代码提示要启动几秒,补全不出来时在命令面板运行 KXBuild: 重启代码提示,更多情况见代码提示与索引。
装到 iPhone 上运行
第一次在真机上运行要做两件事,放好证书,连好手机。以后每次只需要按 F5。
放好证书
真机运行需要 Apple Development 证书(.p12)和包含这部手机 UDID 的 Development 描述文件(.mobileprovision)。没有的话可以用 Appuploader 登录开发者账号生成,也可以从 Mac 的钥匙串导出,几种做法见证书与签名。手机的 UDID 在连上电脑后运行下面的命令查看,第一列就是。
1kxdevice device list
在工程根目录新建 keychain 文件夹,把两个文件放进去,再新建一个 password.properties,写一行「文件名=密码」。
1development.p12=123456
模板自带的 .gitignore 已经排除了 keychain 目录,证书不会被提交到仓库。
连接手机
用数据线把 iPhone 接到电脑,在 KXBuild 视图的「设备」节点点一下手机,选为当前设备。第一次连接依次完成这几件事,详见设备准备。
- 设备节点提示「点击配置」时点一下,安装 Apple 设备驱动,过程中会请求一次管理员权限。
- 手机弹出「信任此电脑」时解锁并点信任。
- 打开手机「设置 → 隐私与安全性 → 开发者模式」,手机会重启一次。这个开关在手机连过电脑之后才出现。
- 第一次调试某个 iOS 版本会自动下载几个 GB 的符号库,大约十多分钟,只有这一次。
按 F5
按 F5,KXBuild 依次构建、签名、安装、启动 App 并附加调试器,手机上出现写死的三条待办。构建日志在「输出 → KXBuild」,编译错误在问题面板,点一下跳到对应的代码行。
启动失败时通知里会写明卡在哪一步。最常见的是描述文件里没有这部手机的 UDID,把 UDID 加进描述文件、重新下载后替换 keychain 里的旧文件即可。其他提示的处理见运行与调试。
第二步,添加、删除和勾选
现在让列表能改。顶部加一个输入框,点一行切换完成状态,左滑删除,右上角的编辑按钮可以批量删。把 ContentView 换成下面的版本,TodoItem 保持不变。
1struct ContentView: View {
2 @State private var items: [TodoItem] = []
3 @State private var newTitle = ""
4
5 var body: some View {
6 NavigationView {
7 List {
8 Section {
9 HStack {
10 TextField("新的待办", text: $newTitle)
11 .onSubmit(add)
12 Button("添加", action: add)
13 .disabled(newTitle.trimmingCharacters(in: .whitespaces).isEmpty)
14 }
15 }
16 Section {
17 ForEach($items) { $item in
18 Button {
19 item.done.toggle()
20 } label: {
21 HStack {
22 Image(systemName: item.done ? "checkmark.circle.fill" : "circle")
23 .foregroundColor(item.done ? .green : .secondary)
24 Text(item.title)
25 .strikethrough(item.done)
26 .foregroundColor(.primary)
27 }
28 }
29 }
30 .onDelete { offsets in
31 items.remove(atOffsets: offsets)
32 }
33 }
34 }
35 .navigationTitle("待办")
36 .toolbar { EditButton() }
37 }
38 }
39
40 private func add() {
41 let title = newTitle.trimmingCharacters(in: .whitespaces)
42 guard !title.isEmpty else { return }
43 items.append(TodoItem(title: title))
44 newTitle = ""
45 }
46}
ForEach($items) { $item in … } 拿到的是每一行的绑定,item.done.toggle() 直接改的就是数组里那一项。onDelete 同时支持左滑删除和编辑模式下的删除。
改完代码要先停止调试(Shift+F5)再按 F5,调试工具栏上的「重启」不会重新编译。
打断点看变量
在 add() 里 items.append(...) 这一行的行号左边点一下,出现红点。回到手机上输入一条待办,点「添加」,App 停住,VS Code 切到调试视图。

左侧「变量」里能看到 title 的值,「调用堆栈」里能看到这次调用是怎么进来的。按 F10 单步执行一行,F5 继续运行。下面的调试控制台可以直接输入 lldb 命令,例如输入 po title 打印当前的标题。代码里的 print 输出显示在「输出 → KXBuild Console」。
第三步,把数据存到文件里
现在关掉 App 再打开,待办就没了。用 JSON 文件把列表存到 App 的 Documents 目录,每次改动自动保存,启动时读回来。
把数据和读写逻辑放进一个 TodoStore,界面只管显示。在 ContentView.swift 里 TodoItem 的下面加上这个类。
1final class TodoStore: ObservableObject {
2 @Published var items: [TodoItem] = [] {
3 didSet { save() }
4 }
5
6 private let fileURL = FileManager.default
7 .urls(for: .documentDirectory, in: .userDomainMask)[0]
8 .appendingPathComponent("todos.json")
9
10 init() {
11 if let data = try? Data(contentsOf: fileURL),
12 let saved = try? JSONDecoder().decode([TodoItem].self, from: data) {
13 items = saved
14 }
15 }
16
17 private func save() {
18 do {
19 let data = try JSONEncoder().encode(items)
20 try data.write(to: fileURL, options: .atomic)
21 } catch {
22 print("保存失败 \(error)")
23 }
24 }
25}
然后改 ContentView 里的四处,把 @State 数组换成这个 store。
1 @StateObject private var store = TodoStore() // 替换 @State private var items
2 // …
3 ForEach($store.items) { $item in // 原来是 ForEach($items)
4 // …
5 store.items.remove(atOffsets: offsets) // onDelete 里
6 // …
7 store.items.append(TodoItem(title: title)) // add() 里
@Published 的数组被修改时会触发 didSet,勾选、删除、添加都会写一次文件。在 init 里给 items 赋值不会触发 didSet,启动读文件时不会反过来又写一遍。
重新按 F5,加几条待办,在手机上把 App 从后台划掉再打开,数据还在。想确认文件确实写了,在 save() 的 let data = … 这行打个断点,勾选一条待办,停住后在调试控制台输入 po fileURL,能看到文件在 App 沙盒里的完整路径。
数据只有几个开关、一两个字符串时,用 @AppStorage("key") 存进 UserDefaults 更省事。待办这种列表数据存成文件更合适,以后想换成 Core Data 或 SwiftData 也好迁移。
改名字和图标
手机桌面上 App 的名字默认是 MyTodo。在 KXBuild 视图中右键 target 选「KXBuild: 编辑目标设置」,在 General 页把 Display Name 改成「待办」,Ctrl+S 保存。同一页还能改版本号和最低 iOS 版本,各字段的含义见工程设置编辑器。
图标在 KXBuild 视图的资源目录节点里打开 Assets.xcassets,给 AppIcon 放一张 1024×1024 的图,其他尺寸自动生成,见资源目录编辑器。改完再按一次 F5 就能在手机上看到。
打包发给别人
调试用的 Debug 包只能装在描述文件里列出的手机上。要发给别人装,按人数和对方是否在身边选一种方式。
用 TestFlight 发给测试员
对方不在身边、人数较多时用 TestFlight。对方在 iPhone 上装 TestFlight App,收到邀请后直接安装,不需要登记 UDID。
在 keychain 目录放入 Apple Distribution 证书和 App Store 类型的描述文件,同样在 password.properties 里加一行密码。然后以下两种做法任选其一。
- VS Code 插件。在 KXBuild 视图的「构建配置」节点切到 Release(也可以在命令面板运行
KXBuild: 选择构建配置),再运行KXBuild: 构建。 - 命令行。在工程目录执行下面的命令。
1kxbuild build .\MyTodo.xcodeproj -c Release
完成后工程目录下生成 build\MyTodo.ipa。在 App Store Connect 建好这个 App,用 Appuploader 上传 ipa,处理完成后在 TestFlight 页添加测试员,详见发布与上架。
用 Ad Hoc 直接装到身边同事的手机
几台固定的测试机可以用 Ad Hoc。在开发者网站登记同事手机的 UDID,生成包含这些设备的 Ad Hoc 描述文件,用 Distribution 证书按上面的步骤构建 Release。如果 keychain 里同时放着 App Store 和 Ad Hoc 两个描述文件,构建时用 -D 指定要用的那个。
1kxbuild build .\MyTodo.xcodeproj -c Release -D CODE_SIGN_STYLE=Manual -D CODE_SIGN_IDENTITY="Apple Distribution" -D PROVISIONING_PROFILE_SPECIFIER="MyTodo AdHoc"
把同事的手机接到你的电脑上,执行下面的命令安装。
1kxdevice install .\build\MyTodo.ipa