昨天,我們成功打造了聊天室的核心介面與互動邏輯,但 ChatMessage
和 NetworkManager
這兩個幕後功臣還只是「概念上」的存在。今天,我們就要來補齊這些基礎建設,讓 App 真正地與世界頂尖的 AI 模型進行對話!
我們將會定義對話所需的資料模型、實作串接 Google Gemini API 的網路層,並探討如何安全地管理我們的 API Key。完成今天的工作後,我們的 App 就完整了!
ChatMessage
與 GeminiResponse
兩種 struct
來管理對話與 API 回應。getGeminiResponse
方法,完成發送請求、接收與解析 JSON 的完整流程。Codable
結構體來解析巢狀的 JSON 資料。首先,我們需要定義兩種資料結構:一種用來在 App 內部表示每一則對話訊息,另一種則是用來解析從 Gemini API 回傳的複雜 JSON 資料。
ChatMessage.swift
這是在 TableView
中顯示資料的基礎模型。
ChatMessage.swift
。// ChatMessage.swift
import Foundation
struct ChatMessage {
let text: String // 訊息內容
let isUser: Bool // true = 使用者, false = Gemini
}
GeminiResponse.swift
這個檔案專門用來解析 Gemini API 回傳的 JSON 格式。你會發現 JSON 是一層一層包起來的,所以我們也需要用多個 struct
來對應它的巢狀結構。
在專案中新增一個 Swift 檔案,命名為 GeminiResponse.swift
。
貼上以下程式碼:
// GeminiResponse.swift
import Foundation
// MARK: - GeminiResponse
// 代表整個 Gemini API 的回應物件
struct GeminiResponse: Decodable {
let candidates: [Candidate]?
}
// MARK: - Candidate
// 代表一個單獨的生成結果
struct Candidate: Decodable {
// 包含由模型生成的實際內容
let content: Content
// 說明生成過程為何結束的理由(例如 "STOP" 或 "SAFETY")
let finishReason: String?
}
// MARK: - Content
// 包含由模型生成的內容細節
struct Content: Decodable {
// 內容的角色,通常為 "model"
let role: String?
// 內容的部分,通常包含文字
let parts: [Part]
}
// MARK: - Part
// 代表內容的單個部分
struct Part: Decodable {
// 生成的純文字內容。如果內容是文字,此欄位會存在
let text: String?
}
我們使用了 Decodable
協定,讓 Swift 的 JSONDecoder
能夠自動將網路回傳的 JSON 資料,轉換成我們定義好的這些 struct
物件。
在我們能呼叫 Gemini API 之前,需要先向 Google 取得一把專屬的「鑰匙」,也就是 API Key。這就像是你的 App 要和 Google AI 服務對話時,需要出示的通行證。取得過程完全免費且非常快速,我們開始吧!
首先,請使用你的 Google 帳號登入 Google AI Studio 官方網站。
在 Google AI Studio 的介面中,找到並點擊「Get API key」(取得 API 金鑰)的按鈕。這個按鈕通常位於頁面的左側選單下方:]
點擊後,會彈出一個視窗。請點擊「Create API key」按鈕。接著,你可以為你的鑰匙命名:
接著,你會看到畫面新增一串資訊,其中由英數字元組成的 API Key,待會在撰寫 NetworkManager
程式碼時會用到它。
NetworkManager
:與 Gemini API 對話的核心接下來是重頭戲!我們將建立一個 NetworkManager.swift
檔案,並在裡面撰寫一個專門用來呼叫 Gemini API 的方法。
在專案中新增一個 Swift 檔案,命名為 NetworkManager.swift
。
貼上以下程式碼:
// NetworkManager.swift
import Foundation
class NetworkManager {
static let apiKey = "請在此處貼上你剛才申請好的 Gemini API Key"
@discardableResult
static func getGeminiResponse(for userInput: String, completion: @escaping (String) -> Void) -> URLSessionDataTask {
let modelName = "gemini-1.5-flash" // 建議使用較新的模型
let urlString = "https://generativelanguage.googleapis.com/v1beta/models/\(modelName):generateContent?key=\(apiKey)"
guard let url = URL(string: urlString) else {
completion("URL 格式錯誤")
fatalError("Invalid URL")
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"contents": [
[
"parts": [
[
"text": userInput
]
]
]
]
]
do {
request.httpBody = try JSONSerialization.data(withJSONObject: body)
} catch {
completion("JSON 轉換失敗")
fatalError("Invalid JSON")
}
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error as NSError?, error.code == NSURLErrorCancelled {
print("使用者中斷 Gemini 回覆")
completion("回覆已取消")
return
}
if let error = error {
print("請求錯誤: \(error)")
completion("網路錯誤,請稍後再試。")
return
}
guard let data = data else {
completion("沒有收到回傳資料")
return
}
// 用來除錯,看看 API 回傳了什麼
if let jsonString = String(data: data, encoding: .utf8) {
print("🧾 Gemini 回傳內容:\(jsonString)")
}
do {
let geminiResponse = try JSONDecoder().decode(GeminiResponse.self, from: data)
// 從巢狀結構中,安全地取出文字
let text = geminiResponse.candidates?.first?.content.parts.first?.text ?? "抱歉,我無法理解。"
completion(text)
} catch {
print("JSON 解析錯誤: \(error)")
completion("解析 API 回應時發生錯誤。")
}
}
task.resume()
return task
}
}
data
後,我們使用 JSONDecoder
和剛剛建立的 GeminiResponse.self
來解析它,並從層層結構中取出最終的文字 text
。直接將 API Key 寫在程式碼中是一個非常不安全的做法!如果直接將這樣的程式碼上傳到 GitHub 等公開平台,你的 Key 就會被洩漏,也很容易被他人濫用。
在大型專案中,我們會使用更複雜的方法(例如 xcconfig
檔案或伺服器端管理)來保護 Key。但對於初學者來說,這裡提供一個簡單且有效的起步方法:使用 Info.plist
。
Info.plist
Info.plist
檔案。Add Row
。GeminiAPIKey
。Value
設為自己的 Gemini API Key 字串。NetworkManager
來讀取 Key修改 NetworkManager.swift
,讓它從 Info.plist
中讀取 Key,而不是直接寫死。
class NetworkManager {
// 從 Info.plist 讀取 API Key
static var apiKey: String {
guard let filePath = Bundle.main.path(forResource: "Info", ofType: "plist") else {
fatalError("找不到 Info.plist 檔案。")
}
let plist = NSDictionary(contentsOfFile: filePath)
guard let value = plist?.object(forKey: "GeminiAPIKey") as? String else {
fatalError("在 Info.plist 中找不到 GeminiAPIKey。")
}
if value.starts(with: "【") {
fatalError("請在 Info.plist 中設定你的 GeminiAPIKey。")
}
return value
}
// ... (getGeminiResponse 方法保持不變) ...
}
別忘了將你的 Info.plist
檔案加入到 .gitignore
中,這樣在使用 Git 時,這個包含 Key 的檔案就不會被上傳到遠端倉庫!
這邊就不教學 Github 倉庫的使用方法,有興趣的朋友們,可以自行上網搜尋相關資料哦!
今天我們完成了 Gemini 聊天室所有「看不見」的幕後英雄!我們定義了清晰的資料模型來處理對話與 API 回應,並打造了一個功能完整的 NetworkManager
來與 Google Gemini API 進行溝通。
最重要的是,我們學習了如何安全地管理 API Key,這是從練習專案邁向真實產品開發的重要一步。現在,我們的 App 已經萬事俱備,準備好真正「開口說話」了!
明天就是我們鐵人賽的最後一天了!
我們將進行最後的總結與測試,確保 App 的所有功能都能正常運作。同時,我們也會回顧這 30 天的學習旅程,從一個 print("Hello, World!")
開始,到今天能串接 AI 模型。
這將是一次充滿成就感的收尾,也是你作為一名 iOS 開發者旅程的全新起點!
敬請期待《Day 30|Xcode 專案實戰:Gemini AI 聊天室總結與鐵人賽回顧!》