Contents
Chào bạn đến với Fx Studio!
Chúng ta lại tiếp tục với series Combine vs. UIKit. Nội dung của bài này là về tương tác Networking trong UIKit, sử dụng Combine code để xử lý. Đây cũng là phần nhiều bạn quan tâm nhất. Ngoài ra, việc tương tác Networking cũng là phần quan trọng nhất trong hầu hết các ứng dụng, nhất là các ứng dụng mạng xã hội.
Cũng không cần chuẩn bị gì nhiều …
Bắt đầu thôi!
Chuẩn bị
- Xcode 11.0
- Swift 5.1
- Playground
Về nội dung demo chỉ cần sử dụng Playground là đủ rồi. Chúng ta sẽ áp dụng code của phần này vào iOS Project ở bài tiếp theo.
1. Cách truyền thống
Đầu tiên, chúng ta nhìn lại cách truyền thống khi tương tác với 1 API như sau:
//somewhere in viewDidLoad let url = URL(string: "abc.xyz.com")! let task = URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { fatalError("Error: \(error.localizedDescription)") } guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { fatalError("Error: invalid HTTP response code") } guard let data = data else { fatalError("Error: missing response data") } do { let decoder = JSONDecoder() let posts = try decoder.decode([Post].self, from: data) print(posts.map { $0.title }) } catch { print("Error: \(error.localizedDescription)") } } task.resume()
Bạn sẽ thấy khi tương tác với Networking, chúng ta cần 1 EndPoint
. Sau đó biến đổi nó thành một đối tượng URL
. Sử dụng đối tượng URLSession
. Tạo ra 1 đối tượng dataTask
để thực hiện việc connect
& parse
khi nhận được response.
Tất nhiên, các bước trên chỉ là setup
. Sau cùng là connection
. Dùng đối tượng dataTask
để thực thi. Kết quả chính là response mà server trả về.
Trong việc parse data thì ta thấy 2 điểm cần quan tâm đó là decode
& error handling
.
Như vậy làm việc với Networking theo cách truyền thống, chúng ta cần quan tâm 2 thực thể chính:
- URLSession
- JSON
Qua bên Combine Framework thì sẽ như thế nào?
2. Data task with Combine
Nếu bạn theo dõi các bài Combine của Fx Studio thì cũng nghe nói tới thuyết âm mưu của Apple. Khi mà Apple đã gài gắm hết các Framework còn lại, đều có mặt Combine code trong đó. Và URLSession
cũng không ngoại lệ.
Chúng ta có 1 extension của URLSession nhưng sau:
URLSession.shared.dataTaskPublisher(for: url)
Dòng code trên sẽ trả về cho bạn 1 Publisher. Ngoài tham số là URL thì còn có thêm URLRequest.
Cơ chế chung của quá trình này như sau:
- Tạo thực thế
cancellable
private var cancellable: AnyCancellable?
- subscription tới Publisher của DataTask. Nôm na chia ra thành 2 phần:
- Publisher & các setup cần thiết
- Có được Publisher từ
dataTaskPublisher
map
để lấy phầndata
từ response trả về. Các dữ liệu khác không cần quan tâmdecode
để từ JSON mà biến đổi thành đối tượng theo các class/struct đã định nghĩareplaceError
để handler các error. Trường hợp này nếu có lỗi thì trả về 1 array rỗngeraseToAnyPublisher
xoá sạch dấu vết để lại
- Có được Publisher từ
- Subscriber
- Sử dụng
sink
để bạn muốn handler nhiều hơn - Sử dụng
assign
để bạn muốn binding dữ liệu trực tiếp lên thuộc tính của đối tượng
- Sử dụng
- Publisher & các setup cần thiết
self.cancellable = URLSession.shared.dataTaskPublisher(for: url) .map { $0.data } .decode(type: [Post].self, decoder: JSONDecoder()) .replaceError(with: []) .eraseToAnyPublisher() .sink(receiveValue: { posts in print(posts.count) })
- Kết thúc cả quá trình
self.cancellable?.cancel()
Combine cung cấp cho chúng ta 1 operator là
decode
để biến đổi data json thành đối tượng. Quá tiện lợi!
3. Error handling
Đây là phần không thể thiếu. Với Combine thì có nhược điểm như thế này:
Các toán tử được gọi một cách liên tục với nhau. Bạn rất khó chen vào giữa để bắt các error phát sinh. Hoặc bạn không thể biết được Error nào là của đối tượng nào hay quá trình nào đã sinh ra.
Chúng ta có một số loại Error có thể phát sinh trong quá trình tương tác với Networking như sau:
Error
mang ý nghĩa chung chung. Không xác định rõ thuộc đối tượng nào.URLError
là error liên quan tới các URL và việc connection của URLSessionJSONDecoder error
liên quan tới việc phân tích dữ liệu nhận đượcCustom Error
là phần chúng ta tự định nghĩa thêm cho project của mình
Trước tiên bạn định nghĩa một enum Error cho riêng bạn
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" } } }
Ví dụ trên chỉ là cá nhân của mình. Bạn có thể tự custom lại theo project của bạn.
Theo như trên thì phần tạo Publisher thì sẽ không thay đổi gì
self.cancellable = URLSession.shared.dataTaskPublisher(for: url)
Tuy nhiên, chúng ta sẽ không sử dụng map
mà thay vào đó là tryMap
. Để phân tích thêm dữ liệu của response
có ổn hay không.
self.cancellable = URLSession.shared.dataTaskPublisher(for: url) .tryMap { output in guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else { throw APIError.statusCode } return output.data } .......
Trong đó:
- Chính xác là vẫn trả về
data
- Nếu có lỗi (
statusCode
khác 200) thì sẽthrow
ra 1 Error.
Bản chất của
tryMap
chính là nó có thể nén ra 1 error.
Công việc tiếp theo sau khi tryMap
là decode
. Xem tiếp code ví dụ sau:
self.cancellable = URLSession.shared.dataTaskPublisher(for: url) .tryMap { output in guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else { throw APIError.statusCode } return output.data } .decode(type: [Post].self, decoder: JSONDecoder()) .mapError { error -> APIError in switch error { case is URLError: return .errorURL case is DecodingError: return .errorParsing default: return error as? API.APIError ?? .unknown } } .eraseToAnyPublisher()
Nhưng lần này đi kèm với việc decode là mapError
. Tại sao vậy?
Vì 2 toán tử
tryMap
&decode
đều sinh ra Error. Và Error có từ nhiều loại khác nhau.mapError
để biến chúng thành 1 loại Error mà ta định nghĩa.
Chốt chặn cuối cùng sẽ là ở việc subscribe
. Xem tiếp code ví dụ sau:
.sink(receiveCompletion: { completion in switch completion { case .finished: break case .failure(let error): fatalError(error.localizedDescription) } }, receiveValue: { posts in print(posts.count) })
Bạn sẽ thấy:
- Các error phát sinh thì sẽ làm publisher phát đi
completion
- Chúng ta phải phân tích trong
completion
finished
là thành côngfailure
là có lỗi
Toán tử
sink
sẽ giúp bạn quản lý phần dữ liệu nhận được với đầy đủ error và output.
Một chú ý thêm:
Các toán tử trên được sắp xếp theo ý đồ của mình. Bạn nên xem bài toán thực tế của bạn, để có thể chọn thêm hoặc lượt bỏ đi vài toán tử trong quá trình xử lý Networking.
4. Assign result to property
Sự linh hoạt của sink
thì bạn đã thấy cách sử dụng như thế nào. Chúng ta hoàn toàn có thể sử dụng tuỳ ý vào bất kì đâu trong code. Bây giờ, chuyển sang assign
thì sẽ như thế nào.
Bạn xem tiếp đoạn code ví dụ giả tưởng sau:
class ViewController: UIViewController { private var cancellable: AnyCancellable? private var posts: [Post] = [] { didSet { print("posts --> \(self.posts.count)") } } override func viewDidLoad() { super.viewDidLoad() let url = URL(string: "xxxxxxx")! self.cancellable = URLSession.shared.dataTaskPublisher(for: url) .map { $0.data } .decode(type: [Post].self, decoder: JSONDecoder()) .replaceError(with: []) .eraseToAnyPublisher() .assign(to: \.posts, on: self) } }
Chúng ta sử dụng 1 ViewController. Có 1 property là posts
, dùng để lưu trữ dữ liệu nhận được. didSet
sẽ chạy khi giá trị đã được gán cho thuộc tính. Có thể sử dụng để reload UI
, như các TableView hay CollectionView ….
Tại viewDidLoad
thì sẽ gọi connect API. Bạn chú ý toán tử assign
. Nó sẽ đưa dữ liệu nhận được thẳng tới property posts
của ViewController. Một điểm cần chú ý khi dùng assign
là
Không bao giờ có lỗi. Hoặc nếu có lỗi thì phải thay thế nó bằng một giá trị mặc định nào đó.
Trong ví dụ chúng ta dùng toán tử replaceError
để thay thế error phát sinh bằng một array rỗng.
5. Group multiple requests
Thực tế, chúng ta phải gọi một lúc nhiều API trong cùng một ViewController. Cách giải quyết vấn đề này thì có rất nhiều cách. Lần này, mình giới thiệu một giải pháp đơn giản nhất.
Giả sử có 2 API liên quan tới Post và Todo. Chúng ta lại tạo các Publisher tương tứng như sau:
let url1 = URL(string: "posts")! let url2 = URL(string: "todos")! let publisher1 = URLSession.shared.dataTaskPublisher(for: url1) .map { $0.data } .decode(type: [Post].self, decoder: JSONDecoder()) let publisher2 = URLSession.shared.dataTaskPublisher(for: url2) .map { $0.data } .decode(type: [Todo].self, decoder: JSONDecoder())
Chọn phương pháp là Publishers.Zip
để sử dụng việc nhóm các Publisher trên lại với nhau.
self.cancellable = Publishers.Zip(publisher1, publisher2) .eraseToAnyPublisher() .catch { _ in Just(([], [])) } .sink(receiveValue: { posts, todos in print(posts.count) print(todos.count) })
Ưu điểm việc này là khi nào nhận được đầy đủ dữ liệu của cả 2 Publisher, thì zip
sẽ phát về 1 lúc. Nên yên tâm là lúc nhận được giá trị, thì có nghĩa mọi connect đã hoàn thành.
6. Multiple subscribers
Vấn đề tiếp theo chắc bạn cũng gặp nhiều lần rồi. Là có 1 cái link mà phải gọi đi gọi lại nhiều lần. Như vậy sẽ tốn tài nguyên và bộ nhớ của máy.
- Toán tử
share()
có thể giải quyết nó. Nhưng bạn cần phải hoàn thành và setup xong tất cả các subscriber trước khi nhận được data. Điều này thì khá hên xui đó à.
Giải pháp ở đây sử dụng toán tử multicast
- Lưu trữ lại
- Tạo ra 1 subject
ConnectablePublisher
, để phát ra giá trị - Cho phép nhiều subscriber
subscribe
vào trước khi gọiconnect
Xem code ví dụ sau:
guard let url = URL(string: "xxxxxx") else { return } let publisher = URLSession.shared .dataTaskPublisher(for: url) .map(\.data) .multicast { PassthroughSubject<Data, URLError>() } //1 publisher .sink(receiveCompletion: { completion in if case .failure(let err) = completion { print("Sink1 Retrieving data failed with error \(err)") } }, receiveValue: { object in print("Sink1 Retrieved object \(object)") }) .store(in: &subscriptions) //2 publisher .sink(receiveCompletion: { completion in if case .failure(let err) = completion { print("Sink2 Retrieving data failed with error \(err)") } }, receiveValue: { object in print("Sink2 Retrieved object \(object)") }) .store(in: &subscriptions) // connect publisher.connect().store(in: &subscriptions)
Trong đó:
multicast
sẽ biến đổi cái gì đó thành 1 subject với kiểu làPassthroughSubject<Data, URLError>
- Tiến hành nhiều
subscribe
tới subject connect
sau khi đã setup xong hết các subscriber
7. Function for Networking
Các phần trên là nêu ra nhiều vấn đề bạn gặp phải trong quá trình xử lý tương tác Networking. Và bạn không thể khai báo hết tất cả trong 1 đoạn code, code chạy từ trên xuống dưới như thế… Cần phải tách function riêng ra, với mục đích thực hiện việc connect.
Việc đầu tiên bạn cần viết là 1 function với giá trị trả về là 1 Publisher.
func getPosts() -> AnyPublisher<[Post], Error> { let url = URL(string: "xxxxxx")! return URLSession.shared.dataTaskPublisher(for: url) .map { $0.data } .eraseToAnyPublisher() }
Chỉ cần như trên là ổn rồi. Bạn có đầy đủ các thành phần cần thiết:
- Return về 1 Publisher với
- Output chính là kiểu dữ liệu bạn mong muốn nhận được
- Error là kiểu lỗi của bạn, có thể dùng của hệ thống hoặc của riêng bạn
- URL là cái cần thiết để trỏ tới server
- Return về Publisher bằng toán tử
dataTaskPublisher
của URLSession - Phải xoá hết dấu vết bằng toán tử
eraseToAnyPublisher
Tiếp theo là bạn thêm các xử lý khác đã trình bày tại mục Error Handling.
Và cuối cùng chọn 1 trong 2 cách subscribe (sink
hay assign
). Xem ví dụ với sink
:
let publisher = getPosts() publisher .sink(receiveCompletion: { completion in switch completion { case .finished: break case .failure(let error): fatalError(error.localizedDescription) } }, receiveValue: { posts in print(posts.count) }) .store(in: &subscriptions)
Bạn thấy nó quen không nào. Vẫn như các cách subscribe ở các bài trước.
OKAY! Tới đây, mình xin kết thúc phần Networking với Combine. Tuỳ thuộc vào yêu cầu mà bạn nên sử dụng các toán tử sao cho hợp lý.
Tạm kết:
- Cách tương tác Networking với URLSession bằng Combine Code
- Quản lý Error
- Parse dữ liệu nhận được
- 2 cách subsription cho DataTaskPublisher
- Nhóm các requests
- Xử lý nhiều lần gọi cùng 1 API
- Viết function cho việc tương tác API (đơn giản nhất). Tạo tiền đề cho việc viết Model.
Cảm ơn bạn đã đọc bài viết này!
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
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)