[Swift] ลองใช้ Interceptor เข้ามาช่วยเตรียมค่าซ้ำ ๆ ก่อนการเรียก API กันดูมั้ย

Wisanu P.
Q-CHANG

--

[Swift] Use interceptor for prepare request api

ในการเรียก API แต่ละครั้งจำเป็นที่จะต้องส่งค่าอะไรบางอย่างเพื่อบอกกับหลังบ้าน (Back-end) ว่าเราต้องการอะไรและด้วยเงื่อนไขอะไรตัวอย่างเช่น

  • เรียกดูข้อมูลส่วนตัวของผู้ใช้งาน {{url}}/users/:id โดยเราสามารถระบุ :id ที่ต้องการได้เช่น id = 1

Ex. https://5b58dc3b-1b73-4a05-be99-9074b07bb2e4.mock.pstmn.io/users/1

User Response:

{
"id": 1,
"firstName": "Somchai",
"lastName": "Rukdee",
"age": 30
}
  • เรียกดูข้อมูลที่อยู่ของผู้ใช้งาน {{url}}/address?user-id=:userId โดยเราสามารถระบุ :userId ที่ต้องการได้เช่น userId = 1 (อีกล้าา) ดังตัวอย่างต่อไปนี้

Ex. https://5b58dc3b-1b73-4a05-be99-9074b07bb2e4.mock.pstmn.io/address/1

Address Response:

{
"id": 1,
"addressNo": "165/x",
"street": "Pattanakarn",
"localtion": "Suanluang, Bangkok",
"postCode": "10240"
}

จากตัวอย่างทั้ง 2 ข้อเพียงเท่านี้เราก็สามารถนำไปเขียนโค๊ดได้อย่างสบายเลยโดยพระเอกของเราในการเรียก API ก็เหมือนเดิม Alamofire นั่นเองมาดูโค๊ดกันเลย

Model:

struct UserInfo: Decodable {
var id: Int?
var firstName: String?
var lastName: String?
var age: Int?

func displayInfo() -> String{
"\(id!) \(firstName!) \(lastName!), \(age!) year old"
}
}

struct UserAddess: Decodable {
var addressNo: String?
var street: String?
var localtion: String?
var postCode: String?

func displayInfo() -> String{
"\(addressNo!) \(street!), \(localtion!) \(postCode!)"
}
}

UserApi:

class UserApi {

static let hostURL = "https://5b58dc3b-1b73-4a05-be99-9074b07bb2e4.mock.pstmn.io/"

class func getInfomation(with id: Int) {
Task {
do {

async let getUser = getUser(with: id)
async let getAddress = getAddress(with: id)
let (user, address) = try await (getUser, getAddress)
debugPrint(user.displayInfo())
debugPrint(address.displayInfo())
}catch {
debugPrint(error.localizedDescription)
}
}
}

class func getUser(with id: Int) async throws -> UserInfo {
let urlRequest = UserApi.hostURL + "users/\(id)"
let user = try await AF.request(
urlRequest
).serializingDecodable(UserInfo.self).value
return user
}

class func getAddress(with userId: Int) async throws -> UserAddess {
let urlRequest = UserApi.hostURL + "address"
let params = ["userId": userId]
let address = try await AF.request(
urlRequest,
parameters: params
).serializingDecodable(UserAddess.self).value
return address
}

}

โดยเมื่อเราเรียก function getInfomation(with:) เราก็จะได้ Output ดังนี้

“ก็ได้แล้วนี่ไง…แล้ว Interceptor ที่ว่ามันอยู่ไหนกันนะ?”

ยังก่อนครับเรามาดูกันต่ออีกซักหน่อยดีกว่า

จาก API ทั้ง 2 เส้นเราจะสังเกตุได้ว่าไม่ว่าใครก็สามารถเข้าถึง API ดังกล่าวได้ไม่มีความปลอดภัยอะไรเลยทางฝั่ง Back-end จึงบอกว่า

Back-end dev: “ต่อไปนี้ให้ฝั่งหน้าบ้าน (Front-end) ต้องไปขอ access token จาก API เส้น xxx มาเก็บไว้จากนั้นเมื่อมีการเรียก API ทุกเส้นจะต้องส่ง access token นี้มากับ HTTP Headers ดังต่อไปนี้”

["Authorization": "Bearer {access_token}"]

“เอาล่ะเหมือนจะเข้าเรื่องซักทีออกทะเลไปตั้งนาน”

โดยปกติแล้ววิธีที่เราจะทำกันในการส่ง token แนบไปกับ header ก็มีหลายวิธีเช่น

  1. Hard code ดิ แปะไปให้ครบทุกเส้นถ้ามี API 100 เส้นก็แปะ 100 ชุด

UserApi:

    class func getUser(with id: Int) async throws -> UserInfo {
let urlRequest = hostURL + "users/\(id)"
let httpHeaders: HTTPHeaders = ["Authorization": "Bearer \(accessToken)"]
let user = try await AF.request(
urlRequest,
headers: httpHeaders
).serializingDecodable(UserInfo.self).value
return user
}

