Firebase iOS Codelab: FriendlyChat

1. ภาพรวม

ยินดีต้อนรับสู่ Codelab FriendlyChat สำหรับ iOS ใน Codelab นี้ คุณจะได้เรียนรู้วิธีสร้างแอปพลิเคชันแชท iOS แบบเรียลไทม์ที่มีฟีเจอร์ครบถ้วนโดยใช้ Firebase และฟีเจอร์ Swift สมัยใหม่ เช่น SwiftUI, Swift Concurrency และเฟรมเวิร์ก Observation

สิ่งที่คุณจะได้เรียนรู้

  • จัดการสถานะด้วยเฟรมเวิร์ก Observation (@Observable) สมัยใหม่
  • ตรวจสอบสิทธิ์ผู้ใช้โดยใช้การตรวจสอบสิทธิ์ Firebase
  • ซิงค์ข้อความแชทแบบเรียลไทม์โดยใช้ Cloud Firestore
  • จัดเก็บและแสดงรูปภาพโดยใช้ Cloud Storage for Firebase
  • เข้าถึง Firebase API โดยใช้ฟีเจอร์ Swift Concurrency สมัยใหม่

สิ่งที่ต้องมี

  • Xcode เวอร์ชันล่าสุด
  • โปรแกรมจำลอง iOS หรืออุปกรณ์ทดสอบจริง
  • บัญชี Google สำหรับสร้างและจัดการโปรเจ็กต์ Firebase

2. รับโค้ดตัวอย่าง

โคลนที่เก็บ GitHub จากบรรทัดคำสั่งโดยใช้คำสั่งต่อไปนี้

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

ที่เก็บ codelab-friendlychat-ios มีโปรเจ็กต์ตัวอย่างหลายรายการ ดังนี้

  • ios-starter/swift-starter \- โปรเจ็กต์ Xcode เริ่มต้นที่มีโครงสร้างเลย์เอาต์ SwiftUI และความคิดเห็น TODO ซึ่งคุณจะใช้เพื่อติดตั้งใช้งานฟีเจอร์ของแอปด้วย Firebase
  • ios/swift \- โปรเจ็กต์ Xcode ที่เสร็จสมบูรณ์แล้วพร้อมแอปพลิเคชันตัวอย่างที่เสร็จสมบูรณ์

3. สร้างแอปเริ่มต้น

สร้างและเรียกใช้แอปพลิเคชันเริ่มต้นเพื่อทำความคุ้นเคยกับอินเทอร์เฟซผู้ใช้

  1. ใน Finder หรือ Terminal ให้ไปที่ไดเรกทอรี ios-starter/swift-starter
  2. ดับเบิลคลิก FriendlyChatSwift.xcodeproj เพื่อเปิดโปรเจ็กต์ใน Xcode
  3. สังเกตว่า Xcode จะแก้ปัญหาการขึ้นอยู่กับ Swift Package Manager ในเบื้องหลังโดยอัตโนมัติ (ดาวน์โหลด firebase-ios-sdk เวอร์ชัน 12 ขึ้นไปจาก GitHub)
  4. เลือกโปรแกรมจำลอง iOS (เช่น iPhone 17 Pro) แล้วคลิกปุ่มเรียกใช้ (cmd+R)

หลังจากผ่านไปสักครู่ หน้าจอเข้าสู่ระบบ FriendlyChat จะปรากฏขึ้น ในตอนนี้ ปุ่มเข้าสู่ระบบและฟีดข้อความเป็นตัวยึดตำแหน่ง คุณจะเชื่อมต่อ Firebase กับแอปในขั้นตอนต่อไป

4. ตั้งค่าโปรเจ็กต์ Firebase

สร้างโปรเจ็กต์ Firebase ใหม่

  1. ลงชื่อเข้าใช้คอนโซล Firebaseด้วยบัญชี Google
  2. คลิกปุ่มเพื่อสร้างโปรเจ็กต์ใหม่ แล้วป้อนชื่อโปรเจ็กต์ (เช่น FriendlyChat)
  3. คลิกต่อไป
  4. หากได้รับแจ้ง ให้อ่านและยอมรับข้อกำหนดของ Firebase แล้วคลิกต่อไป
  5. (ไม่บังคับ) เปิดใช้ความช่วยเหลือจาก AI ในคอนโซล Firebase (เรียกว่า "Gemini ใน Firebase")
  6. สำหรับ Codelab นี้ คุณไม่ จำเป็นต้องใช้ Google Analytics ดังนั้นปิด ตัวเลือก Google Analytics
  7. คลิกสร้างโปรเจ็กต์ รอให้ระบบจัดสรรโปรเจ็กต์ แล้วคลิกต่อไป

