Firebase iOS Codelab: FriendlyChat

1. 概要

iOS 向け FriendlyChat の Codelab へようこそ。この Codelab では、Firebase と最新の Swift 機能(SwiftUI、Swift Concurrency、Observation フレームワークなど)を使用して、フル機能のリアルタイム チャット iOS アプリケーションを構築する方法について学習します。

学習内容

  • 最新の Observation フレームワーク(@Observable を使用して状態を管理する。
  • Firebase Authentication を使用してユーザーを認証する。
  • Cloud Firestore を使用してチャット メッセージをリアルタイムで同期する。
  • Cloud Storage for Firebase を使用して画像を保存し、配信する。
  • 最新の Swift Concurrency 機能を使用して Firebase API にアクセスする。

必要なもの

  • Xcode の最新バージョン
  • iOS シミュレータまたは物理テスト デバイス
  • Firebase プロジェクトを作成して管理するための Google アカウント

2. サンプルコードを取得する

コマンドラインから GitHub リポジトリのクローンを作成します。

$ git clone https://github.com/firebase/codelab-friendlychat-ios

codelab-friendlychat-ios リポジトリには、次のサンプル プロジェクトが含まれています。

  • ios-starter/swift-starter \- SwiftUI レイアウト スケルトンと TODO コメントを含む Xcode プロジェクト。このプロジェクトで、Firebase を使用してアプリの機能を実装します。
  • ios/swift \- 完成したサンプル アプリケーションを含む Xcode プロジェクト。

3. スターター アプリをビルドする

スターター アプリケーションをビルドして実行し、ユーザー インターフェースに慣れてください。

  1. Finder またはターミナルで、ios-starter/swift-starter ディレクトリに移動します。
  2. FriendlyChatSwift.xcodeproj をダブルクリックして、Xcode でプロジェクトを開きます。
  3. Xcode はバックグラウンドで Swift Package Manager の依存関係を自動的に解決します(GitHub から firebase-ios-sdk v12 以降をダウンロードします)。
  4. iOS シミュレータ(iPhone 17 Pro など)を選択し、[実行] ボタン(cmd+R)をクリックします。

数秒後に、FriendlyChat のログイン画面が表示されます。この時点では、ログイン ボタンとメッセージ フィードはプレースホルダです。次の手順で、Firebase をアプリに接続します。

4. Firebase プロジェクトをセットアップする

新しい Firebase プロジェクトを作成する

  1. Google アカウントを使用して Firebase コンソール にログインします。
  2. ボタンをクリックして新しいプロジェクトを作成し、プロジェクト名(FriendlyChat など)を入力します。
  3. [続行] をクリックします。
  4. Firebase の利用規約が表示されたら、内容を読み、同意して [続行] をクリックします。
  5. (省略可)Firebase コンソールで AI アシスタンス(「Gemini in Firebase」)を有効にします。
  6. この Codelab では Google アナリティクスは不要 なので、Google アナリティクスのオプションをオフに切り替え ます。
  7. [プロジェクトを作成] をクリックしてプロジェクトのプロビジョニングが完了するまで待ち、[続行] をクリックします。

Firebase のお支払いプランをアップグレードする

Cloud Storage for Firebase を使用するには、Firebase プロジェクトが従量課金制(Blaze)のお支払いプランに登録されている必要があります。つまり、Cloud 請求先アカウントにリンクされている必要があります。

  • Cloud 請求先アカウントには、クレジット カードなどのお支払い方法を設定する必要があります。
  • Firebase と Google Cloud を初めて使用する場合は、$300 のクレジットと無料トライアル用 Cloud 請求先アカウントを利用してサービスを試せるかどうか確認してください。
  • イベントの一環としてこの Codelab を行う場合は、Cloud クレジットが利用可能かどうかを主催者にお問い合わせください。

プロジェクトを Blaze プランにアップグレードする手順は次のとおりです。

  1. Firebase コンソールで、プランをアップグレードするために を選択します。
  2. Blaze プランを選択します。画面上の手順に沿って、Cloud 請求先アカウントをプロジェクトにリンクします。
    このアップグレードの一環として Cloud 請求先アカウントを作成する必要がある場合は、Firebase コンソールでアップグレード フローに戻ってアップグレードを完了する必要がある場合があります。

iOS アプリを接続する

  1. [プロジェクトの概要] 画面で、iOS アイコンをクリックしてアプリの設定ワークフローを起動します。
  2. バンドル ID を入力します。
    com.google.firebase.codelab.FriendlyChatSwift
    
  3. [アプリの登録] をクリックします。

GoogleService-Info.plist を Xcode プロジェクトに追加する

  1. [GoogleService-Info.plist をダウンロード] をクリックして構成ファイルを保存します。
  2. Xcode で、ダウンロードした GoogleService-Info.plist ファイルをプロジェクト ナビゲータの FriendlyChatSwift メイン グループにドラッグします。
  3. プロンプトが表示されたら、[Copy items if needed] がオンになっていて、FriendlyChatSwift ターゲットが選択されていることを確認し、[Finish] をクリックします。

FriendlyChatSwiftApp.swift で Firebase を構成する

最後に、SwiftUI アプリケーションの起動時にプロジェクト構成で Firebase を初期化します。FriendlyChatSwiftApp.swift を開き、TODO プレースホルダを import FirebaseCoreFirebaseApp.configure() に置き換えます。

import SwiftUI
import FirebaseCore

@main
struct FriendlyChatSwiftApp: App {
  init() {
    FirebaseApp.configure()
  }

  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

5. ユーザーを認証する

Firebase コンソールで認証を構成する

ユーザーを認証する前に、Firebase コンソールで選択した認証プロバイダを有効にする必要があります。このチュートリアルでは、メール/パスワード認証を使用します。

  1. Firebase コンソールで、[セキュリティ] > [認証] に移動し、[始める] をクリックします。
  2. [ログイン方法] タブを選択します。
  3. [メール/パスワード] をクリックし、[有効にする] 切り替えスイッチをクリックして、[保存] をクリックします。

認証ルールで Cloud Firestore を保護する

次に、メッセージの読み取りまたは書き込みを行う前にユーザーの認証を必須にします。

  1. Firebase コンソールで、[データベースとストレージ] > [Firestore データベース] に移動し、[データベースを作成] をクリックします。
  2. ロケーションを選択し、[次へ] をクリックします。
  3. [テストモードで開始] を選択し、[作成] をクリックします。
  4. [ルール] タブを選択し、セキュリティ ルールを次のように更新します。
    rules_version = '2';
    
    service cloud.firestore {
      match /databases/{database}/documents {
        match /messages/{messageId} {
          allow read, write: if request.auth != null;
        }
      }
    }
    
    これらのルールにより、認証済みのユーザーはデータベース内のメッセージを読み書きできます。これは学習に適しています。セキュリティ ルールの詳細については、セキュリティ ルールのドキュメントをご覧ください。
  5. [公開] をクリックします。

UserViewModel.swift で認証状態を監視する

SwiftUI では、Firebase Auth の状態をリアクティブに監視することで、ログイン画面とチャット インターフェースをスムーズに切り替えることができます。

ViewModels/UserViewModel.swift を開き、init()TODOauthStateChanges AsyncSequence を使用して認証状態の変更を監視するように置き換えます。

  init() {
    authTask = Task {
      for await user in Auth.auth().authStateChanges {
        self.user = user
      }
    }
  }

Swift Concurrency を使用してログイン、登録、ログアウトを実装する

UserViewModel.swift で、残りの TODO コメントを最新の async/await 認証メソッドに置き換えます。

  func signIn(email: String, password: String) async {
    errorMessage = nil
    if email.isEmpty || password.isEmpty {
      showError("Please enter both email and password.")
      return
    }
    do {
      try await Auth.auth().signIn(withEmail: email, password: password)
    } catch {
      showError(error.localizedDescription)
    }
  }

  func signUp(email: String, password: String, displayName: String) async {
    errorMessage = nil
    if email.isEmpty || password.isEmpty {
      showError("Please enter both email and password.")
      return
    }
    do {
      let result =
        try await Auth.auth().createUser(withEmail: email, password: password)
      let changeRequest = result.user.createProfileChangeRequest()
      changeRequest.displayName = displayName.isEmpty ? email : displayName
      try await changeRequest.commitChanges()
      self.user = Auth.auth().currentUser
    } catch {
      showError(error.localizedDescription)
    }
  }

  func updateDisplayName(_ displayName: String) async {
    guard let currentUser = Auth.auth().currentUser else { return }
    do {
      let changeRequest = currentUser.createProfileChangeRequest()
      changeRequest.displayName = displayName
      try await changeRequest.commitChanges()
      self.user = Auth.auth().currentUser
    } catch {
      showError(error.localizedDescription)
    }
  }

  func signOut() {
    do {
      try Auth.auth().signOut()
    } catch {
      showError(error.localizedDescription)
    }
  }

ユーザー認証をテストする

  1. Xcode の [実行] ボタンをクリックします。
  2. アプリ シミュレータで [Sign Up] をクリックし、表示名、メールアドレス、パスワードを入力して、[SIGN UP] をタップします。
  3. 自動的に認証され、空のメッセージング画面に移動します。

6. Cloud Firestore から読み取る

Firebase コンソールでサンプル メッセージを追加する

データベースにサンプル メッセージを入力します。

  1. Firebase コンソールで、[Firestore データベース] に移動し、[データ] タブを選択します。
  2. [コレクションを開始] をクリックします。
  3. messages[コレクション ID] に入力し、[次へ] をクリックします。
  4. [ドキュメント ID] は自動生成のままにします(または [自動 ID] をクリックします)。
  5. ドキュメントに次のフィールドを追加します。
    • text(タイプ: string、値: Hello
    • displayName(タイプ: string、値: anonymous
  6. [保存] をクリックします。
  7. 必要に応じて [ドキュメントを追加] をクリックして、サンプル メッセージを追加します。

FriendlyMessageViewModel.swift でメッセージを同期する

SwiftUI では、@Observable モデルによって宣言型 UI の更新が駆動されます。ViewModels/FriendlyMessageViewModel.swift を開き、snapshots AsyncSequence を使用して startListening()stopListening() を実装します。

  func startListening() {
    stopListening()
    listenerTask = Task {
      let db = Firestore.firestore()
      do {
        for try await snapshot in db.collection("messages").snapshots {
          self.messages = snapshot.documents.compactMap { document in
            try? document.data(as: FriendlyMessage.self)
          }
        }
      } catch {
        print("Error listening for messages: \(error)")
      }
    }
  }

  func stopListening() {
    listenerTask?.cancel()
    listenerTask = nil
    messages.removeAll()
  }

SwiftUI がメッセージをレンダリングして自動スクロールする方法

Views/ContentView.swift で、ScrollViewReaderLazyVStack がチャット バブルをレンダリングし、messages.count が変更されるたびに最新のメッセージに自動的にスクロールする方法を確認します。

ScrollViewReader { scrollViewReader in
  ScrollView {
    LazyVStack(spacing: 12) {
      ForEach(messageViewModel.messages) { message in
        FriendlyMessageView(friendlyMessage: message)
          .id(message.id)
      }
    }
    .padding(.horizontal)
    .onChange(of: messageViewModel.messages.count) { _, count in
      guard count > 0,
            let lastId = messageViewModel.messages.last?.id else { return }
      withAnimation(.easeInOut) {
        scrollViewReader.scrollTo(lastId, anchor: .bottom)
      }
    }
  }
}

ビューモデルを介してデータベースの状態を UI の状態に直接関連付けることで、SwiftUI ではアプリコードでトランジションをアニメーションに簡単にマッピングできます。次のステップでは、アプリからデータベースにデータを書き込み、このアニメーションの動作を確認します。

7. メッセージを送信する

Swift Concurrency で sendMessage を実装する

addDocument() を使用してコレクションにドキュメントを追加すると、Cloud Firestore はチャット メッセージごとに一意の ID を生成します。

ViewModels/FriendlyMessageViewModel.swift で、TODOsendMessage(text:imageUrl:)async throws メソッドに置き換えます。

  func sendMessage(text: String?, imageUrl: String?) async throws {
    guard let currentUser = Auth.auth().currentUser else { return }
    let message = FriendlyMessage(
      text: text,
      displayName: currentUser.displayName ?? currentUser.email ?? "Anonymous",
      imageUrl: imageUrl,
      userId: currentUser.uid
    )
    let db = Firestore.firestore()
    _ = try db.collection("messages").addDocument(from: message)
  }

メッセージの送信をテストする

  1. Xcode の [実行] ボタンをクリックします。
  2. アカウントにログインします。
  3. 下部のテキスト フィールドにメッセージを入力し、送信アイコンをタップします。
  4. メッセージがシミュレータにすぐに表示されます。

8. 画像を保存して受信する

Cloud Storage for Firebase を設定する

  1. Firebase コンソールで、[データベースとストレージ] > [ストレージ] に移動し、[始める] をクリックします。
  2. [テストモードで開始] を選択し、[次へ] をクリックします。
  3. デフォルトのストレージ ロケーションをそのまま使用し、[完了] をクリックします。

SwiftUI PhotosPicker を使用したネイティブの画像選択

最新の SwiftUI(iOS 16 以降)では、import PhotosUIPhotosPicker を使用してネイティブの写真選択が可能です。UIKit デリゲート ブリッジや UIImagePickerController ラッパーは必要ありません。

Views/FooterView.swift で、PhotosPicker が選択した画像をバインドする方法を確認します。

PhotosPicker(selection: $selectedItem, matching: .images) {
  Image(systemName: "photo.on.rectangle.angled")
    .font(.system(size: 26))
    .foregroundStyle(.blue)
    .accessibilityLabel("Select photo")
}
.onChange(of: selectedItem) { _, newItem in
  Task {
    if let data = try? await newItem?.loadTransferable(type: Data.self) {
      await uploadAndSendImage(data: data)
    }
  }
}

画像のアップロードを実装する

写真が選択されたら、画像データを Cloud Storage にアップロードし、公開ダウンロード URL を Cloud Firestore に保存します。

Views/FooterView.swift で、uploadAndSendImage(data:)TODO を置き換えます。

  private func uploadAndSendImage(data: Data) async {
    guard let uid = Auth.auth().currentUser?.uid else { return }
    isUploading = true
    defer {
      isUploading = false
      selectedItem = nil
    }
    do {
      let filename = "\(uid)/\(UUID().uuidString).jpg"
      let storageRef =
        Storage.storage().reference().child("images").child(filename)
      let metadata = StorageMetadata()
      metadata.contentType = "image/jpeg"

      _ = try await storageRef.putDataAsync(data, metadata: metadata)
      let downloadURL = try await storageRef.downloadURL()
      try await viewModel.sendMessage(
        text: nil, imageUrl: downloadURL.absoluteString)
    } catch {
      print("Error uploading image: \(error.localizedDescription)")
    }
  }

Cloud Storage を使用した画像の共有

Views/FriendlyMessageImageView.swift では、Swift Concurrency と Cloud Storage SDK を使用して画像 URL が非同期で読み込まれます。

  • 標準の HTTP/HTTPS ダウンロード URL は URLSession を介して非同期で取得されます。
  • Google Cloud Storage URL(gs://... など)は、Storage.storage().reference(forURL:) を使用して取得されます。

画像メッセージをテストする

  1. Xcode の [実行] ボタンをクリックします。
  2. アカウントにログインします。
  3. 写真アイコンをタップしてライブラリから画像を選択すると、画像がアップロードされてチャットに表示されます。

9. まとめ

お疲れさまでした。これで、Swift と Firebase を使用してリアルタイム iOS チャット アプリケーションを構築することができました。

学習した内容

  • firebase-ios-sdk のための Swift Package Manager(SPM) Xcode 統合。
  • SwiftUI の宣言型レイアウト、PhotosPickerScrollViewReader の自動スクロール。
  • 認証、Cloud Firestore、Cloud Storage 全体でのSwift Concurrency(async/await
  • リアクティブな状態監視によるメール/パスワード認証と匿名認証
  • Cloud Firestore のリアルタイム スナップショット リスニングと自動 ID 書き込み。
  • Cloud Storage for Firebase のバイナリ アップロードと公開ダウンロード URL。

詳細