本篇主要講解Alamofire中錯誤的處理機制
前言
在開發中,往往最容易被忽略的內容就是對錯誤的處理。有經驗的開發者,能夠對自己寫的每行代碼負責,而且非常清楚自己寫的代碼在什么時候會出現異常,這樣就能提前做好錯誤處理。
Alamofire的錯誤封裝很經典,是使用swift中enum的一個典型案例。讀完這篇文章,一定能讓大家對swift的枚舉有一個更深的理解,同時增加一些枚舉的高級使用技巧。
那么有一個很重要的問題,我們應該在什么情況下考慮使用枚舉呢?只要結果可能是有限的集合的情況下,我們就盡量考慮使用枚舉。 其實枚舉本身還是數據的一種載體,swift中,枚舉有着很豐富的使用方法,在下邊的內容中,我們會介紹到枚舉的主流用法。
開胃菜
先總結一下swfit中enum中的用法:
1.正常用法
enum Movement {
case Left
case Right
case Top
case Bottom
}
let aMovement = Movement.Left
switch aMovement {
case .Left:
print("left")
default:
print("Unknow")
}
if case .Left = aMovement {
print("Left")
}
if .Left == aMovement {
print("Left")
}
2.聲明為整型
enum Season: Int {
case Spring = 0
case Summer = 1
case Autumn = 2
case Winter = 3
}
3.聲明為字符串類型
enum House: String {
case ZhangSan = "I am zhangsan"
case LiSi = "I am lisi"
}
let zs = House.ZhangSan
print(zs.rawValue)
enum CompassPoint: String {
case North, South, East, West
}
let n = CompassPoint.North
print(n.rawValue)
let s = CompassPoint(rawValue: "South");
4.聲明為浮點類型
enum Constants: Double {
case π = 3.14159
case e = 2.71828
case φ = 1.61803398874
case λ = 1.30357
}
let pai = Constants.π
print(pai.rawValue)
5.其他類型
enum VNodeFlags : UInt32 {
case Delete = 0x00000001
case Write = 0x00000002
case Extended = 0x00000004
case Attrib = 0x00000008
case Link = 0x00000010
case Rename = 0x00000020
case Revoke = 0x00000040
case None = 0x00000080
}
6.enum包含enum
enum Character {
enum Weapon {
case Bow
case Sword
case Lance
case Dagger
}
enum Helmet {
case Wooden
case Iron
case Diamond
}
case Thief
case Warrior
case Knight
}
let character = Character.Thief
let weapon = Character.Weapon.Bow
let helmet = Character.Helmet.Iron
7.結構體和枚舉
struct Scharacter {
enum CharacterType {
case Thief
case Warrior
case Knight
}
enum Weapon {
case Bow
case Sword
case Lance
case Dagger
}
let type: CharacterType
let weapon: Weapon
}
let sc = Scharacter(type: .Thief, weapon: .Bow)
print(sc.type)
8.值關聯
enum Trade {
case Buy(stock: String, amount: Int)
case Sell(stock: String, amount: Int)
}
let trade = Trade.Buy(stock: "Car", amount: 100)
if case let Trade.Buy(stock, amount) = trade {
print("buy \(amount) of \(stock)")
}
enum Trade0 {
case Buy(String, Int)
case Sell(String, Int)
}
let trade0 = Trade0.Buy("Car0", 100)
if case let Trade0.Buy(stock, amount) = trade0 {
print("buy \(amount) of \(stock)")
}
9.枚舉中的函數
enum Wearable {
enum Weight: Int {
case Light = 2
}
enum Armor: Int {
case Light = 2
}
case Helmet(weight: Weight, armor: Armor)
func attributes() -> (weight: Int, armor: Int) {
switch self {
case .Helmet(let w, let a):
return (weight: w.rawValue * 2, armor: a.rawValue * 4)
}
}
}
let test = Wearable.Helmet(weight: .Light, armor: .Light).attributes()
print(test)
enum Device {
case iPad, iPhone, AppleTV, AppleWatch
func introduced() -> String {
switch self {
case .AppleTV: return "\(self) was introduced 2006"
case .iPhone: return "\(self) was introduced 2007"
case .iPad: return "\(self) was introduced 2010"
case .AppleWatch: return "\(self) was introduced 2014"
}
}
}
print (Device.iPhone.introduced())
10.枚舉中的屬性
enum Device1 {
case iPad, iPhone
var year: Int {
switch self {
case .iPad:
return 2010
case .iPhone:
return 2007
}
}
}
let iPhone = Device1.iPhone
print(iPhone.year)
ParameterEncodingFailureReason
通過ParameterEncodingFailureReason
我們能夠很清楚的看出來這是一個參數編碼的錯誤原因。大家注意reason
這個詞,在命名中,有或者沒有這個詞,表達的意境完全不同,因此,Alamofire牛逼就體現在這些細節之中。
public enum AFError: Error {
/// The underlying reason the parameter encoding error occurred.
///
/// - missingURL: The URL request did not have a URL to encode.
/// - jsonEncodingFailed: JSON serialization failed with an underlying system error during the
/// encoding process.
/// - propertyListEncodingFailed: Property list serialization failed with an underlying system error during
/// encoding process.
public enum ParameterEncodingFailureReason {
case missingURL
case jsonEncodingFailed(error: Error)
case propertyListEncodingFailed(error: Error)
}
}
ParameterEncodingFailureReason
本身是一個enum,同時,它又被包含在AFError
之中,這說明枚舉之中可以有另一個枚舉。那么像這種情況我們怎么使用呢?看下邊的代碼:
let parameterErrorReason = AFError.ParameterEncodingFailureReason.missingURL
枚舉的訪問是一級一級進行的。我們再看這行代碼:case jsonEncodingFailed(error: Error)
。jsonEncodingFailed(error: Error)
並不是函數,就是枚舉的一個普通的子選項。(error: Error)
是它的一個關聯值,相對於任何一個子選項,我們都可以關聯任何值,它的意義就在於,把這些值與子選項進行綁定,方便在需要的時候調用。我們會在下邊講解如何獲取關聯值。
參數編碼有一下幾種方式:
- 把參數編碼到URL中
- 把參數編碼到httpBody中
Alamofire中是如何進行參數編碼的,這方面的內容會在后續的ParameterEncoding.swift
這一篇文章中給出詳細的解釋。那么編碼失敗的原因可能為:
missingURL
給定的urlRequest.url為nil的情況拋出錯誤jsonEncodingFailed(error: Error)
當選擇把參數編碼成JSON格式的情況下,參數JSON化拋出的錯誤propertyListEncodingFailed(error: Error)
這個同上
綜上所述,ParameterEncodingFailureReason
封裝了參數編碼的錯誤,可能出現的錯誤類型為Error,說明這些所謂一般是調用系統Api產生的錯誤。
MultipartEncodingFailureReason
public enum MultipartEncodingFailureReason {
case bodyPartURLInvalid(url: URL)
case bodyPartFilenameInvalid(in: URL)
case bodyPartFileNotReachable(at: URL)
case bodyPartFileNotReachableWithError(atURL: URL, error: Error)
case bodyPartFileIsDirectory(at: URL)
case bodyPartFileSizeNotAvailable(at: URL)
case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error)
case bodyPartInputStreamCreationFailed(for: URL)
case outputStreamCreationFailed(for: URL)
case outputStreamFileAlreadyExists(at: URL)
case outputStreamURLInvalid(url: URL)
case outputStreamWriteFailed(error: Error)
case inputStreamReadFailed(error: Error)
}
多部分編碼錯誤一般發生在上傳或下載請求中對數據的處理過程中,這里邊最重要的是對上傳數據的處理過程,會在后續的MultipartFormData.swift
這一篇文章中給出詳細的解釋,我們就簡單的分析下MultipartEncodingFailureReason
子選項錯誤出現的原因:
bodyPartURLInvalid(url: URL)
上傳數據時,可以通過fileURL的方式,讀取本地文件數據,如果fileURL不可用,就會拋出這個錯誤bodyPartFilenameInvalid(in: URL)
如果使用fileURL的lastPathComponent
或者pathExtension
獲取filename為空拋出的錯誤bodyPartFileNotReachable(at: URL)
通過fileURL不能訪問數據,也就是不可達的bodyPartFileNotReachableWithError(atURL: URL, error: Error)
這個不同於bodyPartFileNotReachable(at: URL)
,當嘗試檢測fileURL是不是可達的情況下拋出的錯誤bodyPartFileIsDirectory(at: URL)
當fileURL是一個文件夾時拋出錯誤bodyPartFileSizeNotAvailable(at: URL)
當使用系統Api獲取fileURL指定文件的size出現錯誤bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error)
查詢fileURL指定文件size出現錯誤bodyPartInputStreamCreationFailed(for: URL)
通過fileURL創建inputStream出現錯誤outputStreamCreationFailed(for: URL)
當嘗試把編碼后的數據寫入到硬盤時,創建outputStream出現錯誤outputStreamFileAlreadyExists(at: URL)
數據不能被寫入,因為指定的fileURL已經存在outputStreamURLInvalid(url: URL)
fileURL不是一個file URLoutputStreamWriteFailed(error: Error)
數據流寫入錯誤inputStreamReadFailed(error: Error)
數據流讀入錯誤
綜上所述,這些錯誤基本上都跟數據的操作相關,這個在后續會做出很詳細的說明。
ResponseValidationFailureReason
public enum ResponseValidationFailureReason {
case dataFileNil
case dataFileReadFailed(at: URL)
case missingContentType(acceptableContentTypes: [String])
case unacceptableContentType(acceptableContentTypes: [String], responseContentType: String)
case unacceptableStatusCode(code: Int)
}
Alamofire不管請求是否成功,都會返回response。它提供了驗證ContentType和StatusCode的功能,關於驗證,再后續的文章中會有詳細的解答,我們先看看這些原因:
dataFileNil
保存數據的URL不存在,這種情況一般出現在下載任務中,指的是下載代理中的fileURL缺失dataFileReadFailed(at: URL)
保存數據的URL無法讀取數據,同上missingContentType(acceptableContentTypes: [String])
服務器返回的response不包含ContentType且提供的acceptableContentTypes不包含通配符(通配符表示可以接受任何類型)unacceptableContentType(acceptableContentTypes: [String], responseContentType: String)
ContentTypes不匹配unacceptableStatusCode(code: Int)
StatusCode不匹配
ResponseSerializationFailureReason
public enum ResponseSerializationFailureReason {
case inputDataNil
case inputDataNilOrZeroLength
case inputFileNil
case inputFileReadFailed(at: URL)
case stringSerializationFailed(encoding: String.Encoding)
case jsonSerializationFailed(error: Error)
case propertyListSerializationFailed(error: Error)
}
我們在Alamofire源碼解讀系列(一)之概述和使用中已經提到,Alamofire支持把服務器的response序列成幾種數據格式。
- response 直接返回HTTPResponse,未序列化
- responseData 序列化為Data
- responseJSON 序列化為Json
- responseString 序列化為字符串
- responsePropertyList 序列化為Any
那么在序列化的過程中,很可能會發生下邊的錯誤:
inputDataNil
服務器返回的response沒有數據inputDataNilOrZeroLength
服務器返回的response沒有數據或者數據的長度是0inputFileNil
指向數據的URL不存在inputFileReadFailed(at: URL)
指向數據的URL無法讀取數據stringSerializationFailed(encoding: String.Encoding)
當使用指定的String.Encoding序列化數據為字符串時,拋出的錯誤jsonSerializationFailed(error: Error)
JSON序列化錯誤propertyListSerializationFailed(error: Error)
plist序列化錯誤
AFError
上邊內容中介紹的ParameterEncodingFailureReason
MultipartEncodingFailureReason
ResponseValidationFailureReason
和 ResponseSerializationFailureReason
,他們是定義在AFError
中獨立的枚舉,他們之間是包含和被包含的關系,理解這一點很重要,因為有了這種包含的管理,在使用中就需要通過AFError.ParameterEncodingFailureReason
這種方式進行操作。
那么最重要的問題就是,如何把上邊4個獨立的枚舉進行串聯呢?Alamofire巧妙的地方就在這里,有4個獨立的枚舉,分別代表4大錯誤。也就是說這個網絡框架肯定有這4大錯誤模塊,我們只需要給AFError設計4個子選項,每個子選項關聯上上邊4個獨立枚舉的值就ok了。
這個設計真的很巧妙,試想,如果把所有的錯誤都放到AFError中,就顯得非常冗余。那么下邊的代碼就呼之欲出了,大家好好體會體會在swift下這么設計的妙用:
case invalidURL(url: URLConvertible)
case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
case multipartEncodingFailed(reason: MultipartEncodingFailureReason)
case responseValidationFailed(reason: ResponseValidationFailureReason)
case responseSerializationFailed(reason: ResponseSerializationFailureReason)
AFError的擴展
也許在開發中,我們完成了上邊的代碼就認為夠用了,但對於一個開源框架而言,遠遠是不夠的。我們一點點進行剖析:
現在給定一條數據:
func findErrorType(error: AFError) {
}
我只需要知道這個error是不是參數編碼錯誤,應該怎么辦?因此為AFError提供5個布爾類型的屬性,專門用來獲取當前的錯誤是不是某個指定的類型。這個功能的實現比較簡單,代碼如下:
extension AFError {
/// Returns whether the AFError is an invalid URL error.
public var isInvalidURLError: Bool {
if case .invalidURL = self { return true }
return false
}
/// Returns whether the AFError is a parameter encoding error. When `true`, the `underlyingError` property will
/// contain the associated value.
public var isParameterEncodingError: Bool {
if case .parameterEncodingFailed = self { return true }
return false
}
/// Returns whether the AFError is a multipart encoding error. When `true`, the `url` and `underlyingError` properties
/// will contain the associated values.
public var isMultipartEncodingError: Bool {
if case .multipartEncodingFailed = self { return true }
return false
}
/// Returns whether the `AFError` is a response validation error. When `true`, the `acceptableContentTypes`,
/// `responseContentType`, and `responseCode` properties will contain the associated values.
public var isResponseValidationError: Bool {
if case .responseValidationFailed = self { return true }
return false
}
/// Returns whether the `AFError` is a response serialization error. When `true`, the `failedStringEncoding` and
/// `underlyingError` properties will contain the associated values.
public var isResponseSerializationError: Bool {
if case .responseSerializationFailed = self { return true }
return false
}
}
總而言之,這些都是給AFError這個枚舉擴展的屬性,還包含下邊這些屬性:
-
urlConvertible: URLConvertible?
獲取某個屬性,這個屬性實現了URLConvertible協議,在AFError中只有case invalidURL(url: URLConvertible)這個選項符合要求/// The `URLConvertible` associated with the error. public var urlConvertible: URLConvertible? { switch self { case .invalidURL(let url): return url default: return nil } }
-
url: URL?
獲取AFError中的URL,當然這個URL只跟MultipartEncodingFailureReason這個子選項有關/// The `URL` associated with the error. public var url: URL? { switch self { case .multipartEncodingFailed(let reason): return reason.url default: return nil } }
-
underlyingError: Error?
AFError中封裝的所有的可能出現的錯誤中,並不是每種可能都會返回Error這個錯誤信息,因此這個屬性是可選的/// The `Error` returned by a system framework associated with a `.parameterEncodingFailed`, /// `.multipartEncodingFailed` or `.responseSerializationFailed` error. public var underlyingError: Error? { switch self { case .parameterEncodingFailed(let reason): return reason.underlyingError case .multipartEncodingFailed(let reason): return reason.underlyingError case .responseSerializationFailed(let reason): return reason.underlyingError default: return nil } }
-
acceptableContentTypes: [String]?
可接受的ContentType/// The response `Content-Type` of a `.responseValidationFailed` error. public var responseContentType: String? { switch self { case .responseValidationFailed(let reason): return reason.responseContentType default: return nil } }
-
responseCode: Int?
響應碼/// The response code of a `.responseValidationFailed` error. public var responseCode: Int? { switch self { case .responseValidationFailed(let reason): return reason.responseCode default: return nil } }
-
failedStringEncoding: String.Encoding?
錯誤的字符串編碼/// The `String.Encoding` associated with a failed `.stringResponse()` call. public var failedStringEncoding: String.Encoding? { switch self { case .responseSerializationFailed(let reason): return reason.failedStringEncoding default: return nil } }
這里是一個小的分割線,在上邊屬性的獲取中,也是用到了下邊代碼中的擴展功能:
extension AFError.ParameterEncodingFailureReason {
var underlyingError: Error? {
switch self {
case .jsonEncodingFailed(let error), .propertyListEncodingFailed(let error):
return error
default:
return nil
}
}
}
extension AFError.MultipartEncodingFailureReason {
var url: URL? {
switch self {
case .bodyPartURLInvalid(let url), .bodyPartFilenameInvalid(let url), .bodyPartFileNotReachable(let url),
.bodyPartFileIsDirectory(let url), .bodyPartFileSizeNotAvailable(let url),
.bodyPartInputStreamCreationFailed(let url), .outputStreamCreationFailed(let url),
.outputStreamFileAlreadyExists(let url), .outputStreamURLInvalid(let url),
.bodyPartFileNotReachableWithError(let url, _), .bodyPartFileSizeQueryFailedWithError(let url, _):
return url
default:
return nil
}
}
var underlyingError: Error? {
switch self {
case .bodyPartFileNotReachableWithError(_, let error), .bodyPartFileSizeQueryFailedWithError(_, let error),
.outputStreamWriteFailed(let error), .inputStreamReadFailed(let error):
return error
default:
return nil
}
}
}
extension AFError.ResponseValidationFailureReason {
var acceptableContentTypes: [String]? {
switch self {
case .missingContentType(let types), .unacceptableContentType(let types, _):
return types
default:
return nil
}
}
var responseContentType: String? {
switch self {
case .unacceptableContentType(_, let responseType):
return responseType
default:
return nil
}
}
var responseCode: Int? {
switch self {
case .unacceptableStatusCode(let code):
return code
default:
return nil
}
}
}
extension AFError.ResponseSerializationFailureReason {
var failedStringEncoding: String.Encoding? {
switch self {
case .stringSerializationFailed(let encoding):
return encoding
default:
return nil
}
}
var underlyingError: Error? {
switch self {
case .jsonSerializationFailed(let error), .propertyListSerializationFailed(let error):
return error
default:
return nil
}
}
}
錯誤描述
在開發中,如果程序遇到錯誤,我們往往會給用戶展示更加直觀的信息,這就要求我們把錯誤信息轉換成易於理解的內容。因此我們只要實現LocalizedError協議就好了。這里邊的內容很簡單,在這里就直接把代碼寫上了,不做分析:
extension AFError: LocalizedError {
public var errorDescription: String? {
switch self {
case .invalidURL(let url):
return "URL is not valid: \(url)"
case .parameterEncodingFailed(let reason):
return reason.localizedDescription
case .multipartEncodingFailed(let reason):
return reason.localizedDescription
case .responseValidationFailed(let reason):
return reason.localizedDescription
case .responseSerializationFailed(let reason):
return reason.localizedDescription
}
}
}
extension AFError.ParameterEncodingFailureReason {
var localizedDescription: String {
switch self {
case .missingURL:
return "URL request to encode was missing a URL"
case .jsonEncodingFailed(let error):
return "JSON could not be encoded because of error:\n\(error.localizedDescription)"
case .propertyListEncodingFailed(let error):
return "PropertyList could not be encoded because of error:\n\(error.localizedDescription)"
}
}
}
extension AFError.MultipartEncodingFailureReason {
var localizedDescription: String {
switch self {
case .bodyPartURLInvalid(let url):
return "The URL provided is not a file URL: \(url)"
case .bodyPartFilenameInvalid(let url):
return "The URL provided does not have a valid filename: \(url)"
case .bodyPartFileNotReachable(let url):
return "The URL provided is not reachable: \(url)"
case .bodyPartFileNotReachableWithError(let url, let error):
return (
"The system returned an error while checking the provided URL for " +
"reachability.\nURL: \(url)\nError: \(error)"
)
case .bodyPartFileIsDirectory(let url):
return "The URL provided is a directory: \(url)"
case .bodyPartFileSizeNotAvailable(let url):
return "Could not fetch the file size from the provided URL: \(url)"
case .bodyPartFileSizeQueryFailedWithError(let url, let error):
return (
"The system returned an error while attempting to fetch the file size from the " +
"provided URL.\nURL: \(url)\nError: \(error)"
)
case .bodyPartInputStreamCreationFailed(let url):
return "Failed to create an InputStream for the provided URL: \(url)"
case .outputStreamCreationFailed(let url):
return "Failed to create an OutputStream for URL: \(url)"
case .outputStreamFileAlreadyExists(let url):
return "A file already exists at the provided URL: \(url)"
case .outputStreamURLInvalid(let url):
return "The provided OutputStream URL is invalid: \(url)"
case .outputStreamWriteFailed(let error):
return "OutputStream write failed with error: \(error)"
case .inputStreamReadFailed(let error):
return "InputStream read failed with error: \(error)"
}
}
}
extension AFError.ResponseSerializationFailureReason {
var localizedDescription: String {
switch self {
case .inputDataNil:
return "Response could not be serialized, input data was nil."
case .inputDataNilOrZeroLength:
return "Response could not be serialized, input data was nil or zero length."
case .inputFileNil:
return "Response could not be serialized, input file was nil."
case .inputFileReadFailed(let url):
return "Response could not be serialized, input file could not be read: \(url)."
case .stringSerializationFailed(let encoding):
return "String could not be serialized with encoding: \(encoding)."
case .jsonSerializationFailed(let error):
return "JSON could not be serialized because of error:\n\(error.localizedDescription)"
case .propertyListSerializationFailed(let error):
return "PropertyList could not be serialized because of error:\n\(error.localizedDescription)"
}
}
}
extension AFError.ResponseValidationFailureReason {
var localizedDescription: String {
switch self {
case .dataFileNil:
return "Response could not be validated, data file was nil."
case .dataFileReadFailed(let url):
return "Response could not be validated, data file could not be read: \(url)."
case .missingContentType(let types):
return (
"Response Content-Type was missing and acceptable content types " +
"(\(types.joined(separator: ","))) do not match \"*/*\"."
)
case .unacceptableContentType(let acceptableTypes, let responseType):
return (
"Response Content-Type \"\(responseType)\" does not match any acceptable types: " +
"\(acceptableTypes.joined(separator: ","))."
)
case .unacceptableStatusCode(let code):
return "Response status code was unacceptable: \(code)."
}
}
}
總結
通過閱讀AFError
這篇代碼,給了我很大的震撼,在代碼的設計上,可以參考這種設計方式。
容老衲休息一天,再帶來下一篇Notifications.swift
的源碼解讀。
由於知識水平有限,如有錯誤,還望指出