1. 概要

Friendly Chat の Codelab へようこそ。この Codelab では、Firebase プラットフォームを使用して iOS アプリケーションを作成する方法を学びます。チャット クライアントを実装し、Firebase を使用してパフォーマンスをモニタリングします。
学習内容
- ユーザーにログインを許可します。
 - Firebase Realtime Database を使用してデータを同期します。
 - バイナリ ファイルを Firebase Storage に保存します。
 
必要なもの
- Xcode
 - CocoaPods
 - iOS 8.0 以降を搭載したテストデバイスまたはシミュレータ
 
このチュートリアルをどのように使用されますか?
iOS アプリ作成のご経験についてお答えください。
2. サンプルコードを取得する
コマンドラインから GitHub リポジトリのクローンを作成します。
$ git clone https://github.com/firebase/codelab-friendlychat-ios
3. スターター アプリをビルドする

スターター アプリをビルドするには:
- ターミナル ウィンドウで、ダウンロードしたサンプルコードの 

ios-starter/swift-starterディレクトリに移動します。 - 実行 
pod install --repo-update - FriendlyChatSwift.xcworkspace ファイルを開いて、Xcode でプロジェクトを開きます。
 - [
実行] ボタンをクリックします。 
数秒後に Friendly Chat のホーム画面が表示されます。UI が表示されます。ただし、この時点ではログイン、メッセージの送受信はできません。次のステップを完了するまで、アプリは例外で中止されます。
4. Firebase プロジェクトを設定する
新しい Firebase プロジェクトを作成する
- Google アカウントを使用して Firebase コンソールにログインします。
 - ボタンをクリックして新しいプロジェクトを作成し、プロジェクト名(例: 
FriendlyChat)を入力します。
 - [続行] をクリックします。
 - Firebase の利用規約が表示されたら、内容を読み、同意して [続行] をクリックします。
 - (省略可)Firebase コンソールで AI アシスタンス(「Gemini in Firebase」)を有効にします。
 - この Codelab では Google アナリティクスは必要ないため、Google アナリティクスのオプションをオフに切り替えます。
 - [プロジェクトを作成] をクリックし、プロジェクトのプロビジョニングが完了するまで待ってから、[続行] をクリックします。
 
Firebase 料金プランをアップグレードする
Cloud Storage for Firebase を使用するには、Firebase プロジェクトが従量課金制(Blaze)のお支払いプランに登録されている必要があります。つまり、Cloud 請求先アカウントにリンクされている必要があります。
- Cloud 請求先アカウントには、クレジット カードなどの支払い方法が必要です。
 - Firebase と Google Cloud を初めて使用する場合は、$300 のクレジットと無料トライアル用 Cloud 請求先アカウントを利用できるかどうかご確認ください。
 - この Codelab をイベントの一環として行う場合は、利用可能な Cloud クレジットがあるかどうかを主催者に確認してください。
 
プロジェクトを Blaze プランにアップグレードする手順は次のとおりです。
- Firebase コンソールで、プランをアップグレードします。
 - Blaze プランを選択します。画面の指示に沿って、Cloud 請求先アカウントをプロジェクトにリンクします。
このアップグレードの一環として Cloud 請求先アカウントを作成する必要があった場合は、Firebase コンソールのアップグレード フローに戻ってアップグレードを完了する必要がある場合があります。 
iOS アプリを接続する
- 新しいプロジェクトの [プロジェクトの概要] 画面で、[iOS アプリに Firebase を追加] をクリックします。
 - バンドル ID を「
com.google.firebase.codelab.FriendlyChatSwift」と入力します。 - App Store ID を「
123456」として入力します。 - [アプリを登録] をクリックします。
 
アプリに GoogleService-Info.plist ファイルを追加する
2 番目の画面で [GoogleService-Info.plist をダウンロード] をクリックして、アプリに必要なすべての Firebase メタデータを含む構成ファイルをダウンロードします。そのファイルをアプリケーションにコピーし、FriendlyChatSwift ターゲットに追加します。
ポップアップの右上にある [x] をクリックしてポップアップを閉じます。手順 3 と 4 はスキップします。これらの手順はここで実行します。
Firebase モジュールをインポートする
まず、Firebase モジュールがインポートされていることを確認します。
AppDelegate.swift、FCViewController.swift
import Firebase
AppDelegate で Firebase を構成する
application:didFinishLaunchingWithOptions 関数内の FirebaseApp の「configure」メソッドを使用して、.plist ファイルから基盤となる Firebase サービスを構成します。
AppDelegate.swift
  func application(_ application: UIApplication, didFinishLaunchingWithOptions
      launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  FirebaseApp.configure()
  GIDSignIn.sharedInstance().delegate = self
  return true
}
5. ユーザーを特定する
ルールを使用して認証済みユーザーに制限する
次に、メッセージの読み取りまたは書き込みを行う前に認証を必須にするルールを追加します。そのため、メッセージ データ オブジェクトに次のルールを追加します。Firebase コンソールの [データベース] セクションで、[Realtime Database] を選択し、[ルール] タブをクリックします。次に、ルールを次のように更新します。
{
  "rules": {
    "messages": {
      ".read": "auth != null",
      ".write": "auth != null"
    }
  }
}
この仕組みの詳細(「auth」変数のドキュメントを含む)については、Firebase のセキュリティ ドキュメントをご覧ください。
認証 API を構成する
アプリケーションがユーザーに代わって Firebase Authentication API にアクセスできるようにするには、有効にする必要があります。
- Firebase コンソールに移動して、プロジェクトを選択します。
 - [認証] を選択します。
 - [ログイン方法] タブを選択します。
 - [Google] スイッチを有効(青)に切り替えます。
 - 表示されたダイアログで [保存] を押します。
 
