Contents
Chào bạn đến với Fx Studio!
Bài viết này sẽ là phần tiếp theo của Combine vs. UIKit – Networking. Hướng dẫn việc tích hợp Combine Code vào trong iOS Project. Với yêu cầu chính là lấy dữ liệu từ API và hiển thị nó lên UI Control.
Nếu bạn chưa chuẩn bị kiến thức về tương tác Networking với Combine. Thì bạn nên đọc trước bài viết đó để bổ sung kiến thức. Còn bạn đã oke rồi …
Bắt đầu thôi!
Chuẩn bị
- Xcode 11.0
- Swift 5.1
- iOS 13.0
Project sử dụng trong bài khá bình thường. Chỉ cần 1 UIViewController, với giao diện là 1 UITableView cơ bản. Việc sử dụng hay không sử dụng Storyboard thì không quan trọng.
1. Objects
Chúng ta sử dụng API lấy new feed
của Apple, về các bài hát mới nhất trên iTunes.
https://rss.itunes.apple.com/api/v1/us/apple-music/coming-soon/all/100/aexplicit.json
Bạn nên cái các plug-in để hiển thị JSON trên trình duyệt của bạn. Bạn có thể quan sát cấu trúc JSON như sau:
Các phần mình đánh dấu là màu đỏ
là các phần bạn cần lưu ý về name
. Vì công việc bây giờ của bạn là tạo các class/struct để chứa dữ liệu từ API đó. Nói đúng hơn là cấu trúc của JSON sẽ ảnh hưởng tới các tên và kiểu giá trị các thuộc tính của class/struct đó.
Tạo mới một file có tên là Music.swift
và tiến hành khai báo như sau:
struct Music: Codable { var name: String var id: String var artistName: String var artworkUrl100: String } struct MusicResults: Codable { var results: [Music] var updated: String } struct FeedResults: Codable { var feed: MusicResults }
Trong đó:
- Lựa chọn struct để tiết kiệm đi việc khai báo thêm hàm
init
và chỉ dùng để lưu trữ dữ liệu. Không có biến đổi dữ liệu. - Các tên của các thuộc tính (như : feed, results, … ) trùng với các
key
trong cấu trúc JSON của API trả về Codable
để có thể map dữ liệu từ JSONDecoder thành đối tượng của struct một cách nhanh chóng
Chúng ta đã xong phần cấu trúc Objects dùng để chứa dữ liệu. Tuỳ thuộc vào API mà bạn tương tác, bạn sẽ có một cấu trúc khác.
Ngoài Codable
thì chúng ta còn có 2 class khác:
- Decodable
- Encodable
Dễ hiểu thì thèn Codable
chứa cả 2 thèn đó. Còn 2 thèn đó, mỗi thèn làm 1 nhiệm vụ riêng.
2. Error
Đây là phần bạn hay bỏ sót. Nhưng bạn sẽ ân hận khá nhiều. Nếu bạn không khai báo và sử dụng chúng trong lúc project của bạn còn sơ khai.
Tạo một file mới và đặt tên là APIError.swift
. Tạm thời chúng ta sử dụng lại các case error
như ở bài trước.
import Foundation enum APIError: Error { case error(String) case errorURL case invalidResponse case errorParsing case unknown var localizedDescription: String { switch self { case .error(let string): return string case .errorURL: return "URL String is error." case .invalidResponse: return "Invalid response" case .errorParsing: return "Failed parsing response from server" case .unknown: return "An unknown error occurred" } } }
Trong đó:
- Kế thừa lại protocol
Error
để nó là Error và tương thích với hệ thống - Các
case
là các trường hợp chúng ta cần quản lý. Chú ý 2 caseerror(String)
: dành cho việc muốn thông báo 1 lỗi với 1 stringunknown
: nếu bị lỗi nhưng không xác định nó từ đâu ra
localizedDescription
: giúp bạn có thể lấy được mô tả lỗi của từng trường hợp
3. Models
Bây giờ mới là phần chính của bài sau khi đã setup xong các đối tượng và error. Tạo một file tên là APIMusic.swift
.
Đừng quên việc
import Combine
.
import Foundation import Combine struct APIMusic { }
3.1. EndPoint
Trước tiên bạn phải xác định được URL và thông qua việc có được EndPoint. Và bất kì ai cũng muốn mình có 1 class/struct mà có thể bao quát nhiều trường hợp nhất có thể. EndPoint này cũng không ngoại lệ, khi mà thực tế trong 1 dự án thì có rất nhiều API cần phải xử lý. Nhưng chúng lại giống nhau khá nhiều.
Mở file APIMucis.swift
thêm phần khai báo sau:
struct APIMusic { //MARK: EndPoint enum EndPoint { static let baseURL = URL(string: "https://rss.itunes.apple.com/api/v1/us/apple-music")! case coming_soon(Int) var url: URL { switch self { case .coming_soon(let limit): return EndPoint.baseURL.appendingPathComponent("/coming-soon/all/\(limit)/aexplicit.json") } } } }
Chúng ta sử dụng trường hợp đầu tiên là coming_soon
với 1 tham số là Int. Để lấy số lượng cần lấy từ API. Bạn có thể áp dụng tương tự cho nhiều link khác. Còn cách sử dụng thì như sau:
let url = EndPoint.coming_soon(100).url
3.2. Properties
Tiếp tục file APIMusic.swift
, khai báo thêm các thuộc tính cần thiết.
//MARK: Properties private let decoder = JSONDecoder() private let apiQueue = DispatchQueue(label: "API", qos: .default, attributes: .concurrent)
Về decoder
, dùng để map dữ liệu JSON thành các Object đã được định nghĩa. Class sử dụng ở đây là JSONDecoder
. Bạn có thể sử dụng nhiều loại decoder khác.
Cái này khai báo thêm cho sinh động, chứ cũng không cần thiết mấy. Trừ khi bạn có ý đồ config riêng cho việc decode.
Về apiQueue
thì ở phần trước mình không nhắc tới. Tuy nhiên, khi tương tác trong Project và để tránh ít va chạm xung đột với Main Thread
. Thì việc tốt nhất là subscribe
& receive
ở một Thead khác.
3.3. Request
Tạo 1 function trong APIMusic
để xử lý việc request lấy các bài hát mới nhất từ API.
func comingSoon(limit: Int) -> AnyPublisher<FeedResults, APIError> { }
Vẫn là hình bóng cũ, với giá trị trả về là một Publisher. Trong đó
- Output là
FeedResults
, trong đó có chứa nhiều dữ liệu mà chúng ta cần lấy- Array Music
- Các thông tin bổ sung
- Failure là
APIError
Áp dụng kiến thức bài trước, sử dụng URLSession
để tạo ra 1 DataTaskPublisher
. Bạn thêm việc subscribe
ở apiQueue
vừa tạo ở trên.
func comingSoon(limit: Int) -> AnyPublisher<[Music], APIError> { return URLSession.shared .dataTaskPublisher(for: EndPoint.coming_soon(limit).url) .subscribe(on: apiQueue) ......... .eraseToAnyPublisher() }
Tiếp tục là việc phân tích response. Nếu như không đúng statusCode
thì nén ra 1 error. Vâng, bạn có thể làm nhiều thứ ở đây, trước khi sử dụng tới data
của response.
Thêm đoạn code sau vào function của mình với tryMap
.
func comingSoon(limit: Int) -> AnyPublisher<FeedResults, APIError> { return URLSession.shared .dataTaskPublisher(for: EndPoint.coming_soon(limit).url) .subscribe(on: apiQueue) .tryMap { output in guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else { throw APIError.invalidResponse } return output.data } ..... .eraseToAnyPublisher() }
Công dụng của tryMap
thì giups bạn biến đổi và kèm theo đó là nén được error ra. Tiếp tục với decode
và mapError
để hoàn tất function của chúng ta.
func comingSoon(limit: Int) -> AnyPublisher<FeedResults, APIError> { return URLSession.shared .dataTaskPublisher(for: EndPoint.coming_soon(limit).url) .subscribe(on: apiQueue) .tryMap { output in guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else { throw APIError.invalidResponse } return output.data } .decode(type: FeedResults.self, decoder: decoder) .mapError { error -> APIError in switch error { case is URLError: return .errorURL case is DecodingError: return .errorParsing default: return error as? APIError ?? .unknown } } .eraseToAnyPublisher() }
Về 2 phần decode
và mapError
, mình đã giải thích ở bài trước rồi. Kết thúc funtion này và chuyển sang phần sử dụng.
4. Implements
Bạn tạo 1 file UIViewController, với giao diện là 1 UITableView. Đặt tên dễ thương là MusicsViewController
. Và tiến hành cài đặt đơn giản như sau:
import UIKit import Combine class MusicsViewController: UIViewController { // Outlets @IBOutlet weak var tableView: UITableView! // Properties var musics: [Music] = [] { didSet { self.tableView.reloadData() } } private var subscriptions = Set<AnyCancellable>() override func viewDidLoad() { super.viewDidLoad() // Title title = "Musics" // TableView tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") tableView.delegate = self tableView.dataSource = self } } // UITableView Delegate & DataSource extension MusicsViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { musics.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) let item = musics[indexPath.row] cell.textLabel?.text = item.name return cell } }
Chú ý:
musics
dùng để lưu trữ dữ liệu nhận được từ APIdidSet
của musics dùng đểreload
lại TableView sau khi được gán dữ liệu- extension cơ bản cho delegate & dataSource của UITableView
subscriptions
dùng để lưu trữ các subscribe phát sinh trong ViewController. Và nó mang chức năng tự huỹ để đảm bảo bộ nhớ được giải phóng.
Bây giờ, chúng ta thêm 1 property của APIMusic
private var api = APIMusic()
Tại function viewDidLoad
, thêm phần fetch dữ liệu và tiến hành subscribe
như sau:
// fetch api.comingSoon(limit: 100) .receive(on: DispatchQueue.main) .sink(receiveCompletion: { completion in switch completion { case .finished: break case .failure(let error): fatalError(error.localizedDescription) } }) { apiResult in self.musics = apiResult.feed.results }.store(in: &subscriptions)
Giải thích:
receive
đảm bảo việc nhận được dữ liệu tại ViewController sẽ ởMain Thread
, nhằm update UI- Subscribe bằng
sink
để xử lý được cả 2 trường hợp- Có dữ liệu
- Completion với
finished
&failure
- Tạm thời trong ví dụ chúng ta không update UI với trường hợp có lỗi xãy ra.
- Tại phần nhận được dữ liệu, chúng ta gán dữ liệu cần thiết cho thuộc tính
musics
của ViewController.
Tới đây bạn run project và tận hưởng kết quả nhận được.
Chưa hết!
Mình sẽ bổ sung thêm một cách ngầu hơn để gọi API. Bạn sử dụng assign
để binding dữ liệu nhận được tới thẳng thuộc tính musics
của ViewController. Thay đoạn code sink
trên bằng đoạn code sau.
map
khi bạn chỉ muốn sử dụng Array Music làm Output mới. Và nó trùng với kiểu dữ liệu củamusics
.catch
để bắt Error. Trường hợp này thì thay Error bằng 1 array rỗng (được gọi là giá trị mặc định)
api.comingSoon(limit: 100) .receive(on: DispatchQueue.main) .map { $0.feed.results } .catch{ _ in Empty() } .assign(to: \.musics, on: self) .store(in: &subscriptions)
Muốn sử dụng
assign
thì bạn phải triệt tiêu hết các error.
Run project và kiểm tra nó đã hoạt động đúng như mong muốn hay chưa. Nếu hiển thị dữ liệu nhận được lên TableView, thì chúc mừng bạn đã thành công. Còn nếu không thì bạn bình tĩnh debug nó. EZ game!
OKAY! Bạn đã đủ kiến thức để có thể lấy dữ liệu từ một API bằng Combine Code rồi đó. Và mình xin kết thúc bài viết này và bạn có thể download code demo tại đây.
Tạm kết
- Áp dụng kiến thức tương tác Networking vào iOS Project với Combine Code
- Thiết kế các đối tượng theo cấu trúc JSON của API
- Thiết kế và quản lý Error
- Tạo class/struct cho một Model API, với các thành phần cơ bản (như EndPoint, queue, request …)
- Tích hợp vào ViewController với 2 kiểu subscribe
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Prompt Engineering trong 10 phút
- Một số ví dụ sử dụng Prompt cơ bản khi làm việc với AI
- Prompt trong 10 phút
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
You may also like:
Archives
- December 2024 (3)
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)