diff --git a/TemplateProject.xcodeproj/project.pbxproj b/TemplateProject.xcodeproj/project.pbxproj index bfd258d..bdb9b81 100644 --- a/TemplateProject.xcodeproj/project.pbxproj +++ b/TemplateProject.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 14008A9E20193EB800F84447 /* CurrencyDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14008A9D20193EB800F84447 /* CurrencyDataSource.swift */; }; 141895B82023B4BC0029E238 /* CurrencyDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141895B72023B4BC0029E238 /* CurrencyDataSourceTests.swift */; }; 141895BA2023BBF70029E238 /* CurrencyServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141895B92023BBF70029E238 /* CurrencyServiceTests.swift */; }; - 141895BC2023C82B0029E238 /* sample.json in Resources */ = {isa = PBXBuildFile; fileRef = 141895BB2023C82B0029E238 /* sample.json */; }; 141895BE2023C8770029E238 /* CurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 141895BD2023C8770029E238 /* CurrencyTests.swift */; }; 142350DC2009668500930F1D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142350DB2009668500930F1D /* AppDelegate.swift */; }; 142350DE2009668500930F1D /* CurrencyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 142350DD2009668500930F1D /* CurrencyViewController.swift */; }; @@ -18,6 +17,8 @@ 142350E32009668500930F1D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 142350E22009668500930F1D /* Assets.xcassets */; }; 142350E62009668500930F1D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 142350E42009668500930F1D /* LaunchScreen.storyboard */; }; 1456E4B5200A7F2200DCF2AF /* DynamicValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1456E4B4200A7F2200DCF2AF /* DynamicValue.swift */; }; + 148CF5E6220A8EEA001DE84B /* FileDataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148CF5E5220A8EEA001DE84B /* FileDataService.swift */; }; + 148CF5E7220A8F0B001DE84B /* sample.json in Resources */ = {isa = PBXBuildFile; fileRef = 141895BB2023C82B0029E238 /* sample.json */; }; 14917E0C200A489A00234393 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14917E0B200A489A00234393 /* Result.swift */; }; 14917E0E200A48AB00234393 /* ErrorResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14917E0D200A48AB00234393 /* ErrorResult.swift */; }; 14917E11200A491C00234393 /* RequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14917E10200A491C00234393 /* RequestFactory.swift */; }; @@ -59,6 +60,7 @@ 142350EC2009668500930F1D /* TemplateProjectTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TemplateProjectTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 142350F22009668500930F1D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 1456E4B4200A7F2200DCF2AF /* DynamicValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicValue.swift; sourceTree = ""; }; + 148CF5E5220A8EEA001DE84B /* FileDataService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FileDataService.swift; path = ../../../../../Desktop/FileDataService.swift; sourceTree = ""; }; 14917E0B200A489A00234393 /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 14917E0D200A48AB00234393 /* ErrorResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorResult.swift; sourceTree = ""; }; 14917E10200A491C00234393 /* RequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestFactory.swift; sourceTree = ""; }; @@ -165,6 +167,7 @@ 14917E0F200A490400234393 /* Network */, 14917E0A200A488D00234393 /* Result */, 14917E24200A537600234393 /* CurrencyService.swift */, + 148CF5E5220A8EEA001DE84B /* FileDataService.swift */, ); path = Service; sourceTree = ""; @@ -298,6 +301,7 @@ buildActionMask = 2147483647; files = ( 142350E62009668500930F1D /* LaunchScreen.storyboard in Resources */, + 148CF5E7220A8F0B001DE84B /* sample.json in Resources */, 142350E32009668500930F1D /* Assets.xcassets in Resources */, 142350E12009668500930F1D /* Main.storyboard in Resources */, ); @@ -307,7 +311,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 141895BC2023C82B0029E238 /* sample.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -318,6 +321,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 148CF5E6220A8EEA001DE84B /* FileDataService.swift in Sources */, 14917E19200A508700234393 /* CurrencyCell.swift in Sources */, 14917E14200A4ECD00234393 /* Currency.swift in Sources */, 14917E23200A530800234393 /* ParserHelper.swift in Sources */, diff --git a/TemplateProject/Model/DynamicValue.swift b/TemplateProject/Model/DynamicValue.swift index 3b73663..0e1e9f0 100644 --- a/TemplateProject/Model/DynamicValue.swift +++ b/TemplateProject/Model/DynamicValue.swift @@ -8,9 +8,11 @@ import Foundation -typealias CompletionHandler = (() -> Void) + class DynamicValue { + typealias CompletionHandler = ((T) -> Void) + var value : T { didSet { self.notify() @@ -33,7 +35,7 @@ class DynamicValue { } private func notify() { - observers.forEach({ $0.value() }) + observers.forEach({ $0.value(value) }) } deinit { diff --git a/TemplateProject/Service/FileDataService.swift b/TemplateProject/Service/FileDataService.swift new file mode 100644 index 0000000..80ac30a --- /dev/null +++ b/TemplateProject/Service/FileDataService.swift @@ -0,0 +1,42 @@ +// +// File.swift +// TemplateProject +// +// Created by Benoit PASQUIER on 02/02/2019. +// Copyright © 2019 Benoit PASQUIER. All rights reserved. +// + +import Foundation + +final class FileDataService : CurrencyServiceProtocol { + + func fetchConverter(_ completion: @escaping ((Result) -> Void)) { + + // giving a sample json file + guard let data = FileManager.readJson(forResource: "sample") else { + completion(Result.failure(ErrorResult.custom(string: "No file or data"))) + return + } + + ParserHelper.parse(data: data, completion: completion) + } +} + + +extension FileManager { + + static func readJson(forResource fileName: String ) -> Data? { + + let bundle = Bundle(for: FileDataService.self) + if let path = bundle.path(forResource: fileName, ofType: "json") { + do { + let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) + return data + } catch { + // handle error + } + } + + return nil + } +} diff --git a/TemplateProject/View/CurrencyViewController.swift b/TemplateProject/View/CurrencyViewController.swift index 1e4e022..85c8110 100644 --- a/TemplateProject/View/CurrencyViewController.swift +++ b/TemplateProject/View/CurrencyViewController.swift @@ -25,10 +25,18 @@ class CurrencyViewController: UIViewController { self.title = "£ Exchange rate" self.tableView.dataSource = self.dataSource - self.dataSource.data.addAndNotify(observer: self) { [weak self] in + self.dataSource.data.addAndNotify(observer: self) { [weak self] _ in self?.tableView.reloadData() } + // add error handling example + self.viewModel.onErrorHandling = { [weak self] error in + // display error ? + let controller = UIAlertController(title: "An error occured", message: "Oops, something went wrong!", preferredStyle: .alert) + controller.addAction(UIAlertAction(title: "Close", style: .cancel, handler: nil)) + self?.present(controller, animated: true, completion: nil) + } + self.viewModel.fetchCurrencies() } } diff --git a/TemplateProject/ViewModel/CurrencyViewModel.swift b/TemplateProject/ViewModel/CurrencyViewModel.swift index 0467146..16d0d4d 100644 --- a/TemplateProject/ViewModel/CurrencyViewModel.swift +++ b/TemplateProject/ViewModel/CurrencyViewModel.swift @@ -13,33 +13,27 @@ struct CurrencyViewModel { weak var dataSource : GenericDataSource? weak var service: CurrencyServiceProtocol? - init(service: CurrencyServiceProtocol = CurrencyService.shared, dataSource : GenericDataSource?) { + var onErrorHandling : ((ErrorResult?) -> Void)? + + init(service: CurrencyServiceProtocol = FileDataService.shared, dataSource : GenericDataSource?) { self.dataSource = dataSource self.service = service } - func fetchCurrencies(_ completion: ((Result) -> Void)? = nil) { + func fetchCurrencies() { guard let service = service else { - completion?(Result.failure(ErrorResult.custom(string: "Missing service"))) + onErrorHandling?(ErrorResult.custom(string: "Missing service")) return } service.fetchConverter { result in - DispatchQueue.main.async { switch result { case .success(let converter) : - // reload data self.dataSource?.data.value = converter.rates - completion?(Result.success(true)) - - break case .failure(let error) : - print("Parser error \(error)") - completion?(Result.failure(error)) - - break + self.onErrorHandling?(error) } } } diff --git a/TemplateProjectTests/CurrencyTests.swift b/TemplateProjectTests/CurrencyTests.swift index 00d59ce..df4f4db 100644 --- a/TemplateProjectTests/CurrencyTests.swift +++ b/TemplateProjectTests/CurrencyTests.swift @@ -71,21 +71,3 @@ class CurrencyTests: XCTestCase { } } } - -extension FileManager { - - static func readJson(forResource fileName: String ) -> Data? { - - let bundle = Bundle(for: CurrencyTests.self) - if let path = bundle.path(forResource: fileName, ofType: "json") { - do { - let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) - return data - } catch { - // handle error - } - } - - return nil - } -} diff --git a/TemplateProjectTests/CurrencyViewModelTests.swift b/TemplateProjectTests/CurrencyViewModelTests.swift index d078ab4..6fd720b 100644 --- a/TemplateProjectTests/CurrencyViewModelTests.swift +++ b/TemplateProjectTests/CurrencyViewModelTests.swift @@ -31,50 +31,53 @@ class CurrencyViewModelTests: XCTestCase { func testFetchWithNoService() { + let expectation = XCTestExpectation(description: "No service currency") + // giving no service to a view model viewModel.service = nil // expected to not be able to fetch currencies - viewModel.fetchCurrencies { result in - switch result { - case .success(_) : - XCTAssert(false, "ViewModel should not be able to fetch without service") - default: - break - } + viewModel.onErrorHandling = { error in + expectation.fulfill() } + + viewModel.fetchCurrencies() + wait(for: [expectation], timeout: 5.0) } func testFetchCurrencies() { + let expectation = XCTestExpectation(description: "Currency fetch") + // giving a service mocking currencies service.converter = Converter(base: "GBP", date: "01-01-2018", rates: []) - // expected completion to succeed - viewModel.fetchCurrencies { result in - switch result { - case .failure(_) : - XCTAssert(false, "ViewModel should not be able to fetch without service") - default: - break - } + viewModel.onErrorHandling = { _ in + XCTAssert(false, "ViewModel should not be able to fetch without service") + } + + dataSource.data.addObserver(self) { _ in + expectation.fulfill() } + + viewModel.fetchCurrencies() + wait(for: [expectation], timeout: 5.0) } func testFetchNoCurrencies() { + let expectation = XCTestExpectation(description: "No currency") + // giving a service mocking error during fetching currencies service.converter = nil // expected completion to fail - viewModel.fetchCurrencies { result in - switch result { - case .success(_) : - XCTAssert(false, "ViewModel should not be able to fetch ") - default: - break - } + viewModel.onErrorHandling = { error in + expectation.fulfill() } + + viewModel.fetchCurrencies() + wait(for: [expectation], timeout: 5.0) } }