อัปเกรดแพ็กเกจราคาของ Firebase

หากต้องการใช้ Cloud Storage for Firebase โปรเจ็กต์ Firebase ของคุณต้องอยู่ในแพ็กเกจราคาแบบจ่ายเมื่อใช้ (Blaze) ซึ่งหมายความว่าโปรเจ็กต์ต้องลิงก์กับบัญชีสำหรับการเรียกเก็บเงินใน Cloud

หากต้องการอัปเกรดโปรเจ็กต์เป็นแพ็กเกจ Blaze ให้ทำตามขั้นตอนต่อไปนี้

  1. ในคอนโซล Firebase ให้เลือกเพื่อ อัปเกรดแพ็กเกจ
  2. เลือกแพ็กเกจ Blaze ทำตามวิธีการบนหน้าจอเพื่อลิงก์บัญชีสำหรับการเรียกเก็บเงินใน Cloud กับโปรเจ็กต์
    หากคุณต้องสร้างบัญชีสำหรับการเรียกเก็บเงินใน Cloud เป็นส่วนหนึ่งของการอัปเกรดนี้ คุณอาจต้องกลับไปที่โฟลว์การอัปเกรดในคอนโซล Firebase เพื่อทำการอัปเกรดให้เสร็จสมบูรณ์

เชื่อมต่อแอป iOS

  1. จากหน้าจอภาพรวมโปรเจ็กต์ ให้คลิกไอคอน iOS เพื่อเปิดเวิร์กโฟลว์การตั้งค่าแอป
  2. ป้อนรหัสชุดซอฟต์แวร์
    com.google.firebase.codelab.FriendlyChatSwift
    
  3. คลิกลงทะเบียนแอป

เพิ่ม GoogleService-Info.plist ลงในโปรเจ็กต์ Xcode

  1. คลิกดาวน์โหลด GoogleService-Info.plist เพื่อบันทึกไฟล์การกำหนดค่า
  2. ใน Xcode ให้ลากไฟล์ GoogleService-Info.plist ที่ดาวน์โหลดลงในกลุ่มหลัก FriendlyChatSwift ใน Project Navigator
  3. เมื่อได้รับข้อความแจ้ง ให้ตรวจสอบว่าได้เลือกคัดลอกรายการหากจำเป็น และเลือกเป้าหมาย FriendlyChatSwift แล้ว จากนั้นคลิกเสร็จสิ้น

กำหนดค่า Firebase ใน FriendlyChatSwiftApp.swift

สุดท้าย ให้เริ่มต้น Firebase ด้วยการกำหนดค่าโปรเจ็กต์เมื่อแอปพลิเคชัน SwiftUI เปิดขึ้น เปิด FriendlyChatSwiftApp.swift แล้วแทนที่ตัวยึดตำแหน่ง TODO ด้วย import FirebaseCore และ FirebaseApp.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 แบบโต้ตอบจะช่วยให้ UI เปลี่ยนไปมาระหว่างหน้าจอเข้าสู่ระบบและอินเทอร์เฟซแชทได้อย่างราบรื่น

เปิด ViewModels/UserViewModel.swift แล้วแทนที่ TODO ใน init() เพื่อสังเกตการเปลี่ยนแปลงสถานะการตรวจสอบสิทธิ์โดยใช้ AsyncSequence ของ authStateChanges

  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. ในโปรแกรมจำลองแอป ให้คลิกลงชื่อสมัครใช้ ป้อนชื่อที่แสดง อีเมล และรหัสผ่าน แล้วแตะลงชื่อสมัครใช้
  3. ระบบควรตรวจสอบสิทธิ์และนำคุณไปยังหน้าจอการรับส่งข้อความที่ว่างเปล่าโดยอัตโนมัติ

6. อ่านจาก Cloud Firestore

เพิ่มข้อความตัวอย่างในคอนโซล Firebase

ป้อนข้อความตัวอย่างลงในฐานข้อมูลโดยทำดังนี้

  1. ในคอนโซล Firebase ให้ไปที่ฐานข้อมูล Firestore แล้วเลือกแท็บข้อมูล
  2. คลิกเริ่มคอลเล็กชัน
  3. ป้อน messages สำหรับรหัสคอลเล็กชัน แล้วคลิกถัดไป
  4. ปล่อยให้รหัสเอกสาร ตั้งค่าเป็นสร้างอัตโนมัติ (หรือคลิกรหัสอัตโนมัติ)
  5. เพิ่มช่องต่อไปนี้ลงในเอกสาร
    • text (ประเภท: string, ค่า: Hello)
    • displayName (ประเภท: string, ค่า: anonymous)
  6. คลิกบันทึก
  7. หรือคลิกเพิ่มเอกสาร เพื่อเพิ่มข้อความตัวอย่างเพิ่มเติม