この Codelab の後半で「CONFIGURATION_NOT_FOUND」というメッセージが表示されるエラーが発生した場合は、この手順に戻って作業を再確認してください。
Firebase Auth の依存関係を確認する
Podfile ファイルに Firebase Auth の依存関係が存在することを確認します。
Podfile
pod 'Firebase/Auth'
Google ログイン用に Info.plist を設定します。
XCode プロジェクトにカスタム URL スキームを追加する必要があります。
- プロジェクト構成を開きます(左側のツリービューでプロジェクト名をダブルクリックします)。[ターゲット] セクションでアプリを選択し、[情報] タブを選択して [URL タイプ] セクションを展開します。
 - [+] ボタンをクリックし、反転クライアント ID の URL スキームを追加します。この値を確認するには、GoogleService-Info.plist 構成ファイルを開いて REVERSED_CLIENT_ID キーを探します。見つかったキーの値をコピーし、構成ページの [URL スキーム] ボックスに貼り付けます。その他の入力欄は空白にしておきます。
 - 完了すると、構成は次のようになります(ただし、値はアプリケーションによって異なります)。
 

Google ログインの clientID を設定する
Firebase を構成したら、clientID を使用して「didFinishLaunchingWithOptions:」メソッド内で Google ログインを設定できます。
AppDelegate.swift
  func application(_ application: UIApplication, didFinishLaunchingWithOptions
      launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
  FirebaseApp.configure()
  GIDSignIn.sharedInstance().clientID = FirebaseApp.app()?.options.clientID
  GIDSignIn.sharedInstance().delegate = self
  return true
}
ログイン ハンドラを追加する
Google ログインの結果が成功したら、アカウントを使用して Firebase で認証します。
AppDelegate.swift
  func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error?) {
    if let error = error {
      print("Error \(error)")
      return
    }
    guard let authentication = user.authentication else { return }
    let credential = GoogleAuthProvider.credential(withIDToken: authentication.idToken,
                                                      accessToken: authentication.accessToken)
    Auth.auth().signIn(with: credential) { (user, error) in
      if let error = error {
        print("Error \(error)")
        return
      }
    }
  }
ユーザーを自動的にログインさせます。次に、Firebase Auth にリスナーを追加して、ログインが成功した後にユーザーがアプリにアクセスできるようにします。また、deinit でリスナーを削除します。
SignInViewController.swift
  override func viewDidLoad() {
    super.viewDidLoad()
    GIDSignIn.sharedInstance().uiDelegate = self
    GIDSignIn.sharedInstance().signInSilently()
    handle = Auth.auth().addStateDidChangeListener() { (auth, user) in
      if user != nil {
        MeasurementHelper.sendLoginEvent()
        self.performSegue(withIdentifier: Constants.Segues.SignInToFp, sender: nil)
      }
    }
  }
  deinit {
    if let handle = handle {
      Auth.auth().removeStateDidChangeListener(handle)
    }
  }
ログアウト
ログアウト メソッドを追加する
FCViewController.swift
  @IBAction func signOut(_ sender: UIButton) {
    let firebaseAuth = Auth.auth()
    do {
      try firebaseAuth.signOut()
      dismiss(animated: true, completion: nil)
    } catch let signOutError as NSError {
      print ("Error signing out: \(signOutError.localizedDescription)")
    }
  }
ログイン済みユーザーとしてメッセージを読み取るテスト
- [
実行] ボタンをクリックします。 - すぐにログイン画面が表示されます。[Google でログイン] ボタンをタップします。
 - すべてが正常に動作していれば、メッセージ画面に移動します。
 
6. Realtime Database を有効にする

