diff --git a/solutions/devsprint-matheus-reis-7/FinanceApp.xcodeproj/project.pbxproj b/solutions/devsprint-matheus-reis-7/FinanceApp.xcodeproj/project.pbxproj index 6fd5d08..58abafd 100644 --- a/solutions/devsprint-matheus-reis-7/FinanceApp.xcodeproj/project.pbxproj +++ b/solutions/devsprint-matheus-reis-7/FinanceApp.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 82B6E0630247A13D8D0E4307 /* Pods_FinanceAppTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 566CD2116F7311F978A11179 /* Pods_FinanceAppTests.framework */; }; + 88C5FF2129BAD0590053F63E /* NetworkServiceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C5FF2029BAD0590053F63E /* NetworkServiceError.swift */; }; 93FE75D367A7C9491254C0C7 /* Pods_FinanceApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8FAC3BBBE235ADA9693D0DFD /* Pods_FinanceApp.framework */; }; 98426FAA27A6EC8700B09645 /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98426FA927A6EC8700B09645 /* TabBarController.swift */; }; 98426FAC27A6FC8000B09645 /* UserProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98426FAB27A6FC8000B09645 /* UserProfileHeaderView.swift */; }; @@ -68,6 +69,7 @@ 15252E9092F56A7C7576698E /* Pods-FinanceAppTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FinanceAppTests.release.xcconfig"; path = "Target Support Files/Pods-FinanceAppTests/Pods-FinanceAppTests.release.xcconfig"; sourceTree = ""; }; 3534E844457A0F6B0A6158FC /* Pods-FinanceApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FinanceApp.debug.xcconfig"; path = "Target Support Files/Pods-FinanceApp/Pods-FinanceApp.debug.xcconfig"; sourceTree = ""; }; 566CD2116F7311F978A11179 /* Pods_FinanceAppTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FinanceAppTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 88C5FF2029BAD0590053F63E /* NetworkServiceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkServiceError.swift; sourceTree = ""; }; 8FAC3BBBE235ADA9693D0DFD /* Pods_FinanceApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FinanceApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 98426FA927A6EC8700B09645 /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; 98426FAB27A6FC8000B09645 /* UserProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileHeaderView.swift; sourceTree = ""; }; @@ -250,6 +252,7 @@ isa = PBXGroup; children = ( 98584AC2277E42E80028DBEA /* FinanceService.swift */, + 88C5FF2029BAD0590053F63E /* NetworkServiceError.swift */, 98584AC1277E42CB0028DBEA /* Entities */, ); path = Service; @@ -602,6 +605,7 @@ 98584AF3277E50430028DBEA /* ConfirmationView.swift in Sources */, 98D786222909BE8C0076214A /* UITableViewCell+Extensions.swift in Sources */, 98584AEE277E50430028DBEA /* UserProfileViewController.swift in Sources */, + 88C5FF2129BAD0590053F63E /* NetworkServiceError.swift in Sources */, 98584A6D277E32C30028DBEA /* AppDelegate.swift in Sources */, 98584A6F277E32C30028DBEA /* SceneDelegate.swift in Sources */, 98426FAC27A6FC8000B09645 /* UserProfileHeaderView.swift in Sources */, diff --git a/solutions/devsprint-matheus-reis-7/FinanceApp/Modules/ActivityDetails/ActivityDetailsView.swift b/solutions/devsprint-matheus-reis-7/FinanceApp/Modules/ActivityDetails/ActivityDetailsView.swift index b0e5610..20bd00a 100644 --- a/solutions/devsprint-matheus-reis-7/FinanceApp/Modules/ActivityDetails/ActivityDetailsView.swift +++ b/solutions/devsprint-matheus-reis-7/FinanceApp/Modules/ActivityDetails/ActivityDetailsView.swift @@ -9,27 +9,25 @@ import Foundation import UIKit protocol ActivityDetailsViewDelegate: AnyObject { - func didPressReportButton() } -class ActivityDetailsView: UIView { +final class ActivityDetailsView: UIView { + // MARK: - Delegate weak var delegate: ActivityDetailsViewDelegate? - let stackView: UIStackView = { - + // MARK: - UIView properties + private let stackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.spacing = 8 stackView.distribution = .fill - return stackView }() - let imageView: UIImageView = { - + private let imageView: UIImageView = { let imageView = UIImageView() imageView.image = UIImage(named: "bag.circle.fill") imageView.layer.cornerRadius = 50 @@ -37,48 +35,38 @@ class ActivityDetailsView: UIView { return imageView }() - let activityNameLabel: UILabel = { - + private let activityNameLabel: UILabel = { let label = UILabel() - label.text = "Mall" label.textAlignment = .center label.font = UIFont.boldSystemFont(ofSize: 17) return label }() - let categoryLabel: UILabel = { - + private let categoryLabel: UILabel = { let label = UILabel() - label.text = "Shopping" label.textAlignment = .center return label }() - let priceContainerView: UIView = { - + private let priceContainerView: UIView = { let view = UIView() return view }() - let priceLabel: UILabel = { - + private let priceLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.text = "$100" label.font = UIFont.boldSystemFont(ofSize: 34) return label }() - let timeLabel: UILabel = { - + private let timeLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.text = "8:57 AM" return label }() lazy var reportIssueButton: UIButton = { - let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("Report a issue", for: .normal) @@ -89,10 +77,34 @@ class ActivityDetailsView: UIView { return button }() - + // MARK: - Initializer init() { super.init(frame: .zero) + setup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Protocol conformance + func show(viewModel: ActivityDetails) { + activityNameLabel.text = viewModel.name + categoryLabel.text = viewModel.category + priceLabel.text = String(viewModel.price) + timeLabel.text = viewModel.time + } + @objc + func reportButtonPressed() { + delegate?.didPressReportButton() + } + +} + +// TODO: Implement viewCodable procotol +private extension ActivityDetailsView { + private func setup() { backgroundColor = .white priceContainerView.addSubview(priceLabel) @@ -125,14 +137,4 @@ class ActivityDetailsView: UIView { reportIssueButton.heightAnchor.constraint(equalToConstant: 56) ]) } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc - func reportButtonPressed() { - - delegate?.didPressReportButton() - } } diff --git a/solutions/devsprint-matheus-reis-7/FinanceApp/Modules/ActivityDetails/ActivityDetailsViewController.swift b/solutions/devsprint-matheus-reis-7/FinanceApp/Modules/ActivityDetails/ActivityDetailsViewController.swift index 63fef8f..b5b8bae 100644 --- a/solutions/devsprint-matheus-reis-7/FinanceApp/Modules/ActivityDetails/ActivityDetailsViewController.swift +++ b/solutions/devsprint-matheus-reis-7/FinanceApp/Modules/ActivityDetails/ActivityDetailsViewController.swift @@ -6,28 +6,82 @@ // import UIKit +import RxCocoa +import RxSwift -class ActivityDetailsViewController: UIViewController { - - lazy var activityDetailsView: ActivityDetailsView = { +final class ActivityDetailsViewController: UIViewController { + // MARK: - UIView properties + private lazy var activityDetailsView: ActivityDetailsView = { let activityDetailsView = ActivityDetailsView() activityDetailsView.delegate = self return activityDetailsView }() + // MARK: - DataSource / UseCase dependencies + private let service: FinanceService + + // MARK: - Reactive Properties + private let disposeBag = DisposeBag() + private let activityDetailsObservable = BehaviorRelay( + value: .init(name: "", + price: 0, + category: "", + time: "") + ) + + // MARK: - Initializers + init(service: FinanceService = FinanceService()) { + self.service = service + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { nil } + + // MARK: - UIViewController lifecycle methods override func loadView() { self.view = activityDetailsView } + + override func viewDidLoad() { + bindObservables() + fetchDataView() + } +} + +private extension ActivityDetailsViewController { + + func fetchDataView() { + service.fetchActivityDetails().subscribe(onSuccess: { [weak self] activityDetails in + guard let self = self else { return } + self.activityDetailsObservable.accept(activityDetails) + }, onFailure: { failure in + print("failure:", failure.localizedDescription) + }).disposed(by: disposeBag) + } + +} + +private extension ActivityDetailsViewController { + + func bindObservables() { + activityDetailsObservable + .asDriver() + .drive { [weak self] activityDetails in + guard let self = self else { return } + self.activityDetailsView.show(viewModel: activityDetails) + }.disposed(by: disposeBag) + } + } extension ActivityDetailsViewController: ActivityDetailsViewDelegate { func didPressReportButton() { - let alertViewController = UIAlertController(title: "Report an issue", message: "The issue was reported", preferredStyle: .alert) let action = UIAlertAction(title: "Thanks", style: .default) alertViewController.addAction(action) self.present(alertViewController, animated: true) } + } diff --git a/solutions/devsprint-matheus-reis-7/FinanceApp/Service/FinanceService.swift b/solutions/devsprint-matheus-reis-7/FinanceApp/Service/FinanceService.swift index b378885..b9dae7f 100644 --- a/solutions/devsprint-matheus-reis-7/FinanceApp/Service/FinanceService.swift +++ b/solutions/devsprint-matheus-reis-7/FinanceApp/Service/FinanceService.swift @@ -7,9 +7,31 @@ import Foundation import Combine +import RxSwift -class FinanceService { +enum FinanceServiceEndpoints { + case activityDetails + var urlString: String { + switch self { + case .activityDetails: + return "https://raw.githubusercontent.com/devpass-tech/challenge-viewcode-finance/10ce6c2e9c88199ad8c4e721212099f55b26dfbb/api/activity_details_endpoint.json" + } + } +} + +final class FinanceService { + + private let urlSession: URLSession + private let jsonDecoder: JSONDecoder + + init(urlSession: URLSession = URLSession.shared, + jsonDecoder: JSONDecoder = JSONDecoder()) { + self.urlSession = urlSession + self.jsonDecoder = jsonDecoder + } + + // MARK: - fetchHomeData func fetchHomeData() -> AnyPublisher { let url = URL(string: "https://raw.githubusercontent.com/devpass-tech/challenge-finance-app/main/api/home_endpoint.json")! let decoder = JSONDecoder() @@ -20,36 +42,7 @@ class FinanceService { .eraseToAnyPublisher() } - func fetchActivityDetails(_ completion: @escaping (ActivityDetails?) -> Void) { - - let url = URL(string: "https://raw.githubusercontent.com/devpass-tech/challenge-finance-app/main/api/activity_details_endpoint.json")! - - let dataTask = URLSession.shared.dataTask(with: url) { data, response, error in - - guard error == nil else { - completion(nil) - return - } - - guard let data = data else { - completion(nil) - return - } - - do { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - let activityDetails = try decoder.decode(ActivityDetails.self, from: data) - completion(activityDetails) - } catch { - print(error) - completion(nil) - } - } - - dataTask.resume() - } - + // MARK: - fetchContactList func fetchContactList(_ completion: @escaping ([Contact]?) -> Void) { let url = URL(string: "https://raw.githubusercontent.com/devpass-tech/challenge-finance-app/main/api/contact_list_endpoint.json")! @@ -80,6 +73,7 @@ class FinanceService { dataTask.resume() } + // MARK: - transferAmount func transferAmount(_ completion: @escaping (TransferResult?) -> Void) { let url = URL(string: "https://raw.githubusercontent.com/devpass-tech/challenge-finance-app/main/api/transfer_successful_endpoint.json")! @@ -121,3 +115,42 @@ class FinanceService { } } + +// MARK: - fetchActivityDetails +extension FinanceService { + func fetchActivityDetails() -> Single { + return Single.create { single in + + let urlInString = FinanceServiceEndpoints.activityDetails.urlString + guard let url = URL(string: urlInString) else { + single(.failure(NetworkServiceError.invalidURL)) + return Disposables.create() + } + + let dataTask = self.urlSession.dataTask(with: url) { data, response, error in + + if let error = error { + single(.failure(error)) + return + } + + guard let data = data else { + single(.failure(NetworkServiceError.noData)) + return + } + + do { + self.jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase + let activityDetails = try self.jsonDecoder.decode(ActivityDetails.self, from: data) + single(.success(activityDetails)) + } catch { + single(.failure(NetworkServiceError.decodeError)) + } + } + + dataTask.resume() + + return Disposables.create { dataTask.cancel() } + } + } +} diff --git a/solutions/devsprint-matheus-reis-7/FinanceApp/Service/NetworkServiceError.swift b/solutions/devsprint-matheus-reis-7/FinanceApp/Service/NetworkServiceError.swift new file mode 100644 index 0000000..e25fd7c --- /dev/null +++ b/solutions/devsprint-matheus-reis-7/FinanceApp/Service/NetworkServiceError.swift @@ -0,0 +1,34 @@ +// +// NetworkServiceError.swift +// FinanceApp +// +// Created by Renato F. dos Santos Jr on 09/03/23. +// + +import Foundation + +enum NetworkServiceError: Error { + case decodeError + case noData + case invalidURL + case invalidStatusCode + case networkError +} + +extension NetworkServiceError: LocalizedError { + + var errorDescription: String? { + switch self { + case .decodeError: + return "Error during data decoding" + case .noData: + return "Data error" + case .invalidURL: + return "Invalid URL" + case .invalidStatusCode: + return "Invalid status code" + case .networkError: + return "An error has occurred. Please verify your connection." + } + } +}