Mercurial > public > simoleon
changeset 157:8c3bbd640103
Implement Currency Selector
author | Dennis Concepcion Martin <dennisconcepcionmartin@gmail.com> |
---|---|
date | Sat, 28 Aug 2021 11:15:41 +0100 |
parents | 84137052813d |
children | 82bd84c5973c |
files | Simoleon/ConversionView.swift Simoleon/UI/CurrencyList.swift Simoleon/UI/CurrencySelector.swift SimoleonTests/SimoleonTests.swift |
diffstat | 4 files changed, 252 insertions(+), 25 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/ConversionView.swift Sat Aug 28 11:15:41 2021 +0100 @@ -0,0 +1,68 @@ +// +// ConversionView.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 18/07/2021. +// + +import SwiftUI +import Purchases + +struct ConversionView: View { + var showNavigationView: Bool? + @State var currencyPair: CurrencyPairModel + + // Conversion + @State private var showingConversion = false + @State private var amountIsEditing = false + @State private var amountToConvert = "" + @State private var price: Double = 0 + + var body: some View { + ScrollView(showsIndicators: false) { + VStack(alignment: .leading) { + CurrencySelector(currencyPair: currencyPair) + } + .padding() + } + .onAppear(perform: createUrlAndRequest) + .navigationTitle("Convert") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + if amountIsEditing { + Button(action: { + UIApplication.shared.dismissKeyboard() + amountIsEditing = false + }) { + Text("Done") + } + } + } + } + .if(UIDevice.current.userInterfaceIdiom == .phone && showNavigationView ?? true) { content in + NavigationView { content } + } + } + + private func createUrlAndRequest() { + showingConversion = false + let baseUrl = readConfigVariable(withKey: "API_URL")! + let apiKey = readConfigVariable(withKey: "API_KEY")! + let currencyPair = "\(currencyPair.baseSymbol)/\(currencyPair.quoteSymbol)" + let url = "\(baseUrl)quotes?pairs=\(currencyPair)&api_key=\(apiKey)" + + httpRequest(url: url, model: [CurrencyQuoteModel].self) { response in + if let price = response.first?.price { + self.price = price + showingConversion = true + } + } + } +} + + +//struct ConversionView_Previews: PreviewProvider { +// static var previews: some View { +// ConversionView() +// } +//}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/CurrencyList.swift Sat Aug 28 11:15:41 2021 +0100 @@ -0,0 +1,62 @@ +// +// CurrencyList.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 24/8/21. +// + +import SwiftUI + +struct CurrencyList: View { + var currencies: [String] + @Binding var selectedCurrency: String + @State private var searchCurrency = "" + @Environment(\.presentationMode) private var presentation + let currencyDetails: [String: CurrencyModel] = try! read(json: "Currencies.json") + + var searchResults: [String] { + if searchCurrency.isEmpty { + return currencies.sorted() + } else { + return currencies.filter {$0.contains(searchCurrency.uppercased())} + } + } + + var body: some View { + NavigationView { + List { + SearchBar(placeholder: "Search...", text: $searchCurrency) + .padding(.vertical) + .accessibilityIdentifier("CurrencySearchBar") + + ForEach(searchResults, id: \.self) { symbol in + Button(action: {selectedCurrency = symbol; presentation.wrappedValue.dismiss()}) { + let currency = currencyDetails[symbol]! + CurrencyRow(currency: currency) + } + } + } + .listStyle() + .navigationTitle("Currencies") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(action: { presentation.wrappedValue.dismiss() }) { + Text("Cancel") + } + } + } + } + } +} +extension View { + func listStyle() -> some View { + self.modifier(ListModifier()) + } +} + +struct CurrencyList_Previews: PreviewProvider { + static var previews: some View { + CurrencyList(currencies: ["USD"], selectedCurrency: .constant("USD")) + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Simoleon/UI/CurrencySelector.swift Sat Aug 28 11:15:41 2021 +0100 @@ -0,0 +1,89 @@ +// +// CurrencySelector.swift +// Simoleon +// +// Created by Dennis Concepción Martín on 27/8/21. +// + +import SwiftUI + +struct CurrencySelector: View { + @State var currencyPair: CurrencyPairModel + @State private var showingList = false + @State private var modalSelection: ModalType? = nil + let currencyPairsSupported: [String] = try! read(json: "CurrencyPairsSupported.json") + + private enum ModalType { + case allCurrencies, compatibleCurrencies + } + + var body: some View { + HStack { + Button(action: { + showingList = true + modalSelection = .allCurrencies + }) { + CurrencyButton(selectedCurrency: currencyPair.baseSymbol) + } + + Button(action: { + showingList = true + modalSelection = .compatibleCurrencies + }) { + CurrencyButton(selectedCurrency: currencyPair.quoteSymbol) + } + } + .onChange(of: currencyPair.baseSymbol) { _ in + // If the previous quote symbol is not compatible anymore with base symbol + // return the first symbol of the new compatible symbols list + let compatibleCurrencies = get(currencyType: .compatible(with: currencyPair.baseSymbol), from: currencyPairsSupported) + if !compatibleCurrencies.contains(currencyPair.quoteSymbol) { + currencyPair.quoteSymbol = compatibleCurrencies.sorted().first! + } + } + .sheet(isPresented: $showingList) { + if modalSelection == .allCurrencies { + let currencies = get(currencyType: .all, from: currencyPairsSupported) + CurrencyList(currencies: currencies, selectedCurrency: $currencyPair.baseSymbol) + } else { + let currencies = get(currencyType: .compatible(with: currencyPair.baseSymbol), from: currencyPairsSupported) + CurrencyList(currencies: currencies, selectedCurrency: $currencyPair.quoteSymbol) + } + } + } + + enum CurrencyType { + case all + case compatible(with: String) + } + + func get(currencyType: CurrencyType, from currencyPairsSupported: [String]) -> [String] { + var currencies = Set<String>() + + switch currencyType { + case .all: + for currencyPairSupported in currencyPairsSupported { + let currency = currencyPairSupported.components(separatedBy: "/")[0] + currencies.insert(currency) + } + + return Array(currencies) + + case .compatible(with: let symbol): + for currencyPairSupported in currencyPairsSupported { + if currencyPairSupported.hasPrefix(symbol) { + let compatibleCurrency = currencyPairSupported.components(separatedBy: "/")[1] + currencies.insert(compatibleCurrency) + } + } + + return Array(currencies) + } + } +} + +struct CurrencySelector_Previews: PreviewProvider { + static var previews: some View { + CurrencySelector(currencyPair: CurrencyPairModel(baseSymbol: "USD", quoteSymbol: "EUR")) + } +}
--- a/SimoleonTests/SimoleonTests.swift Sat Aug 28 11:15:25 2021 +0100 +++ b/SimoleonTests/SimoleonTests.swift Sat Aug 28 11:15:41 2021 +0100 @@ -2,56 +2,64 @@ // SimoleonTests.swift // SimoleonTests // -// Created by Dennis Concepción Martín on 08/07/2021. +// Created by Dennis Concepción Martín on 27/8/21. // import XCTest @testable import Simoleon class SimoleonTests: XCTestCase { - let fileController = FileController() override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. - continueAfterFailure = false } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } - func testReadJson() throws { - let currencyPairsSupported: [String]? = try? fileController.read(json: "CurrencyPairsSupported.json") - XCTAssertNotNil(currencyPairsSupported, "An error occurred while reading CurrencyPairsSupported.json") + func testGetAllCurrencies() throws { + // Create test cases + let testCases = [1: ["USD/GBP", "EUR/AED"], 2: ["USD/GBP", "USD/EUR"]] + let expectedResults = [1: ["USD", "EUR"], 2: ["USD"]] - let currencyDetails: [String: CurrencyDetailsModel]? = try? fileController.read(json: "CurrencyDetails.json") - XCTAssertNotNil(currencyDetails, "An error occurred while reading CurrencyDetails.json") + // Test + let currencySelector = CurrencySelector(currencyPair: CurrencyPairModel(baseSymbol: "USD", quoteSymbol: "EUR")) + for testCaseNumber in testCases.keys { + print("Testing case: \(testCaseNumber)") + let mockData = testCases[testCaseNumber]! + let allCurrencies = currencySelector.get(currencyType: .all, from: mockData) + + // Assert + XCTAssertEqual(allCurrencies, expectedResults[testCaseNumber]) + } } - func testCurrencyExistence() throws { - let currencyDetails: [String: CurrencyDetailsModel] = try! fileController.read(json: "CurrencyDetails.json") - - // Remove duplicates from currency pairs supported - let currencyPairsSupported: [String] = try! fileController.read(json: "CurrencyPairsSupported.json") - var currenciesSupported = Set<String>() + func testGetCompatibleCurrencies() throws { + // Create test cases + let testCases = [1: ["USD/GBP", "EUR/AED"], 2: ["USD/GBP", "USD/EUR"], 3: ["EUR/AED"]] + let expectedResults = [1: ["GBP"], 2: ["GBP", "EUR"], 3: []] - for currencyPairSupported in currencyPairsSupported { - let symbols = currencyPairSupported.components(separatedBy: "/") - for symbol in symbols { - currenciesSupported.insert(symbol) - XCTAssertNotNil(currencyDetails[symbol], "Currency details of symbol: \(symbol) can't be found") - XCTAssertTrue((UIImage(named: currencyDetails[symbol]!.flag) != nil), "Flag of symbol: \(symbol) can't be found") - } + // Test + let currencySelector = CurrencySelector(currencyPair: CurrencyPairModel(baseSymbol: "USD", quoteSymbol: "EUR")) + for testCaseNumber in testCases.keys { + print("Testing case: \(testCaseNumber)") + let mockData = testCases[testCaseNumber]! + let compatibleCurrencies = + currencySelector.get( + currencyType: .compatible(with: currencySelector.currencyPair.baseSymbol), from: mockData + ) + + // Assert + XCTAssertEqual(compatibleCurrencies, expectedResults[testCaseNumber]) } - - // Check if there are same number of currencies - XCTAssertEqual(currencyDetails.keys.count, currenciesSupported.count) } func testPerformanceExample() throws { // This is an example of a performance test case. - self.measure { + measure { // Put the code you want to measure the time of here. } } + }