メッセージをインポートする
Firebase コンソールのプロジェクトで、左側のナビゲーション バーの [データベース] 項目を選択します。データベースのオーバーフロー メニューで、[JSON をインポート] を選択します。friendlychat ディレクトリの initial_messages.json ファイルを参照して選択し、[インポート] ボタンをクリックします。これにより、データベース内の既存のデータがすべて置き換えられます。データベースを直接編集することもできます。緑色の + と赤色の x を使用して、アイテムを追加または削除します。

インポート後、データベースは次のようになります。

Firebase データベースの依存関係を確認する
Podfile ファイルの依存関係ブロックで、Firebase/Database が含まれていることを確認します。
Podfile
pod 'Firebase/Database'
既存のメッセージを同期する
新しく追加されたメッセージをアプリの UI に同期するコードを追加します。
このセクションで追加するコードは、次の処理を行います。
- Firebase データベースを初期化し、データベースに対する変更を処理するリスナーを追加します。
 DataSnapshotを更新すると、新着メッセージが表示されます。
FCViewController の deinit、configureDatabase、tableView:cellForRow indexPath: メソッドを変更し、以下で定義されているコードに置き換えます。
FCViewController.swift
  deinit {
    if let refHandle = _refHandle {
      self.ref.child("messages").removeObserver(withHandle: _refHandle)
    }
  }
  func configureDatabase() {
    ref = Database.database().reference()
    // Listen for new messages in the Firebase database
    _refHandle = self.ref.child("messages").observe(.childAdded, with: { [weak self] (snapshot) -> Void in
      guard let strongSelf = self else { return }
      strongSelf.messages.append(snapshot)
      strongSelf.clientTable.insertRows(at: [IndexPath(row: strongSelf.messages.count-1, section: 0)], with: .automatic)
    })
  }
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // Dequeue cell
    let cell = self.clientTable.dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
    // Unpack message from Firebase DataSnapshot
    let messageSnapshot = self.messages[indexPath.row]
    guard let message = messageSnapshot.value as? [String: String] else { return cell }
    let name = message[Constants.MessageFields.name] ?? ""
    let text = message[Constants.MessageFields.text] ?? ""
    cell.textLabel?.text = name + ": " + text
    cell.imageView?.image = UIImage(named: "ic_account_circle")
    if let photoURL = message[Constants.MessageFields.photoURL], let URL = URL(string: photoURL),
        let data = try? Data(contentsOf: URL) {
      cell.imageView?.image = UIImage(data: data)
    }
    return cell
  }
メッセージ同期をテストする
- [
実行] ボタンをクリックします。 - [Sign in to get started](ログインして開始)ボタンをクリックして、メッセージ ウィンドウに移動します。
 - Firebase コンソールで、[messages] エントリの横にある緑色の + 記号をクリックし、次のようなオブジェクトを追加して、新しいメッセージを直接追加します。

 - Friendly-Chat UI に表示されることを確認します。
 
7. メッセージを送信する
メッセージの送信を実装する
値をデータベースにプッシュします。push メソッドを使用して Firebase Realtime Database にデータを追加すると、自動 ID が追加されます。これらの自動生成 ID は順次生成されるため、新しいメッセージが正しい順序で追加されます。
FCViewController の「sendMessage:」メソッドを変更し、次のコードに置き換えます。
FCViewController.swift
  func sendMessage(withData data: [String: String]) {
    var mdata = data
    mdata[Constants.MessageFields.name] = Auth.auth().currentUser?.displayName
    if let photoURL = Auth.auth().currentUser?.photoURL {
      mdata[Constants.MessageFields.photoURL] = photoURL.absoluteString
    }
    // Push data to Firebase Database
    self.ref.child("messages").childByAutoId().setValue(mdata)
  }
メッセージの送信をテストする
- [
実行] ボタンをクリックします。 - [Sign In] をクリックしてメッセージ ウィンドウに移動します。
 - メッセージを入力して送信します。新しいメッセージがアプリの UI と Firebase コンソールに表示されます。
 
8. 画像の保存と受信
Firebase Storage の依存関係を確認する
Podfile の dependencies ブロックで、Firebase/Storage が含まれていることを確認します。
Podfile
pod 'Firebase/Storage'
Cloud Storage for Firebase を設定する
Firebase プロジェクトで Cloud Storage for Firebase を設定する手順は次のとおりです。
- Firebase コンソールの左側のパネルで [ビルド] を展開し、[Storage] を選択します。
 - [開始] をクリックします。
 - デフォルトの Storage バケットのロケーションを選択します。
US-WEST1、US-CENTRAL1、US-EAST1のバケットは、Google Cloud Storage の「無料枠」を利用できます。他のすべてのロケーションのバケットは、Google Cloud Storage の料金と使用量に従います。 - [テストモードで開始] をクリックします。セキュリティ ルールに関する免責条項を確認します。
この Codelab の後半で、データを保護するためのセキュリティ ルールを追加します。Storage バケットのセキュリティ ルールを追加せずに、アプリを配布または公開しないでください。 - [作成] をクリックします。
 
