URLSession iOS
Giới thiệu về Networking iOS:
1. Codable
Codable
trong Swift là một protocol giúp tự động hoán đổi (encode/decode) dữ liệu giữa các kiểu dữ liệu Swift và các định dạng như JSON, plist, v.v.
Nó gồm 2 protocol con:
Encodable
: cho phép chuyển đối tượng Swift thành dữ liệu (ví dụ: JSON).Decodable
: cho phép tạo đối tượng Swift từ dữ liệu.
Ví dụ:
struct User: Codable {
let name: String
let age: Int
}
Với User
, bạn có thể dễ dàng:
- Chuyển từ JSON sang
User
:let user = try JSONDecoder().decode(User.self, from: jsonData)
- Chuyển từ
User
sang JSON:let jsonData = try JSONEncoder().encode(user)
2. URLSession
import Foundation
struct MovieResponse: Codable {
let results: [Movie]
}
struct Movie: Codable {
let title: String
let overview: String
let poster_path: String?
}
func fetchPopularMovies(completion: @escaping ([Movie]) -> Void) {
let apiKey = "YOUR_TMDB_API_KEY"
let urlString = "https://api.themoviedb.org/3/movie/popular?api_key=\(apiKey)&language=en-US&page=1"
guard let url = URL(string: urlString) else {
print("❌ Invalid URL")
return
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
print("❌ Request error: \(error.localizedDescription)")
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print("❌ Server error")
return
}
guard let data = data else {
print("❌ No data received")
return
}
do {
let decoder = JSONDecoder()
let movieResponse = try decoder.decode(MovieResponse.self, from: data)
// **🧠 Update UI on main thread**
DispatchQueue.main.async {
completion(movieResponse.results)
}
} catch {
print("❌ JSON decoding error: \(error.localizedDescription)")
}
}
task.resume()
}
3. Triển khai trong dự án thực tế
Trong dự án thực tế, việc request api sẽ duplicate code nhiều, gây lãng phí thời gian, sau fix bug sẽ khó khăn hơn. Cách triển khai như sau
a. Tạo Enum để route các endpoint
import Foundation
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
}
enum TMDBEndpoint {
case popularMovies(page: Int)
case topRatedMovies(page: Int)
case searchMovies(query: String, page: Int)
case movieDetail(id: Int)
var baseURL: String {
return "https://api.themoviedb.org/3"
}
var path: String {
switch self {
case .popularMovies:
return "/movie/popular"
case .topRatedMovies:
return "/movie/top_rated"
case .searchMovies:
return "/search/movie"
case .movieDetail(let id):
return "/movie/\(id)"
}
}
var method: HTTPMethod {
return .get
}
var params: [String: String] {
var baseParams: [String: String] = [
"api_key": "YOUR_TMDB_API_KEY",
"language": "en-US"
]
switch self {
case .popularMovies(let page),
.topRatedMovies(let page):
baseParams["page"] = String(page)
case .searchMovies(let query, let page):
baseParams["query"] = query
baseParams["page"] = String(page)
case .movieDetail:
break
}
return baseParams
}
func getURL() -> URL? {
var components = URLComponents(string: baseURL + path)
components?.queryItems = params.map { URLQueryItem(name: $0.key, value: $0.value) }
return components?.url
}
}
** b. Viết NetworkManager sử dụng generic:**
import Foundation
final class NetworkManager {
static let shared = NetworkManager()
private init() {}
func request<T: Decodable>(
endpoint: TMDBEndpoint,
responseType: T.Type,
completion: @escaping (Result<T, Error>) -> Void
) {
guard let url = endpoint.getURL() else {
completion(.failure(NetworkError.invalidURL))
return
}
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
DispatchQueue.main.async {
completion(.failure(NetworkError.invalidResponse))
}
return
}
guard let data = data else {
DispatchQueue.main.async {
completion(.failure(NetworkError.noData))
}
return
}
do {
let decodedData = try JSONDecoder().decode(T.self, from: data)
DispatchQueue.main.async {
completion(.success(decodedData))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
task.resume()
}
}
// Define some simple errors
enum NetworkError: Error {
case invalidURL
case invalidResponse
case noData
}
** c. Gọi API sử dụng generic:** Ví dụ fetch popular movies:
struct MovieResponse: Decodable {
let results: [Movie]
}
struct Movie: Decodable {
let id: Int
let title: String
let overview: String
}
NetworkManager.shared.request(endpoint: .popularMovies(page: 1), responseType: MovieResponse.self) { result in
switch result {
case .success(let response):
print("🎬 Movies count: \(response.results.count)")
case .failure(let error):
print("❌ Error: \(error.localizedDescription)")
}
}
All rights reserved