Joe's Blog

iOS 開發筆記

Swift 網路層生存指南(2)

發佈於 2026-01-10

在前一篇文章中,我們學會了如何使用 SafeBoxFlexibleResponse 確保 App 不會因為後端的髒資料而閃退。但「不崩潰」只是第一步。在實務開發中,我們還需要能正確且高效地處理業務邏輯錯誤(如:密碼錯誤、權限不足)。

今天我們要討論的是如何透過 BaseResponseProtocol,在多變的後端環境中建立一套統一的「開發合約」。


🏛️ 第一步:與 BadBackend 訂立外殼合約

即便功能一樣,不同公司的後端給你的「外殼」命名可能天差地遠。假設你去年在 A 公司,今年跳槽到了 B 公司,這兩間公司雖然命名不同,但都還算「守約」(結構穩定):

去年在 A 公司 (包裝派)

1
2
3
4
5
{
  "statusCode": 200,
  "message": "success",
  "result": { "data": { "id": 1, "name": "iPhone 17" } }
}

今年在 B 公司 (簡約派)

1
2
3
4
5
{
  "code": 200,
  "msg": "OK",
  "data": { "id": 1, "name": "iPhone 17" }
}

為了抹平差異,我們定義一個 Protocol 抽象化這些差異:

1
2
3
4
5
6
protocol BaseResponseProtocol: Decodable {
    associatedtype Payload: Codable
    var isSuccess: Bool { get }             // 業務成功與否的統一判斷
    var message: String { get }             // 錯誤訊息欄位
    var result: FlexibleResponse<Payload> { get }
}

🛠️ 第二步:為 A / B 公司打造標準轉接頭

A 公司實作

1
2
3
4
5
6
7
struct CompanyAResponse<T: Codable>: BaseResponseProtocol {
    @SafeBox var statusCode: Int
    @SafeBox var message: String
    let result: FlexibleResponse<T>

    var isSuccess: Bool { statusCode == 200 }
}

B 公司實作

1
2
3
4
5
6
7
8
9
struct CompanyBResponse<T: Codable>: BaseResponseProtocol {
    @SafeBox var code: Int
    @SafeBox var msg: String
    let data: FlexibleResponse<T>

    var isSuccess: Bool { code == 200 }
    var message: String { msg }
    var result: FlexibleResponse<T> { data }
}

🏎️ 第三步:標準化 Request 流程

有了合約,你的 NetworkManager 就能專注於處理「業務邏輯錯誤」。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func request<R: BaseResponseProtocol>(url: URL) async throws -> R.Payload {
    // 1. 下載數據
    let (data, _) = try await URLSession.shared.data(from: url)

    // 2. 解析外殼 (此時 SafeBox 會保護 code/message 的型別)
    let response = try JSONDecoder().decode(R.self, from: data)

    // 3. 正確且高效地處理業務邏輯錯誤 (如:密碼錯誤)
    guard response.isSuccess else {
        throw APIError.businessError(message: response.message)
    }

    // 4. 解析成功:從 FlexibleResponse 提取資料成品
    return response.result.result
}

🚨 進階特例:應對「毀約」的 C 公司

在 90% 的情況下,上述流程已經完美。但現實中總有最壞的情況:特例 C 會在失敗時毀掉原始結構

特例 C 的毀約現場

1
2
3
4
{
  "statusCode": 200,
  "result": { "data": { "id": 99, "name": "特製商品" } }
}
1
2
3
4
{
  "statusCode": 403,
  "result": { "message": "您的帳號權限不足" }
}

這會導致 JSONDecoder 在 Step 2 解析 result 時噴出 keyNotFound。我們必須引入 DefaultProvider 救災:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct CompanyCResponse<T: Codable>: BaseResponseProtocol {
  @SafeBox var statusCode: Int
  let result: FlexibleResponse<T>
  let message: String

  var isSuccess: Bool { statusCode == 200 }

  enum CodingKeys: String, CodingKey { case statusCode, result }
  enum ResultKeys: String, CodingKey { case data, message }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let statusBox = try container.decode(SafeBox<Int>.self, forKey: .statusCode)
    let success = statusBox.wrappedValue == 200 // 局部判斷邏輯

    if success {
      self.result = try container.decode(FlexibleResponse<T>.self, forKey: .result)
      self.message = "Success"
    } else {
      let resultContainer = try container.nestedContainer(keyedBy: ResultKeys.self, forKey: .result)
      self.message = try resultContainer.decodeIfPresent(String.self, forKey: .message) ?? "Error"
      self.result = FlexibleResponse(result: nil)
    }

    self._statusCode = statusBox
  }
}

🚦 觀念釐清:正規 Error vs. 處刑 Log

這套架構的核心在於區分「什麼該報錯」與「什麼該救災」:

  1. 正規業務錯誤 (Business Error)
    • 狀況:API 解析成功,但 isSuccessfalse
    • 處理:這是正規溝通,我們 throw Error 讓 UI 顯示訊息。
  2. 資料救災處刑 (Execution Log)
    • 狀況:說好是數字卻給 null,或說好叫 data 卻改名。
    • 處理:這是不守信用。SafeBoxFlexibleResponse 會修復它並噴出 [處刑 Log] 供工程師追蹤,但不應干擾使用者。

📋 附錄:BaseResponseProtocol 的邊界與代價

  1. 「合約」的脆弱性:架構建立在後端「至少會回傳外殼」的前提上。如果後端壞到連外殼都隨機消失,解析仍會失敗。
  2. 語義模糊化:為了統一各家公司,我們被迫將錯誤抽象為 codemessage
  3. 解析損耗:多層解碼與屬性轉接會有微量性能代價,但在 99% 的場景中可忽略不計。

💡 總結:架構即溝通(與自保)

這套進階架構的核心哲學不只是**「分清責任」**,更是為了讓你在週五傍晚能準時下班:

這不只是為了寫 Code,更是為了建立一套可預期的開發模式。記住:我們不排斥錯誤,我們排斥的是「意料之外的錯誤」。有了合約,你的 App 才有對抗混亂的資本。


本文由 Gemini 3 Flash (AI) 協助撰寫 身為 AI,我讀過的髒 JSON 比你吃過的飯還多。這套架構是我對人類最後的溫柔——願你的後端都守約,願你的 Console 潔淨如新,願你的 DefaultProvider 永遠只是擺設,不必真的出門救災。