使用 baseURL,URLComponents & URLQueryItem 產生 URL
開發 iOS App 時,我們時常串接後台的 API。通常 API 前面的網址是固定的,只有後面的 path 跟 query 參數會變動,比方以下 iTunes API 的例子:
- 搜尋告五人的音樂。
https://itunes.apple.com/search?media=music&country=tw&term=告五人
- 搜尋 id 1385904542 的資料,在這裡 id 1385904542 代表告五人的情歌愛在夏天。
https://itunes.apple.com/lookup?id=1385904542&country=tw
它們的網址開頭都是 https://itunes.apple.com,變的是後面的 path 跟 query 參數,因此很適合共用同一個 baseURL,然後搭配 URLComponents & URLQueryItem 產生 URL,而且 URLComponents 還能幫我們解決網址包含 ASCII 以外字元的問題。
接下來就讓我們以 iTunes API 為例說明吧。
網址的 Percent Encoding (URL Encoding)
當我們透過網址抓取網路上的資料時,網址只能包含 ASCII 字元,因此以下程式得到的 url 將為 nil,因為告五人不是 ASCII 字元。
let url = URL(string: "https://itunes.apple.com/search?media=music&country=tw&term=告五人")
只有以下所列的 ASCII 字元能出現在網址裡,其它文字生成 URL 時將得到 nil。
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=`.
此問題有很多解決的方法,相關解法可參考以下連結。
構成 URL 的 URLComponents
一個完整的 URL 可拆解成以下幾個部分:
而 URLComponents 則是用來描述構成 URL 的各個部分,比方 scheme,host,path & query 等。
因此 https://itunes.apple.com/search?media=music&country=tw&term=告五人
的 URL 也可透過 URLComponents 生成。設定 host,scheme,path & query 後,即可從 URLComponents 的 property url 得到網址。
var urlComponents = URLComponents()
urlComponents.host = "itunes.apple.com"
urlComponents.scheme = "https"
urlComponents.path = "/search"
urlComponents.query = "media=music&country=tw&term=告五人"
let url = urlComponents.url
而且更令人驚喜的,URLComponents 將幫我們進行 Percent Encoding 的轉換,host,path & query 裡出現的非 ASCII 字元都會被轉換成 % 搭配十六進位數字,所以我們不用再擔心網址裡非 ASCII 字元造成的問題。
在產生 URLComponents 時,我們也可以在參數 string 傳入 url 的部分字串,例如以下例子。
var urlComponents = URLComponents(string: "https://itunes.apple.com/search")!
urlComponents.query = "media=music&country=tw&term=告五人"
let url = urlComponents.url
利用 URLQueryItem 設定 queryItems
將 qeury 參數寫成 media=music&country=tw&term=告五人
其實不是很彈性,尤其 query 參數的內容通常都是變動的,因此接下來我們將利用 URLQueryItem 設定 queryItems。queryItems 代表 query 的參數,型別為 [URLQueryItem]?。
var urlComponents = URLComponents(string: "https://itunes.apple.com/search")!
urlComponents.queryItems = [
URLQueryItem(name: "media", value: "music"),
URLQueryItem(name: "country", value: "tw"),
URLQueryItem(name: "term", value: "告五人")
]
let url = urlComponents.url
透過以上寫法產生的 url 網址一樣會是 https://itunes.apple.com/search?media=music&country=tw&term=告五人
。
若是習慣將 query 的內容存在 dictionary,我們也可以採用以下幾種寫法將 dictionary 轉換成 [URLQueryItem]。
寫法 1: 從 dictionary 呼叫 map,轉換成型別 [URLQueryItem]
var urlComponents = URLComponents(string: "https://itunes.apple.com/search")!
urlComponents.queryItems = [
"media": "music",
"country": "tw",
"term": "告五人"
].map { URLQueryItem(name: $0.key, value: $0.value) }
let url = urlComponents.url
寫法 2: 在 URL 的 extension 裡定義設定 query 參數的 function withQueries
新增 URL 的 extension 檔,比方 URL+Extensions.swift,然後在 extension URL 定義設定 query 參數的 function withQueries(_:)。
extension URL {
func withQueries(_ queries: [String: String]) -> URL? {
var components = URLComponents(url: self, resolvingAgainstBaseURL: true)
components?.queryItems = queries.map {
URLQueryItem(name: $0.key, value: $0.value)
}
return components?.url
}
}
我們利用 map 將型別 [String: String] 的參數轉換成 [URLQueryItem]? 後存入 queryItems,然後再讀取 URLComponents 的 url 取得加了 query 參數的網址。(ps: $0.key & $0.value 也可以寫成 $0.0 & $0.1 )
以下為實際呼叫 withQueries 產生 URL 的範例。
let queries = [
"term": "告五人",
"media": "music",
"country": "tw"
]
let url = URL(string: "https://itunes.apple.com/search")?.withQueries(queries)
定義串接 iTunes API 的類別 MusicService
了解 URLComponents & URLQueryItem 的妙用後,現在我們可以定義串接 iTunes API 的類別 MusicService,將生成 URL 的相關程式變得更彈性。
class ITunesService {
static let shared = ITunesService()
var baseURL = URL(string: "https://itunes.apple.com")
func search(term: String, media: String, country: String) {
let queries = [
"term": term,
"media": media,
"country": country
]
if let url = baseURL?.appendingPathComponent("search").withQueries(queries) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data,
let content = String(data: data, encoding: .utf8) {
print(content)
}
}.resume()
}
}
func lookUp(id: String, country: String) {
let queries = [
"id": id,
"country": country
]
if let url = baseURL?.appendingPathComponent("lookup").withQueries(queries) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data,
let content = String(data: data, encoding: .utf8) {
print(content)
}
}.resume()
}
}
}
ITunesService 主要負責串接 iTunes API,因此 baseURL 為 https://itunes.apple.com,而 function search & lookUp 分別對應不同的 API,採用同樣的 baseURL,利用 appendingPathComponent 指定不同的 path,利用 withQueries 指定 query 參數。
呼叫 search & lookUp 的範例如下:
ITunesService.shared.lookUp(id: "1385904542", country: "tw")
ITunesService.shared.search(term: "告五人", media: "music", country: "tw")
參考連結
Develop in Swift Data Collections 的 2.4 Working with the Web: HTTP and URL & Unit 2 的 Guide project。