changeset 160:0c589138a6f3

Implement Conversion Box
author Dennis Concepcion Martin <dennisconcepcionmartin@gmail.com>
date Sun, 29 Aug 2021 19:04:34 +0100
parents 35628bac01f5
children 3913aff613e8
files Simoleon.xcodeproj/project.pbxproj Simoleon/ContentView.swift Simoleon/ConversionView.swift Simoleon/Helpers/ErrorHandling.swift Simoleon/Helpers/FileHelper.swift Simoleon/Helpers/NetworkHelper.swift Simoleon/Info.plist Simoleon/Models/CurrencyPair.swift Simoleon/Models/CurrencyPairModel.swift Simoleon/Models/CurrencyQuoteModel.swift Simoleon/SimoleonApp.swift Simoleon/UI/ConversionBox.swift Simoleon/UI/ConversionTextfield.swift Simoleon/UI/CurrencyButton.swift Simoleon/UI/CurrencyList.swift Simoleon/UI/CurrencySelector.swift Simoleon/UI/FavoriteButton.swift Simoleon/UI/Flag.swift SimoleonTests/SimoleonTests.swift
diffstat 19 files changed, 210 insertions(+), 194 deletions(-) [+]
line wrap: on
line diff
--- a/Simoleon.xcodeproj/project.pbxproj	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon.xcodeproj/project.pbxproj	Sun Aug 29 19:04:34 2021 +0100
@@ -15,11 +15,12 @@
 		9522CD9D26CED2E100DD9D03 /* ErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9522CD9C26CED2E100DD9D03 /* ErrorHandling.swift */; };
 		9531D44226D8E4CF00665D2A /* SimoleonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9531D44126D8E4CF00665D2A /* SimoleonTests.swift */; };
 		953B8B1726D3A970003CF530 /* CurrencyDetailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 953B8B1626D3A970003CF530 /* CurrencyDetailsModel.swift */; };
+		9547423526DBE56600415B3F /* ConversionTextfield.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9547423426DBE56600415B3F /* ConversionTextfield.swift */; };
 		95561E3F26AF25EF00CCB543 /* SubscriptionFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95561E3E26AF25EF00CCB543 /* SubscriptionFeature.swift */; };
 		95562D4D26A8962A0047E778 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95562D4C26A8962A0047E778 /* StoreKit.framework */; };
 		95562D5226A8AEF60047E778 /* Purchases in Frameworks */ = {isa = PBXBuildFile; productRef = 95562D5126A8AEF60047E778 /* Purchases */; };
 		957065E226A5FE0400523E68 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 957065E126A5FE0400523E68 /* SettingsView.swift */; };
-		957DCF3326D7ADEA00BCAB1E /* CurrencyPairModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 957DCF3226D7ADEA00BCAB1E /* CurrencyPairModel.swift */; };
+		957DCF3326D7ADEA00BCAB1E /* CurrencyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 957DCF3226D7ADEA00BCAB1E /* CurrencyPair.swift */; };
 		95851CE326D4DAAE004ADA79 /* CurrencyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95851CE226D4DAAE004ADA79 /* CurrencyButton.swift */; };
 		95851CE526D4DB4C004ADA79 /* Flag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95851CE426D4DB4C004ADA79 /* Flag.swift */; };
 		9585BB1426A6B7F400E3193E /* NetworkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9585BB1326A6B7F400E3193E /* NetworkHelper.swift */; };
@@ -119,11 +120,12 @@
 		9531D44126D8E4CF00665D2A /* SimoleonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimoleonTests.swift; sourceTree = "<group>"; };
 		9531D44326D8E4CF00665D2A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		953B8B1626D3A970003CF530 /* CurrencyDetailsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyDetailsModel.swift; sourceTree = "<group>"; };
+		9547423426DBE56600415B3F /* ConversionTextfield.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversionTextfield.swift; sourceTree = "<group>"; };
 		95561E3E26AF25EF00CCB543 /* SubscriptionFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFeature.swift; sourceTree = "<group>"; };
 		95562D4C26A8962A0047E778 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
 		956088B526B9307600A4FD6C /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = "<group>"; };
 		957065E126A5FE0400523E68 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
-		957DCF3226D7ADEA00BCAB1E /* CurrencyPairModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyPairModel.swift; sourceTree = "<group>"; };
+		957DCF3226D7ADEA00BCAB1E /* CurrencyPair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyPair.swift; sourceTree = "<group>"; };
 		95851CE226D4DAAE004ADA79 /* CurrencyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyButton.swift; sourceTree = "<group>"; };
 		95851CE426D4DB4C004ADA79 /* Flag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Flag.swift; sourceTree = "<group>"; };
 		9585BB0F26A6B58500E3193E /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
@@ -257,7 +259,7 @@
 				95AC820626DAA3ED00CD5C3F /* FavoritePair+CoreDataProperties.swift */,
 				95AEBCA226A0900E00613729 /* CurrencyQuoteModel.swift */,
 				953B8B1626D3A970003CF530 /* CurrencyDetailsModel.swift */,
