Skip to content

Commit

Permalink
Refactor some code
Browse files Browse the repository at this point in the history
  • Loading branch information
Mehran Kamalifard authored and Mehran Kamalifard committed Aug 11, 2024
1 parent cd15758 commit 08e64c4
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 235 deletions.
18 changes: 11 additions & 7 deletions EasyCrypto/Core/Networking/Client/NetworkClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ final class NetworkClient: NetworkClientProtocol {

/// Initializes a new URL Session Client.
///
/// - parameter urlSession: The URLSession to use.
/// Default: `URLSession(configuration: .shared)`.
/// - Parameters:
/// - session: The URLSession to use. Default: `URLSession.shared`.
/// - logging: The logging utility to use. Default: `APIDebugger()`.
///
let session: URLSession
let logging: Logging

init(session: URLSession = .shared, loggin: Logging = APIDebugger()) {
init(session: URLSession = .shared, logging: Logging = APIDebugger()) {
self.session = session
self.logging = loggin
self.logging = logging
}

@discardableResult
Expand All @@ -37,15 +38,19 @@ final class NetworkClient: NetworkClientProtocol {
}
.decode(type: type.self, decoder: decoder)
.mapError { error in
// Improved error handling and logging
if let decodingError = error as? DecodingError {
print("Decoding error: \(decodingError)")
}
return error as? APIError ?? .general
}
.eraseToAnyPublisher()
}

func publisher(request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), APIError> {
private func publisher(request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), APIError> {
return self.session.dataTaskPublisher(for: request)
.mapError { APIError.urlError($0) }
.map { response -> AnyPublisher<(data: Data, response: URLResponse), APIError> in
.flatMap { response -> AnyPublisher<(data: Data, response: URLResponse), APIError> in
self.logging.logResponse(response: response.response, data: response.data)
guard let httpResponse = response.response as? HTTPURLResponse else {
return Fail(error: APIError.invalidResponse(httpStatusCode: 0))
Expand All @@ -62,7 +67,6 @@ final class NetworkClient: NetworkClientProtocol {
.setFailureType(to: APIError.self)
.eraseToAnyPublisher()
}
.switchToLatest()
.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ private struct HTTPHeader {
}

struct HttpRequest: RequestBuilder {

var baseURL: BaseURLType

var version: VersionType

var path: String?

var methodType: HTTPMethod

var queryParams: [String: String]?

var queryParamsEncoding: URLEncoding?
var headers: [String: String]?
var parameters: [String: Any]?
var bodyEncoding: BodyEncoding?
var cachePolicy: URLRequest.CachePolicy?
var timeoutInterval: TimeInterval?

init(request: NetworkTarget) {
self.baseURL = request.baseURL
Expand All @@ -40,94 +40,63 @@ struct HttpRequest: RequestBuilder {
self.queryParamsEncoding = request.queryParamsEncoding
}

var pathAppendedURL: URL {
internal var pathAppendedURL: URL {
var url = baseURL.desc
url.appendPathComponent(version.desc)
url.appendPathComponent(path ?? .empty)
if let path = path {
url.appendPathComponent(path)
}
return url
}

func setQueryTo(urlRequest: inout URLRequest,
urlEncoding: URLEncoding,
queryParams: [String: String]) {
guard let url = urlRequest.url else {
return
}
var urlComponents = URLComponents.init(url: url, resolvingAgainstBaseURL: false)
switch urlEncoding {
internal func setQuery(to urlRequest: inout URLRequest) {
guard let url = urlRequest.url else { return }
var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)

switch queryParamsEncoding {
case .default:
urlComponents?.queryItems = [URLQueryItem]()
for (name, value) in queryParams {
urlComponents?.queryItems?.append(URLQueryItem.init(name: name, value: value))
}
urlRequest.url = urlComponents?.url
urlComponents?.queryItems = queryParams?.map { URLQueryItem(name: $0.key, value: $0.value) }
case .percentEncoded:
urlComponents?.percentEncodedQueryItems = [URLQueryItem]()
for (name, value) in queryParams {
let encodedName = name.addingPercentEncoding(withAllowedCharacters: .nkURLQueryAllowed) ?? name
let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .nkURLQueryAllowed) ?? value
let queryItem = URLQueryItem.init(name: encodedName, value: encodedValue)
urlComponents?.percentEncodedQueryItems?.append(queryItem)
urlComponents?.percentEncodedQueryItems = queryParams?.map {
URLQueryItem(name: $0.key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? $0.key,
value: $0.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? $0.value)
}
urlRequest.url = urlComponents?.url
// Applicable for PUT and POST method.
// When queryParamsEncoding is xWWWFormURLEncoded,
// All query parameters are sent inside body.
case .xWWWFormURLEncoded:
if let queryParamsData = self.queryParams?.urlEncodedQueryParams().data(using: .utf8) {
if let queryParamsData = queryParams?.urlEncodedQueryParams().data(using: .utf8) {
urlRequest.httpBody = queryParamsData
urlRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: HTTPHeader.contentType)
}
default:
break
}

urlRequest.url = urlComponents?.url
}

func encodedBody(bodyEncoding: BodyEncoding,
requestBody: [String: Any]) -> Data? {
internal func encodedBody() -> Data? {
guard let bodyEncoding = bodyEncoding else { return nil }

switch bodyEncoding {
case .JSON:
do {
return try JSONSerialization.data(withJSONObject: requestBody)
} catch {
return nil
}
return try? JSONSerialization.data(withJSONObject: parameters ?? [:])
case .xWWWFormURLEncoded:
do {
return try requestBody.urlEncodedBody()
} catch {
return nil
}
return try? parameters?.urlEncodedBody()
}
}

func buildURLRequest() -> URLRequest {
let url = self.pathAppendedURL
// prepare a url request
var urlRequest = URLRequest(url: url)
// set method for request
urlRequest.httpMethod = self.methodType.name
// set requestHeaders for request
urlRequest.allHTTPHeaderFields = self.headers

// set query parameters for request
if let queryParams = self.queryParams, !queryParams.isEmpty,
let queryParamsEncoding = self.queryParamsEncoding {
self.setQueryTo(urlRequest: &urlRequest,
urlEncoding: queryParamsEncoding,
queryParams: queryParams)
}
// set body for request
if let requestBody = self.parameters {
/// Encoding
if let bodyEncoding = self.bodyEncoding {
urlRequest.httpBody = self.encodedBody(bodyEncoding: bodyEncoding,
requestBody: requestBody)
} else {
urlRequest.httpBody = self.encodedBody(bodyEncoding: .JSON,
requestBody: requestBody)
}
var urlRequest = URLRequest(url: pathAppendedURL)
urlRequest.httpMethod = methodType.name
urlRequest.allHTTPHeaderFields = headers

if let queryParams = queryParams, !queryParams.isEmpty {
setQuery(to: &urlRequest)
}
urlRequest.cachePolicy = self.cachePolicy ?? URLRequest.CachePolicy.useProtocolCachePolicy
urlRequest.timeoutInterval = self.timeoutInterval ?? 60

urlRequest.httpBody = encodedBody()
urlRequest.cachePolicy = cachePolicy ?? .useProtocolCachePolicy
urlRequest.timeoutInterval = timeoutInterval ?? 60

return urlRequest
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import Foundation
protocol RequestBuilder: NetworkTarget {
init(request: NetworkTarget)
var pathAppendedURL: URL { get }
func setQueryTo(urlRequest: inout URLRequest,
urlEncoding: URLEncoding,
queryParams: [String: String])
func encodedBody(bodyEncoding: BodyEncoding, requestBody: [String: Any]) -> Data?
func setQuery(to urlRequest: inout URLRequest)
func encodedBody() -> Data?
func buildURLRequest() -> URLRequest
}
112 changes: 67 additions & 45 deletions EasyCrypto/Presentation/CoinDetail/View/CoinDetailHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,59 +10,81 @@ import SwiftUI
struct CoinDetailHeaderView: View {

var item: CoinUnit

var url: ((String?) -> Void)
var url: (String?) -> Void

var body: some View {
VStack(alignment: .leading, spacing: 30.0) {
HStack {
ImageDownloaderView(withURL: item.image?.safeImageURL() ?? .empty)
.frame(width: 50.0, height: 50.0)
Spacer()
CoinRankView(image: Assets.hashtag, rank: item.marketCapRank ?? 0)
CoinRankView(image: Assets.coinGeckod, rank: item.coingeckoRank ?? 0)
}
HStack {
VStack(alignment: .leading, spacing: 5) {
Text("Name")
.foregroundColor(Color.gray)
.font(FontManager.body)
Text(item.name.orWhenNilOrEmpty(.empty))
.foregroundColor(Color.white)
.font(FontManager.title)
}
Spacer()
.frame(width: 100)
VStack(alignment: .leading, spacing: 5) {
Text("Symbol")
.foregroundColor(Color.gray)
.font(FontManager.body)
Text(item.symbol.orWhenNilOrEmpty(.empty))
.foregroundColor(Color.white)
.font(FontManager.title)
}
}
headerSection
nameAndSymbolSection
linkSection
descriptionSection
}
}

private var headerSection: some View {
HStack {
ImageDownloaderView(withURL: item.image?.safeImageURL() ?? .empty)
.frame(width: 50.0, height: 50.0)
Spacer()
CoinRankView(image: Assets.hashtag, rank: item.marketCapRank ?? 0)
CoinRankView(image: Assets.coinGeckod, rank: item.coingeckoRank ?? 0)
}
}

private var nameAndSymbolSection: some View {
HStack {
VStack(alignment: .leading, spacing: 5) {
Text("Link")
.foregroundColor(Color.gray)
.font(FontManager.body)
Button {
self.url(item.links?.homepage?.first.orWhenNilOrEmpty(.empty))
} label: {
Text(item.links?.homepage?.first ?? .empty)
.foregroundColor(Color.white)
.font(FontManager.title)
}
CoinDetailLabel(title: "Name", value: item.name.orWhenNilOrEmpty(.empty))
}
Spacer()
.frame(width: 100)
VStack(alignment: .leading, spacing: 5) {
Text("Description")
.foregroundColor(Color.gray)
.font(FontManager.body)
Text(item.description?.en ?? .empty)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(Color.white)
CoinDetailLabel(title: "Symbol", value: item.symbol.orWhenNilOrEmpty(.empty))
}
}
}

private var linkSection: some View {
VStack(alignment: .leading, spacing: 5) {
Text("Link")
.foregroundColor(.gray)
.font(FontManager.body)
Button {
url(item.links?.homepage?.first.orWhenNilOrEmpty(.empty))
} label: {
Text(item.links?.homepage?.first ?? .empty)
.foregroundColor(.white)
.font(FontManager.title)
}
}
}

private var descriptionSection: some View {
VStack(alignment: .leading, spacing: 5) {
Text("Description")
.foregroundColor(.gray)
.font(FontManager.body)
Text(item.description?.en ?? .empty)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.white)
.font(FontManager.title)
}
}
}

private struct CoinDetailLabel: View {

var title: String
var value: String

var body: some View {
VStack(alignment: .leading, spacing: 5) {
Text(title)
.foregroundColor(.gray)
.font(FontManager.body)
Text(value)
.foregroundColor(.white)
.font(FontManager.title)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,22 @@ extension CoinDetailViewModel: DataFlowProtocol {
func apply(_ input: Load) {
switch input {
case .onAppear(let id):
self.getCoinDetailData(id: id)
getCoinDetailData(id: id)
}
}

func didTapFirst(url: String) {
guard let url = URL(string: url) else {return}
self.navigateSubject.send(.first(url: url))
guard let url = URL(string: url) else { return }
navigateSubject.send(.first(url: url))
}

func getCoinDetailData(id: String) {
guard !String.isNilOrEmpty(string: id) else {return}
self.call(argument: self.coinDetailUsecase.execute(id: id)) { [weak self] data in
guard let data = data else {return}
guard !id.isEmpty else { return }
isShowActivity = true
call(argument: coinDetailUsecase.execute(id: id)) { [weak self] data in
guard let data = data else { return }
self?.coinData = data
self?.isShowActivity = false
}
}
}
Loading

0 comments on commit 08e64c4

Please sign in to comment.