フルスタックSwift開発を始めるテンプレートを作りました
目次
- はじめに
- テンプレートの全体構成
- Shared層:単一の真実の源
- Backend層:クリーンアーキテクチャの実践
- iOS層:モジュラーなパッケージ構成
- 開発ワークフロー
- デプロイメント
- テンプレートとしての設計意図
- まとめ
1. はじめに
本記事は「サーバーサイドSwiftでiOSアプリ開発をどこまで効率化できるか」シリーズの最終回です。
これまでのシリーズでは、以下のライブラリを紹介してきました。
- 第1回: Swift Configuration(環境変数管理)
- 第2回: swift-statable(状態管理マクロ)
- 第3回: swift-api-contract(型安全なAPI定義)
- 第4回: swift-api-server(サーバー実装の抽象化)
- 第5回: サーバーサイドSwiftを使う場合のiOSアーキテクチャ
本記事では、これらのライブラリを統合した実践的なテンプレートリポジトリ「swift-app-template」を解説します。
テンプレートリポジトリとは
swift-app-templateは、iOSアプリとSwiftバックエンドを同時に開発するための雛形です。GitHubの「Use this template」機能でリポジトリを作成し、設定ファイルを編集するだけで、すぐに動作するTodoアプリが手に入ります。
テンプレートには以下が含まれています。
- 完全に動作するTodoアプリ(iOS + Backend)
- Firebase Authentication(Google/Apple Sign-In)
- Firestore連携
- Firebase Emulatorによるローカル開発環境
- Docker + Cloud Runへのデプロイ構成
- GitHub Actionsによる自動リリース
2. テンプレートの全体構成
ディレクトリ構造
テンプレートは三層構造になっています。
swift-app-template/
├── Backend/ # Swiftサーバー(Vapor)
├── Shared/ # 共有ドメインモデル・API契約
├── iOS/ # iOSアプリケーション
├── firebase/ # Firebase設定・ルール
├── scripts/ # セットアップ・ユーティリティ
├── .github/ # GitHub Actions
└── Makefile # 開発コマンド
三層アーキテクチャ
重要なのは、iOSとBackendの両方がSharedに依存している点です。Shared層がドメインモデルとAPI契約の「単一の真実の源」となり、クライアントとサーバーの同期ズレを原理的に排除します。
3. Shared層:単一の真実の源
Shared層は、iOSとBackendで共有されるコードを格納します。
ドメインエンティティ
ドメインモデルはShared層で一度だけ定義します。
// Shared/Sources/Shared/Entities/Todo/Todo.swift
public struct Todo: Codable, Sendable, Identifiable, Hashable {
public let id: String
public let title: String
public let description: String?
public let isCompleted: Bool
public let categoryId: String?
public let dueDate: Date?
public let createdAt: Date
public let updatedAt: Date
}
このTodo型は、iOSアプリのUI表示にも、バックエンドのAPI応答にも、そのまま使われます。型定義の重複がなく、フィールド名や型の不一致が発生しません。
API契約
第3回で紹介したswift-api-contractを使い、API契約を定義します。
// Shared/Sources/Shared/API/Contracts/TodosAPI.swift
@APIGroup(path: "/v1/todos", auth: .required)
public enum TodosAPI {
@Endpoint(.get)
public struct List {
@QueryParam public var categoryId: String?
@QueryParam public var isCompleted: Bool?
@QueryParam public var limit: Int? = 20
@QueryParam public var offset: Int? = 0
public typealias Output = [Todo]
}
@Endpoint(.get, path: ":todoId")
public struct Get {
@PathParam public var todoId: String
public typealias Output = Todo
}
@Endpoint(.post)
public struct Create {
@Body public var input: CreateTodoInput
public typealias Output = Todo
}
// Update, Delete, Toggle も同様
}
この契約定義から、以下が自動的に導出されます。
- サーバー側で実装すべきServiceプロトコル
- クライアント側で呼び出せるexecuteメソッド
- パス、HTTPメソッド、認証要件、パラメータ型
iOSとBackendが同じ契約定義を参照するため、エンドポイントの不整合がコンパイル時に検出されます。
4. Backend層:クリーンアーキテクチャの実践
Backend層は、クリーンアーキテクチャに基づく5層構造になっています。
レイヤー構造
Serviceの実装
第4回で紹介したswift-api-serverを使い、APIハンドラを実装します。
// Backend/Sources/Server/Services/TodosService.swift
struct TodosService: TodosAPIService {
private let todoUseCase: TodoUseCase
func handle(_ input: TodosAPI.List, context: ServiceContext) async throws -> [Todo] {
let userId = try context.requireUserId()
return try await todoUseCase.getTodos(userId: userId)
}
func handle(_ input: TodosAPI.Get, context: ServiceContext) async throws -> Todo {
let userId = try context.requireUserId()
guard let todo = try await todoUseCase.getTodo(userId: userId, todoId: input.todoId) else {
throw TodosAPIError.notFound(todoId: input.todoId)
}
return todo
}
func handle(_ input: TodosAPI.Create, context: ServiceContext) async throws -> Todo {
let userId = try context.requireUserId()
return try await todoUseCase.createTodo(userId: userId, input: input.input)
}
// Update, Delete, Toggle も同様
}
ポイントは以下の通りです。
-
Vaporへの依存がない:
import Vaporは不要 -
型安全なパラメータ:
input.todoIdやinput.inputで直接アクセス - UseCaseへの委譲: ビジネスロジックはUseCase層に集約
Repository層
データアクセスはRepositoryプロトコルで抽象化されています。
// BackendServices/Domain/Repository/TodoRepository.swift
public protocol TodoRepository: Sendable {
func getAll(userId: String) async throws -> [Todo]
func get(userId: String, todoId: String) async throws -> Todo?
func create(userId: String, input: CreateTodoInput) async throws -> Todo
func update(userId: String, todoId: String, input: UpdateTodoInput) async throws -> Todo
func delete(userId: String, todoId: String) async throws
}
実装はFirestoreを使いますが、プロトコルで分離されているため、テスト時にはモックに差し替えられます。
5. iOS層:モジュラーなパッケージ構成
iOS層は、SPMパッケージで論理的に分割されています。
パッケージ構成
iOS/
├── App/ # メインターゲット(DI設定)
└── Packages/
├── Presentation/ # SwiftUI Views、Stores
└── UseCases/ # ビジネスロジック
UseCaseの実装
iOS側のUseCaseは、Shared層のAPI契約を使ってサーバーと通信します。
// iOS/Packages/UseCases/Sources/UseCases/Implementations/TodoUseCaseImpl.swift
public struct TodoUseCaseImpl<Executor: APIExecutable>: TodoUseCase, Sendable {
private let executor: Executor
public func getTodos(
categoryId: String?,
isCompleted: Bool?,
limit: Int,
offset: Int
) async throws -> [Todo] {
try await TodosAPI.List(
categoryId: categoryId,
isCompleted: isCompleted,
limit: limit,
offset: offset
).execute(using: executor)
}
public func getTodo(id: String) async throws -> Todo {
try await TodosAPI.Get(todoId: id).execute(using: executor)
}
public func createTodo(input: CreateTodoInput) async throws -> Todo {
try await TodosAPI.Create(input: input).execute(using: executor)
}
// update, delete, toggle も同様
}
TodosAPI.ListやTodosAPI.Createは、Shared層で定義したAPI契約そのものです。execute(using:)メソッドでHTTPリクエストが発行され、レスポンスは[Todo]やTodoとして型安全に返されます。
データフロー
iOSアプリからBackendへのデータフローを示します。
iOSとBackendが同じTodosAPI.List契約を参照していることがわかります。
6. 開発ワークフロー
初期セットアップ
テンプレートからリポジトリを作成した後、以下の手順でセットアップします。
# 1. 設定ファイルの生成
make setup
# 2. setup.config を編集(アプリ名、Bundle IDなど)
# 3. Firebase ConsoleからGoogleService-Info.plistをダウンロード
# iOS/App/ に配置
# 4. Xcodeプロジェクト生成
make setup
make setupは、以下を自動的に行います。
-
.envとsetup.configのテンプレート生成 - GoogleService-Info.plistの存在確認
- XcodeGenによるプロジェクト生成
- プレースホルダーの置換(アプリ名、Bundle IDなど)
ローカル開発
ローカル開発では、Firebase Emulatorを使います。
# ターミナル1: Firebase Emulator起動
make emulator
# ターミナル2: Swiftサーバー起動(Emulator接続)
make server-run
サーバーはhttp://localhost:8080で起動し、Firebase Emulatorに接続します。本番のFirebase/Firestoreを汚さずに開発できます。
主要なMakefileコマンド
| コマンド | 説明 |
|---|---|
make setup |
初期セットアップ |
make server-run |
サーバーをローカル実行 |
make server-build |
サーバーをビルド |
make server-test |
サーバーテスト実行 |
make emulator |
Firebase Emulator起動 |
make doctor |
開発環境の健康状態チェック |
7. デプロイメント
Docker + Cloud Run
テンプレートには、Cloud Runへのデプロイ構成が含まれています。
# Artifact Registryのセットアップ(初回のみ)
make deploy-setup
# サーバーをビルド・デプロイ
make deploy-server
Dockerfileは最適化されており、Swiftの静的リンクバイナリを生成します。
GitHub Actionsによる自動デプロイ
mainブランチへのプッシュで、自動的にCloud Runへデプロイされます。
# .github/workflows/deploy-server.yml
on:
push:
branches: [main]
paths:
- 'Backend/**'
- 'Shared/**'
jobs:
deploy:
# Docker build → Push → Cloud Run deploy
Backend/またはShared/の変更があった場合のみトリガーされます。
8. テンプレートとしての設計意図
カスタマイズポイント
テンプレートは、以下の点でカスタマイズを想定しています。
- エンティティの追加: Shared/Entities/に新しいドメインモデルを追加
- API契約の追加: Shared/API/Contracts/に新しいAPIGroupを定義
- サービスの追加: Backend/Sources/Server/Services/に対応するServiceを実装
- 画面の追加: iOS/Packages/Presentation/に新しいViewを追加
新機能追加の手順
例えば「コメント機能」を追加する場合。
-
Shared層:
CommentエンティティとCommentsAPI契約を定義 -
Backend層:
CommentRepository、CommentUseCase、CommentsServiceを実装 -
iOS層:
CommentUseCaseプロトコル、CommentUseCaseImpl、CommentStore、画面を追加
どの層から始めても構いませんが、Shared層を先に定義することで、iOSとBackendで型の整合性が保証されます。
テンプレートが提供する価値
- 即座に動くアプリ: clone直後にTodoアプリとして動作
- 型安全なAPI: コンパイル時にクライアント/サーバーの整合性を検証
- 本番デプロイまでの道筋: Docker + Cloud Runの構成済み
- ローカル開発環境: Firebase Emulatorで本番を汚さず開発
- 自動化されたワークフロー: Makefile、GitHub Actions、XcodeGen
9. まとめ
本シリーズでは、サーバーサイドSwiftを活用してiOSアプリ開発を効率化する手法を解説してきました。
- Swift Configuration: 環境変数の宣言的な管理
- swift-statable: マクロによる状態管理の仕組み化
- swift-api-contract: 型安全なAPI定義と自動コード生成
- swift-api-server: Vaporの隠蔽とビジネスロジックへの集中
- swift-app-template: これらを統合した実践的テンプレート
サーバーサイドSwiftの本質的な価値は、iOSとBackendで同じ型を共有できる点にあります。OpenAPIやgRPCのようなスキーマ言語を介さず、Swiftの型システムがそのまま契約になります。
swift-app-templateは、この価値を実際のプロジェクトで活用するための出発点です。Todoアプリを基盤として、自分のアプリに拡張していくことができます。
参考リンク
テンプレートリポジトリ
本シリーズで紹介したライブラリ
- swift-configuration - Apple公式
- swift-env - 環境変数ラッパー
- swift-statable - 状態管理マクロ
- swift-api-contract - API契約定義
- swift-api-client - HTTPクライアント
- swift-api-server - サーバー抽象化
シリーズ記事
- 第1回: Apple公式Swift Configurationを試してみた
- 第2回: SwiftUIの状態管理をマクロで仕組み化する
- 第3回: サーバーとクライアントでAPIの型を共有する方法
- 第4回: サーバーサイドSwiftをより使いやすく
- 第5回: サーバーサイドSwiftを使う場合のiOSアーキテクチャ
- 第6回: 本記事
Discussion