Nếu bạn muốn trở thành nhà phát triển iOS, có một số kỹ năng cơ bản cần nắm vững. Thứ nhất, quan trọng là quen với việc tạo table view. Thứ hai, bạn nên biết cách điền dữ liệu vào các table view đó. Thứ ba, thật tuyệt nếu bạn có thể lấy dữ liệu từ API và sử dụng dữ liệu đó trong table view.
Điểm thứ ba là những gì chúng ta sẽ đề cập trong bài viết này. Kể từ khi Codable được giới thiệu trong Swift 4, việc gọi API trở nên dễ dàng hơn rất nhiều. Trước đó, hầu hết mọi người dùng các pod như Alamofire và SwiftyJson (bạn có thể đọc về cách làm đó ở đây). Giờ đây, cách làm gốc của Swift đã tốt hơn nhiều, nên không cần tải pod thêm.
Hãy cùng xem qua một số khối xây dựng thường dùng để gọi API. Chúng ta sẽ bao quát các khái niệm này trước, vì chúng là phần quan trọng để hiểu cách gọi API.
- Completion handlers (Xử lý hoàn thành)
URLSessionDispatchQueue- Retain cycles (Chu trình giữ tham chiếu)
Cuối cùng, chúng ta sẽ gộp tất cả lại. Tôi sẽ dùng API Star Wars mã nguồn mở để xây dựng dự án này. Bạn có thể xem toàn bộ mã dự án trên GitHub.
Lưu ý: Tôi mới học code và chủ yếu tự học. Xin lỗi nếu tôi trình bày sai một số khái niệm.
Completion Handlers
Nhớ tập Friends mà Pheobe dính chặt vào điện thoại hàng ngày chờ nói với dịch vụ khách hàng? Hãy tưởng tượng nếu ngay từ đầu cuộc gọi, một người tốt bụng tên Pip nói: "Cảm ơn bạn đã gọi. Tôi không biết bạn phải chờ bao lâu, nhưng tôi sẽ gọi lại cho bạn khi chúng tôi sẵn sàng." Nó sẽ không hài hước bằng, nhưng Pip đang đề xuất làm completion handler cho Pheobe.
Bạn dùng completion handler trong một hàm khi biết hàm đó sẽ mất một lúc để hoàn thành. Bạn không biết mất bao lâu, và không muốn tạm dừng cuộc sống chờ nó xong. Vì vậy, bạn nhờ Pip gõ vai bạn khi cô ấy sẵn sàng đưa câu trả lời. Như vậy bạn có thể đi làm việc khác, chạy việc, đọc sách, xem TV. Khi Pip gõ vai bạn với câu trả lời, bạn có thể lấy câu trả lời đó và sử dụng.
Điều này xảy ra với các cuộc gọi API. Bạn gửi yêu cầu URL đến máy chủ, xin nó trả dữ liệu. Bạn hy vọng máy chủ trả dữ liệu nhanh, nhưng không biết mất bao lâu. Thay vì bắt người dùng chờ đợi kiên nhẫn máy chủ trả dữ liệu, bạn dùng completion handler. Điều này có nghĩa là bạn có thể bảo ứng dụng đi làm việc khác, ví dụ tải phần còn lại của trang.
Bạn bảo completion handler gõ vai ứng dụng khi nó có thông tin bạn muốn. Bạn có thể chỉ định thông tin đó là gì. Như vậy, khi ứng dụng bị gõ vai, nó có thể lấy thông tin từ completion handler và làm gì đó với nó. Thường thì bạn sẽ reload table view để dữ liệu hiện ra cho người dùng.
Dưới đây là ví dụ về completion handler. Ví dụ đầu tiên là thiết lập cuộc gọi API bản thân:
func fetchFilms(completionHandler: @escaping ([Film]) -> Void) {
// Thiết lập biến lotsOfFilms
var lotsOfFilms: [Film]
// Gọi API với một số mã
// Sử dụng dữ liệu từ API, gán giá trị cho lotsOfFilms
// Đưa biến lotsOfFilms cho completion handler
completionHandler(lotsOfFilms)
}
Bây giờ chúng ta muốn gọi hàm fetchFilms. Một số lưu ý:
- Bạn không cần tham chiếu
completionHandlerkhi gọi hàm. Chỉ khi khai báo hàm mới tham chiếucompletionHandler. - Completion handler sẽ trả về dữ liệu để dùng. Dựa trên hàm viết ở trên, chúng ta biết mong đợi dữ liệu kiểu
[Film]. Cần đặt tên dữ liệu để tham chiếu. Dưới đây tôi dùng tênfilms, nhưng có thể làrandomDatahoặc bất kỳ tên biến nào bạn thích.
Mã sẽ trông như sau:
fetchFilms() { (films) in
// Làm gì đó với dữ liệu completion handler trả về
print(films)
}
URLSession
URLSession giống như quản lý của một nhóm. Quản lý không tự làm việc. Cô ấy phân việc cho thành viên trong nhóm, và họ sẽ hoàn thành. Nhóm của cô ấy là dataTasks. Mỗi khi bạn cần dữ liệu, hãy viết cho sếp và dùng URLSession.shared.dataTask.
Bạn có thể đưa cho dataTask các loại thông tin khác nhau để giúp đạt mục tiêu. Việc đưa thông tin cho dataTask gọi là khởi tạo (initialization). Tôi khởi tạo dataTasks với URL. dataTasks cũng dùng completion handlers như một phần của khởi tạo. Đây là ví dụ:
let url = URL(string: "https://www.swapi.co/api/films")
let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
// mã của bạn ở đây
})
task.resume()
dataTasks dùng completion handlers, và luôn trả về cùng các kiểu thông tin: data, response và error. Bạn có thể đặt tên khác cho các kiểu dữ liệu này, như (data, res, err) hoặc (someData, someResponse, someError). Vì quy ước, tốt nhất là giữ tên rõ ràng thay vì đặt tên biến mới lạ.
Hãy bắt đầu với error. Nếu dataTask trả về error, bạn sẽ muốn biết ngay. Điều này cho phép điều hướng mã xử lý lỗi một cách nhẹ nhàng. Nó cũng có nghĩa là bạn không phiền toái cố gắng đọc dữ liệu và làm gì đó với nó vì có lỗi khi trả dữ liệu.
Dưới đây tôi xử lý lỗi rất đơn giản bằng cách in lỗi ra console và thoát hàm. Có nhiều cách khác để xử lý lỗi nếu muốn. Hãy nghĩ về việc dữ liệu này cơ bản đến mức nào với app. Ví dụ, nếu bạn có app ngân hàng và cuộc gọi API này hiển thị số dư người dùng, bạn có thể muốn xử lý lỗi bằng cách hiện modal nói: "Xin lỗi, chúng tôi đang gặp sự cố. Vui lòng thử lại sau."
if let error = error {
print("Error accessing swapi.co: \(error)")
return
}
Tiếp theo xem response. Bạn có thể ép response thành httpResponse. Như vậy bạn có thể xem mã trạng thái và ra quyết định dựa trên mã. Ví dụ, nếu mã trạng thái là 404, bạn biết trang không tìm thấy.
Mã dưới dùng guard kiểm tra hai điều kiện tồn tại. Nếu cả hai tồn tại, cho phép mã tiếp tục câu lệnh sau mệnh đề guard. Nếu một trong hai thất bại, thoát hàm. Đây là trường hợp điển hình của mệnh đề guard. Bạn mong đợi mã sau guard clause là luồng "ngày vui" (tức luồng dễ dàng không lỗi).
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("Error with the response, unexpected status code: \(response)")
return
}
Cuối cùng xử lý chính dữ liệu. Chú ý chúng ta chưa dùng completion handler cho error hay response. Đó là vì completion handler đang chờ dữ liệu từ API. Nếu không đến phần dữ liệu của mã, không cần gọi handler.
Đối với dữ liệu, chúng ta dùng JSONDecoder để phân tích dữ liệu một cách đẹp đẽ. Điều này khá hay, nhưng yêu cầu bạn đã thiết lập model. Model của chúng ta gọi là FilmSummary. Nếu JSONDecoder mới với bạn, hãy tìm hiểu trực tuyến cách dùng nó và Codable. Nó thực sự đơn giản trong Swift 4 trở lên so với thời Swift 3.
Trong mã dưới, trước tiên kiểm tra dữ liệu tồn tại. Chúng ta khá chắc nó tồn tại, vì không có lỗi và không có phản hồi HTTP lạ. Thứ hai, kiểm tra có thể phân tích dữ liệu nhận được theo cách mong đợi. Nếu được, trả về film summary cho completion handler. Phòng trường hợp không có dữ liệu từ API, có dự phòng mảng rỗng.
if let data = data,
let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) {
completionHandler(filmSummary.results ?? [])
}
Vậy toàn bộ mã gọi API trông như sau:
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
Lưu ý: Tôi cực kỳ mới với việc hiểu retain cycles! Đây là tóm gist những gì tôi tìm hiểu trực tuyến.
Retain cycles quan trọng cho quản lý bộ nhơ. Cơ bản là bạn muốn app dọn dẹp các phần bộ nhớ không còn cần. Tôi giả định điều này làm app hiệu năng hơn.
Có nhiều cách Swift giúp bạn tự động làm điều này. Tuy nhiên có nhiều cách bạn có thể vô tình code retain cycles vào app. Retain cycle nghĩa là app sẽ luôn giữ bộ nhớ cho một đoạn mã nhất định. Thường xảy ra khi có hai thứ có con trỏ mạnh (strong pointers) trỏ lẫn nhau.
Để tránh, mọi người thường dùng weak. Khi một phía của mã là weak, không có retain cycle và app sẽ giải phóng bộ nhớ.
Với mục đích của chúng ta, một pattern phổ biến là dùng [weak self] khi gọi API. Điều này đảm bảo khi completion handler trả về mã, app có thể giải phóng bộ nhớ.
fetchFilms { [weak self] (films) in
// mã ở đây
}
DispatchQueue
Xcode dùng các luồng (threads) khác nhau để thực thi mã song song. Lợi thế của nhiều luồng là bạn không bị kẹt chờ một việc xong mới làm việc tiếp. Hy vọng bạn bắt đầu thấy liên kết với completion handlers ở đây.
Các luồng này dường như cũng gọi là dispatch queues. Cuộc gọi API được xử lý trên một queue, thường là queue nền (background). Khi đã có dữ liệu từ API, khả năng lớn bạn muốn hiển thị dữ liệu cho người dùng. Nghĩa là bạn muốn refresh table view.
Table view là một phần của UI, và mọi thao tác UI nên làm trong main dispatch queue. Điều này nghĩa là ở đâu đó trong file view controller, thường như một phần của hàm viewDidLoad, bạn nên có đoạn mã bảo table view refresh.
Chúng ta chỉ muốn table view refresh khi nó có dữ liệu mới từ API. Nghĩa là dùng completion handler gõ vai chúng ta báo khi cuộc gọi API xong. Chúng ta chờ gõ vai đó trước khi refresh table.
Mã sẽ trông giống:
fetchFilms { [weak self] (films) in
self?.films = films
// Reload table view dùng main dispatch queue
DispatchQueue.main.async {
tableView.reloadData()
}
}
viewDidLoad vs viewDidAppear
Cuối cùng bạn cần quyết định nơi gọi hàm fetchFilms. Nó sẽ bên trong view controller sử dụng dữ liệu từ API. Có hai nơi rõ ràng để gọi API. Một là trong viewDidLoad và một là trong viewDidAppear.
Đây là hai trạng thái khác nhau cho app. Hiểu của tôi là viewDidLoad được gọi lần đầu tiên bạn tải view đó lên foreground. viewDidAppear được gọi mỗi khi bạn quay lại view đó, ví dụ khi nhấn nút back để quay lại view.
Nếu bạn mong đợi dữ liệu thay đổi giữa các lần người dùng điều hướng đến và từ view đó, bạn có thể muốn đặt cuộc gọi API trong viewDidAppear. Tuy nhiên tôi nghĩ đối với hầu hết app, viewDidLoad là đủ. Apple khuyến nghị viewDidAppear cho tất cả cuộc gọi API, nhưng đó có vẻ như overkill. Tôi tưởng tượng nó làm app kém hiệu năng hơn vì nó gọi API nhiều hơn cần thiết.
Kết hợp tất cả các bước
Thứ nhất: viết hàm gọi API. Ở trên, đây là fetchFilms. Nó sẽ có completion handler, trả về dữ liệu bạn quan tâm. Trong ví dụ, completion handler trả về mảng film.
Thứ hai: gọi hàm này trong view controller. Làm ở đây vì bạn muốn cập nhật view dựa trên dữ liệu từ API. Trong ví dụ, tôi refresh table view khi API trả dữ liệu.
Thứ ba: quyết định nơi trong view controller gọi hàm. Trong ví dụ, tôi gọi trong viewDidLoad.
Thứ tư: quyết định làm gì với dữ liệu từ API. Trong ví dụ, tôi refresh table view.
Bên trong NetworkManager.swift (hàm này có thể định nghĩa trong view controller nếu muốn, nhưng tôi dùng 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()
}
Bên trong 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()
}
}
}
// mã khác cho view controller
}
Trời ơi, chúng ta làm xong rồi! Cảm ơn bạn đã kiên nhẫn theo dõi.