Chào mừng các bạn đến với một bài viết quan trọng khác trong chuỗi “Lộ trình học Lập trình viên iOS 2025“! Sau khi đã nắm vững Những Kiến Thức Swift Cơ Bản, làm quen với Xây dựng Giao diện Người dùng Cơ bản với UIKit hoặc Lập Trình Giao Diện Khai Báo với SwiftUI, hiểu về Vòng đời của ViewController, hay biết cách Lưu trữ dữ liệu cục bộ, đã đến lúc chúng ta mở rộng ứng dụng của mình ra thế giới bên ngoài: kết nối mạng (networking).
Hầu hết các ứng dụng di động hiện đại đều cần tương tác với các server từ xa để lấy dữ liệu, gửi thông tin, xác thực người dùng, v.v. Trong hệ sinh thái Apple, framework chính và mạnh mẽ nhất để thực hiện các tác vụ mạng là URLSession. Bài viết này sẽ đưa bạn đi sâu vào URLSession, từ những khái niệm cơ bản đến cách sử dụng thực tế để tạo ra các tính năng mạng hiệu quả trong ứng dụng iOS của bạn.
Mục lục
URLSession Là Gì và Tại Sao Lại Quan Trọng?
URLSession là một framework của Apple cung cấp API để tải nội dung từ các URL trên Internet. Nó là sự kế thừa mạnh mẽ và linh hoạt hơn so với API tiền nhiệm là NSURLConnection
(hiện đã lỗi thời). URLSession được giới thiệu trong iOS 7 và macOS 10.9, trở thành nền tảng cho hầu hết các tác vụ mạng trong ứng dụng Apple.
Tầm quan trọng của URLSession:
- Tiêu chuẩn của Apple: Đây là cách được Apple khuyến khích và hỗ trợ tốt nhất để thực hiện networking.
- Linh hoạt: Hỗ trợ nhiều loại tác vụ (tải xuống, tải lên, tác vụ dữ liệu), cấu hình session khác nhau (mặc định, tạm thời, chạy nền).
- Mạnh mẽ: Xử lý các tác vụ phức tạp như xác thực, quản lý cookie, bộ nhớ cache, và cho phép tùy chỉnh cấu hình mạng chi tiết.
- Hỗ trợ chạy nền: Có khả năng thực hiện các tác vụ mạng ngay cả khi ứng dụng không chạy ở foreground (với cấu hình phù hợp).
Hiểu và làm chủ URLSession là kỹ năng thiết yếu đối với bất kỳ lập trình viên iOS nào. Dù bạn có thể sử dụng các thư viện mạng của bên thứ ba như Alamofire, chúng cũng thường được xây dựng dựa trên URLSession. Việc hiểu rõ nền tảng sẽ giúp bạn debug hiệu quả hơn và tùy chỉnh khi cần thiết.
Các Thành Phần Cốt Lõi Của URLSession
URLSession hoạt động dựa trên sự tương tác của một vài đối tượng chính:
Đối Tượng | Mô Tả |
---|---|
URLSession |
Đối tượng chính quản lý các tác vụ mạng. Bạn tạo một session để thực hiện các request. Có các loại session chính: shared, default, ephemeral. |
URLSessionConfiguration |
Đối tượng định nghĩa hành vi và chính sách cho một session. Ví dụ: timeout, chính sách cache, cấu hình chạy nền. |
URLRequest |
Đối tượng biểu diễn một yêu cầu mạng cụ thể. Bao gồm URL, phương thức HTTP (GET, POST, PUT, DELETE…), headers, body data… |
URLSessionTask |
Đối tượng biểu diễn một tác vụ mạng duy nhất trong một session. Có các loại task phổ biến: dataTask (lấy dữ liệu về bộ nhớ), downloadTask (tải về file), uploadTask (tải lên file/dữ liệu). |
Tạo Request Cơ Bản Với URLSession.shared.dataTask
Cách đơn giản nhất để bắt đầu với URLSession là sử dụng URLSession.shared
. Đây là một singleton session được cấu hình mặc định, phù hợp cho các tác vụ mạng đơn giản, không yêu cầu cấu hình đặc biệt hoặc chạy nền.
Chúng ta sẽ thực hiện một request GET để lấy dữ liệu từ một API giả định:
func fetchDataFromAPI() {
// 1. Tạo URL
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1") else {
print("Error: Cannot create URL")
return
}
// 2. Tạo một Data Task
// Data task sử dụng completion handler để xử lý kết quả
let task = URLSession.shared.dataTask(with: url) { data, response, error in
// 3. Xử lý kết quả trong closure (completion handler)
// Kiểm tra lỗi mạng chung (ví dụ: không có kết nối internet)
if let error = error {
print("Error fetching data: \(error.localizedDescription)")
return
}
// Kiểm tra response và status code HTTP
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
// Xử lý các mã trạng thái lỗi HTTP (4xx, 5xx)
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
print("Error: Invalid HTTP response or status code: \(statusCode)")
return
}
// Kiểm tra dữ liệu nhận được
guard let data = data else {
print("Error: No data received")
return
}
// Dữ liệu đã sẵn sàng, bạn có thể xử lý nó ở đây.
// Lưu ý: Closure này chạy trên một background thread!
// Nếu cần cập nhật UI, phải chuyển về main thread.
print("Successfully fetched data: \(data.count) bytes")
// Ví dụ: in dữ liệu dưới dạng String (chỉ nên làm với text/JSON nhỏ)
if let dataString = String(data: data, encoding: .utf8) {
print("Received String: \(dataString)")
}
// --- CẬP NHẬT UI TRÊN MAIN THREAD (NẾU CẦN) ---
DispatchQueue.main.async {
// Cập nhật các UILabel, UIImageView, v.v... ở đây
print("Updating UI on main thread...")
// Ví dụ: self.myLabel.text = dataString
}
}
// 4. Bắt đầu task (request sẽ được thực hiện)
task.resume()
print("Data task initiated...") // Dòng này sẽ chạy ngay sau khi task.resume()
}
Giải thích từng bước:
- Chúng ta tạo một đối tượng
URL
từ String của địa chỉ API. Luôn kiểm tra kết quả vì String có thể không phải là URL hợp lệ. - Chúng ta gọi phương thức
dataTask(with:completionHandler:)
trênURLSession.shared
. Phương thức này nhận mộtURL
(hoặcURLRequest
chi tiết hơn) và mộtcompletionHandler
(một closure). Closure này sẽ được gọi khi tác vụ mạng hoàn thành (thành công hoặc thất bại). - Bên trong
completionHandler
, chúng ta nhận được ba tham số:data
(dữ liệu nhận được),response
(thông tin phản hồi từ server, thường làHTTPURLResponse
), vàerror
(nếu có lỗi xảy ra).- Đầu tiên, kiểm tra
error
để phát hiện các lỗi ở tầng mạng (ví dụ: mất kết nối). - Tiếp theo, ép kiểu
response
sangHTTPURLResponse
để truy cập các thông tin HTTP nhưstatusCode
. Mã 200-299 thường là thành công. - Cuối cùng, kiểm tra
data
để đảm bảo có dữ liệu được trả về. - Quan trọng: Mặc định,
completionHandler
được gọi trên một background thread. Nếu bạn cần cập nhật giao diện người dùng (UI) dựa trên dữ liệu nhận được, bạn phải chuyển công việc đó về main thread bằng cách sử dụngDispatchQueue.main.async
. Điều này đã được đề cập trong bài viết về Đa luồng trong Swift và Sử Dụng GCD, Hàng Đợi và Operations.
- Đầu tiên, kiểm tra
- Gọi
task.resume()
để bắt đầu thực hiện tác vụ mạng. Trước khiresume()
được gọi, task đang ở trạng thái “suspended”.
Chú ý rằng lời gọi task.resume()
là không đồng bộ (asynchronous). Điều này có nghĩa là mã nguồn của bạn sẽ tiếp tục chạy ngay sau dòng task.resume()
mà không chờ request mạng hoàn thành. Kết quả sẽ được xử lý sau đó trong completionHandler
. Hiểu rõ tính chất không đồng bộ này là chìa khóa để tránh Callback Hell và xây dựng ứng dụng mượt mà, không bị đơ khi chờ đợi kết quả mạng.
Xử Lý Dữ Liệu Với Codable
Dữ liệu nhận được từ API thường ở định dạng JSON (hoặc XML). Trong hầu hết các trường hợp, bạn sẽ muốn chuyển đổi dữ liệu JSON này thành các đối tượng Swift có cấu trúc để làm việc dễ dàng hơn. Swift cung cấp protocol Codable
(kết hợp Encodable
và Decodable
) để làm việc này một cách dễ dàng. Chúng ta đã tìm hiểu chi tiết về nó trong bài viết Phân tích JSON và XML trong Swift: Codable và Hơn thế nữa.
Hãy giả sử API trả về dữ liệu có cấu trúc như sau:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nexercitationem quos sit sint…}"
}
Chúng ta có thể tạo một struct Swift tương ứng:
struct Post: Codable {
let userId: Int
let id: Int
let title: String
let body: String
}
Bây giờ, hãy tích hợp việc phân tích JSON vào completion handler:
func fetchAndDecodePost() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1") else {
print("Error: Cannot create URL")
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("Error fetching data: \(error.localizedDescription)")
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
print("Error: Invalid HTTP response or status code: \(statusCode)")
return
}
guard let data = data else {
print("Error: No data received")
return
}
// --- PHÂN TÍCH DỮ LIỆU VỚI CODABLE ---
do {
let decoder = JSONDecoder()
// Nếu tên key trong JSON khác với tên thuộc tính trong Swift struct (ví dụ: snake_case vs camelCase),
// bạn có thể cấu hình decoding strategy:
// decoder.keyDecodingStrategy = .convertFromSnakeCase
let post = try decoder.decode(Post.self, from: data)
// Dữ liệu đã được decode thành công
print("Successfully decoded post:")
print("ID: \(post.id)")
print("Title: \(post.title)")
// ... làm việc với đối tượng 'post' ...
// Cập nhật UI trên main thread nếu cần
DispatchQueue.main.async {
// self.postTitleLabel.text = post.title
}
} catch {
// Xử lý lỗi khi decode (ví dụ: cấu trúc JSON không khớp với struct)
print("Error decoding JSON: \(error.localizedDescription)")
// Bạn có thể in chi tiết lỗi decoding cho mục đích debug:
// print("Decoding error details: \(error)")
}
}
task.resume()
}
Việc sử dụng Codable
và JSONDecoder
giúp việc phân tích JSON trở nên rất dễ dàng và an toàn, loại bỏ phần lớn mã thủ công và giảm thiểu lỗi.
Xử Lý Lỗi Mạng
Xử lý lỗi là một phần không thể thiếu của networking. Lỗi có thể xảy ra ở nhiều giai đoạn:
- Lỗi mạng chung (
URLError
): Do không có kết nối internet, server không phản hồi, timeout, v.v. Những lỗi này thường được trả về trong tham sốerror
của completion handler. - Lỗi HTTP (mã trạng thái): Server nhận request nhưng trả về mã trạng thái cho biết lỗi phía server (5xx) hoặc lỗi phía client (4xx), ví dụ 401 Unauthorized, 404 Not Found. Những lỗi này được kiểm tra thông qua
httpResponse.statusCode
. - Lỗi phân tích dữ liệu (ví dụ: khi dùng
JSONDecoder
): Dữ liệu nhận được không đúng định dạng hoặc không khớp với cấu trúc bạn mong đợi. Những lỗi này xảy ra trong khốido-catch
khi bạn cố gắng parse dữ liệu.
Như bạn đã thấy trong các ví dụ trên, chúng ta cần kiểm tra cả ba loại lỗi này một cách có hệ thống. Tham khảo bài viết Xử lý Lỗi Một Cách Duyên dáng trong Swift để hiểu thêm về các kỹ thuật quản lý lỗi.
URLRequest: Tùy Chỉnh Request HTTP
Đối với các request phức tạp hơn GET (như POST, PUT, DELETE) hoặc cần thêm headers (ví dụ: cho xác thực), bạn sẽ cần tạo một đối tượng URLRequest
thay vì chỉ sử dụng URL
đơn giản.
func postNewItem(item: [String: Any]) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
print("Error: Cannot create URL")
return
}
// 1. Tạo URLRequest
var request = URLRequest(url: url)
// 2. Cấu hình HTTP method
request.httpMethod = "POST" // Hoặc "PUT", "DELETE", v.v.
// 3. Thêm headers (nếu cần)
request.setValue("application/json", forHTTPHeaderField: "Content-Type") // Loại nội dung gửi đi
request.setValue("Bearer YOUR_AUTH_TOKEN", forHTTPHeaderField: "Authorization") // Ví dụ xác thực
// 4. Thêm body data (cho POST, PUT)
do {
// Chuyển Dictionary hoặc Codable object thành JSON Data
let jsonData = try JSONSerialization.data(withJSONObject: item, options: []) // Hoặc JSONEncoder().encode(codableObject)
request.httpBody = jsonData
} catch {
print("Error creating JSON body: \(error.localizedDescription)")
return
}
// 5. Tạo Data Task với URLRequest
let task = URLSession.shared.dataTask(with: request) { data, response, error in
// Xử lý response và error như ví dụ GET
if let error = error {
print("Error posting data: \(error.localizedDescription)")
return
}
guard let httpResponse = response as? HTTPURLResponse else {
print("Invalid response")
return
}
print("POST request finished with status code: \(httpResponse.statusCode)")
// Xử lý dữ liệu trả về từ POST (nếu có)
if let data = data {
// ... decode data ...
}
}
// 6. Bắt đầu task
task.resume()
}
// Sử dụng
// postNewItem(item: ["title": "foo", "body": "bar", "userId": 1])
Các Loại URLSession Khác
Ngoài URLSession.shared
, bạn có thể tạo các session với cấu hình tùy chỉnh bằng cách sử dụng URLSession(configuration:delegate:delegateQueue:)
:
<strong>URLSessionConfiguration.default</strong>
: Giống shared nhưng cho phép tùy chỉnh cấu hình (ví dụ: timeout, cache). Phù hợp cho các tác vụ foreground thông thường cần cấu hình riêng.<strong>URLSessionConfiguration.ephemeral</strong>
: Tương tự default nhưng không lưu trữ cache, cookie hoặc thông tin xác thực vào ổ đĩa. Mọi dữ liệu liên quan đến session sẽ bị xóa khi session kết thúc. Hữu ích cho các tác vụ yêu cầu sự riêng tư hoặc dữ liệu tạm thời.<strong>URLSessionConfiguration.background</strong>
: Cho phép thực hiện các tác vụ tải lên hoặc tải xuống ngay cả khi ứng dụng bị thoát. Yêu cầu cấu hình đặc biệt và sử dụng delegate pattern thay vì completion handler. Đây là một chủ đề nâng cao hơn.
Khi tạo session với cấu hình tùy chỉnh, bạn có thể tùy chọn cung cấp một delegate object. Delegate pattern cho phép bạn nhận được các sự kiện mạng theo thời gian thực (ví dụ: tiến trình tải xuống/tải lên, nhận phản hồi xác thực). Sử dụng delegate phức tạp hơn completion handler nhưng cung cấp quyền kiểm soát granular hơn. Chúng ta đã tìm hiểu về Delegate Pattern trong bài viết trước.
// Ví dụ tạo session với cấu hình default
// Tạo một lớp hoặc struct implement URLSessionDelegate (hoặc URLSessionDataDelegate...)
// class NetworkDelegate: NSObject, URLSessionDelegate { /* ... */ }
// let config = URLSessionConfiguration.default
// config.timeoutIntervalForRequest = 30 // Ví dụ: đặt timeout là 30 giây
// let customSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil) // delegate: nil nếu chỉ dùng completion handler
// Sử dụng customSession thay vì URLSession.shared
// let task = customSession.dataTask(with: url) { ... }
// task.resume()
Sử Dụng Async/Await Với URLSession
Với sự ra đời của async/await trong Swift, việc xử lý các tác vụ không đồng bộ như networking trở nên đơn giản và dễ đọc hơn, loại bỏ sự cần thiết của Callback Hell. URLSession đã được cập nhật để hỗ trợ async/await.
func fetchPostAsync() async throws -> Post {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts/1") else {
throw URLError(.badURL) // Ném lỗi nếu URL không hợp lệ
}
// Sử dụng data(from: delegate:) với async/await
// Lệnh này sẽ tạm dừng hàm hiện tại cho đến khi request hoàn thành
let (data, response) = try await URLSession.shared.data(from: url)
// Kiểm tra response và status code
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
// Ném lỗi với thông tin chi tiết hơn
throw URLError(.badServerResponse, userInfo: [NSLocalizedDescriptionKey: "Invalid HTTP status code: \(statusCode)"])
}
// Phân tích dữ liệu với Codable
let decoder = JSONDecoder()
// decoder.keyDecodingStrategy = .convertFromSnakeCase // Nếu cần
let post = try decoder.decode(Post.self, from: data)
// Trả về đối tượng đã decode
return post
}
// Cách gọi hàm async trong một ngữ cảnh async hoặc từ một task
// Task {
// do {
// let myPost = try await fetchPostAsync()
// print("Fetched post title asynchronously: \(myPost.title)")
// // Cập nhật UI trên main actor (async equivalent of main queue)
// await MainActor.run {
// // self.asyncPostTitleLabel.text = myPost.title
// }
// } catch {
// print("Error fetching post asynchronously: \(error.localizedDescription)")
// }
// }
Cách tiếp cận async/await thường giúp mã nguồn trông gọn gàng và tuyến tính hơn, đặc biệt khi bạn cần thực hiện nhiều request nối tiếp nhau hoặc xử lý luồng dữ liệu phức tạp.
URLSession So Với Thư Viện Bên Thứ Ba (Ví Dụ: Alamofire)
Khi mới học, bạn có thể nghe về các thư viện mạng phổ biến như Alamofire. Vậy tại sao chúng ta vẫn cần học URLSession?
- URLSession là nền tảng: Các thư viện như Alamofire được xây dựng trên URLSession. Hiểu URLSession giúp bạn hiểu cách các thư viện này hoạt động và debug khi có vấn đề.
- Không phụ thuộc bên ngoài: Sử dụng URLSession giúp giảm thiểu phụ thuộc vào các thư viện của bên thứ ba, có thể hữu ích trong các dự án yêu cầu tối giản dependency hoặc cần kiểm soát chặt chẽ.
- Tối ưu hiệu năng và tài nguyên: Vì là framework gốc của hệ thống, URLSession thường được tối ưu tốt nhất về hiệu năng và sử dụng tài nguyên.
Tuy nhiên, các thư viện như Alamofire cung cấp nhiều tiện ích giúp giảm bớt mã lặp đi lặp lại (boilerplate code), xử lý việc mapping response JSON sang object (bao gồm cả Codable), quản lý request queue, intercept request/response, hỗ trợ certificate pinning, v.v. Chúng có thể giúp tăng tốc độ phát triển đáng kể trong các dự án lớn và phức tạp.
Lời khuyên: Hãy bắt đầu bằng việc hiểu vững URLSession. Sau đó, khám phá các thư viện bên thứ ba và cân nhắc sử dụng chúng khi thấy lợi ích rõ ràng trong dự án của bạn.
Các Thực Hành Tốt Nhất (Best Practices)
- Luôn sử dụng HTTPS: Đảm bảo kết nối của bạn được mã hóa để bảo vệ dữ liệu người dùng. App Transport Security (ATS) trong iOS mặc định yêu cầu HTTPS, trừ khi bạn cấu hình ngoại lệ.
- Xử lý lỗi cẩn thận: Luôn kiểm tra cả
error
(lỗi mạng),statusCode
(lỗi HTTP), và lỗi khi parse dữ liệu. Cung cấp phản hồi hữu ích cho người dùng khi có lỗi. - Cập nhật UI trên Main Thread: Nhắc lại lần nữa vì nó rất quan trọng. Mọi thao tác cập nhật UI phải được thực hiện trên main thread.
- Quản lý Timeout: Cấu hình timeout phù hợp trong
URLSessionConfiguration
để tránh ứng dụng bị “treo” vô thời hạn khi mạng chậm hoặc server không phản hồi. - Tối ưu Request: Chỉ yêu cầu dữ liệu bạn thực sự cần. Sử dụng cache và ETag nếu API hỗ trợ để giảm tải mạng.
- Sử dụng Async/Await hoặc Combine/RxSwift: Đối với các luồng dữ liệu phức tạp hoặc nhiều request phụ thuộc, sử dụng các kỹ thuật quản lý bất đồng bộ hiện đại giúp mã nguồn dễ đọc và quản lý hơn so với Callback Hell. Chúng ta đã có bài viết về MVVM với Combine và Khám Phá RxSwift.
Kết Luận
URLSession là xương sống của networking trong các ứng dụng iOS. Việc nắm vững cách sử dụng nó không chỉ giúp bạn kết nối ứng dụng của mình với thế giới mà còn là nền tảng để hiểu sâu hơn về cách dữ liệu di chuyển và được xử lý.
Chúng ta đã đi qua các khái niệm cốt lõi: session, configuration, request, task, cách thực hiện request GET/POST cơ bản, xử lý response, phân tích JSON với Codable, xử lý lỗi, và cách tận dụng async/await. Đây là những kiến thức nền tảng bạn cần để bắt đầu xây dựng các tính năng mạng trong ứng dụng của mình.
Hãy thực hành bằng cách kết nối với các API công khai miễn phí (như JSONPlaceholder, Star Wars API – SWAPI, v.v.) để củng cố kiến thức. Networking là một kỹ năng quan trọng và việc luyện tập thường xuyên sẽ giúp bạn thành thạo hơn.
Tiếp theo trên Lộ trình học Lập trình viên iOS 2025, chúng ta sẽ tiếp tục khám phá các chủ đề khác để hoàn thiện bộ kỹ năng của bạn. Hẹn gặp lại trong các bài viết tới!