หากคุณอยากเป็นนักพัฒนา iOS มีทักษะพื้นฐานบางอย่างที่ควรรู้ ก่อนอื่น ควรคุ้นเคยกับการสร้าง Table View สอง ควรรู้วิธีเติมข้อมูลลง Table View สาม ควรสามารถดึงข้อมูลจาก API มาใช้ใน Table View ได้
จุดที่สามคือสิ่งที่เราจะครอบคลุมในบทความนี้ ตั้งแต่มี Codable ใน Swift 4 การเรียก API ง่ายขึ้นมาก ก่อนหน้านี้ส่วนใหญ่ใช้ Pods อย่าง Alamofire และ SwiftyJSON (อ่านวิธีทำได้ที่นี่) ตอนนี้วิธีของ Swift เองดีกว่ามาก ไม่ต้องโหลด Pod เพิ่ม
มาดู Building Blocks ที่ใช้เรียก API บ่อยๆ:
- Completion Handlers
URLSessionDispatchQueue- Retain Cycles
สุดท้ายเราจะรวมทั้งหมดเข้าด้วยกัน ผมจะใช้ Star Wars API (SWAPI) โอเพนซอร์ส สร้างโปรเจกต์นี้ ดูโค้ดเต็มได้ที่ GitHub
ข้อจำกัดความรับผิดชอบ: ผมเพิ่งเริ่มเขียนโค้ด และเรียนรู้ด้วยตนเอง ขออภัยหากอธิบายบางอย่างไม่ถูกต้อง
Completion Handlers

