結構化 RESTful API 模組與功能 大大提升程式碼的易讀性!
日常工作中,常常需要與後端串接 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 夜晚,我們下次見。