大多数浏览器和
Developer App 均支持流媒体播放。
-
在服务器端开发中运用 Xcode
了解如何在同一个工作区内开发、构建与部署除原有 Xcode 项目以外的 Swift 服务器 App。我们将介绍如何利用 Xcode 创建您自己的本地 App 和测试端点,并探索如何在服务器和客户端 App 之间设计结构与共享代码,以简化您的开发流程。
资源
相关视频
WWDC23
WWDC22
WWDC21
WWDC20
WWDC19
-
下载
♪ ♪
Tom: 大家好 我是 Tom 是 Apple 的 Swift 团队的一员 今天我想分享一下如何将 iOS App 扩展到云端 我们的许多 App 刚开始都 专注于单一设备 通常是 iPhone 随着用户的增长 我们发现 自己希望将其应用于 其他设备上 例如 Mac 和 Apple Watch 或其他 Apple 平台和设备 Xcode 帮助我们为这些平台 组织和构建 App 我们可以使用软件包来共享代码 同时在平台特定的 App 代码中 包含每个设备的独特方面 随着系统的不断完善和发展 App 通常需要使用服务器组件 来补充客户端 App 这些服务器组件使客户端 App 能够将其功能扩展到云中 例如 卸载可以在后台完成的任务 卸载计算量大的任务 或需要访问设备上 无法访问的数据的任务 通常 服务器组件需要使用 不同于客户端组件的工具 和方法来构建 从而产生重复工作和集成挑战 使用 Swift 构建服务器组件 有助于弥合这一技术差距 在整个栈中提供熟悉的环境 让我们看看用 Swift 构建服务器 App 是什么样子的 将服务器 App 建模为 Swift 软件包 此软件包定义了映射到 App 入口点的可执行目标 为了将 App 变成 Web App 我们可以在 Web 框架上 添加依赖项 以帮助我们构建代码 并提供路由等基本实用程序 在此示例中 我们使用 Vapor Web 框架 这是个用于构建 Web 服务的 开源社区项目
与其它基于 Swift 的可执行文件一样 该程序的入口点最好使用 @main 注释来建模 为了集成 Web 框架 我们在 main 函数中 添加了相关的引导代码 本例中使用的 App 类型 是由 Web 框架 提供的 有了基本的引导 我们可以让 App 做些有用的事情 例如 我们添加代码来问候 向服务器发出请求的用户 我们使用 Web 框架 来定义 HTTP 端点 并将其指向提供问候语的方法 再进一步 我们添加了第二个 HTTP 端点 该端点处理 HTTP post 请求 并将请求正文内容回传给调用者 我们看看实际情况 这里我们有 Xcode 中的服务器 App 因为我们刚刚开始 可以在我们自己的机器上 本地运行服务器来进行测试 为了在本地运行 我们选择由 Xcode 为我们生成的 MyServer 方案 使用 My Mac 作为目标 然后点击 运行
一旦 App 启动 我们可以使用 Xcode 控制台 来检查服务器发出的日志消息 在这种情况下 我们可以看到服务器启动 并侦听本地主机地址 (127.0.0.1) 端口 8080 我们可以使用这些信息 来测试我们的服务器 让我们切换到终端 并向公布的服务器地址发出请求 我们使用一个名为 curl 的 实用程序来发出请求 用我们的第一个端点
以及第二个端点 向 echo 传入一些数据
很好 使用终端确实很有趣 但我们真正想知道的是如何从 iOS App 调用服务器 让我们深入研究一下 这里是一个 Swift 数据结构的例子 我们可以用它来抽象与服务器的交互 我们将服务器 API 建模为 抽象的异步方法 因为网络本身就是异步的 我们使用 URLSession 发出异步请求 然后解析服务器响应 最后将其返回给调用者 在本例中 服务器响应是个纯字符串 但实际上 它可能更复杂 例如 响应可能以 JSON 进行编码 在这种情况下 我们可以使用 Swift 的 Codable 系统对其进行解码 让我们把这些放在 Xcode 中 我们正在使用 Xcode 工作区 来同时构建 和测试 iOS 与服务器 App 我们已经准备好了 iOS App 服务器抽象 我们使用放在一起的代码 改变默认的 SwiftUI ContentView 来获取服务器问候语 首先 我们创建一个名为 serverGreeting 的状态变量
接下来 我们将 serverGreeting 绑定到文本显示
最后 我们添加一个任务 来调用服务器 API 并设置状态
准备好代码后 我们可以在模拟器中运行 App 我们选择 MyApp 方案 一个模拟器 然后点击 运行
哦 不 出错了 嗯 似乎是某种连接错误 地址似乎是正确的 所以我们一定是忘记 启动本地服务器了 让我们切换回 Xcode 选择服务器方案 并运行服务器
现在 我们重新启动 App 希望这次能成功
哇哦 果然成功了 为了完成演示的这一部分 我们将 App 部署到云上 有许多云提供商可供选择 包括 AWS Google Cloud Azure Heroku 等等 在本例中 我们将使用 Heroku Heroku 有一个方便的 git 推送部署系统 用于像这个演示 App 一样的小项目 让我们切换到终端开始部署 在设置好我们的帐户 并使用 Heroku 服务配置 App 后 我们可以将代码 推送到 Heroku 远程端
.
就是这样 Heroku 使用一种叫做 buildpacks 的技术远程编译 App 然后将二进制构件部署到临时主机上 Heroku swift buildpack 是由 Swift 开源社区的成员构建 可供所有 Swift on Server 用户使用 部署 App 之后 我们可以使用 curl 对其进行测试 就像对本地服务器所做的那样 我们来测试第一个端点
在这里复制地址
还有第二个
这一次 我们将发送一个 不同的有效载荷
太好了 我们的 App 部署成功了 在继续之前 我们暂停一下 回顾一下这部分演讲的 主要内容 如果您已经在使用 Swift 构建 iOS 或 macOS App 您也可以使用它 来开发系统的服务器端 Xcode 帮助我们在一个工作区 开发和调试 系统的不同组件 包括客户端和服务器 最后 您可以选择云提供商 来部署基于 Swift 的服务器 App 有关部署到这些云平台的更多信息 请参阅 swift.org 上的 Swift Server 文档 现在我们已经看到了一个基本的设置 让我们来看一个更真实的示例 Food Truck 您可能在我们的许多讲座中 看到过这个 App 让我们来深入了解一下如何管理数据 嗯 看起来甜甜圈列表是硬编码的 这意味着 App 的用户 看到的甜甜圈菜单 可能与实际可用的不同 虽然这可能对小型的 Food Truck 操作很有用 可以在现场 制作任何种类的甜甜圈 但我们希望建立一个甜甜圈帝国 菜单是集中的 卡车都是关于客户服务的 让我们设计一下 集中式 Food Truck 系统的外观
我们从 iOS App 的内存存储开始 为了集中菜单 我们可以从 iOS App 中提取存储 并移动到服务器 这将允许 App 的所有用户 共享相同的存储空间 从而共享相同的甜甜圈菜单 与第一部分的示例类似 我们的服务器将公开一个 基于 HTTP 的 API iOS App 将使用抽象来处理这些 API 然后将它们联系到表示层 在本例中为 SwiftUI 我们的设计完成了 是时候写一些甜蜜的代码了 您可以从开发者资源包中 下载 Food Truck 示例 App 我们开始使用 App 框架 来构建服务器 然后为甜甜圈 Web API 定义一个 HTTP 端点 并将其指向我们服务器抽象中的 listDonuts 方法 您可能已经注意到 API 返回了一个 Donuts 类型的 Response 并且 Response.Donuts 复合名为 Content 的协议 Content 协议由 Web 框架定义 帮助我们在线上将响应编码为 JSON 您可能还注意到 API 包含一个 神秘的 Model.Donut 的数组 我们尚未定义它 那么 这就是我们出色的数据模型 甜甜圈 面团 釉料和浇头 这里有一点很有趣 我们从 Food Truck iOS App 中 复制了该模型的定义 因为我们需要服务器和客户端的 数据模型大致对齐 另一个有趣的点是 对可编码协议的一致性 这是必需的 这样我们的服务器才能在线上 将模型对象编码为 JSON 有了数据模型和基本 API 后 我们可以扩展逻辑 以包含存储抽象 存储将向 App 提供 可用的甜甜圈列表 此时 我们应该拥有一个 功能齐全的服务器 但是等等 我们的甜甜圈菜单空了 我们应该从哪里获得集中式菜单呢 在设计服务器端 App 时 存储始终是一个有趣的话题 根据用例的不同 有几种策略可供选择 如果 App 数据是静态的 或者更改非常慢且需要手动 则磁盘上的文件可能会 提供足够好的解决方案 对于以用户为中心的数据 或全局数据集 iCloud 提供了一组 API 您可以直接从 iOS App 中使用 而无需部署专用服务器 在处理动态数据或事务性数据时 数据库提供了一个很好的解决方案 服务器端 App 可以 使用各种数据库技术 每种技术都是针对特定的性能 数据一致性和数据建模需求 而设计的 多年来 Swift 开源社区 开发了有助于与大多数数据库技术 进行本地交互的数据库驱动程序 部分列表包括 Postgres MySQL MongoDB Redis DynamoDB 等 为了简化此演示 我们将仅演示静态文件存储策略 但您可以在 swift.org 上的 Swift Server 文档中 了解有关使用数据库的更多信息 由于我们使用的是静态文件存储策略 所以我们首先创建一个 捕获甜甜圈菜单的 JSON 文件 创建此文件后 我们可以使用 SwiftPM 的资源支持 使其可供 App 访问 有了这些 是时候让我们的 存储抽象更复杂一点了 也就是说 我们添加了一个 load 方法 此方法使用 SwiftPM 生成的 资源访问器来查找资源文件路径 然后使用 FileManager API 将文件内容加载到内存中 最后 我们使用 JSONDecoder 将 JSON 内容解码为 服务器 App 数据模型 一个有趣的变化是 存储现在被定义为参与者 我们选择使用参与者是因为 存储现在有一个可变的甜甜圈变量 并且 load 和 listDonuts 方法 可以同时访问该变量 Actors 是在 Swift 5.5 中首次引入的 可以帮助我们避免数据竞争 并以一种安全但简单的方式 处理共享的可变状态 引入参与者之前 我们需要在使用 Locks 或 Queues 等 API 访问可变状态时记住并添加同步块 随着存储更新的完成 我们可以将它们结合在一起 我们将“引导”方法添加到 服务器抽象 并从那里加载存储 然后 我们将引导程序 连接到可执行文件入口点 请注意 由于存储现在是一个参与者 所以我们在异步上下文中访问它 我们的服务器准备好了 我们切换到客户端 首先添加一个服务器抽象 来帮助我们封装服务器 API 我们使用 URLSession 来发出 HTTP 请求 使用 JSONDecoder 解码服务器响应 并将其从 JSON 转换为 我们的 iOS App 模型 此时 我们可以移除硬编码菜单 并将其替换为来自服务器的异步获取 最后 我们从 ContentView 加载任务 调用服务器 该测试了 这一次 不要忘记启动服务器 我们将在这里选择 FoodTruckServer 方案 点击运行
随着 App 的运行 我们跳转到终端 并看到我们可以访问 API
重新复制地址
这一次 我们将使用一个 名为 jq 的实用程序 更准确地打印 JSON 输出 看起来不错
好了 是时候测试我们的 App 了
切换到 Xcode 选择这里的 Food Truck 方案 模拟器 然后运行它
这样就可以了 我们集中式菜单上的三个甜甜圈 我们可以把它和服务器上的信息 进行交叉比对 我们换回终端 为了方便比较 我们将使用 jq 来查询甜甜圈的名称
Coffee Caramel– 正是我们所期望的 太棒了 但我们可以做得更好 就目前而言 我们的服务器和客户端 App 都具有相同的数据模型代码副本 通过在 iOS 和服务器 App 中 共享模型 我们可以避免重复 并使序列化更安全 我们回顾一下如何进行高级设置 首先 我们为名为 Shared 的库 创建另一个包 并将其添加到 Xcode 工作区 然后 我们可以将数据模型代码 移到 Shared 包中 使用 Target Frameworks 和 Libraries 设置 将 Shared 添加为服务器 App 的依赖项 以及 iOS App 的依赖项 此时 我们可以重构客户端代码 以使用共享模型 并对服务器代码 执行同样的操作
现在情况看起来好多了 结束之前 这里有一些 关于下一步 App 的想法 为了充分利用我们有个 集中式服务器这一事实 我们可能需要并定义 API 以便从菜单中添加 编辑或删除甜甜圈 这将要求我们将存储 从静态文件移到数据库 有了数据库 我们还可以 实现购买和订购 API 此类 API 可以帮助我们 通过甜甜圈业务获利 它们还提供了信号 我们可以用它来实现动态定价 比如那些不太受欢迎甜甜圈的 销售和折扣 机会无穷无尽 总结一下 在本期讲座中 我们已经看到 Swift 是种通用语言 对客户端和服务器端 App 都很有用 在服务器和客户端 App 之间 共享代码 可以减少样板 使我们的系统序列化更安全 URLSession 是与服务器 异步交互的关键工具 最后 Xcode 是整个系统的 强大开发环境 非常感谢您的收看 请享受余下的研讨会之旅吧
-
-
1:36 - Simple, server package manifest
// 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"]), ] )
-
2:00 - Simple, server code
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 "" } }
-
3:42 - Using curl to test the local server
curl http://127.0.0.1:8080/greet; echo curl http://127.0.0.1:8080/echo --data "Hello from WWDC 2022"; echo
-
4:10 - Simple, iOS app server abstraction
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 } }
-
5:00 - Simple, iOS app server call SwiftUI integration
import SwiftUI struct ContentView: View { @State var serverGreeting = "" var body: some View { Text(serverGreeting) .padding() .task { do { let myServerClient = MyServerClient() self.serverGreeting = try await myServerClient.greet() } catch { self.serverGreeting = String(describing: error) } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
-
9:51 - Food truck, basic server
import Foundation import Vapor @main struct FoodTruckServerBootstrap { public static func main() async throws { // initialize the server let foodTruckServer = FoodTruckServer() // initialize the web framework and configure the http routes let webapp = Application() webapp.get("donuts", use: foodTruckServer.listDonuts) try webapp.run() } } struct FoodTruckServer { private let storage = Storage() func listDonuts(request: Request) async -> Response.Donuts { let donuts = self.storage.listDonuts() return Response.Donuts(donuts: donuts) } enum Response { struct Donuts: Content { var donuts: [Model.Donut] } } } struct Storage { var donuts = [Model.Donut]() func listDonuts() -> [Model.Donut] { return self.donuts } } 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? } }
-
12:18 - Food truck, server donuts menu
[ { "id": 0, "name": "Deep Space", "date": "2022-04-20T00:00:00Z", "dough": { "name": "Space Strawberry", "description": "The Space Strawberry plant grows its fruit as ready-to-pick donut dough.", "flavors": { "sweet": 3, "savory": 2 } }, "glaze": { "name": "Delta Quadrant Slice", "description": "Locally sourced, wormhole-to-table slice of the delta quadrant of the galaxy. Now with less hydrogen!", "flavors": { "salty": 1, "sour": 3, "spicy": 1 } }, "topping": { "name": "Rainbow Sprinkles", "description": "Cultivated from the many naturally occurring rainbows on various ocean planets.", "flavors": { "salty": 2, "sweet": 2, "sour": 1 } } }, { "id": 1, "name": "Chocolate II", "date": "2022-04-20T00:00:00Z", "dough": { "name": "Chocolate II", "description": "When Harold Chocolate II discovered this substance in 3028, it finally unlocked the ability of interstellar travel.", "flavors": { "salty": 1, "sweet": 3, "bitter": 1, "sour": -1, "savory": 1 } }, "glaze": { "name": "Chocolate II", "description": "A thin layer of melted Chocolate II, flash frozen to fit the standard Space Donut shape. Also useful for cleaning starship engines.", "flavors": { "salty": 1, "sweet": 2, "bitter": 1, "sour": -1, "savory": 2 } }, "topping": { "name": "Chocolate II", "description": "Particles of Chocolate II moulded into a sprinkle fashion. Do not feed to space whales.", "flavors": { "salty": 1, "sweet": 2, "bitter": 1, "sour": -1, "savory": 2 } } }, { "id": 2, "name": "Coffee Caramel", "date": "2022-04-20T00:00:00Z", "dough": { "name": "Hardened Coffee", "description": "Unlike other donut sellers, our coffee dough is simply a lot of coffee compressed into an ultra dense torus.", "flavors": { "sweet": -2, "bitter": 4, "sour": 2, "spicy": 1 } }, "glaze": { "name": "Caramel", "description": "Some good old fashioned Earth caramel.", "flavors": { "salty": 2, "sweet": 3, "sour": -1, "savory": 1 } }, "topping": { "name": "Nebula Bits", "description": "Scooped up by starships traveling through a sugar nebula.", "flavors": { "sweet": 4, "spicy": 1 } } } ]
-
12:23 - Food truck, server package manifest
// swift-tools-version: 5.7 import PackageDescription let package = Package( name: "FoodTruckServer", platforms: [.macOS("12.0")], products: [ .executable( name: "FoodTruckServer", targets: ["FoodTruckServer"]), ], dependencies: [ .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "4.0.0")), ], targets: [ .executableTarget( name: "FoodTruckServer", dependencies: [ .product(name: "Vapor", package: "vapor") ], resources: [ .copy("menu.json") ] ), .testTarget( name: "FoodTruckServerTests", dependencies: ["FoodTruckServer"]), ] )
-
12:30 - Food truck, server with integrated storage
import Foundation import Vapor @main struct FoodTruckServerBootstrap { public static func main() async throws { // initialize the server let foodTruckServer = FoodTruckServer() try await foodTruckServer.bootstrap() // initialize the web framework and configure the http routes 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] } } } 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 } } 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? } }
-
14:42 - Using curl and jq to test the local server
curl http://127.0.0.1:8080/donuts | jq . curl http://127.0.0.1:8080/donuts | jq '.donuts[] .name'
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。