จำได้ไหมตอน Friends ที่ Phoebe ติดอยู่ที่โทรศัพท์หลายวันรอฝ่ายบริการลูกค้า? 想像ว่า ตอนต้นสาย โทรหาคนดีๆ ชื่อ Pip พูดว่า: "ขอบคุณที่โทร ไม่รู้ว่าจะรอนานแค่ไหน แต่ผมจะโทรกลับตอนพร้อมแล้ว" มันจะไม่น่าหัวเราะเท่าเดิม แต่ Pip กำลังเสนอเป็น Completion Handler ให้ Phoebe
ใช้ Completion Handler ในฟังก์ชันเมื่อรู้ว่าฟังก์ชันนั้นจะใช้เวลานาน ไม่รู้แน่ชัดว่าเท่าไหร่ และไม่อยากหยุดชีวิตรอจบ 所以ขอให้ Pip โบกบ้างตอนพร้อมให้คำตอบ ทำให้คุณไปทำอย่างอื่นได้ ไปรับของ อ่านหนังสือ ดูทีวี ตอน Pip โบกมาพร้อมคำตอบ คุณก็เอาคำตอบไปใช้ต่อ
API Calls ทำงานแบบนี้ ส่ง URL Request ไป Server ขอข้อมูล หวังว่า Server ตอบเร็ว แต่ไม่รู้ว่าจะนานแค่ไหน แทนที่จะให้ User รอ ใช้ Completion Handler บอก App ไปทำอย่างอื่นได้ เช่น โหลดหน้าอื่น
บอก Completion Handler โบก App ตอนมีข้อมูลที่ต้องการ สามารถระบุได้ว่าข้อมูลคืออะไร ตอน App ถูกโบก สามารถเอาข้อมูลจาก Completion Handler ไปใช้งานได้ ปกติจะ Reload Table View ให้ข้อมูลขึ้นให้ User เห็น
ตัวอย่าง Completion Handler ฟังก์ชันที่ตั้งค่า API Call:
func fetchFilms(completionHandler: @escaping ([Film]) -> Void) {
// Setup variable lotsOfFilms
var lotsOfFilms: [Film]
// Call API with some code
// Using data from API, assign value to lotsOfFilms
// Give completion handler the variable, lotsOfFilms
completionHandler(lotsOfFilms)
}
ตอนนี้เรียกฟังก์ชัน fetchFilms สิ่งที่ควรทราบ:
- ไม่ต้องอ้างอิง
completionHandlerตอนเรียกฟังก์ชัน อ้างอิงได้แค่ใน Declaration ของฟังก์ชัน - Completion Handler จะคืนข้อมูลกลับมา ตามฟังก์ชันด้านบน คาดหวังข้อมูลชนิด
[Film]ต้องตั้งชื่อข้อมูลเพื่ออ้างอิง ด้านล่างใช้ชื่อfilmsแต่ตั้งชื่ออื่นก็ได้ เช่นrandomData
โค้ดจะประมาณนี้:
fetchFilms() { (films) in
// Do something with the data the completion handler returns
print(films)
}
URLSession
URLSession เหมือน Manager ทีม Manager เองไม่ทำงาน จัดงานให้คนในทีม ทีมคือ dataTasks ทุกครั้งที่ต้องการข้อมูล เขียนให้ Boss ผ่าน URLSession.shared.dataTask
สามารถส่งข้อมูลประเภทต่างๆ ให้ dataTask ช่วยบรรลุเป้าหมาย เรียกว่า Initialization ผม Initialize dataTasks ด้วย URLs dataTasks ใช้ Completion Handlers ในการ Initialize ตัวอย่าง:
let url = URL(string: "https://www.swapi.co/api/films")
let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
// your code here
})
task.resume()
dataTasks ใช้ Completion Handlers และคืนค่าเหมือนกันเสมอ: data, response และ error สามารถตั้งชื่อต่างกันได้ เช่น (data, res, err) แต่เพื่อ Convention ควรตั้งชื่อชัดเจน
เริ่มจาก error ถ้า dataTask คืน error อยากรู้ทันที เพื่อจัดการ Error อย่างเหมาะสม และไม่ต้องพยายามอ่านข้อมูลเพราะมี Error ในการส่งคืน
ด้านล่างจัดการ Error แบบง่าย Print Error และ Return ออกจากฟังก์ชัน มีวิธีอื่นเยอะมาก คิดว่าข้อมูลนี้สำคัญต่อแอปแค่ไหน เช่น แอปธนาคาร API นี้แสดงยอดเงิน อาจจะ Present Modal บอก User ว่า "ขออภัย มีปัญหา กรุณาลองใหม่ภายหลัง"
if let error = error {
print("Error accessing swapi.co: \(error)")
return
}
ต่อไปดู Response สามารถ Cast Response เป็น HTTPURLResponse ดู Status Code ตัดสินใจได้ เช่น 404 = หน้าไม่เจอ
โค้ดด้านล่างใช้ guard เช็ค 2 เงื่อนไข ถ้าทั้งคู่มีอยู่ โค้ดจะต่อไป ถ้าไหนไหน Fail จะ Return ออก นี่คือ Use Case แบบคลาสสิกของ Guard คาดหวังโค้ดหลัง Guard จะเป็น Happy Path (ไหลลื่น ไม่มี Error)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("Error with the response, unexpected status code: \(response)")
return
}
สุดท้ายจัดการ Data เอง สังเกตว่าไม่ได้ใช้ Completion Handler สำหรับ Error หรือ Response เพราะ Completion Handler รอ Data จาก API ถ้าไม่ถึงส่วน Data ไม่ต้อง Invoke Handler
สำหรับ Data ใช้ JSONDecoder Parse อย่างสวยงาม น่าสนใจ แต่ต้องมี Model ก่อน Model ของเราชื่อ FilmSummary ถ้า JSONDecoder ใหม่ ไปอ่านออนไลน์วิธีใช้กับ Codable มันง่ายมากใน Swift 4+ เทียบ Swift 3
โค้ดด้านล่าง เช็ค Data มีอยู่จริง แล้วเช็ค Parse ได้ตามที่คาด ถ้าได้ คืน Film Summary ให้ Completion Handler Fallback เป็น Empty Array ถ้า API ไม่คืนอะไร
if let data = data,
let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) {
completionHandler(filmSummary.results ?? [])
}
โค้ดเต็มสำหรับ API Call:
func fetchFilms(completionHandler: @escaping ([Film]) -> Void) {
let url = URL(string: domainUrlString + "films/")!
let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
print("Error with fetching films: \(error)")
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("Error with the response, unexpected status code: \(response)")
return
}
if let data = data,
let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) {
completionHandler(filmSummary.results ?? [])
}
})
task.resume()
}
Retain Cycles
NB: ผมเพิ่งเริ่มเข้าใจ Retain Cycles! นี่คือสรุปจากที่หาออนไลน์
Retain Cycles สำคัญสำหรับ Memory Management อยากให้ App ทำความสะอาด Memory ที่ไม่ต้องแล้ว ผมคิดว่าทำให้ App เร็วขึ้น
Swift ช่วยอัตโนมัติหลายวิธี แต่เราสามารถเขียน Retain Cycles เข้าไปโดยไม่ได้ตั้งใจได้หลายวิธี Retain Cycle คือ App จะยึด Memory ไว้ตลอดสำหชิ้นโค้ดหนึ่ง มักเกิดเมื่อมี 2 สิ่งที่มี Strong Pointer ชี้กัน
วิธีแก้ ใช้ weak ตัวหนึ่ง ไม่มี Retain Cycle App จะปล่อย Memory ได้
สำหรับเรา Pattern ปกติคือ ใช้ [weak self] ตอนเรียก API เพื่อให้ App ปล่อย Memory ได้ตอน Completion Handler คืนค่า
fetchFilms { [weak self] (films) in
// code in here
}
DispatchQueue
Xcode ใช้ Thread หลายตัวรันโค้ดแบบขนาน ข้อดีคือไม่ติดอยู่รออย่างหนึ่งจบก่อนจึงทำอย่างถัดไป ได้เห็น Link กับ Completion Handlers
Thread เหล่านี้เรียกว่า Dispatch Queues API Calls จัดการบน Queue หนึ่ง มักเป็น Background Queue ตอนมีข้อมูลจาก API มักจะอยากโชว์ให้ User คือ Refresh Table View
Table View เป็น UI ทุก Manipulation UI ควรทำใน Main Dispatch Queue หมายความว่าใน View Controller มักใน viewDidLoad ควรมีโค้ดบอก Table View Refresh
อยากให้ Table View Refresh ตอนมีข้อมูลใหม่จาก API ใช้ Completion Handler โบกตอน API จบ รอโบกก่อนถึง Refresh Table
โค้ดประมาณนี้:
fetchFilms { [weak self] (films) in
self.films = films
// Reload table view using main dispatch queue
DispatchQueue.main.async {
tableView.reloadData()
}
}
viewDidLoad vs viewDidAppear
สุดท้าย ตัดสินใจว่าจะเรียก fetchFilms ที่ไหน จะอยู่ใน View Controller ที่ใช้ข้อมูลจาก API มี 2 ที่ชัดเจน: viewDidLoad และ viewDidAppear
เป็น 2 สถานะต่างกัน ผมเข้าใจว่า viewDidLoad เรียกครั้งแรกที่โหลด View ใน Foreground viewDidAppear เรียกทุกครั้งที่กลับมาที่ View เช่น กด Back กลับมา
ถ้าคาดว่าข้อมูลจะเปลี่ยนระหว่างที่ User Navigate ไปมาที่ View อาจจะเอา API Call ไว้ viewDidAppear แต่ผมคิดเกือบทุกแอป viewDidLoad พอ Apple แนะนำ viewDidAppear สำหทุก API Call แต่ดู Overkill ทำให้ App ช้าลงเพราะเรียก API มากกว่าที่ต้อง
รวมทุกขั้นตอน
1. เขียนฟังก์ชันเรียก API คือ fetchFilms มี Completion Handler คืนข้อมูลที่สนใจ ตัวอย่างคืน Array of Films
2. เรียกฟังก์ชันนี้ใน View Controller เพราะอยาก Update View จากข้อมูล API ตัวอย่าง Refresh Table View ตอน API คืนข้อมูล
3. ตัดสินใจว่าจะเรียกใน View Controller ที่ไหน ตัวอย่างเรียกใน viewDidLoad
4. ตัดสินใจว่าจะทำอะไรกับข้อมูลจาก API ตัวอย่าง Refresh Table View
ใน NetworkManager.swift (ฟังก์ชันนี้อาจ Define ใน View Controller ก็ได้ แต่ผมใช้ Pattern MVVM)
func fetchFilms(completionHandler: @escaping ([Film]) -> Void) {
let url = URL(string: domainUrlString + "films/")!
let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error {
print("Error with fetching films: \(error)")
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("Error with the response, unexpected status code: \(response)")
return
}
if let data = data,
let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) {
completionHandler(filmSummary.results ?? [])
}
})
task.resume()
}
ใน FilmsViewController.swift:
final class FilmsViewController: UIViewController {
private var films: [Film]?
override func viewDidLoad() {
super.viewDidLoad()
NetworkManager().fetchFilms { [weak self] (films) in
self?.films = films
DispatchQueue.main.async {
self?.tableView.reloadData()
}
}
}
// other code for the view controller
}
เยี่ยม! ขอบคุณที่อ่านจนจบครับ