-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a770b2a
commit 7963b33
Showing
6 changed files
with
280 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] { | ||
[:] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.