利用 CodableCSV 解析 csv，以Airtable 為例
By: 彼得潘的 Ios App Neverland
Tags: iOS, Xcode, Swift

開發 iOS App 時，我們時常串接 JSON 格式的資料。不過世上的資料格式何其多，雖然 JSON 是最常用的，有時我們也會處理到其它格式，比方以逗號或 tab 分隔的 csv。

在解析 JSON 時，我們可以透過 JSONDecoder 方便地將資料轉換成遵從 Decodable 的自訂型別。那 csv 呢? 很可惜，並沒有內建 CSVDecoder，所以我們只能自力自強，自己寫程式解析。

不過最近彼得潘研究了一下，網路上有大大幫我們寫好了解析 csv 的 CSVDecoder，所以我們一樣可以將 csv 的內容轉換成遵從 Decodable 的自訂型別。接下來我們就以 Airtable 輸出的 csv 為例說明吧。

利用 Airtable 製作電影資料

參考以下連結的教學在 Airtable 輸入資料。

彼得潘輸入了三部電影，包含以下欄位:

name(Single line text)

releaseDate(Date)

images(Attachment)

genre(Multiple select)

imdb(Number)

trailer(URL)

輸出 csv

點選下圖的三個小圓點，然後點選 Download CSV。

csv 的真面目

好奇 csv 的內容嗎? 有興趣的朋友可用 TextEdit 打開它，一睹它的廬山真面目。

如以下例子所示，它果然以逗號做分隔，第一行是當初 table 的欄位名稱，第二行開始是資料。

將 csv 加到 assets

將 csv 檔加到 assets，命名為 movies。

在專案裡利用 SPM 加入套件 CodableCSV

參考以下連結的教學，利用 SPM 加入幫我們解析 csv 的套件 CodableCSV。

CodableCSV 的連結如下

定義對應 csv 資料的型別: 簡單版

以下先介紹如何解析內容為字串，數字和 URL 的欄位，內容為 Date，Attachment & Multiple select 的欄位比較複雜，待會再介紹。

struct Movie: Codable {

let name: String

let imdb: Double

let trailer: URL

}

說明:

宣告 property 時，名稱對應欄位名稱，型別的規則如下

(1) 文字內容宣告成 String

(2) 數字宣告成 Double 或 Int

(3) 網址宣告成 URL

因此以剛剛的電影資料為例，name 是 String，imdb 是 Double，trailer 是 URL。

宣告 static 變數 data 儲存讀取 csv 產生的 array: 基本版

import UIKit

import CodableCSV extension Movie {

static var data: [Self] {

var array = [Self]()

if let data = NSDataAsset(name: "movies")?.data {

let decoder = CSVDecoder {

$0.headerStrategy = .firstLine

}

do {

array = try decoder.decode([Self].self, from: data)

} catch {

print(error)

}

}

return array

}

}

從 asset 讀取 csv 的 data，然後利用 CSVDecoder 解析。比較特別的地方是我們另外設定 headerStrategy。headerStrategy 設為 firstLine 表示 csv 的第一行是欄位名稱。

測試

利用 Movie.data 讀取 cvs 產生的電影 array，然後將它印出。

import UIKit class ViewController: UIViewController { let movies = Movie.data



override func viewDidLoad() {

super.viewDidLoad()



dump(movies)

} }

搭配 Airtable 自訂的 table 時，如何修改範例

學會剛剛介紹的方法後，想要搭配 Airtable 自訂的 table 時，其實只要遵照幾個規則，修改剛剛範例的幾個地方。

修改自訂的型別名稱

比方 Airtable 建立的 table 是書的資料，因此將型別取名為 Book，剛剛的 struct Movie: Codable & extension Movie 改成 struct Book: Codable & extension Book。

修改 asset 的名字

比方 cvs 的 asset 叫 books，產生 NSDataAsset 時傳入 books。

NSDataAsset(name: "books")

