1. 概览

欢迎学习 Friendly Chat Codelab。在此 Codelab 中,您将学习如何使用 Firebase 平台创建 iOS 应用。您将实现一个聊天客户端,并使用 Firebase 监控其性能。
学习内容
- 允许用户登录。
- 使用 Firebase 实时数据库同步数据。
- 在 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 主屏幕。界面应随即显示。不过,此时您无法登录、发送或接收邮件。在完成后续步骤之前,应用将因出现异常而中止。
4. 设置 Firebase 项目
创建新的 Firebase 项目
- 使用您的 Google 账号登录 Firebase 控制台。
- 点击相应按钮以创建新项目,然后输入项目名称(例如 FriendlyChat)。
 
- 点击继续。
- 如果看到相关提示,请查看并接受 Firebase 条款,然后点击继续。
- (可选)在 Firebase 控制台中启用 AI 辅助功能(称为“Gemini in Firebase”)。
- 在此 Codelab 中,您不需要使用 Google Analytics,因此请关闭 Google Analytics 选项。
- 点击创建项目,等待项目完成预配,然后点击继续。
升级您的 Firebase 定价方案
如需使用 Cloud Storage for Firebase,您的 Firebase 项目必须采用随用随付 (Blaze) 定价方案,这意味着该项目与一个 Cloud Billing 账号相关联。
- Cloud Billing 账号要求提供付款方式,例如信用卡。
- 如果您刚开始接触 Firebase 和 Google Cloud,请确认您是否有资格获得 $300 赠金和免费试用 Cloud Billing 账号。
- 如果您是在活动中完成此 Codelab,请询问活动组织者是否有可用的 Cloud 积分。
如需将项目升级到 Blaze 方案,请按以下步骤操作:
- 在 Firebase 控制台中,选择升级您的方案。
- 选择 Blaze 方案。按照屏幕上的说明将 Cloud Billing 账号与您的项目相关联。
 如果您需要在此升级过程中创建 Cloud Billing 账号,则可能需要返回 Firebase 控制台中的升级流程以完成升级。
关联 iOS 应用
- 在新建项目的“项目概览”界面中,点击将 Firebase 添加到您的 iOS 应用。
- 输入软件包 ID,例如“com.google.firebase.codelab.FriendlyChatSwift”。
- 输入 App Store ID,格式为“123456”。
- 点击注册应用。
将 GoogleService-Info.plist 文件添加到您的应用
在第二个界面上,点击下载 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 之前,您必须先启用该 API
- 前往 Firebase 控制台,然后选择您的项目
- 选择身份验证
- 选择登录方法标签页
- 将 Google 开关切换为启用状态(蓝色)
- 按相应对话框中的保存
如果您在本 Codelab 的后续步骤中遇到“CONFIGURATION_NOT_FOUND”错误消息,请返回此步骤并仔细检查您的工作。
确认 Firebase Auth 依赖项
确认 Podfile 文件中存在 Firebase Auth 依赖项。
Podfile
pod 'Firebase/Auth'
为 Google 登录设置 Info.plist。
您需要向 Xcode 项目添加自定义网址方案。
- 打开项目配置:在左侧的树状视图中双击项目名称。在“目标”部分中选择您的应用,然后选择“信息”标签页,并展开“网址类型”部分。
- 点击“+”按钮,并为您的倒序客户端 ID 添加一个网址方案。如需查找此值,请打开 GoogleService-Info.plist 配置文件,然后查找 REVERSED_CLIENT_ID 键。复制该键的值,并将其粘贴到配置页面上的“网址方案”框中。将其他字段留空。
- 完成上述操作后,您的配置应显示如下(但其中的值应替换为您的应用的值):

为 Google 登录设置 clientID
配置 Firebase 后,我们可以在“didFinishLaunchingWithOptions:”方法中使用 clientID 设置 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 文件,选择该文件,然后点击导入按钮。这会替换数据库中当前的所有数据。您还可以直接修改数据库,使用绿色加号和红色叉号添加和移除商品。

导入后,您的数据库应如下所示:

确认 Firebase 数据库依赖项
在 Podfile 文件的依赖项代码块中,确认是否包含 Firebase/Database。
Podfile
pod 'Firebase/Database'
同步现有消息
添加可将新添加的消息同步到应用界面的代码。
您在此部分中添加的代码将:
- 初始化 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
  }
测试消息同步
- 点击 运行按钮。 运行按钮。
- 点击登录以开始使用按钮前往消息窗口。
- 点击“消息”条目旁边的绿色 + 符号,直接在 Firebase 控制台中添加新消息,并添加如下对象: 
- 确认它们显示在 Friendly-Chat 界面中。
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)
  }
测试发送消息
- 点击 运行按钮。 运行按钮。
- 点击登录,前往消息窗口。
- 输入消息,然后点击“发送”。新消息应显示在应用界面和 Firebase 控制台中。
8. 存储和接收图片
确认 Firebase Storage 依赖项
在 Podfile 的依赖项代码块中,确认是否包含 Firebase/Storage。
Podfile
pod 'Firebase/Storage'
设置 Cloud Storage for Firebase
以下是在 Firebase 项目中设置 Cloud Storage for Firebase 的方法:
- 在 Firebase 控制台的左侧面板中,展开构建,然后选择存储。
- 点击开始使用。
- 为默认存储分区选择位置。US-WEST1、US-CENTRAL1和US-EAST1中的存储分区可为 Google Cloud Storage 使用“始终免费”层级。所有其他位置的存储分区都遵循 Google Cloud Storage 价格和用量。
- 点击以测试模式开始。阅读有关安全规则的免责声明。
 在本 Codelab 的后面部分,您将添加安全规则来保护您的数据。在未为您的存储桶添加安全规则的情况下,请不要公开分发或公开应用。
- 点击创建。
配置 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. 发送图片消息
实现“存储并发送图片”功能
上传用户提供的图片,然后将此图片的存储网址同步到数据库,以便在消息中发送此图片。
修改 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])
      }
    }
  }
测试发送和接收图片消息
- 点击 运行按钮。 运行按钮。
- 点击登录,前往消息窗口。
- 点击“添加照片”图标以选择照片。包含照片的新消息应显示在应用界面和 Firebase 控制台中。
10. 恭喜!
您已使用 Firebase 轻松构建了一个实时聊天应用。
所学内容
- Realtime Database
- 联合登录
- 存储
