Skip to content

Commit

Permalink
Create classes
Browse files Browse the repository at this point in the history
  • Loading branch information
ranieriAguiar committed Feb 3, 2024
1 parent a770b2a commit 7963b33
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 0 deletions.
22 changes: 22 additions & 0 deletions Example/Pods/Local Podspecs/networkingModule.podspec.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions networkingModule/Classes/Endpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// Endpoint.swift
// PipocaFlix
//
// Created by Ranieri Aguiar on 25/10/22.
//

import Foundation

enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
}

protocol Endpoint {
associatedtype Response

var url: String { get }
var method: HTTPMethod { get }
var headers: [String : String] { get }
var queryItems: [String : String] { get }

func decode(_ data: Data) throws -> Response
}

extension Endpoint where Response: Decodable {
func decode(_ data: Data) throws -> Response {
let decoder = JSONDecoder()
return try decoder.decode(Response.self, from: data)
}
}

extension Endpoint {
var headers: [String : String] {
[:]
}

var queryItems: [String : String] {
[:]
}
}
24 changes: 24 additions & 0 deletions networkingModule/Classes/ErrorResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// ErrorResponse.swift
// PipocaFlix
//
// Created by Ranieri Aguiar on 25/10/22.
//

enum ErrorResponse: String, Error {
case apiError
case invalidEndpoint
case invalidResponse
case noData
case serializationError

public var description: String {
switch self {
case .apiError: return "Ooops, there is something problem with the api"
case .invalidEndpoint: return "Ooops, there is something problem with the endpoint"
case .invalidResponse: return "Ooops, there is something problem with the response"
case .noData: return "Ooops, there is something problem with the data"
case .serializationError: return "Ooops, there is something problem with the serialization process"
}
}
}
112 changes: 112 additions & 0 deletions networkingModule/Classes/ImageDownloader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Created by Jayesh Kawli on 26/04/20. Downloaded in https://gist.github.com/jayesh15111988/b95030bca927304fc31e8cbc0123f72f

import UIKit

// Image downloader utility class. We are going to use the singleton instance to be able to download required images and store them into in-memory cache.

final class ImageDownloader {

static let shared = ImageDownloader()

private var cachedImages: [String: UIImage]
private var imagesDownloadTasks: [String: URLSessionDataTask]

// A serial queue to be able to write the non-thread-safe dictionary
let serialQueueForImages = DispatchQueue(label: "images.queue", attributes: .concurrent)
let serialQueueForDataTasks = DispatchQueue(label: "dataTasks.queue", attributes: .concurrent)

// MARK: Private init
private init() {
cachedImages = [:]
imagesDownloadTasks = [:]
}

/**
Downloads and returns images through the completion closure to the caller

- Parameter imageUrlString: The remote URL to download images from
- Parameter completionHandler: A completion handler which returns two parameters. First one is an image which may or may
not be cached and second one is a bool to indicate whether we returned the cached version or not
- Parameter placeholderImage: Placeholder image to display as we're downloading them from the server
*/
func downloadImage(with imageUrlString: String?,
completionHandler: @escaping (UIImage?, Bool) -> Void,
placeholderImage: UIImage?) {

guard let imageUrlString = imageUrlString else {
completionHandler(placeholderImage, true)
return
}

if let image = getCachedImageFrom(urlString: imageUrlString) {
completionHandler(image, true)
} else {
guard let url = URL(string: imageUrlString) else {
completionHandler(placeholderImage, true)
return
}

if let _ = getDataTaskFrom(urlString: imageUrlString) {
return
}

let task = URLSession.shared.dataTask(with: url) { (data, response, error) in

guard let data = data else {
return
}

if let _ = error {
DispatchQueue.main.async {
completionHandler(placeholderImage, true)
}
return
}

let image = UIImage(data: data)
self.serialQueueForImages.sync(flags: .barrier) {
self.cachedImages[imageUrlString] = image
}

_ = self.serialQueueForDataTasks.sync(flags: .barrier) {
self.imagesDownloadTasks.removeValue(forKey: imageUrlString)
}

DispatchQueue.main.async {
completionHandler(image, false)
}
}
// We want to control the access to no-thread-safe dictionary in case it's being accessed by multiple threads at once
self.serialQueueForDataTasks.sync(flags: .barrier) {
imagesDownloadTasks[imageUrlString] = task
}

task.resume()
}
}

private func cancelPreviousTask(with urlString: String?) {
if let urlString = urlString, let task = getDataTaskFrom(urlString: urlString) {
task.cancel()
// Since Swift dictionaries are not thread-safe, we have to explicitly set this barrier to avoid fatal error when it is accessed by multiple threads simultaneously
_ = serialQueueForDataTasks.sync(flags: .barrier) {
imagesDownloadTasks.removeValue(forKey: urlString)
}
}
}

private func getCachedImageFrom(urlString: String) -> UIImage? {
// Reading from the dictionary should happen in the thread-safe manner.
serialQueueForImages.sync {
return cachedImages[urlString]
}
}

private func getDataTaskFrom(urlString: String) -> URLSessionTask? {

// Reading from the dictionary should happen in the thread-safe manner.
serialQueueForDataTasks.sync {
return imagesDownloadTasks[urlString]
}
}
}
78 changes: 78 additions & 0 deletions networkingModule/Classes/NetworkService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// NetworkService.swift
// PipocaFlix
//
// Created by Ranieri Aguiar on 25/10/22.
//

import Foundation

protocol NetworkService {
func request<E: Endpoint>(
request: E,
completion: @escaping (Result<E.Response, Error>) -> Void
)
}

final class DefaultNetworkService: NetworkService {
func request<E: Endpoint>(
request: E,
completion: @escaping (Result<E.Response, Error>) -> Void
) {

guard var urlComponent = URLComponents(string: request.url) else {
let error = NSError(
domain: ErrorResponse.invalidEndpoint.rawValue,
code: 404,
userInfo: nil
)

return completion(.failure(error))
}

var queryItems: [URLQueryItem] = []

request.queryItems.forEach {
let urlQueryItem = URLQueryItem(name: $0.key, value: $0.value)
urlComponent.queryItems?.append(urlQueryItem)
queryItems.append(urlQueryItem)
}

urlComponent.queryItems = queryItems

guard let url = urlComponent.url else {
let error = NSError(
domain: ErrorResponse.invalidEndpoint.rawValue,
code: 404,
userInfo: nil
)

return completion(.failure(error))
}

var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = request.method.rawValue
urlRequest.allHTTPHeaderFields = request.headers

URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if let error = error {
return completion(.failure(error))
}

guard let response = response as? HTTPURLResponse, 200..<300 ~= response.statusCode else {
return completion(.failure(NSError()))
}

guard let data = data else {
return completion(.failure(NSError()))
}

do {
try completion(.success(request.decode(data)))
} catch let error as NSError {
completion(.failure(error))
}
}
.resume()
}
}
Empty file.

0 comments on commit 7963b33

Please sign in to comment.