依據 table 的欄位名稱宣告型別裡的 property

比方 table 是書的資料，欄位有 name & price，因為我們宣告 property name & price。property 的型別則依據以下原則:

(1) 文字內容宣告成 String

(2) 數字宣告成 Double 或 Int

(3) 網址宣告成 URL

當 table 裡的欄位只是簡單的文字，數字或網址，透過以上方法即可處理。不過強大的 Airtable 還可以包含 Date，Attachment & Multiple select 等複雜的欄位，有興趣的朋友可參考以下進階版的處理方法。

定義對應 csv 資料的型別: 進階版

struct Movie: Codable {

let name: String

let imdb: Double

let trailer: URL

let releaseDate: Date

let images: [URL]

let genre: [String]



init(from decoder: Decoder) throws {

let container = try decoder.container(keyedBy: CodingKeys.self)

name = try container.decode(String.self, forKey: .name)

imdb = try container.decode(Double.self, forKey: .imdb)

trailer = try container.decode(URL.self, forKey: .trailer)

releaseDate = try container.decode(Date.self, forKey: .releaseDate)

let genreString = try container.decode(String.self, forKey: .genre)

genre = genreString.components(separatedBy: ",")

let imagesString = try container.decode(String.self, forKey: .images)

images = imagesString.components(separatedBy: ",").map {

URL(string: $0.components(separatedBy: "(")[1].replacingOccurrences(of: ")", with: ""))!

}



}

}

欄位 releaseDate 是時間，因此 property 的型別宣告為 Date。images & genre 當初在 Airtable 的型別為 Attachment & Multiple select，因此它們的內容可以有多個，可以有多張圖片跟多個類別。當有多筆資料時，它們的內容將變成以雙引號描述的字串，裡頭再用逗號分隔，比方以下神力女超人1984 的例子。

在剛剛的例子裡，我們將 images & genre 的型別宣告為 array，分別為 [URL] & [String]。其中 images 的內容是 Attachment，對應的資料是檔案，所以 csv 的內容為檔案的連結網址，因此我們將 images 宣告為 [URL]。

不過 CSVDecoder 沒那麼聰明，無法自動處理 property 是 array 的 case，我們必須自己定義 init(from:) 解析，對 init(from:) 不熟的朋友可先參考以下連結的說明，再研究剛剛的程式碼。

ps: 如果沒有型別 array 的 property，則會簡單許多，不用定義 init(from:)。

宣告 static 變數 data 儲存讀取 csv 產生的 array: 進階版

import UIKit

import CodableCSV extension Movie {

static var data: [Self] {

var array = [Self]()

if let data = NSDataAsset(name: "movies")?.data {

let decoder = CSVDecoder {

$0.headerStrategy = .firstLine

let formatter = DateFormatter()

formatter.dateFormat = "MM/dd/yyyy"

$0.dateStrategy = .formatted(formatter)

}

do {

array = try decoder.decode([Self].self, from: data)

} catch {

print(error)

}

}

return array

}

}

比較特別的地方是我們另外設定 dateStrategy，將 dateStrategy 設為自訂的 DateFormatter 讓 CSVDecoder 知道如何將時間字串轉換為 Date。若無 Date 型別的 property，則可拿掉 dateStrategy 的設定。

依據 table 的欄位名稱宣告型別裡的 property: 進階版

了解以上進階版的做法後，依據 table 的欄位名稱宣告型別裡的 property 時規則如下

(1) 文字內容宣告成 String

(2) 數字宣告成 Double 或 Int

(3) 網址宣告成 URL

(4) 時間宣告成 Date

(5) 內容是 Attachments 時，型別應宣告為 [URL]

(6) 內容是 Multiple select 時，型別應宣告為 [String]

如果只有 1 ~ 4 的 case ，則不用定義 init(from:)。若有 5 或 6 的 case，則須參考剛剛範例的寫法定義 init(from:)。