使用 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。

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com