ซิงค์ข้อความใน FriendlyMessageViewModel.swift

ใน SwiftUI โมเดล @Observable จะขับเคลื่อนการอัปเดต UI แบบประกาศ เปิด ViewModels/FriendlyMessageViewModel.swift แล้วใช้ AsyncSequence ของ snapshots เพื่อใช้ 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 ให้สังเกตวิธีที่ ScrollViewReader และ LazyVStack แสดงบับเบิลแชทและเลื่อนไปที่ข้อความล่าสุดโดยอัตโนมัติเมื่อใดก็ตามที่ 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 โดยตรงผ่าน View Model ช่วยให้โค้ดแอป SwiftUI สามารถจับคู่การเปลี่ยนภาพกับการเคลื่อนไหวได้อย่างชัดเจน ในขั้นตอนถัดไป คุณจะเขียนข้อมูลลงในฐานข้อมูลจากแอปเพื่อสังเกตการเคลื่อนไหวนี้

7. ส่งข้อความ

ใช้ sendMessage ด้วย Swift Concurrency

เมื่อคุณเพิ่มเอกสารลงในคอลเล็กชันโดยใช้ addDocument() Cloud Firestore จะสร้างรหัสที่ไม่ซ้ำกันสำหรับข้อความแชทแต่ละรายการ

ใน ViewModels/FriendlyMessageViewModel.swift ให้แทนที่ TODO ใน sendMessage(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. ยอมรับตำแหน่งพื้นที่เก็บข้อมูลเริ่มต้น แล้วคลิกเสร็จสิ้น

การเลือกรูปภาพแบบเนทีฟด้วย PhotosPicker ของ SwiftUI

SwiftUI สมัยใหม่ (iOS 16 ขึ้นไป) มีการเลือกรูปภาพแบบเนทีฟผ่าน PhotosPicker จาก import PhotosUI โดยไม่จำเป็นต้องใช้บริดจ์ผู้รับมอบสิทธิ์ UIKit หรือ Wrapper 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 ให้แทนที่ TODO ใน uploadAndSendImage(data:)

  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 ระบบจะโหลด URL รูปภาพแบบไม่พร้อมกันโดยใช้ Swift Concurrency และ Cloud Storage SDK

  • ระบบจะดึง URL ดาวน์โหลด HTTP/HTTPS มาตรฐานแบบไม่พร้อมกันผ่าน URLSession
  • ระบบจะดึง URL ของ Google Cloud Storage (เช่น gs://...) โดยใช้ Storage.storage().reference(forURL:)

ทดสอบข้อความรูปภาพ

  1. คลิกปุ่มเรียกใช้ ใน Xcode
  2. ลงชื่อเข้าใช้บัญชีของคุณ
  3. แตะไอคอนรูปภาพ เลือกรูปภาพจากคลังภาพ แล้วดูรูปภาพอัปโหลดและปรากฏในแชท

9. บทสรุป

ขอแสดงความยินดี คุณสร้างแอปพลิเคชันแชท iOS แบบเรียลไทม์โดยใช้ Swift และ Firebase ได้สำเร็จ

สิ่งที่คุณได้เรียนรู้

  • การผสานรวม Swift Package Manager (SPM) กับ Xcode สำหรับ firebase-ios-sdk
  • เลย์เอาต์แบบประกาศ PhotosPicker และการเลื่อนอัตโนมัติ ScrollViewReader ของ SwiftUI
  • Swift Concurrency (async/await) ในการตรวจสอบสิทธิ์ Cloud Firestore และ Cloud Storage
  • การตรวจสอบสิทธิ์ด้วยอีเมล/รหัสผ่านและการตรวจสอบสิทธิ์แบบไม่ระบุตัวตน พร้อมการสังเกตสถานะแบบโต้ตอบ
  • การฟังข้อมูลสแนปช็อตแบบเรียลไทม์และการเขียนรหัสอัตโนมัติของ Cloud Firestore
  • การอัปโหลดไบนารีและ URL ดาวน์โหลดสาธารณะของ Cloud Storage for Firebase

ดูข้อมูลเพิ่มเติม