+1

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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí