Highlight
Xcode 14 支持在同一个 workspace 中同时开发 iOS 客户端和 Swift 服务器端应用,使用 Vapor 等 Web 框架构建 HTTP 服务,通过共享 Swift Package 在客户端和服务端之间复用数据模型,实现端到端的 Swift 全栈开发。
核心内容
很多应用从单设备起步,随着用户增长需要扩展到多端,最终往往需要一个服务器组件来处理后台任务、计算密集型工作或集中式数据存储。
传统做法是客户端用 Swift,服务端用其他语言。这带来了技术栈割裂、代码重复、团队协作成本高等问题。
Swift 作为通用编程语言,可以统一客户端和服务端的技术栈。Xcode 14 进一步强化了这个能力:你可以在同一个 workspace 中同时开发 iOS App 和 Swift 服务器应用,共享代码,统一调试。
服务器应用的基本结构
Swift 服务器应用是一个 Swift Package,定义一个可执行目标(01:36):
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "MyServer",
platforms: [.macOS("12.0")],
products: [
.executable(name: "MyServer", targets: ["MyServer"]),
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "4.0.0")),
],
targets: [
.executableTarget(
name: "MyServer",
dependencies: [.product(name: "Vapor", package: "vapor")]
),
.testTarget(
name: "MyServerTests",
dependencies: ["MyServer"]
),
]
)
关键点:
platforms指定 macOS 12.0,服务器应用运行在 Mac 或 Linux 上products声明可执行文件- 依赖 Vapor 框架处理 HTTP 路由和请求响应
服务端代码使用 @main 标记入口点(02:00):
import Vapor
@main
public struct MyServer {
public static func main() async throws {
let webapp = Application()
webapp.get("greet", use: Self.greet)
webapp.post("echo", use: Self.echo)
try webapp.run()
}
static func greet(request: Request) async throws -> String {
return "Hello from Swift Server"
}
static func echo(request: Request) async throws -> String {
if let body = request.body.string {
return body
}
return ""
}
}
关键点:
Application是 Vapor 提供的 Web 应用类型get/post定义 HTTP 端点- 处理函数用
async throws,天然支持 Swift 并发
本地测试
在 Xcode 中选择 MyServer scheme,目标选 “My Mac”,点击运行。控制台会显示服务器启动信息:
Server starting on http://127.0.0.1:8080
用 curl 测试端点(03:42):
curl http://127.0.0.1:8080/greet; echo
curl http://127.0.0.1:8080/echo --data "Hello from WWDC 2022"; echo
从 iOS App 调用
创建一个客户端抽象层,把服务器 API 封装成 Swift 方法(04:10):
import Foundation
struct MyServerClient {
let baseURL = URL(string: "http://127.0.0.1:8080")!
func greet() async throws -> String {
let url = baseURL.appendingPathComponent("greet")
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
guard let responseBody = String(data: data, encoding: .utf8) else {
throw Errors.invalidResponseEncoding
}
return responseBody
}
enum Errors: Error {
case invalidResponseEncoding
}
}
在 SwiftUI 视图中调用(05:00):
import SwiftUI
struct ContentView: View {
@State var serverGreeting = ""
var body: some View {
Text(serverGreeting)
.padding()
.task {
do {
let client = MyServerClient()
self.serverGreeting = try await client.greet()
} catch {
self.serverGreeting = String(describing: error)
}
}
}
}
关键点:
- 网络请求天然异步,用
async/await处理 .task修饰符在视图出现时自动启动异步任务- 记得先在 Xcode 中启动服务器,再运行 iOS App
详细内容
Food Truck 示例:完整的服务端应用
Session 用一个 Food Truck(餐车)App 演示全栈开发。服务端提供甜甜圈菜单 API,iOS 客户端展示菜单。
数据模型
服务端和客户端需要一致的数据模型(09:51):
enum Model {
struct Donut: Codable {
var id: Int
var name: String
var date: Date
var dough: Dough
var glaze: Glaze?
var topping: Topping?
}
struct Dough: Codable {
var name: String
var description: String
var flavors: FlavorProfile
}
struct Glaze: Codable {
var name: String
var description: String
var flavors: FlavorProfile
}
struct Topping: Codable {
var name: String
var description: String
var flavors: FlavorProfile
}
public struct FlavorProfile: Codable {
var salty: Int?
var sweet: Int?
var bitter: Int?
var sour: Int?
var savory: Int?
var spicy: Int?
}
}
关键点:
- 所有模型遵循
Codable,用于 JSON 编解码 - 口味配置文件用可选整数表示各种味道强度
- 这些模型最初从 iOS App 复制过来,后面会提取到共享包中
服务端架构
服务端分层设计:
import Foundation
import Vapor
@main
struct FoodTruckServerBootstrap {
public static func main() async throws {
let foodTruckServer = FoodTruckServer()
try await foodTruckServer.bootstrap()
let webapp = Application()
webapp.get("donuts", use: foodTruckServer.listDonuts)
try webapp.run()
}
}
struct FoodTruckServer {
private let storage = Storage()
func bootstrap() async throws {
try await self.storage.load()
}
func listDonuts(request: Request) async -> Response.Donuts {
let donuts = await self.storage.listDonuts()
return Response.Donuts(donuts: donuts)
}
enum Response {
struct Donuts: Content {
var donuts: [Model.Donut]
}
}
}
关键点:
FoodTruckServer是业务逻辑层,不直接处理 HTTPStorage是数据访问层,用 actor 保证线程安全Response.Donuts遵循 Vapor 的Content协议,自动编码为 JSON
存储层:用 Actor 保护可变状态
存储层用 JSON 文件作为数据源,用 actor 避免数据竞争(12:30):
actor Storage {
let jsonDecoder: JSONDecoder
var donuts = [Model.Donut]()
init() {
self.jsonDecoder = JSONDecoder()
self.jsonDecoder.dateDecodingStrategy = .iso8601
}
func load() throws {
guard let path = Bundle.module.path(forResource: "menu", ofType: "json") else {
throw Errors.menuFileNotFound
}
guard let data = FileManager.default.contents(atPath: path) else {
throw Errors.failedLoadingMenu
}
self.donuts = try self.jsonDecoder.decode([Model.Donut].self, from: data)
}
func listDonuts() -> [Model.Donut] {
return self.donuts
}
enum Errors: Error {
case menuFileNotFound
case failedLoadingMenu
}
}
关键点:
actor确保对donuts数组的所有访问都是串行的- 不需要手动使用锁或调度队列
Bundle.module是 SwiftPM 为资源文件生成的访问器iso8601日期解码策略处理标准日期格式
Package manifest 中声明资源文件(12:23):
.executableTarget(
name: "FoodTruckServer",
dependencies: [.product(name: "Vapor", package: "vapor")],
resources: [
.copy("menu.json")
]
)
测试服务端 API
启动服务器后,用 curl 和 jq 测试(14:42):
# 查看完整菜单
curl http://127.0.0.1:8080/donuts | jq .
# 只提取甜甜圈名称
curl http://127.0.0.1:8080/donuts | jq '.donuts[] .name'
共享数据模型
客户端和服务端最初各自维护一份数据模型代码。更好的做法是提取到共享 Swift Package 中:
- 创建名为 “Shared” 的 Swift Package,放入 Xcode workspace
- 把
Model代码移到 Shared 包中 - 在服务器 Package 和 iOS App 中都添加 Shared 依赖
- 两端都使用共享的模型定义
这样做的好处:
- 避免代码重复
- 序列化更安全,模型变化时两端同步更新
- 编译期检查确保数据结构一致
存储策略选择
Session 提到了几种数据存储方案(11:12):
静态文件:数据变化很少或手动维护时,直接用 JSON/CSV 文件。适合菜单、配置等场景。
iCloud:用户数据或全局数据集,可以直接从 iOS App 使用 CloudKit API,不需要部署独立服务器。
数据库:动态或事务性数据。Swift 开源社区提供了多种数据库驱动:PostgreSQL、MySQL、MongoDB、Redis、DynamoDB 等。
核心启发
1. 实时协作白板
做什么:用 Swift 服务端 + SwiftUI 客户端做一个多人实时协作白板,支持笔画同步。
为什么值得做:WebSocket 服务端用 Swift 编写,与 iOS/macOS 客户端共享笔画数据模型,一套代码理解整个系统。
怎么开始:用 Vapor 的 WebSocket 扩展处理实时连接,用 actor 管理房间状态,客户端用 URLSessionWebSocketTask 连接。
2. 个人数据同步服务
做什么:为现有 iOS App 添加私有云同步功能,用户数据存储在自托管服务器上。
为什么值得做:用 Swift 写服务端可以复用客户端的 Codable 模型,不需要维护两套数据结构定义。
怎么开始:定义共享 Package 存放数据模型,服务端用 Vapor 提供 CRUD API,客户端用 URLSession + JSONDecoder 消费。
3. 家庭媒体服务器
做什么:在 Mac 或树莓派上运行 Swift 服务器,管理家庭照片和视频库,iOS App 浏览和播放。
为什么值得做:Swift 的 AVFoundation 知识可以部分复用到服务端,处理媒体元数据和转码逻辑。
怎么开始:用 Vapor 提供媒体文件流式传输 API,用 FileManager 扫描媒体目录生成索引,客户端用 SwiftUI 展示网格。
4. IoT 设备管理中心
做什么:用 Swift 服务端收集家庭传感器数据(温度、湿度),iOS App 展示历史趋势和实时告警。
为什么值得做:Swift 服务端可以跑在轻量级设备上,用 async/await 处理大量并发设备连接。
怎么开始:定义传感器数据模型共享包,服务端用 UDP/TCP 接收设备上报,存入 SQLite 或 InfluxDB,提供查询 API。
5. 团队内部工具平台
做什么:用 Swift 全栈开发团队内部工具(如代码审查助手、发布管理系统)。
为什么值得做:团队已经熟悉 Swift,全栈统一技术栈降低维护成本,Xcode workspace 同时调试前后端。
怎么开始:用 Vapor 提供 REST API,集成 GitHub/GitLab Webhook,客户端用 SwiftUI 展示待办事项和统计图表。
关联 Session
- Meet Swift Package plugins — 用插件自动化服务器端代码生成和部署工作流
- Swift Concurrency — 深入理解
async/await和actor,编写高并发服务端代码 - Swift Regex — 用 Swift Regex 解析请求参数、路由匹配和日志分析
评论
GitHub Issues · utterances