🚀

フルスタックSwift開発を始めるテンプレートを作りました

に公開

目次

  1. はじめに
  2. テンプレートの全体構成
  3. Shared層:単一の真実の源
  4. Backend層:クリーンアーキテクチャの実践
  5. iOS層:モジュラーなパッケージ構成
  6. 開発ワークフロー
  7. デプロイメント
  8. テンプレートとしての設計意図
  9. まとめ

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.todoIdinput.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.ListTodosAPI.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は、以下を自動的に行います。

  1. .envsetup.configのテンプレート生成
  2. GoogleService-Info.plistの存在確認
  3. XcodeGenによるプロジェクト生成
  4. プレースホルダーの置換(アプリ名、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. テンプレートとしての設計意図

カスタマイズポイント

テンプレートは、以下の点でカスタマイズを想定しています。

  1. エンティティの追加: Shared/Entities/に新しいドメインモデルを追加
  2. API契約の追加: Shared/API/Contracts/に新しいAPIGroupを定義
  3. サービスの追加: Backend/Sources/Server/Services/に対応するServiceを実装
  4. 画面の追加: iOS/Packages/Presentation/に新しいViewを追加

新機能追加の手順

例えば「コメント機能」を追加する場合。

  1. Shared層: CommentエンティティとCommentsAPI契約を定義
  2. Backend層: CommentRepositoryCommentUseCaseCommentsServiceを実装
  3. iOS層: CommentUseCaseプロトコル、CommentUseCaseImplCommentStore、画面を追加

どの層から始めても構いませんが、Shared層を先に定義することで、iOSとBackendで型の整合性が保証されます。

テンプレートが提供する価値

  1. 即座に動くアプリ: clone直後にTodoアプリとして動作
  2. 型安全なAPI: コンパイル時にクライアント/サーバーの整合性を検証
  3. 本番デプロイまでの道筋: Docker + Cloud Runの構成済み
  4. ローカル開発環境: Firebase Emulatorで本番を汚さず開発
  5. 自動化されたワークフロー: 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アプリを基盤として、自分のアプリに拡張していくことができます。


参考リンク

テンプレートリポジトリ

本シリーズで紹介したライブラリ

シリーズ記事

著者

Discussion