FirebaseStorage を構成する
FCViewController.swift
  func configureStorage() {
    storageRef = Storage.storage().reference()
  }
既存のメッセージで画像を受信する
Firebase Storage から画像をダウンロードするコードを追加します。
FCViewController の「tableView: cellForRowAt indexPath:」メソッドを変更し、以下のコードに置き換えます。
FCViewController.swift
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // Dequeue cell
    let cell = self.clientTable .dequeueReusableCell(withIdentifier: "tableViewCell", for: indexPath)
    // Unpack message from Firebase DataSnapshot
    let messageSnapshot: DataSnapshot! = self.messages[indexPath.row]
    guard let message = messageSnapshot.value as? [String:String] else { return cell }
    let name = message[Constants.MessageFields.name] ?? ""
    if let imageURL = message[Constants.MessageFields.imageURL] {
      if imageURL.hasPrefix("gs://") {
        Storage.storage().reference(forURL: imageURL).getData(maxSize: INT64_MAX) {(data, error) in
          if let error = error {
            print("Error downloading: \(error)")
            return
          }
          DispatchQueue.main.async {
            cell.imageView?.image = UIImage.init(data: data!)
            cell.setNeedsLayout()
          }
        }
      } else if let URL = URL(string: imageURL), let data = try? Data(contentsOf: URL) {
        cell.imageView?.image = UIImage.init(data: data)
      }
      cell.textLabel?.text = "sent by: \(name)"
    } else {
      let text = message[Constants.MessageFields.text] ?? ""
      cell.textLabel?.text = name + ": " + text
      cell.imageView?.image = UIImage(named: "ic_account_circle")
      if let photoURL = message[Constants.MessageFields.photoURL], let URL = URL(string: photoURL),
          let data = try? Data(contentsOf: URL) {
        cell.imageView?.image = UIImage(data: data)
      }
    }
    return cell
  }
9. 画像メッセージを送信する
Store and Send Images を実装する
ユーザーから画像をアップロードし、この画像のストレージ URL をデータベースに同期して、この画像がメッセージ内に送信されるようにします。
FCViewController の「imagePickerController: didFinishPickingMediaWithInfo:」メソッドを変更し、次のコードに置き換えます。
FCViewController.swift
  func imagePickerController(_ picker: UIImagePickerController,
    didFinishPickingMediaWithInfo info: [String : Any]) {
      picker.dismiss(animated: true, completion:nil)
    guard let uid = Auth.auth().currentUser?.uid else { return }
    // if it's a photo from the library, not an image from the camera
    if #available(iOS 8.0, *), let referenceURL = info[UIImagePickerControllerReferenceURL] as? URL {
      let assets = PHAsset.fetchAssets(withALAssetURLs: [referenceURL], options: nil)
      let asset = assets.firstObject
      asset?.requestContentEditingInput(with: nil, completionHandler: { [weak self] (contentEditingInput, info) in
        let imageFile = contentEditingInput?.fullSizeImageURL
        let filePath = "\(uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000))/\((referenceURL as AnyObject).lastPathComponent!)"
        guard let strongSelf = self else { return }
        strongSelf.storageRef.child(filePath)
          .putFile(from: imageFile!, metadata: nil) { (metadata, error) in
            if let error = error {
              let nsError = error as NSError
              print("Error uploading: \(nsError.localizedDescription)")
              return
            }
            strongSelf.sendMessage(withData: [Constants.MessageFields.imageURL: strongSelf.storageRef.child((metadata?.path)!).description])
          }
      })
    } else {
      guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else { return }
      let imageData = UIImageJPEGRepresentation(image, 0.8)
      let imagePath = "\(uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000)).jpg"
      let metadata = StorageMetadata()
      metadata.contentType = "image/jpeg"
      self.storageRef.child(imagePath)
        .putData(imageData!, metadata: metadata) { [weak self] (metadata, error) in
          if let error = error {
            print("Error uploading: \(error)")
            return
          }
          guard let strongSelf = self else { return }
          strongSelf.sendMessage(withData: [Constants.MessageFields.imageURL: strongSelf.storageRef.child((metadata?.path)!).description])
      }
    }
  }
画像メッセージの送受信をテストする
- [
実行] ボタンをクリックします。 - [Sign In] をクリックしてメッセージ ウィンドウに移動します。
 - [写真を追加] アイコンをクリックして写真を選択します。写真付きの新しいメッセージがアプリの UI と Firebase コンソールに表示されます。
 
10. 完了
Firebase を使用してリアルタイム チャット アプリケーションを簡単に作成しました。
学習した内容
- Realtime Database
 - フェデレーション ログイン
 - ストレージ
 