class func getAddress(with userId: Int) async throws -> UserAddess {
let urlRequest = hostURL + "address"
let httpHeaders: HTTPHeaders = ["Authorization": "Bearer \(accessToken)"]
let params = ["userId": userId]
let address = try await AF.request(
urlRequest,
parameters: params,
headers: httpHeaders
).serializingDecodable(UserAddess.self).value
return address
}

“อยากจะร้องไห้ T-T”

2. ทำเป็น Core function แล้วกันเผื่ออนาคตมีเพิ่มลด header ก็จะได้แก้ที่จุดเดียว

UserApi:

    class func getUser(with id: Int) async throws -> UserInfo {
let urlRequest = hostURL + "users/\(id)"
let user = try await AF.request(
urlRequest,
headers: buildHttpHeaders()
).serializingDecodable(UserInfo.self).value
return user
}

class func getAddress(with userId: Int) async throws -> UserAddess {
let urlRequest = hostURL + "address"
let params = ["userId": userId]
let address = try await AF.request(
urlRequest,
parameters: params,
headers: buildHttpHeaders()
).serializingDecodable(UserAddess.self).value
return address
}

class func buildHttpHeaders() -> HTTPHeaders{
var httpHeaders: HTTPHeaders = [:]
httpHeaders.update(name: "Authorization", value: "Bearer \(accessToken)")
return httpHeaders
}

วิธีที่ 2. ดูเข้าท่านะเวลาแก้ก็ไม่กระทบมากแต่…ผมขอเสนอทางเลือกที่ 3. คือการนำเอา Interceptor เข้ามาช่วยเตรียมให้

3. นำ RequestInterceptor เข้ามาช่วยเตรียมให้ดีกว่า

ApiInterceptor:

import Alamofire

class ApiInterceptor: RequestInterceptor {
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var adaptedRequest = urlRequest

adaptedRequest.setValue("Bearer \(getAccessToken())", forHTTPHeaderField: "Authorization")

completion(.success(adaptedRequest))
}
}

UserApi:

    class func getUser(with id: Int) async throws -> UserInfo {
let urlRequest = hostURL + "users/\(id)"
let user = try await AF.request(
urlRequest,
interceptor: ApiInterceptor()
).serializingDecodable(UserInfo.self).value
return user
}

class func getAddress(with userId: Int) async throws -> UserAddess {
let urlRequest = hostURL + "address"
let params = ["userId": userId]
let address = try await AF.request(
urlRequest,
parameters: params,
interceptor: ApiInterceptor()
).serializingDecodable(UserAddess.self).value
return address
}

จาก code จะสังเกตุได้ว่าเราจะเปลี่ยนจากการส่ง parameter จาก headers เป็น interceptor ซึ่งจะต้องเป็นคลาสที่สืบทอดมาจาก RequestInterceptor

“แล้วเราจะรู้ได้ยังไงนะว่าเจ้าคลาส ApiInterceptor ที่เราทำขึ้นมาจะเตรียมของให้เราจริง ๆ”

ผมได้ใช้ netfox มา log ดู Request/Response ของ API ซึ่งติดตั้งบนแอฟพลิเคชันของเราได้ง่ายมาก ๆ เพียงแค่เรานำ NFX.sharedInstance().start() ไปใส่ไว้ที่ AppDelegate

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
NFX.sharedInstance().start()
return true
}

เมื่อติดตั้งแล้วเรามาลองทดสอบดูกันเลย

Step:

  1. ลองเรียก API ที่ต้องการดู Request/Response
  2. เมื่อได้ข้อมูลกลับมาแล้วให้ลองเขย่า (Shake) หรือถ้าเป็น simulator ให้กด ^+cmd+z

3. จะเห็น list ของรายการ api ที่เราเรียกเมื่อกดเข้าไปดูเราจะเจอ 3 แทป (แต่เราจะสนใจที่แทป “Request”)

จากรูปด้านบนจะสังเกตุได้ว่า headers ได้มีการแนบ Authorization Field และมีค่าเป็น Bearer … ตามที่เราได้กำหนดเอาไว้ จึงสรุปได้ว่า ApiInterceptor สามารถเข้ามาช่วยเราเตรียมข้อมูลได้จริง ๆ

นอกจากความสามารถในการเตรียมข้อมูลก่อนเรียก API ของ function adapt() แล้ว RequestInterceptor ยังมี function retry() เพื่อทำอะไรบางอย่างหากการเรียก API ของเรานั้นไม่สำเร็จเช่น กลับไปเรียก API เส้นนั้นซ้ำเมื่อไม่สามารถเชื่อมต่อ API ได้หรือทำการขอ token ใหม่ก่อนเรียก API ถ้ามีโอกาสดี ๆ ผมจะขอมาเล่าการทำงานของ function retry() ในโอกาสต่อไป

หวังเป็นอย่างยิ่งว่าบทความนี้จะเป็นประโยชน์กับใครหลาย ๆ คนนะครับ

แล้วพบกันใหม่ครับ

Ref:

https://github.com/Alamofire/Alamofire
https://github.com/kasketis/netfox

--

--

Wisanu P.
Q-CHANG
Editor for

Senior Mobile Developer At Q-CHANG, there is music as the sound of the heart beats and loves to travel.