日常工作中,常常需要與後端串接 RESTful API,然而 API 網址常常很難管理與統一路口,今天這篇文章,想與大家分享在公司的經驗,一起規範出一整套 RESTful API 串接的體系與模組。今天這篇文章需要大家搭配源碼閱讀。讓我們開始吧!
要點內容
- 統一 API 底層入口,利用泛型來解決所有 JSON Data to Model 轉換
- 規範 API Function 結構,不再讓 URL 散落一地
- 統一的錯誤獲取,讓 debug 不再頭大
- 結合 PromiseKit 與 Alamofire,製作屬於你的非同步 API 網路應用
準備工作
這邊我們需要快速建立一個 UI 程式碼。為了聚焦重點,就不詳細說明 UI 的組裝過程了。
首先,我們需要先建立一個新專案,進行 pod init
,並且編輯 Podfile
加入必要組件如下:
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'ActualCombatSwiftNetwork' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for ActualCombatSwiftNetwork
pod 'Alamofire', '~> 4.8'
pod 'PromiseKit', '~> 6.8'
pod 'PKCHelper', '~> 0.1.1'
end
編輯完成後就可以執行 pod install
。然後,讓我們開啟 .xcworkspace
檔案,開始我們的 Coding之旅!
我們需要加入兩個 UIViewController
,一個 UITableViewController
:
LoginViewController
– 登入頁面範例SignUpViewController
– 註冊頁面範例ProductsViewController
– 獲取產品頁面範例
另外加入一個 UITabBarController
,命名為 MainTabBarController
。參考程式碼如下:
import UIKit
import PKCHelper
class MainTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
setupViewController()
}
fileprivate func setupViewController() {
// Login
let loginController = LoginViewController()
loginController.title = "Login"
let navLoginController = templateNavController(image: nil, rootViewController: loginController, selectImage: nil)
// SignUp
let signUpController = SignUpViewController()
signUpController.title = "SignUp"
let navSignUpController = templateNavController(image: nil, rootViewController: signUpController, selectImage: nil)
// Products
let productsViewController = ProductsViewController()
productsViewController.title = "Products"
let navProductsViewController = templateNavController(image: nil, rootViewController: productsViewController, selectImage: nil)
// tabBar
// tabBar.tintColor = UIColor.reddishOrange
tabBar.unselectedItemTintColor = .brownishGrey
viewControllers = [
navLoginController,
navSignUpController,
navProductsViewController
]
}
fileprivate func templateNavController(image: UIImage?, rootViewController: UIViewController = UIViewController(), selectImage: UIImage? = nil) -> UINavigationController {
let viewNavController = UINavigationController(rootViewController: rootViewController)
if let image = image {
viewNavController.tabBarItem.image = image
}
if let selectImage = selectImage {
viewNavController.tabBarItem.selectedImage = selectImage
}
return viewNavController
}
}
另外,修改 AppDelegate
文件,讓我們的 window
連接到剛剛做好的 MainTabBarController
。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.main.bounds)
window?.makeKeyAndVisible()
let mainTabBar = MainTabBarController()
window?.rootViewController = mainTabBar
return true
}
完成後就可以編譯和執行,你現在應該可以看到 UI 組件正常工作了!如果當中遇到困難的話,請參考源碼。
到目前為止,我們的準備工作就完成了,讓我們進入主題吧!
註冊頁面 SignUpViewController
首先,我們需要先建立一個註冊頁面,模擬用戶註冊真實狀況。在頁面中,我們會需要幾個欄位,來配合 API
介面。
註冊 API 文件說明
POST
https://yasuoyuhao-restfulapi.herokuapp.com/api/account/signup
參數
字段 類型 描述
- name String 使用者名稱
- email String 使用者 email(登入帳號)
- password String 使用者密碼
- emailContext String 註冊成功 email 內容
參數範例:
{
"name": "xxxx",
"email": "[email protected]",
"password": "xxxx",
"emailContext": "xxxx",
}
Response (example):
HTTP/1.1 200 註冊成功
{
"success": "true",
"message: "享受你的token吧",
"token": "is jwt token"
}
Response (example):
HTTP/1.1 200 重複註冊
{
"success": "false",
"message": "帳號已經存在"
}
註冊頁面 UI 組件 Layout
我們查看 API
文檔中,發現需要輸入四個欄位。因此,我們需要在 SignUpViewController
建立四個 UITextField
和一個按鈕UIButton
。
首先,讓我們製作 UI
組件,並命名五個組件變數:
lazy var emailTextField: UITextField = {
let tf = UITextField()
tf.placeholder = "email 帳號"
tf.borderStyle = .roundedRect
return tf
}()
lazy var passwordTextField: UITextField = {
let tf = UITextField()
tf.placeholder = "密碼"
tf.borderStyle = .roundedRect
tf.isSecureTextEntry = true
tf.textContentType = .newPassword
return tf
}()
lazy var nameTextField: UITextField = {
let tf = UITextField()
tf.placeholder = "姓名"
tf.borderStyle = .roundedRect
return tf
}()
lazy var emailContentTextField: UITextField = {
let tf = UITextField()
tf.placeholder = "註冊成功 Email, 確認內容"
tf.borderStyle = .roundedRect
return tf
}()
lazy var signupButton: UIButton = {
let bt = UIButton(type: .system)
bt.setTitle("註冊", for: .normal)
bt.setTitleColor(.white, for: .normal)
bt.backgroundColor = UIColor.arizonaStateUniversityRed
bt.addTarget(self, action: #selector(handleSignUp), for: .touchUpInside)
bt.layer.cornerRadius = 8
return bt
}()
然後,在 viewDidLoad()
中 layout
我們的組件:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = .aboutMeGreen
view.addSubview(emailTextField)
view.addSubview(passwordTextField)
view.addSubview(nameTextField)
view.addSubview(emailContentTextField)
view.addSubview(signupButton)
emailTextField.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor, padding: .init(top: 50, left: 16, bottom: 0, right: 16), size: .init(width: 0, height: 50))
passwordTextField.anchor(top: emailTextField.bottomAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor, padding: .init(top: 16, left: 16, bottom: 0, right: 16), size: .init(width: 0, height: 50))
nameTextField.anchor(top: passwordTextField.bottomAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor, padding: .init(top: 16, left: 16, bottom: 0, right: 16), size: .init(width: 0, height: 50))
emailContentTextField.anchor(top: nameTextField.bottomAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor, padding: .init(top: 16, left: 16, bottom: 0, right: 16), size: .init(width: 0, height: 50))
signupButton.anchor(top: emailContentTextField.bottomAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor, padding: .init(top: 32, left: 16, bottom: 0, right: 16), size: .init(width: 0, height: 50))
}
完成後,應該可以看到以下畫面!
建置 API Services
有畫面之後,我們就可以開始重頭戲了:串接我們的 API
。
首先,我們需要建立 BaseAPIServices
。BaseAPIServices
是我們 API
最底層打包請求的服務,當中有兩個關鍵的重要方法:
requestGenerator
:負責生成我們的Http
請求setupResponse
:讀取生成的Http
請求、發送請求、並解析json data to model
你會注意到,setupResponse
是允許 Codable
泛型的,也就是說我們可以把model
丟進來讓它負責解析。
class BaseAPIServices {
/**
Base Http Request Generator
- Parameter url: 請求資源位置
- Parameter parameters: Parameters
- Parameter method: HTTPMethod
- Parameter encoding: URLEncoding
*/
public func requestGenerator(route: APIServicesURLProtocol, parameters: Parameters? = nil, method: HTTPMethod = .get, encoding: ParameterEncoding = URLEncoding.default) -> DataRequest {
let url = ServicesURL.baseurl + route.url
return Alamofire.request(
url,
method: method,
parameters: parameters,
encoding: encoding,
headers: nil
)
}
/**
Base API Producer
- Parameter dataRequest: 請求數據
- Parameter type: 回應模型
- Returns: Promise.
*/
public func setupResponse<T: Codable>(_ dataRequest: DataRequest, type: T.Type) -> Promise<T> {
return Promise<T>.init(resolver: { (resolver) in
dataRequest.validate().responseJSON(queue: DispatchQueue.global(), options: JSONSerialization.ReadingOptions.mutableContainers, completionHandler: { (response) in
switch response.result {
case .success(let json):
do {
let decoder: JSONDecoder = JSONDecoder()
let jsonData = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
let content = try decoder.decode(T.self, from: jsonData)
resolver.fulfill(content)
} catch let error {
resolver.reject(error)
}
case .failure(let error):
PKCLogger.shared.error(error)
if let aferror = error as? AFError {
// Error Catch
switch aferror {
case .invalidURL:
()
case .parameterEncodingFailed:
()
case .multipartEncodingFailed:
()
case .responseValidationFailed:
()
case .responseSerializationFailed:
()
}
}
resolver.reject(error)
}
})
})
}
}
接下來,建立模組化的 URL 資源位置 APIServicesURLProtocol
。我們先建立兩個 Protocol
public protocol ServicesURLProtocol {
var url: String { get }
}
public protocol APIServicesURLProtocol: ServicesURLProtocol {
var rootURL: String { get }
}
再建立一個 Struct
存放基本路徑。請注意,如果有不同環境的路徑處理,也通常會在這邊處理,比如生產環境跟開發環境,baseurl 要做切換。
struct ServicesURL {
static var baseurl: String {
return "https://yasuoyuhao-restfulapi.herokuapp.com/api/"
}
}
然後,我們建立授權模組的URL
:
enum AuthAPIURL: APIServicesURLProtocol {
public var rootURL: String {
return "accounts/"
}
case login
case signup
public var url: String {
return getURL()
}
private func getURL() -> String {
var resource = ""
switch self {
case .login:
resource = "login"
case .signup:
resource = "signup"
}
return "\(rootURL)\(resource)"
}
}
如此一來,我們的模組化資源路徑就算完成了!
接下來,讓我們建立 AccountsAPIServices
負責 AuthAPIURL
的資源動作:
import PromiseKit
import Alamofire
import PKCHelper
class AccountsAPIServices: BaseAPIServices {
static let shared = AccountsAPIServices()
func signup(email: String, password: String, name: String, emailContent: String) -> Promise<String> {
// 返回 Promise
return Promise<String>.init(resolver: { (resolver) in
// 組合參數
var parameters = [String: AnyObject]()
parameters.updateValue(email as AnyObject, forKey: "email")
parameters.updateValue(password as AnyObject, forKey: "password")
parameters.updateValue(name as AnyObject, forKey: "name")
parameters.updateValue(emailContent as AnyObject, forKey: "emailContent")
// 生成 Request
let req = self.requestGenerator(route: AuthAPIURL.signup, parameters: parameters, method: .post, encoding: JSONEncoding.default)
firstly {
// 發送請求,並且給予型別
self.setupResponse(req, type: LoginResult.self)
}.then { (tokenRes) -> Promise<String> in
// setup record token
return Promise<String>.init(resolver: { (resolver) in
PKCLogger.shared.debug(tokenRes)
// Token 處理,解碼與儲存
if let token = tokenRes.token {
resolver.fulfill(token)
} else {
resolver.reject(AuthError.tokenIsNotExist)
}
})
}.done { (currectUserId) in
// 完成流程
resolver.fulfill(currectUserId)
}.catch(policy: .allErrors) { (error) in
// 錯誤處理
resolver.resolve(nil, error)
}
})
}
}
enum AuthError: Error {
case tokenIsNotExist
var localizedDescription: String {
return getLocalizedDescription()
}
private func getLocalizedDescription() -> String {
switch self {
case .tokenIsNotExist:
return "token is not find."
}
}
}
我們的流程有幾個步驟:
- 建立
Promise
- 組合
Post
參數 - 生成
Request
- 發送
Request
- 接收
Response
(由Base
層解析json
) - 處理已經解析好的資訊(商業邏輯)
- 完成流程 / 錯誤處理
只此為止,我們已經達成了規範 API Function 結構,資料解析、錯誤獲取、生成請求通一入口了。
串接頁面
我們完成了註冊需要的 API Services
之後,就可以來 Controller
層介接資料了。
讓我們來實作 handleSignUp
吧:
@objc fileprivate func handleSignUp() {
// 打印欄位訊息
PKCLogger.shared.debug(emailTextField.text)
PKCLogger.shared.debug(passwordTextField.text)
PKCLogger.shared.debug(nameTextField.text)
PKCLogger.shared.debug(emailContentTextField.text)
guard let email = emailTextField.text,
let password = passwordTextField.text,
let name = nameTextField.text,
let emailContent = emailContentTextField.text else { return }
// 呼叫註冊服務
_ = AccountsAPIServices.shared.signup(email: email, password: password, name: name, emailContent: emailContent).done { (token) in
PKCLogger.shared.debug(token)
}.catch { (error) in
PKCLogger.shared.error(error.localizedDescription)
if let authError = error as? AuthError {
PKCLogger.shared.error(authError.localizedDescription)
}
}.finally {
// Update UI...
}
}
在上面的程式碼中,我們先打印出輸入的欄位訊息,並在驗證欄位呼叫我們剛剛做好的註冊服務層。
現在我們可以進行測試了!(建議帳號密碼要記下來,等下登入會用到)
首先,像這樣輸入欄位資料:
然後,按下註冊按鈕,並查看控制台訊息
太好了!我們完成了註冊頁面的製作與 API
的串接囉!接下來,讓我們處理登入頁面。
登入頁面 LoginViewController
登入 API 文件說明
User – 使用者登入
POST
https://yasuoyuhao-restfulapi.herokuapp.com/api/account/login
參數
字段 類型 描述
- email String 使用者 EMail(登入帳號)
- password String 使用者密碼
Response (example):
HTTP/1.1 200 登入成功
{
"success": "true",
"message: "享受你的token吧",
"token": "is jwt token"
}
Response (example):
HTTP/1.1 200 登入失敗
{
"success": "false",
"message": "登入失敗,找不到使用者"
}
登入頁面 UI 組件 Layout
我們查看 API
文檔中,發現需要輸入兩個欄位。因此,我們需要在 LoginViewController
建立兩個 UITextField
,和一個登入按鈕UIButton
。
我們需要先建立 UI 組件。首先,命名三個組件變數:
lazy var emailTextField: UITextField = {
let tf = UITextField()
tf.placeholder = "email 帳號"
tf.borderStyle = .roundedRect
return tf
}()
lazy var passwordTextField: UITextField = {
let tf = UITextField()
tf.placeholder = "密碼"
tf.borderStyle = .roundedRect
tf.isSecureTextEntry = true
return tf
}()
lazy var loginButton: UIButton = {
let bt = UIButton(type: .system)
bt.setTitle("登入", for: .normal)
bt.setTitleColor(.white, for: .normal)
bt.backgroundColor = UIColor.tiffanyBlue
bt.addTarget(self, action: #selector(handleLogin), for: .touchUpInside)
bt.layer.cornerRadius = 8
return bt
}()
接下來,在 viewDidLoad()
中 layout
我們的組件:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = .faceBookBlue
view.addSubview(emailTextField)
view.addSubview(passwordTextField)
view.addSubview(loginButton)
emailTextField.anchor(top: view.safeAreaLayoutGuide.topAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor, padding: .init(top: 100, left: 16, bottom: 0, right: 16), size: .init(width: 0, height: 50))
passwordTextField.anchor(top: emailTextField.bottomAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor, padding: .init(top: 16, left: 16, bottom: 0, right: 16), size: .init(width: 0, height: 50))
loginButton.anchor(top: passwordTextField.bottomAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor, padding: .init(top: 32, left: 16, bottom: 0, right: 16), size: .init(width: 0, height: 50))
}
完成後,你應該會看到這個畫面:
建置登入的 API Services
下一步,我們需要到 AccountsAPIServices
加入登入方法:
func login(email: String, password: String) -> Promise<String> {
// 返回 Promise
return Promise<String>.init(resolver: { (resolver) in
// 組合參數
var parameters = [String: AnyObject]()
parameters.updateValue(email as AnyObject, forKey: "email")
parameters.updateValue(password as AnyObject, forKey: "password")
// 生成 Request
let req = self.requestGenerator(route: AuthAPIURL.login, parameters: parameters, method: .post, encoding: JSONEncoding.default)
firstly {
// 發送請求,並且給予型別
self.setupResponse(req, type: LoginResult.self)
}.then { (tokenRes) -> Promise<String> in
// 處理回應
return Promise<String>.init(resolver: { (resolver) in
PKCLogger.shared.debug(tokenRes)
// Token 處理,解碼與儲存
if let token = tokenRes.token {
resolver.fulfill(token)
} else {
resolver.reject(AuthError.tokenIsNotExist)
}
})
}.done { (currectUserId) in
// 完成流程
resolver.fulfill(currectUserId)
}.catch(policy: .allErrors) { (error) in
// 錯誤處理
resolver.resolve(nil, error)
}
})
}
完成了註冊需要的 API Services
之後,我們就可以來 Controller
層介接資料了。
先如此實作 handleLogin
:
@objc fileprivate func handleLogin() {
PKCLogger.shared.debug(emailTextField.text)
PKCLogger.shared.debug(passwordTextField.text)
guard let email = emailTextField.text, let password = passwordTextField.text else { return }
_ = AccountsAPIServices.shared.login(email: email, password: password).done { (token) in
PKCLogger.shared.debug(token)
}.catch { (error) in
PKCLogger.shared.error(error.localizedDescription)
if let authError = error as? AuthError {
PKCLogger.shared.error(authError.localizedDescription)
}
}.finally {
// Update UI...
}
}
然後,輸入剛剛成功註冊的帳號密碼來進行測試:
接著,來查看控制台訊息:
成功了!我們完成了註冊與登入的 API 串接,並且發現模組化規範 API 之後,我們串接的速度越來越快了!
進階:商品頁面 ProductsViewController
這邊是針對更接近實戰演練的串接,我們會先登入,並且帶入 Token
獲取商品列表。有興趣的讀者可以先自己實作看看,再來參考以下步驟。
取得產品 API 文件
Product – 取得產品
GET
https://yasuoyuhao-restfulapi.herokuapp.com/api/products
允許: Authorization
Header
字段 類型 描述
authorization String Authorization value.
Response (example):
HTTP/1.1 200 成功
{
"success": true,
"message": "成功找到產品",
"products": [
{
"_id": "5b5b7c7008f1c9bc5efe820b",
"created": "2018-07-27T20:11:28.380Z",
"name": "p助教戰手冊",
"owner": "5b4c334c34bf87ab17a9d860",
"description": "p助這些年的教戰心得",
"image": "https://amazon-yasuoyuhao-webapplication.s3.amazonaws.com/1532722285753.png",
"category": "5b5b573634ebdf994de6e1f4",
"__v": 0
}
]
}
Response (example):
HTTP/1.1 200 查詢失敗
{
"success": "false",
"message": "error message"
}
首先,建立AuthToken
加入變數 token
。這樣我們進行請求時,就可以帶入 Token
。
class AuthToken {
static let shared = AuthToken()
var token: String?
}
接下來,調整修改 requestGenerator
方法,把 token
加入 Http Header
中:
public func requestGenerator(route: APIServicesURLProtocol, parameters: Parameters? = nil, method: HTTPMethod = .get, encoding: ParameterEncoding = URLEncoding.default) -> DataRequest {
let url = ServicesURL.baseurl + route.url
var headers: HTTPHeaders?
if let token = AuthToken.shared.token {
headers = HTTPHeaders()
headers?.updateValue(token, forKey: "Authorization")
}
return Alamofire.request(
url,
method: method,
parameters: parameters,
encoding: encoding,
headers: headers
)
}
headers
參數,我們當然可以再抽出 function
參數作為傳入值;但這邊先不深入探討這一點,有興趣的讀者可以自行優化。然後,修改登入與註冊,以暫存 token:
// Token 處理,解碼與儲存
if let token = tokenRes.token {
//---加入此行
AuthToken.shared.token = token
//---
resolver.fulfill(token)
}
下一步,讓我們逐一建立 Products Model
、ProductAPIURL
、和 ProductsAPIServices
:
- Products Model
import Foundation
struct ProductRes : Codable {
let success : Bool?
let message : String?
let products : [Products]?
enum CodingKeys: String, CodingKey {
case success = "success"
case message = "message"
case products = "products"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
success = try values.decodeIfPresent(Bool.self, forKey: .success)
message = try values.decodeIfPresent(String.self, forKey: .message)
products = try values.decodeIfPresent([Products].self, forKey: .products)
}
}
struct Products : Codable {
let _id : String?
let created : String?
let name : String?
let owner : String?
let description : String?
let image : String?
let category : String?
let __v : Int?
enum CodingKeys: String, CodingKey {
case _id = "_id"
case created = "created"
case name = "name"
case owner = "owner"
case description = "description"
case image = "image"
case category = "category"
case __v = "__v"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
_id = try values.decodeIfPresent(String.self, forKey: ._id)
created = try values.decodeIfPresent(String.self, forKey: .created)
name = try values.decodeIfPresent(String.self, forKey: .name)
owner = try values.decodeIfPresent(String.self, forKey: .owner)
description = try values.decodeIfPresent(String.self, forKey: .description)
image = try values.decodeIfPresent(String.self, forKey: .image)
category = try values.decodeIfPresent(String.self, forKey: .category)
__v = try values.decodeIfPresent(Int.self, forKey: .__v)
}
}
ProductAPIURL
與 ProductsAPIServices
是用於存放獲取產品後的資料。
- ProductAPIURL
enum ProductAPIURL: APIServicesURLProtocol {
public var rootURL: String {
return "products/"
}
case fetch
public var url: String {
return getURL()
}
private func getURL() -> String {
var resource = ""
switch self {
case .fetch:
resource = ""
}
return "\(rootURL)\(resource)"
}
}
- ProductsAPIServices
class ProductsAPIServices: BaseAPIServices {
static let shared = ProductsAPIServices()
func fetchProducts() -> Promise<[Products]> {
// 返回 Promise
return Promise<[Products]>.init(resolver: { (resolver) in
// 生成 Request
let req = self.requestGenerator(route: ProductAPIURL.fetch, method: .get)
firstly {
// 發送請求,並且給予型別
self.setupResponse(req, type: ProductRes.self)
}.then { (productRes) -> Promise<[Products]> in
// 處理回應
return Promise<[Products]>.init(resolver: { (resolver) in
PKCLogger.shared.debug(productRes.success ?? false)
// 確認產品是否有資料
if let products = productRes.products {
resolver.fulfill(products)
} else {
resolver.reject(ProductError.productIsNotFind)
}
})
}.done { (currectUserId) in
// 完成流程
resolver.fulfill(currectUserId)
}.catch(policy: .allErrors) { (error) in
// 錯誤處理
resolver.resolve(nil, error)
}
})
}
}
enum ProductError: Error {
case productIsNotFind
var localizedDescription: String {
return getLocalizedDescription()
}
private func getLocalizedDescription() -> String {
switch self {
case .productIsNotFind:
return "product is not find."
}
}
}
我們已經自定義了資料型別,並且放入 self.setupResponse
自動解析。現在,我們可以來測試看看了!
讓我們在 ProductsViewController
中加入下列程式碼:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
_ = ProductsAPIServices.shared.fetchProducts().done { (products) in
PKCLogger.shared.debug(products)
}
}
然後,執行並查看控制台(別忘了先到登入頁登入):
成功了!我們加入了 token
驗證機制,並且成功取得資料。
最後,讓我們在 ProductsViewController
完善 UI
:
import UIKit
import PKCHelper
class ProductsViewController: UITableViewController {
private let cellId = "cellId"
lazy var products: [Products] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
_ = ProductsAPIServices.shared.fetchProducts().done { (products) in
PKCLogger.shared.debug(products)
self.products = products
}.catch({ (_) in
// 錯誤處理
}).finally {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return products.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
cell.textLabel?.text = products[indexPath.row].name
return cell
}
}
讓我們來看看成果吧:
總結
在本篇教學中,我們透過抽離 API Services
層,來分化我們與後端串接的耦合性。實戰中還會有一層 Services 負責 View Model
的轉換,我們並不會直接使用後端來的 model
,而是透過自定義錯誤處理,分離商業邏輯錯誤與系統錯誤。而且,我們也引入了 Promise
來幫助我們細分流程與非同步編程。這樣,不但大大優化了程式碼的易讀性,也單一職責化了各個服務,可以說是大規模的 App 中不可或缺的一部分。
恭喜!至此整個文章與概念已完全交付!強烈建議整個過程搭配源碼消化服用,祝你有個美好的 Coding 夜晚,我們下次見。