WWDC Quick Look 💓 By SwiftGGTeam
Use Xcode for server-side development

Use Xcode for server-side development

观看原视频

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 是业务逻辑层,不直接处理 HTTP
  • Storage 是数据访问层,用 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 中:

  1. 创建名为 “Shared” 的 Swift Package,放入 Xcode workspace
  2. Model 代码移到 Shared 包中
  3. 在服务器 Package 和 iOS App 中都添加 Shared 依赖
  4. 两端都使用共享的模型定义

这样做的好处:

  • 避免代码重复
  • 序列化更安全,模型变化时两端同步更新
  • 编译期检查确保数据结构一致

存储策略选择

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

评论

GitHub Issues · utterances