嗨,大家今天過得好嗎?歡迎來到鐵人賽 Day 20。
昨天我們用 JSON Schema 強制解析了 n8n 的輸出,我們的無程式碼後端基本上已經進化成了「商業完全體」。現在,是時候把前幾天在 SwiftUI 寫的 APIService 換成真實的 n8n Webhook 網址,讓前後端正式大會師了!
但是,當你興高采烈地把網址換掉,按下「產生推薦菜單」時,你很快就會面臨全端 AI 開發的第一個震撼教育:AI 實在是太慢了。
以前我們打傳統後端 API(例如去資料庫撈個會員資料),回應時間通常在 100 毫秒以內。但在我們現在的 n8n 工作流中,系統要去 PostgreSQL 撈資料、去抓即時匯率、最後還要丟給 OpenAI / Gemini 思考。
這個「思考」的過程,根據當下的網路狀況與 Token 數量,通常會耗時 3 到 10 秒,甚至更久。
如果我們的前端沒有做好對應的防護,會發生兩件災難:
URLSession 若遇到網路波動,很容易拋出 URLError.timedOut,如果你的 Controller 沒有接住這個 Error,畫面就會直接卡死在轉圈圈。今天,我們要用 Vibe Coding 搭配工程師心法,為我們的 App 加上強韌的「錯誤防護罩」。
還記得我們 Day 11 寫的 APIService 嗎?當時我們使用的是 URLSession.shared。這個預設的 Session 對於長時間等待的 LLM 請求來說不太友善。
我們需要自己建立一個帶有 Timeout 設定的 Session。你可以請 AI 幫你這樣改,或是直接手動更新 APIService.swift:
import Foundation
class APIService {
static let shared = APIService()
// 建立一個自訂的 URLSession
private let session: URLSession
private init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30.0 // 把 Timeout 拉長到 30 秒,給 AI 思考的時間
config.timeoutIntervalForResource = 60.0
self.session = URLSession(configuration: config)
}
func fetchRecommendation(budget: Int, purpose: String, sessionId: String) async throws -> HardwareMenu {
// 請換成你的 n8n Webhook Test URL (記得加上 ngrok 或是內網 IP)
guard let url = URL(string: "[http://192.168.](http://192.168.)X.X:5678/webhook-test/recommend") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// 把 Session ID 也一起包進 Payload 送給後端記憶體
let payload: [String: Any] = [
"budget": budget,
"purpose": purpose,
"sessionId": sessionId
]
request.httpBody = try JSONSerialization.data(withJSONObject: payload)
// 改用我們自訂的 session 發送請求
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(HardwareMenu.self, from: data)
}
}
網路層設定好了,接下來是邏輯層。
當 API 發生錯誤時(例如 n8n 掛了、AI API Key 沒錢了),我們必須把錯誤訊息接下來,通知前端畫面。
打開你的 HomeController.swift,我們新增兩個專門用來處理錯誤的狀態:
import Foundation
import SwiftUI
@MainActor
class HomeController: ObservableObject {
@Published var requestData = HardwareRequest()
@Published var isLoading = false
@Published var resultMenu: HardwareMenu?
// 新增:錯誤處理狀態
@Published var showError = false
@Published var errorMessage = ""
let purposeOptions = ["3A 遊戲", "影音剪輯", "文書處理"]
func submitRequest() {
guard let budgetInt = Int(requestData.budget), budgetInt > 0 else {
self.errorMessage = "請輸入有效的預算金額"
self.showError = true
return
}
isLoading = true
resultMenu = nil
Task {
do {
let menu = try await APIService.shared.fetchRecommendation(
budget: budgetInt,
purpose: requestData.purpose,
sessionId: requestData.sessionId
)
self.resultMenu = menu
self.isLoading = false
} catch {
// 接住錯誤,並更新給 UI 顯示
print("API 錯誤:\(error.localizedDescription)")
self.errorMessage = "AI 伺服器連線失敗,請稍後再試。\n(\(error.localizedDescription))"
self.showError = true
self.isLoading = false // 記得關閉轉圈圈
}
}
}
}
狀態都準備好了,我們終於可以請 AI 幫忙修飾畫面了。
這一步非常簡單,把這段 Prompt 餵給 AI:
這是我目前的 ContentView.swift (貼上 Day 9 的程式碼)。
我的 HomeController 新增了兩個錯誤狀態:`showError` (Bool) 與 `errorMessage` (String)。
請幫我在 ContentView 加上 SwiftUI 原生的 `.alert` 彈窗。當 showError 為 true 時,顯示 errorMessage。
AI 會在你的 ContentView 的大括號結尾處,優雅地加上這段 Modifier:
.alert(isPresented: $controller.showError) {
Alert(
title: Text("發生錯誤"),
message: Text(controller.errorMessage),
dismissButton: .default(Text("確定"))
)
}
** UX 守門員:禁用連點**
記得我們在 Day 9 已經偷偷在送出按鈕加上了.disabled(controller.isLoading)。這個小小的 Modifier,在真實接上 AI API 後將成為你的救命恩人,它徹底阻絕了使用者在等待的 10 秒內連點 100 次按鈕的慘劇。
今天我們補齊了全端開發中經常被新手忽略,卻是上線前最重要的一環:Error Handling 與 Timeout 防護機制。
這就是為什麼在 Day 1 我就強調,Vibe Coding 雖然快,但「工程師的架構思維」才是專案成敗的關鍵。AI 不會主動提醒你設定 Timeout,也不會考慮你的使用者會不會狂點按鈕,這些都需要我們用經驗去引導它。
明天,我們的前後端整合已經正式宣告完成!我們將實作一個超酷的進階商業場景:「結合 AI 審核機制與資料庫回寫」。不只要讓 AI 讀資料,還要讓 AI 把算好的菜單「存回 PostgreSQL」作為歷史訂單!
我們 Day 21 見!