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.
         }
     }
+
 }