ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
サーバサイド開発でのXcodeの使用
SwiftサーバAppを、既存のXcodeプロジェクトと一緒に同じワークスペース内で作成、ビルド、デプロイする方法をご確認ください。独自のローカルAppを作成したり、Xcodeを使用するエンドポイントをテストしたりする方法をはじめ、サーバAppとクライアントApp間でコードを構造化したり共有したりして、開発プロセスを容易にする方法について解説します。
リソース
関連ビデオ
WWDC23
WWDC22
WWDC21
WWDC20
WWDC19
-
ダウンロード
♪ ♪
こんにちは Tomです 私は AppleのSwiftチームの 一員です iOS Appをクラウドに拡張 するための方法を紹介します Appの多くは 最初は 単一のデバイス 通常はiPhoneに 焦点を当てています 利用者が増えれば増えるほど MacやWatchなど Appleの 他のプラットフォームや デバイスにも 導入したいと 思うようになります Xcodeはプラットフォーム向けに Appの整理をし構築を助けます プラットフォーム固有のAppコード で各デバイスのユニークな 側面を活用しながら パッケージを使って コードを共有できるのです システムが成長し 進化し続ける中で Appはしばしば サーバコンポーネントで クライアントAppを 補完する必要があります これらのサーバコンポーネントは クライアントAppが その機能をクラウドに 拡張することを可能にします 例えば バックグラウンド処理で実行できる タスクの転送 計算量の多いタスクの転送 デバイスにないデータへの 接続を必要とする タスクの転送などです たいてい サーバコンポーネントは クライアント側と 異なるツールや方法論で 構築する必要があり 重複作業や集積課題が 発生します サーバコンポーネントの構築に Swiftを使用することで 技術差を埋めることができ スタック全体で使い慣れた 環境を提供できます サーバAppの構築が どんなものか見てみましょう サーバAppはSwiftの パッケージでモデリングされています パッケージは Appの入り口点に マップされる 実行ターゲットを定義します AppをWeb Appにする ために コードの構造化を助け ルーティングのような 基本的なユーティリティを 提供するWebフレームワークへの 依存関係を追加できます 例では Webサービス構築の オープンソースプロジェクト として人気の高い Vapor Webフレームワークを使用します
他の実行ファイルと同様に プログラムの入り口点は @mainアノテーションを使用して 最適にモデル化されます Webフレームワークを 統合するために関連する ブート処理コードを メイン関数に追加します 例で使われるAppタイプは Vapor Webフレームワークで提供します 基本的なブート処理が できたので Appに何か便利なことを させることができます 例えば サーバにリクエストする ユーザ挨拶コードを追加します Webフレームワークを使用して HTTPエンドポイントを定義し 挨拶を提供する方法を 指定します さらに 2つ目のHTTP エンドポイントを追加して HTTPリクエスト後のを処理し リクエスト本体の内容を 呼び出し元にエコーバック するようにします では 実際に見てみましょう ここでは Xcodeで サーバAppを作成します まだ始めたばかりなので 自分のマシンで サーバをローカルに動かして 試行しましょう ローカルで実行するには Xcodeが生成してくれた 「MyServer」スキーマを選び 「My Mac」を保存先にし 「実行」を押します
Appが起動したら Xcodeコンソールを使って サーバが発するログメッセージを 調べることができます この際サーバが起動しlocal- hostアドレス(127.0.0.1) ポート8080で待ち受けして いることが確認できます この情報は サーバの テストに利用できます ターミナルに切り替えて アドバタイズされた サーバのアドレスに リクエストしてみましょう リクエストには「curl」という ユーティリティを使用します 最初のエンドポイントを使用します
そして2番目です 何らかのデータを渡して 表示させます
いいですね! ターミナルの使用は確かに楽しい ですが 本当に知りたいのは iOS Appからサーバを 呼び出す方法です それを掘り下げてみましょう サーバとのインタラクションの 抽象化に使用できる Swiftのデータ構造の 例を示します ネットワークは元来非同期 なので サーバAPIを 抽象化して 非同期方法として モデル化しました URLSessionを使って 非同期リクエストを行い サーバのレスポンスを解析して 最後に呼び出し元に返します この場合 サーバのレスポンスは 普通の文字列でも 実際には もっと精巧なものである 可能性が高いです 例えば レスポンスがJSONで 暗号化されている場合は SwiftのCodableシステムを 使って復号できます Xcodeでまとめましょう Xcodeのワークスペースを使用して iOSとサーバAppを 並べてビルドし テストします iOS Appサーバの抽象化の 準備はできています 束ねたコードを使ってサーバの 挨拶を取得するのに初期値の SwiftUI ContentViewを 変更してみましょう まず serverGreetingという 状態変数を作成します
次に serverGreetingを Text表示にバインドします
最後に サーバAPIを 呼び出して 状態を設定する タスクを追加します
準備できたらシミュレータで Appを実行します シミュレータである 「MyApp」スキーマを選び 「実行」を押します
ああ エラーになりました! うーん これは何らかの 接続エラーのようです アドレスは合っている ようなので ローカルサーバの起動を 忘れていたのでしょう Xcodeに戻り サーバスキーマを選んで 実行しましょう
では Appを 再起動しましょう 祈るばかりです
やった! うまくいきました このデモの部分を 完成させるために Appをクラウドに 導入してみましょう AWS Google Cloud Azure Herokuなど 多くの クラウドプロバイダから 選択できます この例では Herokuを 使用します Herokuには デモAppの ような小規模企画に便利な git pushによる デプロイシステムがあります ターミナルに切り替えて 導入を開始しましょう アカウントを設定し Heroku サービスでAppを設定して コードをHerokuリモートに git pushできます
そして 始めます Herokuはビルドパックという 技術で Appを遠隔で コンパイルし バイナリを 一時的なホストに導入します Heroku swift buildpackは Swiftオープンソース コミュニティのメンバーに よって構築され すべての Swift on Serverユーザーが 利用可能です Appを導入して ローカルサーバでしたように curlを使ってテストできます 最初のエンドポイントをテスト してみましょう
アドレスをここに コピーします
そして 2つ目も
今回は違うペイロードを 送ります
よかった Appの導入に成功しました 話を続ける前に ここで一旦 立ち止まり この部分の 主な重要点を おさらいしましょう iOSやmacOS Appの構築に 既にSwiftを使っている場合 サーバ側の開発にも Swiftを使用できます Xcodeは クライアントとサーバ両方の システムの様々な構成要素を 1つのワークスペースで開発 修正するためのものです そして最後に Swiftベースの サーバAppを導入する クラウドプロバイダを 選択できます クラウドプラットフォーム への導入に関する追加情報は swift.orgの Swift Serverの付随資料に 記載されています さて 基本的な手順を 見たところで より現実的な例として フードトラックを見てみましょう このAppは セッションで たくさん見たと思います どうデータが管理されるのか 裏側を覗いてみましょう うーん ドーナツリストが ハードコードされています つまり Appを利用する ユーザーには 実際とは 異なるドーナツのメニューが 表示されることがあります どんなドーナツでもその場で 作れる小さな フードトラックには 有効かもしれませんが メニューを集約化し 屋台は顧客サービスに徹する ドーナツ帝国を 築きたいのです フードトラックの集約化システムは どんなものか設計してみます
インメモリストレージを搭載した iOS Appからスタートです メニューを集約化するために iOS Appからストレージを 取り出し サーバに 移動させることができます これで 全Appユーザーが 同じストレージを共有でき こうして同じ ドーナツメニューをだせます この講演の最初の部分の例と 同様に 私たちのサーバは HTTPベースのAPIを 公開することになります iOS Appは これらのAPIを 操作するための抽象化を使用し プレゼンテーションレイヤ この例では SwiftUIに 結びつけます 私たちの設計は完成しました 素敵なコードを書く時間です 開発者リソースキットから フードトラックのサンプルAppを ダウンロードして 追従できます まず Appのスケルトンから サーバを構築し Web APIである"donuts"の HTTPエンドポイントを定義し サーバ抽象化の"listDonuts" メソッドに指定します APIはDonuts型のレスポンスを返し Response.DonutsはContentと 呼ばれるプロトコルに準拠している ことに気づいたでしょうか Contentプロトコルは Webフレームワークで 定義されワイヤ上でレスポンスを JSONとしてコード化するのに 役立ちます また APIにはModel.Donutと いう謎の配列が 含まれていることに お気づきかもしれませんが これはまだ 定義されていません ドーナツ 生地 グレーズ そしてトッピングです 興味深いのはサーバと クライアントのデータモデルを ほぼ一致させる必要がある ため このモデルの定義を フードトラックOS Appから コピーした点です もうひとつ興味深いのは Encodableプロトコルの準拠です これはサーバがモデルオブジェクトを JSONとしてコード化 できるようにするために 必要です データモデルと 基本的なAPIが揃ったので ロジックを拡張して ストレージを抽象化できます ストレージは利用可能なドーナツ リストをAppに提供します この時点で 完全に機能する サーバのはずです でも ちょっと待って ドーナツメニューが空です メニューはどこから入手 すればいいのでしょうか サーバ側のAppの設計時 ストレージは興味深い話題です 使用事例に応じて いくつかの戦略から 選択できます Appのデータが静的な時や ごく遅く手動で変更する時は ディスク上のファイルでも 解決策を提供でき得ます ユーザ中心のデータや グローバルデータセットでは iCloudは 専用サーバを 導入せず iOS Appから 直接利用できるAPI群を 提供しています 動的なデータや 取引のデータを扱う場合 データベースは優れた 解決策を提供します サーバー側のAppで利用 できるデータベース技術には 様々なものがあります 各々の技術は 特定の性能 データの一貫性 データのモデル化のニーズに 合わせて設計されています 何年もの間 Swiftのオープン ソースコミュニティは 大抵のデータベース技術と ネイティブとのインタラクションに 役立つデータベース ドライバを開発しました 部分的リストには Postgres MySQL MongoDB Redis DynamoDB その他多数が 含まれます このデモを簡略化するために 静的なファイルストレージの 戦略だけをデモしますが データベースの使用について swift.orgのSwift Serverの 付随資料でより詳しく 知ることができます 静的ファイルストレージ戦略で まず ドーナツメニューを 取得するJSONファイルの 作成から始めます このファイルを作成した後 SwiftPMのリソースサポート を使って Appから 利用可能にします これで ストレージの抽象化をより 精巧なものにできました すなわち 「load」メソッドを 追加しています この方法は SwiftPMが生成 したリソースアクセサーを 使って リソースファイルの パスを見つけ FileManager APIを使ってファイルの 内容をメモリにロードします 最後に JSONDecoderを使って JSONの内容を サーバAppの データモデルに復号します 興味深い変更点はストレージを アクターと定義したことです アクターを選んだのはストレージ が変更可能な"donuts"変数を 持ち "load"と"listDonuts" が同時に接続できるからです Swift 5.5で初導入された アクターは データ競合を 回避し 安全かつ簡単な 方法で 変異可能な状態を 共有することを支援します アクターが導入される以前は LockやQueueなどのAPIを 使って変更可能な状態に 接続する場合 同期ブロックを覚えて 追加する必要がありました ストレージの更新が完了すれば すべてを結びつけられます サーバの抽象化に 「bootstrap」メソッドを追加し そこからストレージを ロードし 立ち上げを実行ファイルの 入り口点に配線します ストレージがアクターなので 非同期での接続に注意します サーバの準備ができました クライアント側に切り替えてみます まず サーバAPIを カプセル化するのに役立つ サーバ抽象化を追加する ことから始めます HTTPrequestを行うために URLSessionを使用し サーバのレスポンスを復号して JSONからiOS Appの モデルに変換するために JSONDecoderを使用します この時 ハードコードされた メニューを削除しサーバから 非同期フェッチに 置き換えできます ContentViewのloadタスクから サーバへ呼び出します テストの時間です 今回は サーバの起動を 忘れないようにしましょう 「FoodTruckServer」スキーマを 選択します 「実行」を押します
Appを起動した状態で ターミナルに飛び APIに接続できるか 確認しましょう
再度アドレスをコピーします
今回は jqという ユーティリティを使って JSONの出力をよりきれいに 出力します これはかなり良さそうです
さて いよいよ Appのテストです
Xcodeに切り替えます Food Trackのスキーマは こちらで選んでください シミュレータです そして実行します
そして 集約化したメニューの ドーナツ3種が出来ました それをサーバから見たものと 相互参照できます ターミナルに切り替えてみましょう 比較を簡単にするため jqで ドーナツ名だけ照会します
ディープスペース チョコレート2 コーヒーキャラメル… まさに期待通りのものでした すごかったですね でも もっと良くできます 現状では サーバAppと クライアントAppの両方が 同一データモデルコードの コピーを持つことになります iOSとサーバAppで モデルを共有することで 複写を避け シリアル化を より安全に行えます その設定方法を高いレベルで おさらいしておきましょう まず "Shared"という名の ライブラリ用のパッケージを もう一つ作成し Xcodeの ワークスペースに追加します 次に データモデルのコードを Sharedパッケージに移動し Target Frameworks and 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'
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。