-				957DCF3226D7ADEA00BCAB1E /* CurrencyPairModel.swift */,
+				957DCF3226D7ADEA00BCAB1E /* CurrencyPair.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -399,6 +401,7 @@
 				95CE6A3526D50B7700D9DCBD /* CurrencyList.swift */,
 				95C5179826A5EC9F00BC2B24 /* FavoriteButton.swift */,
 				95B54F4926A4A450001DC0D8 /* ConversionBox.swift */,
+				9547423426DBE56600415B3F /* ConversionTextfield.swift */,
 				95AEBC9C26A04D4600613729 /* CurrencyRow.swift */,
 				95851CE426D4DB4C004ADA79 /* Flag.swift */,
 				95B54F5026A4ACAC001DC0D8 /* Sidebar.swift */,
@@ -708,7 +711,7 @@
 				95D8C8D126A9BC6200BCC188 /* LockedCurrencyPicker.swift in Sources */,
 				95C517A126A5F6C000BC2B24 /* ResignKeyboard.swift in Sources */,
 				95CE6A3626D50B7700D9DCBD /* CurrencyList.swift in Sources */,
-				957DCF3326D7ADEA00BCAB1E /* CurrencyPairModel.swift in Sources */,
+				957DCF3326D7ADEA00BCAB1E /* CurrencyPair.swift in Sources */,
 				95AEBC9D26A04D4600613729 /* CurrencyRow.swift in Sources */,
 				95AEBCA326A0900E00613729 /* CurrencyQuoteModel.swift in Sources */,
 				9585BB1426A6B7F400E3193E /* NetworkHelper.swift in Sources */,
@@ -718,6 +721,7 @@
 				953B8B1726D3A970003CF530 /* CurrencyDetailsModel.swift in Sources */,
 				95C5179126A5DC8E00BC2B24 /* ConditionalWrapper.swift in Sources */,
 				95B54F5126A4ACAC001DC0D8 /* Sidebar.swift in Sources */,
+				9547423526DBE56600415B3F /* ConversionTextfield.swift in Sources */,
 				95561E3F26AF25EF00CCB543 /* SubscriptionFeature.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
--- a/Simoleon/ContentView.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/ContentView.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -15,18 +15,17 @@
     }
     
     @ViewBuilder var adjustedView: some View {
-        let currencyPair = CurrencyPairModel(baseSymbol: "USD", quoteSymbol: "EUR")
         
         // MARK: - iPad
         if UIDevice.current.userInterfaceIdiom == .pad {
             NavigationView {
 //                Sidebar()
-                ConversionView(currencyPair: currencyPair)
+                ConversionView()
             }
         } else {
             // MARK: - iPhone
             TabView(selection: $tab) {
-                ConversionView(currencyPair: currencyPair)
+                ConversionView()
                     .tabItem {
                         Label("Convert", systemImage: "arrow.counterclockwise.circle")
                     }
--- a/Simoleon/ConversionView.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/ConversionView.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -10,62 +10,31 @@
 
 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
+    @StateObject var currencyPair = CurrencyPair()
     
     var body: some View {
         ScrollView(showsIndicators: false) {
-            VStack(alignment: .leading) {
+            VStack(alignment: .leading, spacing: 20) {
                 HStack {
                     CurrencySelector(currencyPair: currencyPair)
                     FavoriteButton(currencyPair: currencyPair)
                 }
+                
+                ConversionBox(currencyPair: currencyPair)
+                    .padding(.top)
             }
             .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(showNavigationView: true, currencyPair: CurrencyPairModel(baseSymbol: "USD", quoteSymbol: "EUR"))
+        ConversionView(showNavigationView: true)
     }
 }
--- a/Simoleon/Helpers/ErrorHandling.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/Helpers/ErrorHandling.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -13,4 +13,8 @@
         case loadFailed(cause: String)
         case parseFailed(cause: String)
     }
+    
+    enum Networking: Error {
+        case invalidURL
+    }
 }
--- a/Simoleon/Helpers/FileHelper.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/Helpers/FileHelper.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -7,10 +7,8 @@
 
 import Foundation
 
-/*
- Decode and read json file
- */
-func read<T: Decodable>(json filename: String) throws -> T {
+
+func readJson<T: Decodable>(from filename: String) throws -> T {
     let data: Data
     
     guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
@@ -32,10 +30,7 @@
     }
 }
 
