跳到主要内容
教程目录

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