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ầndatatừ 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ỗngeraseToAnyPublisherxoá 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:
Errormang ý nghĩa chung chung. Không xác định rõ thuộc đối tượng nào.URLErrorlà error liên quan tới các URL và việc connection của URLSessionJSONDecoder errorliên quan tới việc phân tích dữ liệu nhận đượcCustom Errorlà 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 (
statusCodekhác 200) thì sẽthrowra 1 Error.
Bản chất của
tryMapchí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
completionfinishedlà thành côngfailurelà có lỗi
Toán tử
sinksẽ 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 assignlà
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
subscribevà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 đó:
multicastsẽ biến đổi cái gì đó thành 1 subject với kiểu làPassthroughSubject<Data, URLError>- Tiến hành nhiều
subscribetới subject connectsau 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ử
dataTaskPublishercủ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
- Multi-Layer Prompt Architecture – Chìa khóa Xây dựng Hệ thống AI Phức tạp
- Khi “Prompt Template” Trở Thành Chiếc Hộp Pandora
- Vòng Lặp Ảo Giác
- Giàn Giáo Nhận Thức (Cognitive Scaffold) trong Prompt Engineering
- Bản Thể Học (Ontology) trong Prompt Engineering
- Hướng Dẫn Vibe Coding với Gemini CLI
- Prompt Bản Thể Học (Ontological Prompt) và Kiến Trúc Nhận Thức (Cognitive Architecture Prompt) trong AI
- Prompt for Coding – Code Translation Nâng Cao & Đối Phó Rủi Ro và Đảm Bảo Chất Lượng
- Tại sao cần các Chiến Lược Quản Lý Ngữ Cảnh khi tương tác với LLMs thông qua góc nhìn AI API
- Prompt for Coding – Code Translation với Kỹ thuật Exemplar Selection (k-NN)
Archives
- October 2025 (1)
- September 2025 (4)
- August 2025 (5)
- July 2025 (10)
- June 2025 (1)
- May 2025 (2)
- April 2025 (1)
- March 2025 (8)
- January 2025 (7)
- December 2024 (4)
- 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)