-/*
- Read configuration variables from Config.xconfig
- */
-func readConfigVariable(withKey: String) -> String? {
+func readConfig(withKey: String) -> String? {
     return (Bundle.main.infoDictionary?[withKey] as? String)?
         .replacingOccurrences(of: "\\", with: "")
 }
--- a/Simoleon/Helpers/NetworkHelper.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/Helpers/NetworkHelper.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -7,32 +7,29 @@
 
 import Foundation
 
-// MARK: - HTTP Request
-func httpRequest<T: Decodable>(url: String, model: T.Type, completion: @escaping (_ result: T) -> Void) {
-    
-    // We take some model data T.Type
-    guard let url = URL(string: url) else {
-        print("Invalid URL")
-        return
+class NetworkHelper {
+    func httpRequest<T: Decodable>(url: String, model: T.Type, completion: @escaping (_ result: T) -> Void) throws {
+        // We take some model data T.Type
+        guard let url = URL(string: url) else { throw ErrorHandling.Networking.invalidURL }
+        
+        let request = URLRequest(url: url)
+        URLSession.shared.dataTask(with: request) { data, response, error in
+            if let data = data {
+                do {
+                    // Decode response with the model passed
+                    let decodedResponse = try JSONDecoder().decode(model, from: data)
+                    DispatchQueue.main.async {
+                        completion(decodedResponse)
+                    }
+                    return
+                } catch {
+                    // Return error regarding the escaping code
+                    print(error)
+                }
+            }
+            // Error with the request
+            print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
+        }
+        .resume()
     }
-    
-    let request = URLRequest(url: url)
-    URLSession.shared.dataTask(with: request) { data, response, error in
-        if let data = data {
-            do {
-                // Decode response with the model passed
-                let decodedResponse = try JSONDecoder().decode(model, from: data)
-                DispatchQueue.main.async {
-                    completion(decodedResponse)
-                }
-                return
-            } catch {
-                // Return error regarding the escaping code
-                print(error)
-            }
-        }
-        // Error with the request
-        print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
-    }
-    .resume()
 }
--- a/Simoleon/Info.plist	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/Info.plist	Sun Aug 29 19:04:34 2021 +0100
@@ -4,8 +4,6 @@
 <dict>
 	<key>API_KEY</key>
 	<string>$(API_KEY)</string>
-	<key>API_URL</key>
-	<string>$(API_URL)</string>
 	<key>CFBundleDevelopmentRegion</key>
 	<string>$(DEVELOPMENT_LANGUAGE)</string>
 	<key>CFBundleDisplayName</key>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Models/CurrencyPair.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -0,0 +1,18 @@
+//
+//  CurrencyPair.swift
+//  Simoleon
+//
+//  Created by Dennis Concepción Martín on 26/8/21.
+//
+
+import Foundation
+
+class CurrencyPair: ObservableObject {
+    /*
+     Forex pair -> XXX/YYY
+     Where XXX is the base currency, and YYY the quote currency
+     */
+    
+    @Published var baseSymbol = "USD"
+    @Published var quoteSymbol = "EUR"
+}
--- a/Simoleon/Models/CurrencyPairModel.swift	Sat Aug 28 19:18:50 2021 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-//
-//  CurrencyPairModel.swift
-//  Simoleon
-//
-//  Created by Dennis Concepción Martín on 26/8/21.
-//
-
-import Foundation
-
-struct CurrencyPairModel {
-    /*
-     Forex pair -> XXX/YYY
-     Where XXX is the base currency, and YYY the quote currency
-     */
-    
-    var baseSymbol: String
-    var quoteSymbol: String
-}
--- a/Simoleon/Models/CurrencyQuoteModel.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/Models/CurrencyQuoteModel.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -8,14 +8,14 @@
 import Foundation
 
 struct CurrencyQuoteModel: Codable, Hashable {
-    var symbol: String?
+    var pair: String?
     var price: Double?
     var bid: Double?
     var ask: Double?
     var timeStamp: Int?
     
     private enum CodingKeys: String, CodingKey {
-        case symbol = "s"
+        case pair = "s"
         case price = "p"
         case bid = "b"
         case ask = "a"
--- a/Simoleon/SimoleonApp.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/SimoleonApp.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -14,7 +14,7 @@
     let persistenceController = PersistenceController.shared
     
     init() {
-        let apiKey = readConfigVariable(withKey: "PURCHASES_KEY")!
+        let apiKey = readConfig(withKey: "PURCHASES_KEY")!
         Purchases.configure(withAPIKey: apiKey)
     }
     
--- a/Simoleon/UI/ConversionBox.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/UI/ConversionBox.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -1,83 +1,95 @@
-////
-////  ConversionBox.swift
-////  Simoleon
-////
-////  Created by Dennis Concepción Martín on 18/07/2021.
-////
-//
-//import SwiftUI
 //
-//struct ConversionBox: View {
-//    var currencyDetails: CurrencyDetailsModel
-//    @State var currencyPair: CurrencyPairModel
-//    
-//    var body: some View {
-//        VStack(alignment: .leading) {
-//            Text("\(baseName) (\(currencyPair.baseSymbol))")
-//                .font(.callout)
-//                .fontWeight(.semibold)
-//                .padding(.top, 40)
-//            
-//            ZStack(alignment: .trailing) {
-//                TextField("Enter amount", text: $amount) { startedEditing in
-//                    if startedEditing {
-//                        withAnimation {
-//                            amountIsEditing = true
-//                        }
-//                    }
-//                }
-//                onCommit: {
-//                    withAnimation {
-//                        amountIsEditing = false
-//                    }
-//                }
-//                .keyboardType(.decimalPad)
-//                .font(Font.title.weight(.semibold))
-//                .lineLimit(1)
-//                .accessibilityIdentifier("ConversionTextField")
-//            }
-//            
-//            Divider()
+//  ConversionBox.swift
+//  Simoleon
+//
+//  Created by Dennis Concepción Martín on 18/07/2021.
 //
-//            let quoteName = currencyDetails[currencyPair.quoteSymbol]!.name
-//            Text("\(quoteName) (\(currencyPair.quoteSymbol))")
-//                .font(.callout)
-//                .fontWeight(.semibold)
-//                .padding(.top, 10)
-//            
-//            if showingConversion {
-//                Text("\(makeConversion(), specifier: "%.2f")")
-//                    .font(Font.title.weight(.semibold))
-//                    .lineLimit(1)
-//                    .padding(.top, 5)
-//            } else {
-//                ProgressView()
-//                    .padding(.top, 5)
-//            }
-//        }
-//        .onAppear(perform: request)
-//    }
-//    
-//    /*
-//     if the amount can be converted to Double:
-//     * Return amount
-//     else:
-//     * Return zero
-//     */
-//    func makeConversion() -> Double {
-//        if let amountToConvert = Double(amount) {
-//            return amountToConvert * price  // Conversion
-//        } else {
-//            return 0
-//        }
-//    }
-//}
-//
-//
-//struct ConversionBox_Previews: PreviewProvider {
-//    static var previews: some View {
-//        let fileController = File()
-//        let currencyDetails: [String: CurrencyDetailsModel] = try! fileController.read(json: "CurrencyDetails.json")
-//        ConversionBox(currencyPair: CurrencyPair(), currencyDetails: currencyDetails)
-//    }
-//}
+
+import SwiftUI
+
+struct ConversionBox: View {
+    @ObservedObject var currencyPair: CurrencyPair
+    @State private var amount = ""
+    @State private var isEditing = false
+    @State private var showingConversion = false
+    @State private var currencyQuote = CurrencyQuoteModel()
+    @State private var showingAlert = false
+    
+    let networkHelper = NetworkHelper()
+    let currencyDetails: [String: CurrencyModel] = try! readJson(from: "Currencies.json")
+    
+    var body: some View {
+        VStack(alignment: .leading) {
+            let baseCurrencyName = currencyDetails[currencyPair.baseSymbol]!.name
+            Text("\(baseCurrencyName) (\(currencyPair.baseSymbol))")
+                .font(.callout)
+                .fontWeight(.semibold)
+            
+            ConversionTextfield(amount: $amount, isEditing: $isEditing)
+            Divider()
+            
+            let quoteCurrencyName = currencyDetails[currencyPair.quoteSymbol]!.name
+            Text("\(quoteCurrencyName) (\(currencyPair.quoteSymbol))")
+                .font(.callout)
+                .fontWeight(.semibold)
+            
+            if showingConversion {
+                let conversion = convert()
+                Text("\(conversion, specifier: "%.2f")")
+                    .font(Font.title.weight(.semibold))
+                    .lineLimit(1)
+            } else {
+                ProgressView()
+            }
+        }
+        .toolbar {
+            ToolbarItem(placement: .navigationBarTrailing) {
+                if isEditing {
+                    Button(action: {
+                        UIApplication.shared.dismissKeyboard()
+                        isEditing = false
+                    }) {
+                        Text("Done")
+                    }
+                }
+            }
+        }
+        
+        .onAppear {
+            showingConversion = false
+            let pair = "\(currencyPair.baseSymbol)/\(currencyPair.quoteSymbol)"
+            let apiKey = readConfig(withKey: "API_KEY")!
+            let url = "https://api.1forge.com/quotes?pairs=\(pair)&api_key=\(apiKey)"
+            try? networkHelper.httpRequest(url: url, model: [CurrencyQuoteModel].self) { response in
+                if let currencyQuote = response.first {
+                    self.currencyQuote = currencyQuote
+                } else {
+                    showingAlert = true
+                }
+                
+                showingConversion = true
+            }
+        }
+        .alert(isPresented: $showingAlert) {
+            Alert(
+                title: Text("Currencies not supported."),
+                message: Text("Currently, we are unable to convert from \(currencyPair.baseSymbol) to \(currencyPair.quoteSymbol)."),
+                dismissButton: .default(Text("Dismiss")
+                )
+            )
+        }
+    }
+    
+    private func convert() -> Double {
+        guard let amount = Double(amount) else { return 0  }
+        guard let price = currencyQuote.price else { return 0 }
+        
+        return amount * price
+    }
+}
+
+struct ConversionBox_Previews: PreviewProvider {
+    static var previews: some View {
+        ConversionBox(currencyPair: CurrencyPair())
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/UI/ConversionTextfield.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -0,0 +1,39 @@
+//
+//  ConversionTextfield.swift
+//  Simoleon
+//
+//  Created by Dennis Concepción Martín on 29/8/21.
+//
+
+import SwiftUI
+
+struct ConversionTextfield: View {
+    @Binding var amount: String
+    @Binding var isEditing: Bool
+    
+    var body: some View {
+        ZStack {
+            TextField("Enter amount", text: $amount) { startedEditing in
+                if startedEditing {
+                    withAnimation {
+                        isEditing = true
+                    }
+                }
+            }
+            onCommit: {
+                withAnimation {
+                    isEditing = false
+                }
+            }
+            .keyboardType(.decimalPad)
+            .font(Font.title.weight(.semibold))
+            .lineLimit(1)
+        }
+    }
+}
+
+struct ConversionTextfield_Previews: PreviewProvider {
+    static var previews: some View {
+        ConversionTextfield(amount: .constant("1000"), isEditing: .constant(false))
+    }
+}
--- a/Simoleon/UI/CurrencyButton.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/UI/CurrencyButton.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -9,7 +9,7 @@
 
 struct CurrencyButton: View {
     var selectedCurrency: String
-    let currencyDetails: [String: CurrencyModel] = try! read(json: "Currencies.json")
+    let currencyDetails: [String: CurrencyModel] = try! readJson(from: "Currencies.json")
     
     var body: some View {
         let currency = currencyDetails[selectedCurrency]!
--- a/Simoleon/UI/CurrencyList.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/UI/CurrencyList.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -12,7 +12,7 @@
     @Binding var selectedCurrency: String
     @State private var searchCurrency = ""
     @Environment(\.presentationMode) private var presentation
-    let currencyDetails: [String: CurrencyModel] = try! read(json: "Currencies.json")
+    let currencyDetails: [String: CurrencyModel] = try! readJson(from: "Currencies.json")
     
     var searchResults: [String] {
         if searchCurrency.isEmpty {
--- a/Simoleon/UI/CurrencySelector.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/UI/CurrencySelector.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -8,10 +8,10 @@
 import SwiftUI
 
 struct CurrencySelector: View {
-    @State var currencyPair: CurrencyPairModel
+    @ObservedObject var currencyPair: CurrencyPair
     @State private var showingList = false
     @State private var modalSelection: ModalType = .allCurrencies
-    let currencyPairsSupported: [String] = try! read(json: "CurrencyPairsSupported.json")
+    let currencyPairsSupported: [String] = try! readJson(from: "CurrencyPairsSupported.json")
     
     private enum ModalType {
         case allCurrencies, compatibleCurrencies
@@ -84,6 +84,6 @@
 
 struct CurrencySelector_Previews: PreviewProvider {
     static var previews: some View {
-        CurrencySelector(currencyPair: CurrencyPairModel(baseSymbol: "USD", quoteSymbol: "EUR"))
+        CurrencySelector(currencyPair: CurrencyPair())
     }
 }
--- a/Simoleon/UI/FavoriteButton.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/UI/FavoriteButton.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -8,7 +8,7 @@
 import SwiftUI
 
 struct FavoriteButton: View {
-    @State var currencyPair: CurrencyPairModel
+    @ObservedObject var currencyPair: CurrencyPair
     @State private var scale: CGFloat = 1
     @Environment(\.managedObjectContext) private var viewContext
     @FetchRequest(sortDescriptors: []) private var favoritePairs: FetchedResults<FavoritePair>
@@ -91,6 +91,6 @@
 
 struct FavoriteButton_Previews: PreviewProvider {
     static var previews: some View {
-        FavoriteButton(currencyPair: CurrencyPairModel(baseSymbol: "USD", quoteSymbol: "EUR"))
+        FavoriteButton(currencyPair: CurrencyPair())
     }
 }
--- a/Simoleon/UI/Flag.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/Simoleon/UI/Flag.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -14,9 +14,8 @@
         Image(flag)
             .resizable()
             .aspectRatio(contentMode: .fill)
-            .frame(width: 30, height: 30)
+            .frame(width: 35, height: 35)
             .clipShape(Circle())
-            .overlay(Circle().stroke(Color(.secondaryLabel), lineWidth: 1))
     }
 }
 
--- a/SimoleonTests/SimoleonTests.swift	Sat Aug 28 19:18:50 2021 +0100
+++ b/SimoleonTests/SimoleonTests.swift	Sun Aug 29 19:04:34 2021 +0100
@@ -24,7 +24,7 @@
         let expectedResults = [1: ["USD", "EUR"], 2: ["USD"]]
         
         // Test
-        let currencySelector = CurrencySelector(currencyPair: CurrencyPairModel(baseSymbol: "USD", quoteSymbol: "EUR"))
+        let currencySelector = CurrencySelector(currencyPair: CurrencyPair())
         for testCaseNumber in testCases.keys {
             print("Testing case: \(testCaseNumber)")
             let mockData = testCases[testCaseNumber]!
@@ -41,7 +41,7 @@
         let expectedResults = [1: ["GBP"], 2: ["GBP", "EUR"], 3: []]
         
         // Test
-        let currencySelector = CurrencySelector(currencyPair: CurrencyPairModel(baseSymbol: "USD", quoteSymbol: "EUR"))
+        let currencySelector = CurrencySelector(currencyPair: CurrencyPair())
         for testCaseNumber in testCases.keys {
             print("Testing case: \(testCaseNumber)")
             let mockData = testCases[testCaseNumber]!