七月到了,天氣也越來越炎熱,已經到了沒有冷氣就不能靜下心來工作的時節,在冷氣房裡雖然舒服,也請別忘記還是要常起來走動,多多補充水分,一直久坐不動對開發職涯來說也是不健康的呀!
這週我們將會來分享一些簡單的資源,作為一個開發者,平常除了維護工作上的專案,你是不是偶爾也會想嘗試一些不一樣的新技術或方向?
我們常常會看到 Apple 官方又發表了一些新的工具或語法可以幫助開發,但卡在專案支援的版本而無法使用,等到真的可以應用在產品上時,可能已經又過了兩三年。因此對開發者來說,Side Project 非常的重要!你可以按照自己的步調來學習新的技術,並且在專案中實際練習與應用,甚至上架到 App Store 讓世界看見你的作品!但是獨立開發並不是一件容易的事,如果你想要做一個串接 API 來顯示資料的 App,不像在公司裡有後端工程師,可以幫你處理資料的串接,你必須要想辦法自己完成。這時候,Open Data 就派上用場了!
這篇文章將介紹台灣兩個超實用的開放資料平台:「政府資料開放平台」、「Cafe Nomad」,透過平台提供的接口和資料,來實作一個簡單的 API Service。
Open Data 是指政府或機構將資料開放給公眾使用,通常以開放授權的方式提供,讓任何人都可以自由使用、重製、散布、改作等。
資料來源 - 政府資料開放平台、Cafe Nomad
在動手寫程式碼之前,我們先了解一下這兩個平台。
政府資料開放平台
政府資料開放平台是台灣政府根據《政府資訊公開法》所建立的資料開放平台,自 2013 年起開始服務,提供超過 100 個單位、超過 100,000 筆的資料,涵蓋了天氣、交通、人口統計等各種資訊。
舉例來說,我們想要取得空氣品質的資料,可以點開 空氣品質指標(AQI) 這個資料集,可以看到主要欄位說明、下載網址(以這個範例來說提供了 JSON、CSV、XML 三種格式)、提供機關、聯絡人等等資訊。
以大部分 iOS 開發者來說,最常使用的格式就是 JSON,因此我們點擊 JSON 格式的按鈕,便可以看到資料內容。
你可以在平台上找到各式各業政府整理後公開的資料,串接到你的 App 中,再補上顯示邏輯,就可以完成一個簡單的 App。
Cafe Nomad
Cafe Nomad 是由工程師 尤川豪 在2016年開發的咖啡廳資訊平台,提供全台咖啡廳資訊,包括咖啡廳的地址、電話、營業時間等資訊。
專案是全開源,可以從官網上連到 GitHub 來查看。
雖然目前專案看起來已經進入維護狀態,雖然有看到宣告要做 v2.0 的更新,也已經有測試站,但看起來進度就停在這邊,希望有機會能看到作者更新。
但就算這樣還是別擔心,舊有的 API 仍然可以正常呼叫使用,因此要作為開發練習的話,還是沒有問題。
網站上面也提供了透過這個服務所建立的 App 清單,並且開放新增,如果你也透過他們的 API 完成了自己的作品,可以到 這邊 點擊表單來分享。
檢查了一下大部分的 App 都已經下架了,應該也是當初的 Developer 的練習作品。
實作
既然咖啡是工程師工作上不可或缺的夥伴之一,那麼接下來,我們便將串接 Cafe Nomad 的 API,實作一個 API Service,並將他封裝在 Swift Package Manager 中,方便在其他專案中使用。
建立專案
首先,我們先建立一個新的 Swift Package Manager 專案,並且選擇 Swift Package Manager 作為專案類型。
swift package init --type library --name CafeNomadService
如果不熟悉 CLI,也可以使用 Xcode 來建立新的 Swift Package Manager。
根據 Cafe Nomad API V1.2 的文件,因為基本上只有開放兩支 API(All Cafes, Cafes by city),並不複雜,我們先根據資料欄位來建立對應的 Model。
先在 Sources/CafeNomadService
底下建立一個 Models
資料夾,並且建立一個 Cafe.swift
檔案,來存放 Cafe 的資料。
public struct Cafe: Codable, Sendable {
public let id: String
public let name: String
public let wifi: Double
public let seat: Double
public let quiet: Double
public let tasty: Double
public let cheap: Double
public let music: Double
public let address: String
public let latitude: String
public let longitude: String
public let url: String
public let limitedTime: LimitedTime?
public let socket: Socket?
public let standingDesk: StandingDesk?
public let mrt: String?
public let openTime: String?
// Codable 的實作
}
// 是否有限時
public enum LimitedTime: String, Codable, Sendable {
case yes = "yes" // 一律有限時
case maybe = "maybe" // 看情況,假日或客滿限時
case no = "no" // 一律不限時
}
// 是否有插座
public enum Socket: String, Codable, Sendable {
case yes = "yes" // 很多
case maybe = "maybe" // 還好,看座位
case no = "no" // 很少
}
// 是否有站立座位
public enum StandingDesk: String, Codable, Sendable {
case yes = "yes" // 有些座位可以
case no = "no" // 無法
}
我們將 LimitedTime
、Socket
、StandingDesk
三個資料欄位透過 enum
來表示,方便後續的判斷。
至此,簡單的 Cafe
Model 就完成了。
接下來,就是來實作 API 的串接。
我們將使用 URLSession
,透過 Concurrency
的方式來實作非同步的 API 串接。
先想像我們希望讓使用這個 Package 的開發者,可以用什麼樣的方式來接資料:
let cafeService = CafeNomadService()
let cafes = try await cafeService.fetchAllCafes()
let cafesInTaipei = try await cafeService.fetchCafes(for: "taipei")
根據文件,我們可以透過兩支 API 來取得全部的咖啡廳資料,或者特定城市的咖啡廳資料。
我們這邊保留未來的擴充性,將 city 參數使用 String
,如果開發者有需要,也可以自行建立一個 enum
來方便管理。
關於enum,
可以參考這篇:
已經定義好 Model
和 API
接口的話,接下來就是實作 CafeNomadService
的內容了:
public final class CafeNomadService: Sendable {
private let baseURL = "https://cafenomad.tw/api/v1.2/cafes"
private let session: URLSession
public init(session: URLSession = .shared) {
self.session = session
}
// MARK: - Public Methods
/// 取得全台灣咖啡廳資料
@available(macOS 12.0, iOS 15.0, watchOS 8.0, *)
public func fetchAllCafes() async throws -> [Cafe] {
// ...
}
/// 取得指定城市咖啡廳資料
/// - Parameter city: 城市名稱 (例如: "taipei", "hsinchu", "kaohsiung")
@available(macOS 12.0, iOS 15.0, watchOS 8.0, *)
public func fetchCafes(for city: String) async throws -> [Cafe] {
// ...
}
}
因為兩個 API 的網址都相同,只是參數不同,因此我們可以將 fetchCafes
方法抽離出來,讓兩個 API 共用。
而且因為 Method
都是 GET
,因此我們對 URLSession
也不必額外再多做設定。
private func fetchCafes(from url: URL) async throws -> [Cafe] {
let (data, response) = try await session.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw CafeNomadError.invalidResponse
}
guard 200...299 ~= httpResponse.statusCode else {
throw CafeNomadError.httpError(httpResponse.statusCode)
}
do {
let decoder = JSONDecoder()
return try decoder.decode([Cafe].self, from: data)
} catch {
throw CafeNomadError.decodingError(error)
}
}
接下來,我們就可以實作 fetchAllCafes
和 fetchCafes
方法了。
/// 取得全台灣咖啡廳資料
public func fetchAllCafes() async throws -> [Cafe] {
guard let url = URL(string: baseURL) else {
throw CafeNomadError.invalidURL(baseURL)
}
return try await fetchCafes(from: url)
}
/// 取得指定城市咖啡廳資料
/// - Parameter city: 城市名稱 (例如: "taipei", "hsinchu", "kaohsiung")
public func fetchCafes(for city: String) async throws -> [Cafe] {
let urlString = "\(baseURL)/\(city)"
guard let url = URL(string: urlString) else {
throw CafeNomadError.invalidURL(urlString)
}
return try await fetchCafes(from: url)
}
這樣就完成了一個簡單的 API Service!可以用它來串接 Cafe Nomad 的 API,並且在 App 中使用。
如果有需要的話,我把這次的專案放在 GitHub 上,有興趣的話可以直接拿來使用。
CafeNomad 是我從以前就很喜歡的服務,希望透過這次的介紹,讓更多人認識她,也期待有開發夥伴能繼續使用他們的服務,創造更多新的 App。
當然也還有政府資料開放平台,上面的各種資料非常值得大家去研究,練習技術之餘還能幫助民眾更加了解台灣政府資訊。
以上,就是這次分享的內容,希望對你有幫助!
喜歡這篇文章,或者覺得對你有幫助的話,可以分享給需要的人,也歡迎留言討論!我們下次再見!
參考資料
本篇文章完成的專案使用 Cursor IDE 協作完成。