模仿 Apple 官方範例串接 JSON API,定義 function 型別的 completion 參數
Apple 最新的寫法搭配 Result type,請參考以下連結
舊版的寫法
Apple 的 App Development with Swift 第五章 Guided Project: Restaurant Menu 介紹了適合初學者參考的 JSON API 串接寫法,接下來就讓我們一步步介紹如何模仿抄襲 Apple 大大的範例吧。
下載 App Development with Swift Teacher Guide
點選 Download Teacher materials 下載範例程式
啟動第五章的 server 程式 Open Restaurant
Apple 的範例需要從 server 抓取資料,因此我們必須先啟動 server 程式。Apple 已經幫我們將 server 程式寫好了,請切換到範例第五章 Guided Project — Restaurant 的資料夾,雙擊 Open Restaurant 啟動程式。
從 Server App 的視窗畫面可進行後台資料的編輯,點選右下角的 Start 啟動後台,讓 App 可以連線抓取資料。
執行 App
切換到範例第五章 Guided Project — OrderApp 的資料夾,雙擊 OrderApp.xcodeproj 打開專案。
執行 App 後,我們可從 Server 抓取 Restaurant 的相關資料並訂購想購買的商品。
研究模仿 Apple 範例程式
- 串接 JSON
範例程式將跟後台溝通的相關程式統一定義在 MenuController 裡,方便 App 的各個畫面使用,比方將抓取商品類別的 API 定義成 function fetchCategories,上傳訂單的 API 定義成 function submitOrder。
值得注意的,後台回傳的資料將帶在 function 型別的參數裡,比方呼叫 function fetchMenuItems 時,它的參數 completion 將包含商品類別的 array。
func fetchMenuItems(forCategory categoryName: String, completion: @escaping ([MenuItem]?) -> Void)
之後 App 各個畫面的 view controller 只要透過 MenuController.shared 即可呼叫 function,串接後台的 API。
比方以下程式利用 MenuController.shared.fetchCategories 取得 categories。
override func viewDidLoad() {
super.viewDidLoad()
MenuController.shared.fetchCategories { categories in
if let categories = categories {
self.updateUI(with: categories)
}
}
}
MenuController 的完整程式如下。
import UIKit
class MenuController {
static let shared = MenuController()
static let orderUpdatedNotification = Notification.Name("MenuController.orderUpdated")
let baseURL = URL(string: "http://localhost:8090/")!
var order = Order() {
didSet {
NotificationCenter.default.post(name: MenuController.orderUpdatedNotification, object: nil)
}
}
func fetchCategories(completion: @escaping ([String]?) -> Void) {
let categoryURL = baseURL.appendingPathComponent("categories")
let task = URLSession.shared.dataTask(with: categoryURL) { data, response, error in
let jsonDecoder = JSONDecoder()
if let data = data,
let categories = try? jsonDecoder.decode(Categories.self, from: data) {
completion(categories.categories)
} else {
completion(nil)
}
}
task.resume()
}
func fetchMenuItems(forCategory categoryName: String, completion: @escaping ([MenuItem]?) -> Void) {
let initialMenuURL = baseURL.appendingPathComponent("menu")
var components = URLComponents(url: initialMenuURL, resolvingAgainstBaseURL: true)!
components.queryItems = [URLQueryItem(name: "category", value: categoryName)]
let menuURL = components.url!
let task = URLSession.shared.dataTask(with: menuURL) { data, response, error in
let jsonDecoder = JSONDecoder()
if let data = data,
let menuItems = try? jsonDecoder.decode(MenuItems.self, from: data) {
completion(menuItems.items)
} else {
completion(nil)
}
}
task.resume()
}
func fetchImage(url: URL, completion: @escaping (UIImage?) -> Void) {
print("fetchImage", url)
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data,
let image = UIImage(data: data) {
completion(image)
} else {
completion(nil)
}
}
task.resume()
}
func submitOrder(forMenuIDs menuIDs: [Int], completion: @escaping (Int?) -> Void) {
let orderURL = baseURL.appendingPathComponent("order")
var request = URLRequest(url: orderURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let data: [String: [Int]] = ["menuIds": menuIDs]
let jsonEncoder = JSONEncoder()
let jsonData = try? jsonEncoder.encode(data)
request.httpBody = jsonData
let task = URLSession.shared.dataTask(with: request) { data, response, error in
let jsonDecoder = JSONDecoder()
if let data = data,
let preparationTime = try? jsonDecoder.decode(PreparationTime.self, from: data) {
completion(preparationTime.prepTime)
} else {
completion(nil)
}
}
task.resume()
}
}
初學者撰寫串接後台的程式時可模仿範例的寫法,將跟後台溝通的相關程式統一定義在某個檔案,比方串接 TMBD API 開發電影 App 時,將相關的 API 定義成 NetworkController 或 MovieController 的 function,然後利用 static 宣告的變數 shared 產生物件呼叫。
以下幾種都是網路抓資料常見的檔名結尾。
- Controller
- Service
- Client
- Manager
利用 capture list 解決 closure 可能產生的記憶體問題
呼叫 API 時,我們也可以在 closure 裡加上 capture list,比方 [weak self]
。
override func viewDidLoad() {
super.viewDidLoad()
MenuController.shared.fetchCategories { [weak self] categories in
guard let self else { return }
if let categories = categories {
self.updateUI(with: categories)
}
}
}
它的好處可參考以下連結的說明。
串接 REST API,練習 http get,post,delete,put,以 Reqres API 為例
剛剛 Apple 的範例已經很完整,不過常見的 REST API 通常包含了 HTTP get,post,delete & put,因此接下來讓我們以 Reqres API 為例,試試串接 REST API。
- model 資料型別
struct UsersResponse: Decodable {
let page: Int
let perPage: Int
let total: Int
let totalPages: Int
let data: [User]
}
struct User: Decodable {
let id: Int
let email: String
let firstName: String
let lastName: String
let avatar: URL
}
struct CreateUserResponse: Decodable {
let name: String
let job: String
let id: String
}
struct UpdateUserResponse: Decodable {
let name: String
let job: String
}
struct CreateUserBody: Encodable {
let name: String
let job: String
}
struct UpdateUserBody: Encodable {
let name: String
let job: String
}
- 定義設定網址參數的 URL extension
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
}
}
- 定義錯誤型別 NetworkError
enum NetworkError: Error{
case invalidURL
}
- 串接 API 的 NetworkController
import Foundation
class NetworkController {
static let shared = NetworkController()
let baseURL = URL(string: "https://reqres.in/api")!
func getUsers(page: Int, completion: @escaping (UsersResponse?) -> Void) {
let queries = ["page": "\(page)"]
guard let url = baseURL.appendingPathComponent("users").withQueries(queries) else {
completion(nil)
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
print(String(data: data, encoding: .utf8)!)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let usersResponse = try decoder.decode(UsersResponse.self, from: data)
completion(usersResponse)
} catch {
print(error)
completion(nil)
}
} else {
completion(nil)
}
}.resume()
}
func createUser(_ user: CreateUserBody, completion: @escaping (CreateUserResponse?) -> Void) {
let url = baseURL.appendingPathComponent("users")
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let encoder = JSONEncoder()
let data = try? encoder.encode(user)
request.httpBody = data
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do {
let decoder = JSONDecoder()
let createUserResponse = try decoder.decode(CreateUserResponse.self, from: data)
completion(createUserResponse)
} catch {
print(error)
completion(nil)
}
} else {
completion(nil)
}
}.resume()
}
func deleteUser(userId: Int, completion: @escaping (String?) -> Void) {
let url = baseURL.appendingPathComponent("users/\(userId)")
var request = URLRequest(url: url)
request.httpMethod = "DELETE"
URLSession.shared.dataTask(with: request) { data, response, error in
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 204 {
completion("ok")
} else {
completion(nil)
}
}.resume()
}
func updateUser(_ user: UpdateUserBody, completion: @escaping (UpdateUserResponse?) -> Void) {
let url = baseURL.appendingPathComponent("users")
var request = URLRequest(url: url)
request.httpMethod = "PUT"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let encoder = JSONEncoder()
let data = try? encoder.encode(user)
request.httpBody = data
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do {
let decoder = JSONDecoder()
let updateUserResponse = try decoder.decode(UpdateUserResponse.self, from: data)
completion(updateUserResponse)
} catch {
print(error)
completion(nil)
}
} else {
completion(nil)
}
}.resume()
}
}
- 呼叫 NetworkController 定義的 function
import UIKit
class ViewController: UIViewController {
@IBAction func get(_ sender: Any) {
NetworkController.shared.getUsers(page: 1) { userResponse in
if let userResponse = userResponse {
print(userResponse)
}
}
}
@IBAction func post(_ sender: Any) {
let user = CreateUserBody(name: "fox", job: "student")
NetworkController.shared.createUser(user) { createUserResponse in
if let createUserResponse = createUserResponse {
print(createUserResponse)
}
}
}
@IBAction func remove(_ sender: Any) {
NetworkController.shared.deleteUser(userId: 5) { result in
if let result = result {
print(result)
}
}
}
@IBAction func put(_ sender: Any) {
let user = UpdateUserBody(name: "Peter", job: "Writer")
NetworkController.shared.updateUser(user) { updateUserResponse in
if let updateUserResponse = updateUserResponse {
print(updateUserResponse)
}
}
}
}
其它 API 串接範例:
iTunes API
LBTA 的 AppStore JSON APIs。