模仿 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。

--

--

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

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